Lua+C#(без с++) DLL

Автор записи: bdfyjdbx1

Вашему вниманию предлагается решение написания dll модуля lua скрипта полностью на C#  без использования С++.

Как известно  между LUA и .NET есть 2 проблемы, первая это невозможность стандартными способами выполнить export функции из .net сборки, и вторая это разная работа со стеком, так называемое соглашение о вызове функций.
В этой статье будет предложено решение этих проблем, а так же показана реализация вызовов не импортируемый функций lua для quik на примере CreateDataSource и предложен вариант отладки создаваемых модулей с использованием Visual Studio.  Все примеры и решения основываются на использовании Visual Studio 2015 Community Edition.
 Экспорт функций из .NET сборок.

Создаваемый проект в Visual Studio должен быть как Class Library и в свойствах проекта в разделе Build, Target Platform в обязательном порядке должен быть установлен в "x86". Это обязательное условие связано с решением проблем экспорта функций и с 32 разрядной системой Quik.

Далее запускаем "Tools\Nuget Package Manager\Package Manager Console". В открытой консоле вводим команду "Install-Package UnmanagedExports". Именно это расширение позволит экспортировать функции в net сборках. Установка "target platform=x86" должна быть выполнена до инсталляции пакета. Вторым условием является английский интерфейс windows, это к сожалению условие пакета, при русском интерфейсе windows компиляция выполнятся не будет.

Обозначение как и какую функцию экспортировать

1
2
3
4
5
6
7
8
public static class StartupDLL
 {
 [DllExport("luaopen_InQuikModule", CallingConvention = CallingConvention.Cdecl)]
 public static int InQuikModuleStartUp(IntPtr L)
 {
 return 0;
 }
 }

"luaopen_InQuikMudule" - это имя экспортируемой функции в рамках соглашения LUA где InQuikMudule имя dll модуля. Имя функции внутри C# класса может быть любым.  "CallingCinvention=CalllingConvention.Cdecl" решает вторую проблему соглашения о вызовах функций для работы со стеком.

Компиляция и все мы получили dll с экспортированной функцией "luaopen_InQuikMudule" и  создаем lua скрипт с текстом "require (InQuikModule)".
Можно выразить благодарность разработчикам "UnmanagedExports" которые так максимально облегчили решение данной задачи.

Импорт функций LUA и перехват callback функций.

