Под катом описание довольно примитивного FFI для Lua под Win_x64.

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

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/releases/tag/0.1a

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

Комментарии (6)


  1. vilgeforce
    16.08.2023 11:47
    +1

    " пример с использованием магических констант как делать не надо " - ну и не только в константах магических дело: если бы вы вместо чисел использовали именованные контстанты типа E_LFANEW это тоже было бы не очень, потому что нужно учитывать поле SizeOfOptionalHeader


    1. pvvv Автор
      16.08.2023 11:47
      +1

      ну да, надо бы конечно,

      а этот опциональный заголовок он вообще может быть разного размера? ну кроме 0 и 176 байт, когда из него EXPORT_DIRECTORY и берётся


      1. vilgeforce
        16.08.2023 11:47

        Блин, перепутал с секциями, позор мне! :-(
        Можно фиксированные смещения использовать. Это вот с секциями нельзя, а директориями можно


  1. starfair
    16.08.2023 11:47

    Спасибо за труд! Было бы отлично, если бы подобная либа была и под библиотеки на Linux!


    1. pvvv Автор
      16.08.2023 11:47
      +2

      Так-то есть ffi от luajit, и alien который lua обёртка для libffi, вполне себе кроссплатформенные.

      Я слишком туп оказался чтобы их под виндами с lua54 собрать, пришлось пилить своё :)

      Там "платформеннозависимого" кода не сильно много (собственно всё вынесено в win64.c), только пара функций, которые ассембленый код с учётом calling convention генерят, в них только порядок запихивания аргументов в стэк/регистры поменять, а остальное dlsym/GetProcAddress и mprotect/VirtualProtect только названиями отличаются, может когда-нибудь руки дойдут.


      1. starfair
        16.08.2023 11:47

        Ок. Спасибо ещё раз за пояснения! Мне пока это не очень актуально, но скоро очень даже может пригодиться!