Files
Ronald Caesar 28129be379 dashboard: generate lua ffi bindings for file dialog
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>
2026-06-14 03:57:39 -04:00

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))