Вашему вниманию предлагается решение написания 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
Всем успехов и профитов.
Любопытную штуку наблюдаю.
Создаю библиотеку DLL простейшую, 1 класс, 1 ничего не делающий метод, собираю под х86, .net4.5
добовляю в, компилируется без ошибок.
В Квике ошибка, Изображение:

если закоментить
//Class1.mess();
. накидать логику из хоть из примера то все работает.
Ни как не могу сообразить где ошибка. Понятно что как то хитро собрать надо, но как? Все .net библиотеки работают.
Кто сталкивался? Есть мысли?
Сделал маленький проект.
Ссылка на Ядиск с проектом. https://yadi.sk/d/o7vOPVr-rQmK6
Может кто проверит у себя, работает ли у вас. Проект если запустить
1) когда
, у меня работает и выдает сообщение "adfas"
2) 1) когда
, у меня не работает. Происходит ошибка в QUIK
В QUIK надо дабавить ... \LuaDll\LuaDll\bin\Debug\ML.lua
Вопрос снят! Всем спасибо.
Здравствуйте bdfyjdbx1 =)
Хочу сказать огромное спасибо за подсказку с C# через UnmanagmentExport .
Вопрос.
Есть функция luaL_register, которая передает указатель на luaStatе, строку -имя библиотеки и структуру LuaL_Reg .
Пытаюсь зарегистрировать, квик падает = ( ( Не подскажете как решить данную проблему
это выглядеть так с\с++
__________________________________________________________________
typedef int (*lua_CFunction) (lua_State *L);
typedef struct luaL_Reg {
const char *name;
lua_CFunction func;
} luaL_Reg;
LUALIB_API void (luaL_register) (lua_State *L, const char *libname, const luaL_Reg *l);
_____________________________________________________________________________________________________________
на C#
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I4)]
public delegate int LuaCFunction(IntPtr luaState);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct LuaLReg
{
public string Name;
public LuaCFunction Func;
}
[DllImport(PathDll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "luaL_register")]
public static extern void LuaLRegister(IntPtr luaState, string libName, ref LuaLReg luaReg);
Подскажите, используя данный пример, как передать информацию из dll в приложение на c#, использовать MemoryMappedFile или есть какой нибудь другой способ
Методов масса, нормальных около 4. Все на ура ищутся в Google. MemoryMappedFile, pipen, и еще так сложно вспомнить.
По своему опыту я бы советовал начать с WCF (если dll и приложение на одном компьютере - используйте протокол net.pipe, если на разных - net.tcp). Другие варианты имеет смысл рассматривать только если этот вариант ну совсем при совсем не устроит.
Да конечно реализовать можно почти что угодно, но имеет ли смысл велосипед изобретать?
Здравствуйте! Скачал решение пробую скомпилировать, выдает ошибку. Изображение:

Вроде никаких ошибок нет в проекте. Пробовал новый создать, да перенести код туда, та же ошибка. В чем может быть проблема?
Изображение:

Отвечу сам. Это как раз: "Вторым условием является английский интерфейс windows, это к сожалению условие пакета, при русском интерфейсе windows компиляция выполнятся не будет."
"=Наличие проблемы с тем что сборка не компилируется с русским интерфейсом не подтверждаю. Все отлично компилируется и работает.
Возможно это на windows 10 или еще какая-нибудь комбинация. Но у меня к сожалению не компилировалось. Поэтому на всякий случай обозначил проблему и возможное решение."
Наличие проблемы в Win10 подтверждаю.
Странно, у меня работало нормально. Возможно причина в том что у вас локализация студии русская?
Добрый день!
Подскажите, пожалуйста, если делать через dll на С++, то приходится задавать цикл (в потоке) для того, чтобы опрашивать память на наличие новых данных.
А в Вашем случае можно ли сделать без цикла чисто по событийной модели.
Именно это в примере и делается, только через делегаты, а не события, но это почти одно и тоже (события это немного урезанные делегаты).
Обратите внимание на StartupDLL.
OnStop, OnClose - это оно.
Делаете себе такой же, но, например, OnTrade.
Для тех кто будет использовать эту технологию - пара дополнительных советов.
1. Не смотря на то что я написал ранее - потокобезопасность для _runServer важна, т.к. main выполняется в одном потоке, а On обработчики в другом.
2. Если использовать одновременно On обработчики и main так как в это примере - периодически могут возникать исключения CallbackOnCollectedDelegate.
Это связано с тем, что сборщик мусора не находит ссылок на делегат (т.к. все ссылки на него находятся в неуправляемом коде) и удаляет его. Для решения проблемы надо оставить ссылку на делегат в управляемом коде, например объявив его как статическую переменную. Например так.
К сожалению ошибся в форматировании, а отредактировать не могу. Атрибут у метода разумеется остается.
Интереснейшая статья. 10 из 5.
Сначала ничего не понял (неплохо знаю C#, но совершенно не знал lua/qlua), но потом более менее разобрался и кое-что под себя переписал.
Наличие проблемы с тем что сборка не компилируется с русским интерфейсом не подтверждаю. Все отлично компилируется и работает.
Не много не понял почему вы используете
Разве эта операция в принципе может стать не атомарной? В моем понимании даже необходимость потокобезопасности в данном случае сомнительна (ведь источник вызова один единственный поток QLua), ну разве что совсем на всякий случай. Так тогда и писать:
А то и вовсе ManualResetEvent/ManualResetEventSlim...
Или я какую-то особенность упускаю?
=Наличие проблемы с тем что сборка не компилируется с русским интерфейсом не подтверждаю. Все отлично компилируется и работает.
Возможно это на windows 10 или еще какая-нибудь комбинация. Но у меня к сожалению не компилировалось. Поэтому на всякий случай обозначил проблему и возможное решение.
=Разве эта операция в принципе может стать не атомарной? В моем понимании даже необходимость потокобезопасности в данном случае сомнительна (ведь источник вызова один единственный поток QLua)
Это я по привычке написал, у меня роботы все сильно многопоточные. Вы правы. ))
Спасибо за статью. Очень интересная.
Вы можете оставить свою почту? Есть несколько технических вопросов по использованию данного модуля, хотелось бы обсудить. Или напишите мне на почту diving_73@mail.ru
С уважением, Александр
Данной технологией экспорта можно воспользоваться и для программирования советников в MetaTrader,
Поясните пожалуйста.
Вообщем все просто, советник пишется или на MQL или подключается внешняя DLL(http://docs.mql4.com/ru/basis/preprosessor/import).
Так что с помощью пакета экспорта функций можно писать на C#.
Да и для MT нужно использовать соглашение о вызовах. CallingConvention.StdCall
Да, это круто!!! Спасибо огромное за то, что поделились со всеми новой мощной технологией!!! Я подозревал, что можно без C++ обойтись, но не знал как, супер! Подскажите, пожалуйста, Windows переводить в английский интерфейс только для компиляции нужно, потом длл будет работать с русским интерфейсом, или нужно вообще чисто английскую версию Windows ставить изначально?
Английский интерфейс windows необходим только для компиляции, это особенность пакета UnmanagedExports. На исполнение это не влияет.
Спасибо
Спасибо за пост.
Скачал пример, скомпилировал, создал скрип луа с текстом [require "InQuikModule"], запустил в вике и получил ошибку
".\InQuikModule.lua:1: loop or previous error loading module 'InQuikModule'"
Версия Квика 7.2.1.5
В LUA я новичок, поэтому подвис.
В чём может быть проблема ?