Lua FFI

от автора

Под катом описание довольно примитивного FFI для Lua под x64 windows, а теперь ещё и linux (и не только, SysV ABI).

Но который, тем не менее, позволяет делать:

local ffi = require ("ffi") local msg = ffi("user32.dll", "MessageBoxA") msg(0, "Message", "Title", 0)

или взять, например glfw3.dll, и путём

local glfw = ffi("glfw3")

сделать все экcпортируемые библиотекой glfw3.dll функции доступными для вызова из Lua.

Размер самой ffi.dll при этом получился аж 9 Кбайт, вот она целиком на картинке размером 32х96 пикселей. Можно сохранить это изображение, сконвертировать в bmp (хабр не умеет в bmp, пришлось дополнительно упаковать в png), потом руками удалить первые 54 байта заголовка (до ‘MZ’) и пользоваться.

Но очень осторожно, так как в результате всё-таки получилось, что в аккуратную детскую песочницу Lua залезли грязными сапогами, притащили туда всякие небезопасные штуки из С, вроде ручного управления памятью и обращения с указателями вида *(double*) (void * ptr), и вообще использование таких вещей учит всякому нехорошему.

Тут недавно выходил ряд статей про «вредные советы» в программировании «60 антипаттернов для С++ программиста». Под катом можно найти практическое воплощение большинства из них, не всех конечно, но только потому, что не все из них применимы к С, без плюсов 🙂

Сначала примеры использования, во второй части — описание устройства и богатого внутреннего мира библиотеки.

Чтобы не загружать все функции подряд, можно указать имена определённых функций

local gl = ffi("opengl32", {"glClearColor", "glClear", "glViewport"})

Загруженные из динамической библиотеки функции, если их несколько, и так сидят в своей отдельной таблице (считай namespace), так что можно убрать из названий функций префикс, чтобы использовать как glfw.Init()

local glfw = {} for k,v in pairs(ffi("glfw3","*")) do glfw[k:match("^glfw(.+)") or k] = v end

либо сделать их глобальными

for k,v in pairs(ffi("glfw3","*")) do _G[k] = v end

и использовать как glfwInit()

Тип аргументов

Тип аргументов и возвращаемых значений при загрузке функций по одной можно указать явно

local cosf = ffi("msvcrt.dll", "cosf", "ff") print( cosf(math.pi / 3), math.cos(math.pi / 3) )

тогда все необходимые преобразования типов будут сделаны внутри и в функцию будет передано число, сконвертированное во float, а в Lua будет возвращён нормальный для неё lua_Number т.е. double.

Cигнатура C-функции передаётся в виде дополнительной стоки, по символу на аргумент: ‘v’ = void, ‘i’ = int, ‘f’ = float, ‘d’ = double, ‘b’ = boolean, ‘p’ = pointer, ‘s’ = string. Первым символом — всегда тип возвращаемого значения, затем агрументы.

Можно было бы прикрутить и человеческий парсер объявления функций на языке С, но сделать простой парсер, чтобы он при этом не падал от конструкций вида «функция которая возвращает указатель на функцию, которая возвращает указатель на функцию, которая возвращает …» как-то сходу не получилось, полного соответсвия типов между Lua и С тоже нет и всё равно потребует каких-нибудь костылей, да и прикручивать полноценный готовый парсер показалось несколько избыточным, поэтому пока так, потом можно будет добавить поверх.

Если не указаны, типы аргументов преобразуются автоматически, исходя из типов аргументов, передаваемых получившейся Lua функции-обёрткe при её вызове.

Однако, так как в целом с типизацией в Lua так себе, а для плавающей запятой есть только double, то чтобы в библиотечную С-функцию можно было передать именно float, есть дополнительная конвертация через ffi.float(x), которая преобразует число во float и пакует его в младшие 32 разряда double. Соответственно, если С функция возвращает float, его тоже надо распаковать через тот же ffi.float(), чтобы сконвертировать обратно в lua_Number.

local cosf = ffi("msvcrt.dll", "cosf") print( ffi.float( cosf( ffi.float(math.pi / 3) ) ) )

Функция-обёртка с автоматическим преобразованием типов возвращает два значения, (и целый и с плавающей запятой из обоих регистров rax и xmm0, подробности ниже) по этому признаку ffi.float и отличает аргумент который надо «запаковать» от возвращаемого значения, которое надо «распаковать» обратно в double. Также есть функции ffi.double, ffi.string, ffi.boolean, ffi.integer, ffi.pointer, для преобразования соответсвтвующих типов возвращаемых значений обратно в Lua.

Различные структуры можно передавать как указатель (с помощью ffi.userdata() можно выделять память на стороне Lua и передавать этот указатель в С), либо как строку (в Lua есть string.pack для упаковки бинарных данных), за исключением случая, когда структура целиком влазит в 8 байт, тогда её надо упаковать в integer (особенности calling convention).

Если передать в качестве аргумента таблицу со строками, то она будет преобразована в указатель для С функции, сначала будет выделен массив указателей const char * по размеру таблицы и проинициализирован указателями на строки, для передачи массивов строк типа const char **.

