mirror of
https://github.com/pound-emu/ballistic.git
synced 2026-06-17 04:16:48 -07:00
28129be379
I've added `tools/generate_ffi.lua` which generates lua ffi bindings to C for all structs in a header file and functions in that have the `BAL_EXPORT` keyword. Right now only `tools/dashboard/dashboard_file_dialog.h` have ffi bindings generated for it and saved to `tools/dashboard/scripts/generated/dashboard_file_dialog_ffi.lua`. The script only handles one header file at a time. Later on I will need to add support for recursively scanning all header files in a folder with options to exclude certain folders. Signed-off-by: Ronald Caesar <github43132@proton.me>
165 lines
5.0 KiB
Lua
165 lines
5.0 KiB
Lua
--[[
|
|
C-to-LuaJIT FFI Bindings Generator
|
|
|
|
Usage: luajit generate_ffi.lua <header.h> <output.lua>
|
|
]]
|
|
|
|
|
|
|
|
local ffi = require("ffi")
|
|
|
|
local args = { ... }
|
|
|
|
if #args < 2 then
|
|
io.stderr:write("Usage: luajit generate_ffi.lua <header.h> <output.lua>\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
local header_path = args[1]
|
|
local output_path = args[2]
|
|
local file = assert(io.open(header_path, "r"), "Failed to open header: " .. header_path)
|
|
local header_content = file:read("*a")
|
|
file:close()
|
|
|
|
local defines = {}
|
|
|
|
for name, value in header_content:gmatch("#define%s+([%w_]+)%s+([%w_]+)") do
|
|
if tonumber(value) or defines[value] then
|
|
defines[name] = value
|
|
end
|
|
end
|
|
|
|
-- Resolve nested defines.
|
|
local changed = true
|
|
|
|
while changed do
|
|
changed = false
|
|
|
|
for k, v in pairs(defines) do
|
|
if not tonumber(v) and defines[v] then
|
|
defines[k] = defines[v]
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local function clean_for_ffi(str)
|
|
for name, value in pairs(defines) do
|
|
str = str:gsub("%f[%w_]" .. name .. "%f[^%w_]", value)
|
|
end
|
|
|
|
str = str:gsub("BAL_ALIGNED%s*%b()", "")
|
|
str = str:gsub("BAL_EXPORT", "")
|
|
str = str:gsub("BAL_RESTRICT", "")
|
|
str = str:gsub("BAL_HOT", "")
|
|
str = str:gsub("BAL_COLD", "")
|
|
str = str:gsub("static_assert%s*%b();?", "")
|
|
str = str:gsub("_Static_assert%s*%b();?", "")
|
|
str = str:gsub("%s+", " ")
|
|
str = str:gsub("^%s+", ""):gsub("%s+$", "")
|
|
return str
|
|
end
|
|
|
|
local exported_functions = {}
|
|
|
|
for function_declaration in header_content:gmatch("BAL_EXPORT%s+([^;]+;)") do
|
|
table.insert(exported_functions, clean_for_ffi(function_declaration))
|
|
end
|
|
|
|
local clean_header = header_content
|
|
clean_header = clean_header:gsub("BAL_ALIGNED%s*%b()", "")
|
|
clean_header = clean_header:gsub("static_assert%s*%b();?", "")
|
|
clean_header = clean_header:gsub("_Static_assert%s*%b();?", "")
|
|
|
|
local struct_definitions = {}
|
|
|
|
for struct_block in clean_header:gmatch("(typedef%s+struct[%s%w_]*%b{}[%s%w_]*;)") do
|
|
table.insert(struct_definitions, clean_for_ffi(struct_block))
|
|
end
|
|
|
|
local validation_buffer = {}
|
|
|
|
for _, struct in ipairs(struct_definitions) do
|
|
table.insert(validation_buffer, struct)
|
|
end
|
|
|
|
for _, fn in ipairs(exported_functions) do
|
|
table.insert(validation_buffer, fn)
|
|
end
|
|
|
|
local validation_string = table.concat(validation_buffer, "\n")
|
|
|
|
if validation_string == "" then
|
|
io.stderr:write("Warning: No structs or functions extracted from " .. header_path .. "\n")
|
|
end
|
|
|
|
local ok, err = pcall(ffi.cdef, validation_string)
|
|
if not ok then
|
|
io.stderr:write("FFI validation failed for " .. header_path .. ": " .. tostring(err) .. "\n")
|
|
io.stderr:write("--- Generated C Definitions ---\n")
|
|
io.stderr:write(tostring(validation_string) .. "\n")
|
|
io.stderr:write("-------------------------------\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
local out = assert(io.open(output_path, "w"), "Failed to open output: " .. output_path)
|
|
out:write("-- Auto-generated FFI bindings. DO NOT EDIT.\n")
|
|
out:write("-- Source: " .. header_path .. "\n")
|
|
out:write("-- Generated by tools/generate_ffi.lua\n\n")
|
|
out:write("local ffi = require('ffi')\n\n")
|
|
out:write("ffi.cdef [[\n")
|
|
|
|
for _, struct in ipairs(struct_definitions) do
|
|
out:write(struct .. "\n\n")
|
|
end
|
|
|
|
for _, fn in ipairs(exported_functions) do
|
|
out:write(fn .. "\n")
|
|
end
|
|
|
|
out:write("]]\n\n")
|
|
out:write("local M = {}\n")
|
|
out:write("local C = ffi.C\n\n")
|
|
|
|
for _, struct in ipairs(struct_definitions) do
|
|
local type_name = struct:match("}%s*([%w_]+)%s*;")
|
|
|
|
if type_name then
|
|
out:write(string.format("local %s_mt = { __index = {} }\n", type_name))
|
|
out:write(string.format("%s_mt.__index.__type = '%s'\n", type_name, type_name))
|
|
out:write(string.format("ffi.metatype('%s', %s_mt)\n\n", type_name, type_name))
|
|
end
|
|
end
|
|
|
|
for _, fn in ipairs(exported_functions) do
|
|
local return_type, function_name, parameters = fn:match("^(%S+)%s+([%w_]+)%s*%((.*)%)%s*;")
|
|
|
|
if function_name then
|
|
local namespace = function_name:match("^([%w_]-)_[%w_]+$") or "Global"
|
|
local pascal_namespace = namespace:gsub("_(%w)", string.upper):gsub("^(%w)", string.upper)
|
|
out:write(string.format("M.%s = M.%s or {}\n", pascal_namespace, pascal_namespace))
|
|
out:write(string.format("function M.%s.%s(", pascal_namespace, function_name))
|
|
|
|
local parameter_names = {}
|
|
|
|
if parameters and parameters ~= "void" and parameters ~= "" then
|
|
for parameter in parameters:gmatch("[^,]+") do
|
|
local name = parameter:match("([%w_]+)%s*$") or parameter:match("%*([%w_]+)")
|
|
|
|
if name then
|
|
table.insert(parameter_names, name)
|
|
end
|
|
end
|
|
end
|
|
|
|
out:write(table.concat(parameter_names, ", ") .. ")\n")
|
|
out:write(string.format(" return C.%s(%s)\n", function_name, table.concat(parameter_names, ", ")))
|
|
out:write("end\n\n")
|
|
end
|
|
end
|
|
|
|
out:write("return M\n")
|
|
out:close()
|
|
|
|
io.stdout:write(string.format("[generate_ffi] Generated %d structs, %d functions -> %s\n", #struct_definitions,
|
|
#exported_functions, output_path)) |