Как определить функции C с помощью LuaJIT?

Этот:

local ffi = require "ffi"

ffi.cdef[[
  int return_one_two_four(){
    return 124;
  }
]]

local function print124()
  print(ffi.C.return_one_two_four())
end

print124()

Выдает ошибку:

Error: main.lua:10: cannot resolve symbol 'return_one_two_four': The specified procedure could not be found.

У меня есть своего рода умеренное понимание C, и я хотел использовать некоторые из его хороших сторон для нескольких вещей, но я не смог найти много примеров в библиотеке FFI LuaJIT. Похоже, что cdef используется только для объявлений функций, а не для определений. Как я могу создавать функции на C, а затем использовать их в Lua?


person EpichinoM2    schedule 16.12.2018    source источник
comment
Вы должны написать их на C и скомпилировать как разделяемые библиотеки (которые затем загружаете через ffi.load), вы не можете написать их напрямую на Lua.   -  person UnholySheep    schedule 16.12.2018
comment
Есть один обходной путь — писать функции на машинном коде, а не на C, но это работает только в том случае, если вы используете Windows (x64 или x86), а ваш LuaJIT - x86 и построен с помощью MinGW.   -  person Egor Skriptunoff    schedule 17.12.2018


Ответы (3)


LuaJIT — это компилятор Lua, но не компилятор C. Сначала вы должны скомпилировать свой код C в разделяемую библиотеку. Например с

gcc -shared -fPIC -o libtest.so test.c
luajit test.lua

с файлами test.c и test.lua, как показано ниже.

test.c

int return_one_two_four(){
    return 124;
}

test.lua

local ffi = require"ffi"

local ltest = ffi.load"./libtest.so"

ffi.cdef[[
int return_one_two_four();
]]

local function print124()
    print(ltest.return_one_two_four())
end

print124()

Живой пример на Wandbox

JIT в LuaJIT

В комментариях под вопросом кто-то упомянул обходной путь для написания функций в машинном коде и их выполнения в LuaJIT в Windows. На самом деле, то же самое возможно в Linux, по существу реализуя JIT внутри LuaJIT. В то время как в Windows вы можете просто вставить код операции в строку, привести ее к указателю функции и вызвать ее, в Linux это невозможно из-за ограничений страницы. В Linux память доступна либо для записи, либо для выполнения, но никогда не для того и другого одновременно, поэтому мы должны выделить страницу в режиме чтения-записи, вставить сборку, а затем изменить режим на чтение-выполнение. Для этого просто используйте функции ядра Linux, чтобы получить размер страницы и отображенную память. Однако, если вы сделаете даже малейшую ошибку, например, опечатку в одном из опкодов, программа выдаст segfault. Я использую 64-битную сборку, потому что использую 64-битную операционную систему.

Важно! Прежде чем выполнять это на своем компьютере, проверьте магические числа в <bits/mman-linux.h>. Они не одинаковы в каждой системе.

local ffi = require"ffi"

ffi.cdef[[
typedef unsigned char uint8_t;
typedef long int off_t;

// from <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
int munmap(void *addr, size_t length);
int mprotect(void *addr, size_t len, int prot);

// from <unistd.h>
int getpagesize(void);
]]

-- magic numbers from <bits/mman-linux.h>
local PROT_READ     = 0x1  -- Page can be read.
local PROT_WRITE    = 0x2  -- Page can be written.
local PROT_EXEC     = 0x4  -- Page can be executed.
local MAP_PRIVATE   = 0x02 -- Changes are private.
local MAP_ANONYMOUS = 0x20 -- Don't use a file.

local page_size = ffi.C.getpagesize()
local prot = bit.bor(PROT_READ, PROT_WRITE)
local flags = bit.bor(MAP_ANONYMOUS, MAP_PRIVATE)
local code = ffi.new("uint8_t *", ffi.C.mmap(ffi.NULL, page_size, prot, flags, -1, 0))

local count = 0
local asmins = function(...)
    for _,v in ipairs{ ... } do
        assert(count < page_size)
        code[count] = v
        count = count + 1
    end
end

asmins(0xb8, 0x7c, 0x00, 0x00, 0x00) -- mov rax, 124
asmins(0xc3) -- ret

ffi.C.mprotect(code, page_size, bit.bor(PROT_READ, PROT_EXEC))

local fun = ffi.cast("int(*)(void)", code)
print(fun())

ffi.C.munmap(code, page_size)

Живой пример на Wandbox

Как найти коды операций

Я вижу, что этот ответ вызвал некоторый интерес, поэтому я хочу добавить то, с чем мне сначала было трудно, а именно, как найти коды операций для инструкций, которые вы хотите выполнить. В Интернете есть несколько ресурсов, в первую очередь Руководства по архитектуре Intel® 64 и IA-32 для разработчиков программного обеспечения. но никто не хочет просматривать тысячи страниц PDF только для того, чтобы узнать, как сделать mov rax, 124. Поэтому некоторые люди сделали таблицы, в которых перечислены инструкции и соответствующие коды операций, например. http://ref.x86asm.net/, но поиск кодов операций в таблице также затруднен, потому что даже mov может иметь много разных кодов операций в зависимости от того, что является целевым и исходным операндами. Вместо этого я пишу короткий файл сборки, например

mov rax, 124
ret

Вы можете задаться вопросом, почему в моем файле сборки нет ни функций, ни чего-то вроде segment .text. Что ж, поскольку я не хочу когда-либо ссылаться на это, я могу просто пропустить все это и немного напечатать. Затем просто соберите его, используя

$ nasm -felf64 -l test.lst test.s

Опция -felf64 говорит ассемблеру, что я использую 64-битный синтаксис, опция -l test.lst, что я хочу иметь листинг сгенерированного кода в файле test.lst. Листинг выглядит примерно так:

$ cat test.lst
     1 00000000 B87C000000              mov rax, 124
     2 00000005 C3                      ret

Третий столбец содержит интересующие меня коды операций. Просто разделите их на единицы по 1 байту и вставьте их в свою программу, т. е. B87C000000 станет 0xb8, 0x7c, 0x00, 0x00, 0x00 (к счастью, шестнадцатеричные числа в Lua нечувствительны к регистру, и мне больше нравятся строчные).

person Henri Menke    schedule 16.12.2018

LuaJIT включает распознаватель объявлений C, но не является полноценным компилятором C. Цель его системы FFI — иметь возможность определить, какие функции C экспортирует конкретная DLL, чтобы она могла загрузить эту DLL (через ffi.load) и позволить вам вызывать эти функции из Lua.

LuaJIT может загружать предварительно скомпилированный код через DLL-интерфейс на основе C, но не может компилировать C самостоятельно.

person Nicol Bolas    schedule 16.12.2018

Технически вы можете делать то, что хотите, без особых проблем (при условии, что код достаточно прост). Используя что-то вроде этого: https://github.com/nucular/tcclua с tcc (что очень маленький, и вы даже можете легко развернуть его) это довольно хороший способ получить лучшее из обоих миров, все в одном пакете :)

person David Lannan    schedule 21.06.2020