Написание собственных .dll-библиотек на С++ для подключения их к коду Lua (QLua) и вызов своих функций из этих библиотек
Здесь примеры будут рассматриваться на C++ (т.к. именно этот и только этот язык рассмотрен в документации LUA), но если у кого-то будет острое желание делать внешние модули на другом языке - пишите в комментариях. (Есть отдельная тема [url=https://quik2dde.ru/viewtopic.php?id=40]про Delphi / Lazarus[/url].)
Скажу сразу: здесь не планируется и не будет исчерпывающего описания взаимодействия LUA и Си. Просто потому, что такого материала в интернете просто завались, ничего нового я не открою.
Однако хотелось бы привести описание шагов, позволяющих сделать «быстрый старт» в написании своей библиотеки, попутно указав на некоторые неочевидные нюансы настройки проекта.
Ссылки: [url=http://www.lua.org/manual/5.1/manual.html#3]официальная документация по сращиванию LUA и C[/url], эта документация также [url=http://www.lua.ru/doc/3.html] доступна по-русски[/url].
Начну в каком-то смысле «с конца».
Чтобы подключить внешнюю DLL библиотеку к LUA, в скрипте необходимо необходимо вписать строчку:
luacdll = require("luacdll")
В результате выполнения этой строчки в LUA будет закружена библиотека с именем luacdll.dll (причем расширение в require не указывается), и из этой библиотеки будет прочитана информация об имеющихся в ней функциях, доступных из LUA (позже мы увидим каким именно образом).
Для простоты и надежности я очень советую положить скомпилированный dll-файл в тот же каталог, где расположен сам терминал QUIK, это проще и надежнее всего. Хотя можно настроить в LUA-скрипте содержимое переменной package.cpath до строки с require, если очень хочется.
Уточнение для QUIK 8.11 и более новых версий по поводу расположения файла скомпилированного dll-файла. Начиная с этой версии, для выполнения скриптов доступен выбор версии интерпретатора Lua: 5.3 или 5.4. Это добавляет проблем с выбором места, где расположить изготовленную нами библиотеку. Если вы гарантированно используете лишь одну версию (либо 5.3, либо 5.4) для выполнения всех своих скриптов - то можете также положить библиотеку в корень терминала QUIK. Это будет просто и надёжно. Однако, если вы планируете запускать скрипты с разными версиями интерпретатора, что придется держать 2 версии библиотеки (скомпилированные для Lua5.3 и Lua5.4) и подгружать в скрипт соответствующую версию библиотеки (при использовании неподходящей сборки dll-библиотеки непременно будут проблемы в работе библиотеки). [url=https://quik2dde.ru/viewtopic.php?id=334]Подробнее этот момент рассмотрен в отдельной ветке форума[/url].
Настройка C++ проекта
Итак, собственно переходим к C++.
Для его успешной сборки нам обязательно понадобятся следующие файлы из поставки LUA (полный собранный дистрибутив доступен на lua.org)
lauxlib.h
lua.h
luaconf.h
lua5.1.lib
Первые три файла содержат описание типов и прототипы интерфейсных функций LUA, четвертый - библиотека для статический линковки с внешней библиотекой интерпретатора lua5.1.dll.
Открываем MS Visual Studio, создаем новый проект DLL.
В свойствах проекта для всех конфигураций, какие мы будем собирать (обычно это Release и Debug), необходимо добавить библиотеку lua5.1.lib в дополнительные библиотеки. В приложенном примере она лежит в подпапке contrib:
Открываем cpp-файл нашего проекта. Вначале добавляем в него заголовочные файлы LUA, причем перед их включением (это важно!) добавляем определение двух переменных препроцессора: они необходимы для случая сборки DLL, доступной из LUA. Если бы мы собирали наоборот LUA-интерпретатор, запускающий из себя LUA-скрипты, то необходимо было бы сделать другие определения.
#define LUA_LIB
#define LUA_BUILD_AS_DLL
#include "lua.hpp"
Подключаем единый файл lua.hpp, в котором:
подключаются все необходимые заголовочные файлы Lua;
т.к. наш проект C++, то заголовочные файлы Lua необходимо подключать под extern "C", что уже сделано в этом едином файле.
В приведённом здесь демонстрационном примере #define для определения LUA_LIB и LUA_BUILD_AS_DLL написан непосредственно в исходном коде просто для наглядности.
Однако в большом проекте удобнее определения LUA_LIB и LUA_BUILD_AS_DLL сделать параметрах проекта, настройка Preprocessor Definitions (удобно, если проект содержит множество файлов). Тогда не придется вставлять соотв. #define в каждый файл исходного кода.
Еще одно соображение.
Чтобы у вас не возникло проблем с переносом и использованием вашей библиотеки на других компьютерах (если такая надобность есть), то есть смысл скомпилировать её со статическим run-time, т.е. весь код собственно библиотек C++ будет полностью внутри вашей DLL. Это несколько увеличит её размер, но кого сейчас волнует размер файла? Отвечу: никого. Зато вам не придётся дополнительно к вашей библиотеке таскать несколько системных DLL, или заставлять пользователей вашей устанавливать C++ Redistributable Package, причем определённой версии. Вы ведь не хотите создавать себе и другим лишние сложности? и правильно, поэтому статический run-time - это наш путь.
Чтобы его включить, необходимо сделать следующие настройки в свойствах проекта. Причем в зависимости от конфигурации следует выбрать верный тип run-time'а, иначе и debug будет не debug, и release не пойми что.
Для release-конфигураций выбираем:
Для debug-конфигураций выбираем:
(Немного подробнее [url=https://quik2dde.ru/viewtopic.php?pid=505#p505]про run-time библиотеки вот в этом сообщении[/url].)
Теперь собственно код библиотеки на C++
Т.к. библиотеку мы назвали luacdll и именно это имя указываем в Lua-скрипте в require, то при загрузке нашей библиотеки LUA-интерпретатор будет искать экспортируемую из нее функцию с определенным именем, состоящем из "двух частей". В нашем случае это имя будет luaopen_luacdll(). Здесь luaopen_ это предопределенный префикс (см. документацию), а luacdll собственно имя нашей библиотеки. Разумеется, тип и аргументы этой функции тоже предопределены.
Для Lua 5.1 эта функция должна содержать следующие строки:
extern "C" LUALIB_API int luaopen_luacdll(lua_State *L) {
luaL_openlib(L, "luacdll", ls_lib, 0);
return 1;
}
Здесь мы регистрируем в LUA-интерпретаторе (путем вызова luaL_openlib) те функции, которые мы предоставляем из нашей библиотеки, что делает их доступными для вызова из LUA-скриптов. Вторым параметром функции передается namespace (имя глобальной переменной), в котором будут доступны функции нашей библиотеки при вызове; чтобы не запутаться, namespace делаем совпадающим с именем нашей библиотеки.
Для Lua 5.2 и более новых версий эта функция должна содержать следующие строки:
extern "C" LUALIB_API int luaopen_luacdll(lua_State *L) {
luaL_newlib(L, ls_lib);
return 1;
}
Здесь мы тоже регистрируем в LUA-интерпретаторе (путем вызова luaL_newlib) те функции, которые мы предоставляем из нашей библиотеки, что делает их доступными для вызова из LUA-скриптов. В отличии от варианта для Lua 5.1 никакая глобальная переменная не задаётся, функции библиотеки становятся доступными в той переменной, которой присвоим результат выполнения require() в коде Lua-скрипта.
luaL_newlib() - это удобный макрос, который сразу включает в себя вызов трех интерфейсных функций Lua:
luaL_checkversion - для проверки версии интерпретатора Lua, особенно актуально для QUIK 8.11;
luaL_newlibtable и luaL_setfuncs - функции, последовательно вызываемые для регистрации функций, реализованных в библиотеке.
(Функция luaL_openlib() удалена из Lua 5.2 как устаревшая.)
В нашей простейшей библиотеке будут реализованы 3 функции, доступные из LUA:
GetCurrentThreadId - получить ID текущего потока
MultTwoNumbers - перемножает 2 числа, заданных в качестве аргументов
MultAllNumbers - перемножает все числа, встретившиеся в аргументах
Сам список функций (имя и указатель на соответствующую Си-функцию) описан в константном массиве:
static struct luaL_reg ls_lib[] = {
{"GetCurrentThreadId", forLua_GetCurrentThreadId},
{"MultTwoNumbers", forLua_MultTwoNumbers},
{"MultAllNumbers", forLua_MultAllNumbers},
{nullptr, nullptr}
};
Реализация функции, возвращающей ID текущего потока:
static int forLua_GetCurrentThreadId(lua_State *L) {
// возвращаем одно целочисленное значение, полученное от Win API функции
lua_pushinteger(L, GetCurrentThreadId());
return(1);
}
Ее прототип предопределен и един для всех интерфейсных функций: принимает единственный параметр L - указатель на стек LUA (см. документацию).
В данном случае функция не подразумевает никаких аргументов и возвращает единственное целочисленное значение.
Исходный код остальных функций можно посмотреть в приложенном архиве, на мой взгляд, в особых комментариях он не нуждается. В реализации MultAllNumbers можно посмотреть как производится обработка вызова функции из LUA с произвольным числом и типом параметров.
После компиляции получившийся DLL-файл (как было сказано) копируем в тот же каталог, где расположен терминал QUIK (обязательно перепишите туда же файл lua5.1.dll!), и запускаем в нем следующий LUA-скрипт, предварительно сохраним его в виде файла:
require("luacdll")
message(tostring(luacdll.GetCurrentThreadId()), 1)
r = luacdll.MultTwoNumbers(5.6, 2.17)
message (tostring(r), 1)
r = luacdll.MultAllNumbers(6, 3, "23423", {2.17}, 1.1)
message (tostring(r), 1)
function main()
end
В результате его выполнения будет выведено 3 сообщения: с ID основного потока терминала, число 12.152 и число 1.1.
Полностью исходные тексты и скомпилированную dll можно скачать по ссылкам в первом посте этой темы.