Такой «константный» массив живет только пока вызывается функция, наружу не возвращается и будет потом уничтожен сборщиком мусора, а при попытке С функции что-нибудь записать по этому указателю ничего хорошего скорее всего не получится, но и совсем плохого, если не выходить за размеры массивов, наверное, тоже. Для передачи неконстантного массива, в который С функция может что-то записать надо сначала создать его со стороны Lua с помощью ffi.userdata().

Например вызов

void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length);

в Lua будет выглядеть как

gl.ShaderSource(shader, 2, {shader_src1, shader_src2}, 0)

Таблицы могут быть вложенными, для формирования более многомерных char ****.

Callback

Если же в ffi передать Lua функцию, а не строку с именем библиотеки или указатель (на C функцию, который, например, вернула какая-нибудь другая С функция, вроде wglGetProcAddress), то переданная Lua функция будет преобразована в обратную сторону, в callback (С указатель на функцию).

Для передачи callback’ов из Lua в С типы аргуметнов надо указывать явно, так как вызызвать эту функцию будет кто-то снаружи и как-то из самой функции узнать типы переданных аргументов и, особенно, возвращаемого значения не представляется возможным.

local GLFW = {KEY_ESCAPE = 256, PRESS = 1, RELEASE = 0, TRUE = 1, FALSE = 0}  local function key_callback(window, key, scancode, action, mods) --  print(key,scancode,action)   if (key == GLFW.KEY_ESCAPE and action == GLFW.PRESS) then     glfw.SetWindowShouldClose(window, GLFW.TRUE)   end end  key_callback = ffi(key_callback, "vpiiii")  glfw.SetKeyCallback(window, key_callback);

Именованные константы

Именованные константы — отдельная печаль при вызове С библиотек из других языков, но примитивный парсер заголовочного файла, чтобы выдернуть из него необходимые константы может выглядеть довольно просто:

for s in io.lines("glfw3.h") do    k,v = s:match("#define GLFW_([%w_]+)%s+(%g+)")    if k and tonumber(v) then print("GLFW."..k.. " = " .. tonumber(v)) end end

надо только немного доработать, чтобы парсил ещё и вложенные выражения вроде #define A (B|C).

Userdata

Для работы с памятью и указателями добавлена функция ffi.userdata() которая выделяет память со стороны Lua, и можно передавать получившийся объект как указатель в С функцию, а также складывать/доставать в/из этого куска памяти значения со стороны Lua.
Индексация userdata как массива со стороны Lua через [] пока недоделана.

  • x = ffi.userdata(N) выделит N байт.
  • x = ffi.userdata(«qweqweqwe»), выделит память по длине строки +1 для дополнительного ‘\0’ и проинициализирует содержимым строки.
  • x = ffi.userdata({N,K,M}) выделит сначала три указателя и под каждый выделит соответствующие N,K,M байт, т.е. двухмерный массив из трёх элементов с размерами N, M, K байт каждый.
  • x = ffi.userdata({«qwe», «asdfgh»}) то же самое только с инициализацией указателями на строки и копированием данных строк.
  • x:int(), x:uint(), x:float(), x:double(), x:boolean(), x:string(), делают *(int*)x, *(float*)x, *(double*)x и возвращают в Lua соответствующие значения.
  • x:pack(fmt, v1, v2, …) — то же самое что string.pack(fmt, v1, v2, …), запишет бинарные данные в начало строки.
  • x:unpack(fmt [, pos]) == string.unpack(fmt, x:string() [, pos]) — распакует бинарные данные начиная с pos.

Расширения OpenGL

Также можно добавить немного синтаксического сахара и сделать «ленивую» загрузку функций, например, OpenGL расширений. Для этого надо добавить таблице gl метатаблицу, чтобы при попытке вызвать функцию, которой ещё нет в таблице gl (метаметод __index как раз срабатывает когда по заданному ключу значение в таблице отсутствует), она бы автоматически загружалась из opengl32.dll, если там такой функции вдруг нет, то через wglGetProcAddress, ну или будет вызана функция __lib_error (из метатаблицы же, чтобы не загрязнять основную таблицу gl), которую пользователь может потом переопределить, чтобы не валиться с ошибкой, если какую-нибудь не очень обязательную функцию загрузить не удалось.

gl.GetProcAddress = ffi("opengl32", "wglGetProcAddress", "ps") local mt = getmetatable(gl) or {} mt.__index = function(t, k)   t[k] = ffi("opengl32", "gl"..k) or ffi(t.GetProcAddress("gl"..k)) or getmetatable(t).__lib_error(t, "gl"..k)   return t[k] end mt.__lib_error = function (t, name) error("Unable to load '"..name.."' from opengl32.dll or wglGetProcAddress") end setmetatable(mt)

Ленивая загрузка функций

Похожим образом ведет себя и сама библиотека ffi. Если ей передать только имя библиотеки или таблицу со списком библиотек, без указания функций, то она вернёт пустую таблицу с метаметодом __index, который при обращении к неизвестной функции поищет её в библиотеках из списка и, если найдёт, — загрузит. Что позволяет пользователю задать список библиотек и потом просто нарямую вызывать любую функцию по имени без каких-либо дополнительных телодвижений

