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>
This commit is contained in:
Ronald Caesar
2026-06-14 02:58:20 -04:00
parent 80706ef77b
commit 28129be379
6 changed files with 350 additions and 31 deletions
+4 -1
View File
@@ -215,6 +215,10 @@ endif ()
# Compile Tools
# -----------------------------------------------------------------------------
message(STATUS "[Ballistic] Configuring developer tools...")
set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE)
list(APPEND CMAKE_BUILD_RPATH "$ORIGIN/../../lib/${BALLISTIC_PLATFORM}")
add_external_subdirectory(extern/LuaJIT-2.1)
set(DECODER_CLI_NAME "decoder_cli")
add_executable(${DECODER_CLI_NAME} tools/decoder_cli.c)
target_link_libraries(${DECODER_CLI_NAME} PRIVATE ${PROJECT_NAME})
@@ -333,7 +337,6 @@ if (BALLISTIC_BUILD_TESTS)
gtest_discover_tests(${target_name})
endforeach ()
add_external_subdirectory(extern/LuaJIT-2.1)
add_test(
NAME doctest
Vendored
+1 -1
+15 -2
View File
@@ -7,6 +7,20 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(FFI_GENERATOR_SCRIPT ${CMAKE_SOURCE_DIR}/tools/generate_ffi.lua)
set(DASHBOARD_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/dashboard_file_dialog.h)
set(GENERATED_FFI_BINDINGS ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generated/dashboard_file_dialog_ffi.lua)
add_custom_command(
OUTPUT ${GENERATED_FFI_BINDINGS}
COMMAND $<TARGET_FILE:luajit_exe> "${FFI_GENERATOR_SCRIPT}" "${DASHBOARD_HEADER}" "${GENERATED_FFI_BINDINGS}"
DEPENDS ${FFI_GENEERATOR_SCRIPT} ${DASHBOARD_HEADER} luajit_exe
COMMENT " [Dashboard] Generating FFI bindings from hashboard_file_dialog.h"
WORKING_DIRECTORY ${BALLISTIC_LIBRARIES_DIRECTORY}
VERBATIM
)
add_custom_target(dashboard_ffi_bindings ALL DEPENDS ${GENERATED_FFI_BINDINGS})
add_executable(${PROJECT_NAME}
main.c
dashboard_backend.cpp
@@ -32,9 +46,8 @@ target_include_directories(${PROJECT_NAME} PRIVATE
${glfw_SOURCE_DIR}/include
)
# TODO: find_package(OpenGL) for macOS.
add_dependencies(${PROJECT_NAME} dashboard_ffi_bindings)
target_link_libraries(${PROJECT_NAME} PRIVATE Ballistic glfw cimgui_glfw LuaJIT)
set_target_properties(${PROJECT_NAME} PROPERTIES ENABLE_EXPORTS TRUE)
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+103 -27
View File
@@ -6,35 +6,111 @@ package.path = package.path .. ";" .. script_dir .. "?.lua"
local ffi = require("ffi")
local imgui = require("imgui.glfw")
local dialog_ffi = require("generated.dashboard_file_dialog_ffi")
local dashboard = dialog_ffi.Dashboard
ffi.cdef [[
typedef struct
{
char name[248];
uint32_t is_directory;
uint32_t pad;
} bal_file_entry_t;
local function dashboard_render_file_dialog(p_file_dialog)
if p_file_dialog == nil then
return false
end
typedef struct
{
bal_file_entry_t *file_entries;
uint32_t file_entries_count;
uint32_t file_entries_capacity;
bool is_open;
bool just_opened;
char current_path[1024];
char selected_path[1024];
uint8_t pad[46];
} bal_file_dialog_t;
local dialog = ffi.cast("bal_file_dialog_t*", p_file_dialog)
void dashboard_file_dialog_open(bal_file_dialog_t *dialog);
void dashboard_file_dialog_refresh(bal_file_dialog_t *dialog);
void dashboard_file_dialog_append_path(bal_file_dialog_t *dialog, const char *name);
void dashboard_file_dialog_navigate_home(bal_file_dialog_t *dialog);
const char *dashboard_file_dialog_get_current_path(const bal_file_dialog_t *dialog);
]]
if dialog.just_opened then
imgui.OpenPopup("Select Directory")
dialog.just_opened = false
end
local C = ffi.C
local result = false
local is_open = ffi.new("bool[1]", dialog.is_open)
imgui.SetNextWindowSize(imgui.ImVec2(600, 400), imgui.lib.ImGuiCond_Appearing)
if imgui.BeginPopupModal("Select Directory", is_open, imgui.lib.ImGuiWindowFlags_NoCollapse) then
dialog.is_open = is_open[0]
imgui.TextUnformatted("Path:")
imgui.SameLine()
local back_button_width = imgui.CalcTextSize("Back").x + (imgui.GetStyle().FramePadding.x * 2.0)
local ballistic_button_width = imgui.CalcTextSize("*").x + (imgui.GetStyle().FramePadding.x * 2.0)
local spacing = imgui.GetStyle().ItemSpacing.x
local text_width = imgui.GetContentRegionAvail().x - back_button_width - ballistic_button_width - (spacing * 2)
imgui.SetNextItemWidth(text_width)
imgui.PushStyleColor(imgui.lib.ImGuiCol_ChildBg, imgui.GetStyleColorVec4(imgui.lib.ImGuiCol_FrameBg)[1])
imgui.PushStyleVar(imgui.lib.ImGuiStyleVar_ChildRounding, imgui.GetStyle().FrameRounding)
local input_flags = bit.bor(imgui.lib.ImGuiInputTextFlags_EnterReturnsTrue, imgui.lib
.ImGuiInputTextFlags_AutoSelectAll)
if imgui.InputText("##path_input", dialog.current_path, 1024, input_flags) then
imgui.SetKeyboardFocusHere(-1)
dashboard.dashboard_file_dialog_refresh(dialog)
end
imgui.SameLine()
if imgui.Button("*") then
dashboard.dashboard_file_dialog_navigate_home(dialog)
end
imgui.SameLine()
if imgui.Button("Back") then
dashboard.dashboard_file_dialog_append_path(dialog, "..")
dashboard.dashboard_file_dialog_refresh(dialog)
end
imgui.PopStyleColor()
imgui.PopStyleVar()
imgui.Separator()
local child_height = -imgui.GetFrameHeightWithSpacing() - 10
imgui.BeginChild("##dir_list", imgui.ImVec2(0, child_height), true)
for i = 0, dialog.file_entries_count - 1 do
local entry = dialog.file_entries[i]
imgui.PushStyleVar(imgui.lib.ImGuiStyleVar_SelectableTextAlign, imgui.ImVec2(0, 0.5))
imgui.PushID(i)
local name = ffi.string(entry.name)
if name == ".." then
if imgui.Selectable("[..]", false, 0, imgui.ImVec2(0, 20)) then
dashboard.dashboard_file_dialog_append_path(dialog, "..")
dashboard.dashboard_file_dialog_refresh(dialog)
end
else
if imgui.Selectable(name, false, imgui.lib.ImGuiSelectableFlags_AllowDoubleClick, imgui.ImVec2(0, 20))
then
if imgui.IsMouseDoubleClicked(imgui.lib.ImGuiMouseButton_Left) then
dashboard.dashboard_file_dialog_append_path(dialog, name)
dashboard.dashboard_file_dialog_refresh(dialog)
end
end
end
imgui.PopID()
imgui.PopStyleVar()
end
imgui.EndChild()
if imgui.Button("Select Current Directory") then
ffi.copy(dialog.selected_path, dialog.current_path, 1024)
result = true
dialog.is_open = false
imgui.CloseCurrentPopup()
end
imgui.SameLine()
if imgui.Button("Cancel") then
dialog.is_open = false
imgui.CloseCurrentPopup()
end
imgui.EndPopup()
else
dialog.is_open = is_open[0]
end
return result
end
local function dashboard_render_file_dialog(p_file_dialog)
if p_file_dialog == nil then
@@ -167,14 +243,14 @@ function dashboard_render(host_context, p_file_dialog)
imgui.PushStyleVar(imgui.lib.ImGuiStyleVar_FrameRounding, 8.0)
if imgui.Button("Generate Minimal Working Example", imgui.ImVec2(button_width, button_height)) then
C.dashboard_file_dialog_open(p_file_dialog)
dashboard.dashboard_file_dialog_open(p_file_dialog)
end
imgui.PopStyleColor(3)
imgui.PopStyleVar(1)
if dashboard_render_file_dialog(p_file_dialog) then
local selected_path = ffi.string(C.dashboard_file_dialog_get_current_path(p_file_dialog))
local selected_path = ffi.string(dashboard.dashboard_file_dialog_get_current_path(p_file_dialog))
print("Directory chosen: " .. selected_path)
end
@@ -0,0 +1,62 @@
-- Auto-generated FFI bindings. DO NOT EDIT.
-- Source: /home/apollo/dev/ballistic/tools/dashboard/dashboard_file_dialog.h
-- Generated by tools/generate_ffi.lua
local ffi = require('ffi')
ffi.cdef [[
typedef struct { char name[248]; uint32_t is_directory; uint32_t pad; } bal_file_entry_t;
typedef struct { bal_file_entry_t *file_entries; uint32_t file_entries_count; uint32_t file_entries_capacity; bool is_open; bool just_opened; char current_path[1024]; char selected_path[1024]; uint8_t pad[46]; } bal_file_dialog_t;
void dashboard_file_dialog_init(bal_file_dialog_t *dialog);
void dashboard_file_dialog_shutdown(bal_file_dialog_t *dialog);
void dashboard_file_dialog_open(bal_file_dialog_t *dialog);
void dashboard_file_dialog_refresh(bal_file_dialog_t *dialog);
void dashboard_file_dialog_append_path(bal_file_dialog_t *dialog, const char *name);
void dashboard_file_dialog_navigate_home(bal_file_dialog_t *dialog);
const char *dashboard_file_dialog_get_current_path(const bal_file_dialog_t *dialog);
]]
local M = {}
local C = ffi.C
local bal_file_entry_t_mt = { __index = {} }
bal_file_entry_t_mt.__index.__type = 'bal_file_entry_t'
ffi.metatype('bal_file_entry_t', bal_file_entry_t_mt)
local bal_file_dialog_t_mt = { __index = {} }
bal_file_dialog_t_mt.__index.__type = 'bal_file_dialog_t'
ffi.metatype('bal_file_dialog_t', bal_file_dialog_t_mt)
M.Dashboard = M.Dashboard or {}
function M.Dashboard.dashboard_file_dialog_init(dialog)
return C.dashboard_file_dialog_init(dialog)
end
M.Dashboard = M.Dashboard or {}
function M.Dashboard.dashboard_file_dialog_shutdown(dialog)
return C.dashboard_file_dialog_shutdown(dialog)
end
M.Dashboard = M.Dashboard or {}
function M.Dashboard.dashboard_file_dialog_open(dialog)
return C.dashboard_file_dialog_open(dialog)
end
M.Dashboard = M.Dashboard or {}
function M.Dashboard.dashboard_file_dialog_refresh(dialog)
return C.dashboard_file_dialog_refresh(dialog)
end
M.Dashboard = M.Dashboard or {}
function M.Dashboard.dashboard_file_dialog_append_path(dialog, name)
return C.dashboard_file_dialog_append_path(dialog, name)
end
M.Dashboard = M.Dashboard or {}
function M.Dashboard.dashboard_file_dialog_navigate_home(dialog)
return C.dashboard_file_dialog_navigate_home(dialog)
end
return M
+165
View File
@@ -0,0 +1,165 @@
--[[
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))