Импорт функций lua лучше осуществлять из qlua.dll. Методика проста.
Создается static class и в нем описываются импортируемые функции.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static class Lua
 {
  [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
  [return: MarshalAs(UnmanagedType.I4)]
  public delegate int LuaNativeFunction(IntPtr L);
 
 [DllImport("qlua.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "lua_tolstring")]
 public static extern IntPtr lua_tolstring(IntPtr luaState, int index, out uint strLen);
 
 [DllImport("qlua.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
 public static extern int lua_pushcclosure(IntPtr L, LuaNativeFunction fn, int n);
 
[DllImport("qlua.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
 public static extern int lua_setfield(IntPtr L, int idx, [MarshalAs(UnmanagedType.LPStr)] string k);
}

Хочу обратить внимание на некоторые вещи. Первая это обьявление delegate именно так должен быть обьявлен delegate для перехвата callback функций. В этом обьявлении так же соблюдено соглашение о вызовах. Второе это обьявление импортируемых функций lua - явно указана qlua.dll и можно указать явно функцию "EntyPoint="lua_tolstring"", тогда имя функции C# может быть любым.
Вот так выглядит класс с перехваченными callback функциями. Достаточно просто.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static class StartupDLL
 {
 [DllExport("luaopen_InQuikModule", CallingConvention = CallingConvention.Cdecl)]
 public static int InQuikModuleStartUp(IntPtr L)
 {
 Lua.lua_pushcclosure(L, forLua_OnStop, 0);
 Lua.lua_setfield(L, -10002, "OnStop");
 
 Lua.lua_pushcclosure(L, forLua_OnClose, 0);
 Lua.lua_setfield(L, -10002, "OnClose");
 
 Lua.lua_pushcclosure(L, forLua_Main, 0);
 Lua.lua_setfield(L, -10002, "main");
 
 return 0;
 }
 
 private static int _runServer = 0;
 static int forLua_OnClose(IntPtr L)
 {
 Interlocked.Exchange(ref _runServer, 1);
 return 0;
 }
 
 static int forLua_OnStop(IntPtr L)
 {
 Interlocked.Exchange(ref _runServer, 1);
 return 0;
 }
 
 static int forLua_Main(IntPtr L)
 {
 
 while (_runServer == 0)
 {
 Thread.Sleep(100);
 }
 
 Interlocked.Exchange(ref _runServer, 0);
 return 0;
 }
 }

 

Вызов встроенных(не импортируемых) lua функций.

В lua для quik существуют функции, которые нельзя импортировать из qlua.dll. Это такие функции как CreateDataSource, однако их можно вызвать использую возможности lua. Для понимания последующего кода необходимо знание работы стека lua. Если вы не обладаете данными знаниями отсылаю к Программирование на языке Lua.
В двух словах что бы вызвать функции типа CreateDataSource необходимо поместить в правильной последовательности параметры вызова функции и саму функцию в стек, по завершению получить результат работы и подчистить стек.

Пример реализации на С# вот такого кода на LUA

1
2
3
4
5
ds = CreateDataSource("SPBFUT","SiH6",INTERVAL_TICK)
ds.SetEmptyCallback()
size = ds.Size
cl = ds.C(10)
ds.Close

Для вызовов встроенных функций необходимо использовать стек lua из main (перхваченной).

Вызов CreateDataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 Lua.lua_getfield(luaState, -10002, "CreateDataSource");
 Lua.lua_pushstring(luaState, "SPBFUT");
 Lua.lua_pushstring(luaState, "SiH6");
 Lua.lua_pushnumber(luaState, 0); // tick
 
 Lua.lua_call(luaState, 3, 2);
 int r = Lua.lua_type(luaState, 1);
 if (r == (int)LUA_TYPE.LUA_TSTRING)
 {
 uint ll = 0;
 string v = Marshal.PtrToStringAnsi(Lua.lua_tolstring(luaState, -1, out ll));
 Lua.lua_settop(luaState, -3);
 return;
 }
 
 Lua.lua_pop(luaState, 1);

Берется из глобального индекса функция CreateDataSource и помещается в стек, далее последовательно помещаются в стек параметры для вызова функции и осуществляется вызов функции
Lua.lua_call(luaState, 3, 2). Проверяется результат выполнения функции и если неудачный то берется строка об ошибке. Все операции идут на стеке, вот почему важно разбираться в работе Lua стека.
Хотелось бы обратить внимание на импортируемую функцию lua_type. Данная функция проверят тип данных размещенный в стеке это можно использовать при отладке для проверки правильной последовательности и типов в стеке.
Lua типа можно описать через вот такое перечисление

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum LUA_TYPE
 {
 LUA_TNONE =-1,
 LUA_TNIL = 0,
 LUA_TBOOLEAN = 1,
 LUA_TLIGHTUSERDATA = 2,
 LUA_TNUMBER = 3,
 LUA_TSTRING = 4,
 LUA_TTABLE = 5,
 LUA_TFUNCTION = 6,
 LUA_TUSERDATA = 7,
 LUA_TTHREAD = 8
 }

Далее вызывем все что нам нужно от таблицы полученной после вызова CreateDataSource, Обращаю внимание что  lua_rawget(luaState, -2) помещает в стек таблицу как параметр для вызова подфункций CreateDataSource таких как SetEmptyCallBack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//---------- SetEmptyCallback
 Lua.lua_pushstring(luaState, "SetEmptyCallback");
 Lua.lua_rawget(luaState, -2);
 // таблицу как параметр
 Lua.lua_pushvalue(luaState, -2);
 Lua.lua_call(luaState, 1, 1);
 Lua.lua_pop(luaState, 1);
 //----------
 
 //----------
 Lua.lua_pushstring(luaState, "Size");
 Lua.lua_rawget(luaState, -2);
 Lua.lua_pushvalue(luaState, -2);
 Lua.lua_call(luaState, 1, 1);
 
 var size_table = Lua.lua_tonumber(luaState, -1);
 
 Lua.lua_pop(luaState, 1);
 //----------
 
 //----------
 Lua.lua_pushstring(luaState, "C");
 Lua.lua_rawget(luaState, -2);
 Lua.lua_pushvalue(luaState, -2);
 Lua.lua_pushinteger(luaState, 10);
 
 Lua.lua_call(luaState, 2, 1);
 
 var rr = Lua.lua_tonumber(luaState, -1);
 Lua.lua_pop(luaState, 1);
 //----------
 
 //----------
 Lua.lua_pushstring(luaState, "Close");
 Lua.lua_rawget(luaState, -2);
 Lua.lua_pushvalue(luaState, -2);
 Lua.lua_call(luaState, 1, 1);
 
 Lua.lua_pop(luaState, 1);
 //----------

ВНИМАНИЕ: Вызов C# функций для работы со встроенными QLUA функциями должны вызываться  с lua стеком функции main.

 

Удаленная отладка с использованием Visual Studio

Если Вы использовали настройка своего проекта по умолчанию то результат компиляции будет сохраняться "путь до проекта/bin/Debug".
В это каталог необходимо поместить ваш простой lua скрипт и 2 файла из каталога quik это qlua.dll и qlist.dll. В настройках запуска lua скрипта в quik необходимо указать именно этот путь до Вашего скрипта.

В квике делаете запуск скрипта и остановку, если он сам остановится по причине ошибки ничего страшного. Это делается для того что бы квик запустил машину lua и загрузил созданную dll. Иначе отладчик Visual Studio не увидит вашу dll.
В visual stidio устанавливаете точку остановки программы, "Dedug/Attach to process" (Alt-Ctrl-P) и в списке находите свою копию quik и делаете "Attach".
В quik запускаете скрипт и в visual studio получите остановку программы в указанной вами точке и далее можно осуществлять отладку.

P.S. При необходимости перекомпиляции проекта quik перезапускается для освобождения dll.
Скачать решение Visual Studio
Всем успехов и профитов.