libs = ffi({"user32", "msvcrt", "kernel32"}) libs.MessageBoxA(0,"Message","Caption",0)

Немного ООП

В библиотеке GLFW довольно много функций вида glfwXXX(GLFWwindow * w, …).
Соответственно можно немного доработать функцию glfw.CreateWindow(), чтобы она возвращала «класс» окна со всеми этими методами, в которые не надо передавать GLFWwindow первым аргументом.

local create_window = glfw.CreateWindow function glfw.CreateWindow(...) return setmetatable({window = create_window(...)}, {__index = function(t,k) return function(self,...) return glfw[k](self.window,...) end end }) end

Теперь glfw.CreateWindow вместо указателя вернёт таблицу с метаметодом __index и можно делать так:

window = glfw.CreateWindow(...) window:MakeContextCurrent() window:SetKeyCallback(key_callback)

Hello glfw window

Таким образом пример glfw «hello window» полностью будет выгдядеть как-то так:

Hello window

local ffi = require("ffi") local gl = require ("gl") local glfw = require ("glfw") local GL = gl.GL local GLFW = glfw.GLFW  glfw.Init() local window = glfw.CreateWindow(640, 480, "Simple example", 0, 0) window:MakeContextCurrent()  local key_callback = function(window, key, scancode, action, mods)   if (key == GLFW.KEY_ESCAPE and action == GLFW.PRESS) then glfw.SetWindowShouldClose(window, GLFW.TRUE) end end key_callback = ffi(key_callback, "vpiiii") window:SetKeyCallback(key_callback)    local vs = gl.shader(GL.VERTEX_SHADER, [[#version 150 in vec2 p;  void main(void){ gl_Position = vec4(p.xy, 0.0f, 1.0f); }]])  local fs = gl.shader(GL.FRAGMENT_SHADER, [[#version 150 uniform vec2 resolution; uniform float zoom = 1.0; uniform vec2 pos = vec2(1.25, 0.05); void main(void){   vec2 uv = ((2.0 * gl_FragCoord.xy - resolution) / resolution.y) * zoom - pos;   int i = 0;   for (vec2 z = vec2(0.0); (dot(z, z) < 65536.0) && (i < 100); i++) z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + uv;   gl_FragColor = vec4(vec3(sin(i * 0.05)) * vec3(0.5, 1.0, 1.3), 1.0); }]])  local prog = gl.program(fs, vs) local pwidth, pheight  = ffi.userdata(4), ffi.userdata(4)  --allocate 4 byte userdata in lua to pass as pointer to C function  gl.ClearColor(0.3, 0.4, 0.5, 1.0)  while window:WindowShouldClose() == GLFW.FALSE do   window:GetFramebufferSize(pwidth, pheight)   local w,h = pwidth:int(), pheight:int()   --dereference userdata pointer to integer, same as pwidth:upack("I4")   gl.Viewport(0, 0, w, h)       prog.resolution = {w, h}          --GLSL uniforms by name with __index/__newindex of prog, type convertion inside   prog.zoom = math.exp(-5+5*math.cos(os.clock()))   gl.Clear(GL.COLOR_BUFFER_BIT)   gl.Rects(-1,-1,1,1)   window:SwapBuffers()   glfw.PollEvents() end  window:DestroyWindow() glfw.Terminate()

В gl.lua вынесены вспомогательные функции для компиляции/компоновки шэйдеров, а также доступ к uniform переменным просто по имени через метаметод индексации.
https://github.com/pavel212/uffi/tree/master/example

fractal

«Мишка сверху весь из плюша, интересно что внутри?»

Обычно для того, чтобы из Lua позвать какую-нибудь C функцию, например

int cfunc(const char *, double);

надо для неё сделать следующую обертку, которая в качестве аргумента принимает указатель на состояние Lua, а возвращает количество значений возвращаемых в Lua (сколько значений положили на Lua стэк).

int luacfunc(lua_State * L){   const char * arg1 = lua_tostring(L, 1);   double       arg2 = lua_tonumber(L, 2);   int ret = cfunc(arg1, arg2);   lua_pushinteger(L, ret);   return 1; }

То есть, достать аргументы со стэка Lua, позвать функцию, затем положить обратно на Lua стэк то, что вернула функция, и вернуть количество возвращаемых значений. Функции в С обычно имеют единственное возвращаемое значение.

Если же хочется позвать какую-нибудь функцию из .dll, но типы и количество аргументов и возвращаемых значений известны только во время исполнения, то код вышеописанной функции придётся сгенерировать на ходу.

На ассемблере эта же функция должна выглядеть как-то примерно так:

;int luacfunc(lua_State * L){ ;prologue   push 'nonvolatile regs'   sub rsp, NSTACK    ;reserve and align stack  ;const char * arg1 = lua_tostring(L, 1);   mov rcx, L   mov rdx, 1   mov r8, 0   call lua_tolstring         ;#define to_tostring(L,idx) lua_tolstring(L,idx,0)   mov [rsp+ARG1], rax               ;store arg1 on stack  ;double       arg2 = lua_tonumber(L, 2);   mov rcx, L   mov rdx, 2   call lua_tonumber   mov [rsp+ARG2], xmm0              ;store arg2 on stack  ;int ret = cfunc(arg1, arg2);   mov rcx, [rsp+ARG1]               ;load arg1   mov xmm1, [rsp+ARG2]              ;load arg2   call cfunc  ;lua_pushinteger(L, ret);   mov rcx, L   mov rdx, rax               ;cfunc() returned integer value in rax -> as second argument for lua_push in rdx   call lua_pushinteger  ;epilogue   add rsp, NSTACK   ;restore stack   pop 'nonvolatile regs'  ;return 1   mov eax, 1   ret 

Как это на самом деле выглядит на ассемблере можно посмотреть в objdump -d, или тут godbolt.org/z/ec6q4h5dT

В двух словах про соглашение о вызове функций в Win_x64:

  • первые 4 аргумента передаются в регистрах rcx/rdx/r8/r9 или xmm0/xmm1/xmm2/xmm3 для плавающей запятой, остальные через стэк.
  • возвращаемое значение в rax/xmm0.
  • всё что по размеру целиком не влазит в 8 байт регистра — через указатель.
  • также надо в любом случае зарезервировать на стэке хотя бы 32 байта для четырёх первых аргументов, даже если их меньше и не смотря на то, что передаются они через регистры.
  • стэк при этом надо бы выравнивать по 16 байтам, так как предыдущий call, положил в последний момент на стэк адрес возврата в размере 8 байт, поэтому стэк изначально по 16 байтам не выровнен.

Осталось лишь обернуть этот псевдоассемблерный код в величественные макросы вида:

#define _push_rbx(p)           _B(p, 0x53) #define _pop_rbx(p)            _B(p, 0x5B) #define _mov_rbx_rcx(p)        _B(p, 0x48, 0x89, 0xCB) #define _sub_rsp_DW(p,x)       _B(p, 0x48, 0x81, 0xEC, _DW(x)) #define _ld_xmm0(p,x)          _B(p, 0xF3, 0x0F, 0x7E, 0x84, 0x24, _DW(x))

которые пишут в память машинный код соответствующих инструкций, а также проверить типы аргументов, чтобы вызывать соответствующие им функции lua_to* / lua_push*.

Перегрузка макроса _B по количеству аргументов для записи произвольного количества байт по указателю, (с костылями для msvc, у которого своеобразный взгляд на разворачивание макросов), а также макросы _DW, _QW для записи слов целиком, но ногами вперёд.

Страшно, очень страшно

#define _ARGN(A1,A2,A3,A4,A5,A6,A7,A8,A9,A10,A11,A12,A13,A14,A15,A16,A17,...) A17 #define _ARGC(...) _EXPAND_MSVC(_ARGN(__VA_ARGS__,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1)) #define _EXPAND_MSVC(x) x #define _B(x,...) _EXPAND_MSVC(_ARGN(__VA_ARGS__,B16,_B15,_B14,_B13,_B12,_B11,_B10,_B09,_B08,_B07,_B06,_B05,_B04,_B03,_B02,_B01) (x,__VA_ARGS__)) #define _B01(x,A)                               ( *(uint8_t*)x++ = (uint8_t)A, x) #define _B02(x,A,B)                             ( _B01(x,A),                             _B01(x,B) ) #define _B03(x,A,B,C)                           ( _B02(x,A,B),                           _B01(x,C) ) #define _B04(x,A,B,C,D)                         ( _B03(x,A,B,C),                         _B01(x,D) ) #define _B05(x,A,B,C,D,E)                       ( _B04(x,A,B,C,D),                       _B01(x,E) ) #define _B06(x,A,B,C,D,E,F)                     ( _B05(x,A,B,C,D,E),                     _B01(x,F) ) #define _B07(x,A,B,C,D,E,F,G)                   ( _B06(x,A,B,C,D,E,F),                   _B01(x,G) ) #define _B08(x,A,B,C,D,E,F,G,H)                 ( _B07(x,A,B,C,D,E,F,G),                 _B01(x,H) ) #define _B09(x,A,B,C,D,E,F,G,H,I)               ( _B08(x,A,B,C,D,E,F,G,H),               _B01(x,I) ) #define _B10(x,A,B,C,D,E,F,G,H,I,J)             ( _B09(x,A,B,C,D,E,F,G,H,I),             _B01(x,J) ) #define _B11(x,A,B,C,D,E,F,G,H,I,J,K)           ( _B10(x,A,B,C,D,E,F,G,H,I,J),           _B01(x,K) ) #define _B12(x,A,B,C,D,E,F,G,H,I,J,K,L)         ( _B11(x,A,B,C,D,E,F,G,H,I,J,K),         _B01(x,L) ) #define _B13(x,A,B,C,D,E,F,G,H,I,J,K,L,M)       ( _B12(x,A,B,C,D,E,F,G,H,I,J,K,L),       _B01(x,M) ) #define _B14(x,A,B,C,D,E,F,G,H,I,J,K,L,M,N)     ( _B13(x,A,B,C,D,E,F,G,H,I,J,K,L,M),     _B01(x,N) ) #define _B15(x,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O)   ( _B14(x,A,B,C,D,E,F,G,H,I,J,K,L,M,N),   _B01(x,O) ) #define _B16(x,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) ( _B15(x,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O), _B01(x,P) )  #define _DW(x) x, (x)>>8, (x)>>16, (x)>>24 #define _QW(x) x, (x)>>8, (x)>>16, (x)>>24, (x)>>32, (x)>>40, (x)>>48, (x)>>56

Совсем без макросов в С, пожалуй, не обойтись для перегрузки функций по количеству аргментов, но можно сделать немного красивее и понадеяться на вменяемость компилятора, что он достаточно догадливый, чтобы не дёргать каджый раз memcpy для копирования целого байта.

#define _B(x,...) (memcpy(x, (const uint8_t[]){__VA_ARGS__}, sizeof((uint8_t[]){__VA_ARGS__}) ), (uint8_t*)x += sizeof((uint8_t[]){__VA_ARGS__}))

Не то что бы быстродействие генерации кода обёртки хоть на что-то влияло, но когда для записи единственного байта в память задействуются вызовы функций, где-то грустит один котик, который ещё не знает, что предварительная оптимизация это плохо. Тем не менее msvc справился и даже два идущих подряд макроса

_push_rbx(p); _mov_rbx_rcx(p);

развёрнутые препроцессором в

(memcpy(p, (const uint8_t[]){0x53}, sizeof((uint8_t[]){0x53}) ), (uint8_t*)p += sizeof((uint8_t[]){0x53}));         (memcpy(p, (const uint8_t[]){0x48, 0x89, 0xCB}, sizeof((uint8_t[]){0x48, 0x89, 0xCB}) ), (uint8_t*)p += sizeof((uint8_t[]){0x48, 0x89, 0xCB})); 

были в результате скомпилированы вместе просто в одну единственную инструкцию mov, мелочь а приятно.

00037    c7 01 53 48 89 cb      mov DWORD PTR [rcx], -880195501 ; cb894853H

В результате получилась вот такая замечательная мешанина из С и псевдоассемблера, генерирующая машинный код обёртки для вызова С функции из Lua.

int make_func(char * code, void * func, const char * argt){   int argc = strlen(argt);   int stack = 16 * ((argc + 4)>>1);  //32 bytes of shadow space + space for function arguments multiple of 16 bytes to keep stack alignment   char * p = code;  { //prologue     _push_rbx(p);        //additional push rbx makes stack aligned to 16.     _mov_rbx_rcx(p);         //as we are in luaCfunction: int f(lua_State * L); L is passed in rcx. store lua_State * L in rbx,     _sub_rsp_DW(p,stack);   //reserve stack  //get arguments from lua stack and store them on stack     for (int i = 1; i < argc; i++){  //argt[0] - return type       _mov_rcx_rbx(p);        //first argument lua_State * L        _mov_rdx_DW(p, i+1);  //second argument n+1 - position of argument on lua stack, [1] == self, [2] == first arg, ...       _clr_r8(p);         //some lua_to functions could have 3rd argument, #define lua_tostring(L, i) lua_tolstring(L, i, 0)       _call(p, lua_to(argt[i]));       if (argt[i] == 'f') _mov_xmm0f_xmm0(p);                                                            //double -> float conversion, double lua_tonumber();       if (argt[i] == 'f' || argt[i] == 'd') _st_xmm0(p, 24+i*8); else _st_rax(p, 24+i*8);    //store argument on stack with 32 bytes offset of shadow store     }      _add_rsp_DW(p, 32);    //adjust stack by shadow 32 bytes that were reserved for lua_to functions, so other (4+) arguments on stack with a proper offset  //put back first four arguments to registers     if (argc > 1){ if (argt[1] == 'f' || argt[1] == 'd') _ld_xmm0(p, 0);  else _ld_rcx(p, 0);  }     if (argc > 2){ if (argt[2] == 'f' || argt[2] == 'd') _ld_xmm1(p, 8);  else _ld_rdx(p, 8);  }     if (argc > 3){ if (argt[3] == 'f' || argt[3] == 'd') _ld_xmm2(p, 16); else _ld_r8 (p, 16); }     if (argc > 4){ if (argt[4] == 'f' || argt[4] == 'd') _ld_xmm3(p, 24); else _ld_r9 (p, 24); }      _call(p, func);  //lua_push() function to put return value onto lua stack     _mov_rcx_rbx(p);                                      //first argument lua_State * L      if      (argt[0] == 'f') _mov_xmm1_xmm0f(p);          //convert float to double and mov return value(xmm0) to (xmm1) second argument  of lua_push     else if (argt[0] == 'd') _mov_xmm1_xmm0(p);           //mov double without conversion     else _mov_rdx_rax(p);                                 //other types returned in rax -> second argument in rdx     _clr_r8(p);                                       //some lua_push functions could have 3rd argument     _call(p, lua_push(argt[0])); //return 1     _mov_rax_DW(p, 1);                                    //return 1, even void func() returns 1 and lua_pushnil just for uniformity.     _add_rsp_DW(p, stack-32);                              //restore stack pointer     _pop_rbx(p);                                        //and rbx     _ret(p);   }   return p - code;             //length of generated code }
Вспомогательные функции lua_to / lua_push возвращают указатель на функцию для работы с аргументом соответствующего типа

void * lua_to  (const char t){    switch (t){     case 'v': return voidfunc;        //do nothing: void voidfunc() {}     case 'i': return lua_tointegerx;     case 'f': return lua_tonumberx;     case 'd': return lua_tonumberx;     case 'b': return lua_toboolean;     case 's': return lua_tolstring;     case 'p': return luaF_topointer;  //lua_tointeger || lua_touserdata     case 't': return luaF_totable;     default : return voidfunc;   }   return voidfunc; }

Сгенерированный в памяти кусок кода надо разрешить на исполнение

  DWORD protect;   VirtualProtect(code, size, PAGE_EXECUTE_READWRITE, &protect);

Сам код хранится в Lua как userdata, и чтобы его можно было вызвать как Lua функцию, надо добавить ему метаметод __call, который возьмёт указатель на самого себя из первого аргумента (в метаметоды первым аргументом всегда передаётся сам объект self) и просто вызовет его как функцию.

int __call(lua_State * L){    int (*f)(lua_State*) = lua_touserdata(L, 1);    return f ? f(L) : 0;  }

ммм, Б — безопасный код: «нам тут передали какой-то указатель, а давайте просто перейдём по нему исполнять код», ну хотя бы на 0 проверили.

А в «деструкторe» __gc, в тот момент, когда эту обёртку уже пришел забирать сборщик мусора, можно заодно освободить библиотеку и обратно убрать возможность исполнения с этого куска памяти. И разложить, тем самым, здесь замечательные грабли, так как память выделяет lua_newuserdata как попало, а VirtualProtect оперирует страницами (~4кБ), то, сняв, при уничножении, разрешение на исполнение с одной функции, заодно можно случайно запретить исполнение и другим, пока ещё живым функциям, к их несчастью, попавшим в эти же 4кБ. То есть надо либо самому выделять память в отдельном, разрешённом на исполнение куске (подменять временно аллокатор на свой через lua_setallocf), или выделять по 4кБ странице на каждую функцию, либо городить подсчёт ссылок на отдельные страницы. Или же, придерживаясь ранее принятой концепции безопасности кода, просто оставлять после себя куски памяти с разрешением на исполнение кода.

При загрузке на каждую функцию из библиотеки вызывалась LoadLibrary, тем самым увеличивая её внутренний счётчик, так что вызов FreeLibrary для одной функции не закроет эту библиотеку для других.

Указатель на библиотеку можно хранить в uservalue. К каждым userdata можно прикреплять произвольные значения, которые доступны из С через lua_getiuservalue.

__gc

int __gc(lua_State * L){   lua_getiuservalue(L, 1, 1);   FreeLibrary(lua_touserdata(L, -1));   lua_pop(L, 1);   DWORD protect;   VirtualProtect(lua_touserdata(L, 1), lua_rawlen(L, 1), PAGE_READWRITE, &protect);   return 0; }
добавление метатаблицы из С

  lua_newtable(L, 0, 2);   lua_pushcfunction(L, __gc);   lua_setfield(L, -2, "__gc");   lua_pushcfunction(L, __call);   lua_setfield(L, -2, "__call");   lua_setmetatable(L, -2);

Автоматическое определение типов

Если типы аргументов функции не указываны явно, можно попробовать угадать их исходя из типов аргументов Lua функции. Это будет несколько медленнее, так как придётся уже в сгенерированном коде деграть Lua по вопросам типов аргументов, т.е. во время исполнения, а не во время «компиляции», и полностью переложить на пользователя заботу о типе возвращаемого значения и преобразования его в надлежащий для Lua вид, просто отдав ему оба регистра и целочисленный rax и xmm0 с плавающей запятой, в одном из которых возвращается значение вызываемой функции (в зависимости от типа возвращаемого значения) пусть делает с ними что хочет.

Зато, так как всё равно ничего не известно про типы аргументов, код этой функций не обязательно генерировать на ходу, можно просто написать метаметод __call целиком на человеческом ассемблере. Полностью на С, через функции с переменным количеством аргументов, насколько я понял, не получится, так как там вроде бы всякие непотребства, вроде автоматического преобразования float->double происходят. А частично, с небольшими вкраплениями ассемблера только для запихивания произвольного количества аргументов на стэк перед вызовом функции, тоже не выйдет, так как msvc — компилятор, который уже больше 30 лет пилят, в процессе все возможных улучшений при переходе на 64 бита вдруг внезапно разучился делать inline assembly.

call.asm

extrn lua_pushnumber : proc extrn lua_pushinteger: proc extrn lua_touserdata : proc extrn lua_gettop : proc extrn luaF_isfloat : proc extrn luaF_typeto : proc  .code func__call_auto PROC   push rbx   push r12   push r13   push r14   push r15      sub rsp, 32    mov rbx, rcx               ;first argument lua_State * L passed in rcx, store in rbx    mov rdx, 1   call lua_touserdata   mov r15, [rax]             ;store function pointer in r15    mov rcx, rbx   call lua_gettop              mov r12, rax               ;store number of arguments in r12    shr eax, 1   shl eax, 4   sub rsp, rax               ;reserve stack ((n+1)/2)*16  [1] - self  ;//for (int i = 1; i < argc; i++) arg[i] = arg_to(L, i+1)   xor r13, r13   inc r13 arg_loop:   cmp r13, r12   jge arg_loop_end   inc r13    mov rcx, rbx   mov rdx, r13   call luaF_isfloat   mov r14, rax               ;r14 - arg_isfloat?    mov rcx, rbx   mov rdx, r13   call luaF_typeto                ;get pointer to proper lua_to function     mov rcx, rbx   mov rdx, r13   xor r8, r8   call rax                   ;call lua_to     cmp r14, 0   jz store_int store_float:   movq  QWORD PTR [rsp + r13*8 + 16], xmm0  ;store floating point arg   jmp arg_loop store_int:   mov [rsp + r13*8 + 16], rax               ;store integer arg   jmp arg_loop arg_loop_end:    add rsp, 32  ;load args to registers, both r*x & xmm*, should be faster than checks for floating point / integer type ;and loading all 4 args (garbage if <4) probably also faster than checks for actual number of arguments  ;  mov r13, r12  ;  dec r13 ;  jz call_cfunc   mov rcx, [rsp]   movq xmm0, rcx    ;  dec r13 ;  je call_cfunc   mov rdx, [rsp+8]   movq xmm1, rdx  ;  dec r13 ;  jz call_cfunc   mov r8, [rsp+16]   movq xmm2, r8  ;  dec r13 ;  jz call_cfunc   mov r9, [rsp+24]   movq xmm3, r9  call_cfunc:   call r15    movq r14, xmm0    mov rcx, rbx   mov rdx, rax   call lua_pushinteger         ;first return integer    mov rcx, rbx   movq xmm1, r14   call lua_pushnumber          ;then return floating point    shr r12, 1   shl r12, 4   add rsp, r12                 ;restore stack    pop r15   pop r14   pop r13   pop r12   pop rbx    mov eax, 2   ret func__call_auto ENDP END

Вспомогательная функция arg_to, как и lua_to ранее, возвращает указатель на соответствующую функцию для чтения аргументов с Lua стэка, но исходя из lua_type(L,idx).

Список библиотечных функций

После загрузки .dll через LoadLibrary, её содержимое оказывается в памяти по адресу который LoadLibrary собственно и возвращает. Функция dllfuncname слегка проверяет сингнатуру и тип, и, полазив по заголовкам PE, находит и возвращает указатель на имя функции.

Здесь наглядное описание заголовков PE, но для x64 некоторые поля адресов по 8 байт и, соответственно, смещение EXPORT_DIRECTORY в 136 байт, а не 120 как на картинке.

Очередной замечательный пример с использованием магических констант как делать не надо:

const char * dllfuncname(const uint32_t * lib, int idx){   if (lib == 0) return 0;   if (idx < 0) return 0;   const uint32_t * p = lib;   if ((p[0] & 0xFFFF) != 0x5A4D) return 0; //MZ   p = &lib[p[15] >> 2];                    //e_lfanew   if (p[0] != 0x00004550) return 0;        //signature   if ((p[1] & 0xFFFF) != 0x8664) return 0; //machine   p = &lib[p[34] >> 2];                    //EXPORT_DIRECTORY   if (idx >= p[6]) return 0;               //NumberOfNames   p = &lib[(p[8] >> 2) + idx];             //AddressOfNames[idx]     return &((char*)lib)[*p];               }
Пример использования для вывода списка всех библиотечных функций

void main(int argc, char ** argv){   const char * name = argv[1];   char path[260];   if (SearchPath(NULL, name, ".dll", sizeof(path), path, 0)) name = path;   HMODULE lib = LoadLibrary(name);   int idx = 0;   while (0 != (name = dllfuncname(lib, idx))) printf("%d: %s\n", idx++, name);   FreeLibrary(lib); }

Callback

Для обратного превращения Lua функции в C callback, надо положить на Lua cтэк сначала саму функцию, потом её аргументы, сделать lua_pcall, который заберет со стэка функцию и её аргументы, выполнит её, и положит обратно возвращаемые значения, которые надо забрать с Lua стэка и вернуть в С.

Саму Lua функцию (да и указатель lua_State * L) надо как-то передать внутрь C callback функции и для этого можно воспользоваться LUA_REGISTRYINDEX — глобальной таблицей для хранения ссылок, и потом захаркодить эту ссылку, вместе с lua_State * L, в генерируемый код. Заодно это предотвратит ситуацию, когда саму Lua функцию вдруг случайно забрал сборщик мусора (за ненадобностью), отдельно от пока ещё живой callback обёртки, которая может попытаться её позвать, не зная что самой функции уже нет.

При генерации кода обёртки надо сохранить ссылку на функцию через int funcref = luaL_ref(L, LUA_REGISTRYINDEX), а в самой обёртке во время исполнения достать и положить эту Lua функцию на Lua стэк через lua_rawgeti(L, LUA_REGISTRYINDEX, funcref); И можно добавить для сгенерированного кода (для Lua это по прежнему userdata) метаметод __gc, который при уничножении обёртки заодно почистит ссылки через luaL_unref(L, LUA_REGISTRYINDEX, funcref). И метаметод __call который просто позовёт исходную Lua функцию.
То есть надо сгенерировать код для:

int callback(const char * arg1, double arg2){   lua_rawgeti(L, LUA_REGISTRYINDEX, funcref)   lua_pushstring(L, arg1);   lua_pushnumber(L, arg2);   lua_pcall(L, 2, 1);   return lua_tointeger(L, -1); }
От прямого оборачивания С функции в Lua принципиально не отличается, всё то же самое, только в обратную сторону.

int make_cb(char * code, int size, lua_State * L, int ref, const char * argt){   int argc = strlen(argt);   char * p = code; {     _push_rbx(p);     //push rbx to align stack to 16 and store lua_State * L in rbx     _mov_rbx_QW(p, (uintptr_t)L);   //as we are generating ordinary C function, lua_State * L is passed as argument from ffi and stored as immediate constant in code.      if (argc > 1) if (argt[1] == 'f' || argt[1] == 'd') _st_xmm0(p, 16); else _st_rcx(p, 16);  //store 4 arguments in shadow     if (argc > 2) if (argt[2] == 'f' || argt[2] == 'd') _st_xmm1(p, 24); else _st_rdx(p, 24);     if (argc > 3) if (argt[3] == 'f' || argt[3] == 'd') _st_xmm2(p, 32); else _st_r8 (p, 32);     if (argc > 4) if (argt[4] == 'f' || argt[4] == 'd') _st_xmm3(p, 40); else _st_r9 (p, 40);      _sub_rsp_DW(p, 48); //32 shadow space + additional 8 bytes for 5th argument of lua_callk + 8 bytes to keep 16 bytes alignment      _mov_rcx_rbx(p);     _mov_rdx_DW(p, LUA_REGISTRYINDEX);     _mov_r8_DW(p, ref);        //lua function we are going to wrap (ref) as C callback is stored in lua registry, so it is not garbage collected accidentally     _call(p, lua_rawgeti);   //get lua function on stack from registry lua_rawgeti(L, REIGSTRYINDEX, funcref)      for (int i = 1; i < argc; i++){       _mov_rcx_rbx(p);   //first argument lua_State * L        if (argt[i] == 'f' || argt[i] == 'd') _ld_xmm1(p, 56+i*8); else _ld_rdx(p, 56+i*8);           //get second argument for lua_push from stack       if (argt[i] == 'f') _mov_xmm1_xmm1f(p);                                                           //float -> double conversion for lua_pushnumber       _clr_r8(p);     //some lua_push functions have 3rd argument       _call(p, lua_push(argt[i]));     }      _mov_rcx_rbx(p);     _mov_rdx_DW(p, argc-1);     _mov_r8_DW(p, 1);     _clr_r9(p);     _st_r9(p, 32);     _call(p, lua_callk);   //lua_callk(L, argc-1, 1, 0, 0); lua function always return 1 value, nil for void f() callback      _mov_rcx_rbx(p);        //first argument lua_State * L      _mov_rdx_DW(p, -1);   //return value is on top of the lua stack after lua_call     _clr_r8(p);         //some functions have 3rd argument, lua_tostring == lua_tolstring(L, idx, 0);     _call(p, lua_to(argt[0]));      if (argt[0] == 'f' || argt[0] == 'd') _st_xmm0(p, 32); else _st_rax(p, 32); //store return value on stack to call lua_pop(L, 1)      _mov_rcx_rbx(p);     _mov_rdx_DW(p, -2);     _call(p, lua_settop); //lua_pop(L, 1) == lua_settop(L, -2);      if (argt[0] == 'f' || argt[0] == 'd') _ld_xmm0(p, 32); else _ld_rax(p, 32); //return value in rax/xmm0      _add_rsp_DW(p, 48);    //restore stack     _pop_rbx(p);        //and rbx     _ret(p);   }   return p - code; }

Заключение

В результате получилась небольшая библиотека без зависимостей, которая если и не полностью помогает избавиться от дополнительного кода обёрток для вызова внешних библиотек в Lua, то хотя бы позволяет писать его непосредственно на Lua.

github.com/pavel212/uffi


ссылка на оригинал статьи https://habr.com/ru/articles/746658/