mirror of
https://github.com/pound-emu/ballistic.git
synced 2026-06-17 04:16:48 -07:00
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:
+4
-1
@@ -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
Submodule extern/glfw updated: b00e6a8a88...567b1ec244
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user