local codegen = require "codegen" local idl = codegen.idl "bgfx.idl" local java_package = "io.github.bkaradzic.bgfx" local java_imports = [[ import java.lang.AutoCloseable; import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; import java.lang.foreign.SegmentAllocator; import java.lang.foreign.StructLayout; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; import java.lang.invoke.VarHandle; import java.nio.file.Path; import java.util.Objects; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; import static ]] .. java_package .. [[.BgfxUtil.*; ]] local java_header = [[ // Copyright 2011-2026 Branimir Karadzic. All rights reserved. // License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE // // AUTO GENERATED! DO NOT EDIT! // ]] local java_template = java_header .. [[ package ]] .. java_package .. [[; ]] .. java_imports .. [[ /** * Java FFM bindings for the bgfx C99 API. */ @NullUnmarked @SuppressWarnings("restricted") public final class Bgfx { private Bgfx() { } $native $funcs $types } ]] local function hasSuffix(str, suffix) return suffix == "" or str:sub(-#suffix) == suffix end local function gisub(s, pat, repl, n) pat = string.gsub(pat, '(%a)', function(v) return '[' .. string.upper(v) .. string.lower(v) .. ']' end) if n then return string.gsub(s, pat, repl, n) else return string.gsub(s, pat, repl) end end local primitive_types = { bool = { java = "boolean", plain = "boolean", layout = "ValueLayout.JAVA_BOOLEAN" }, char = { java = "byte", plain = "byte", layout = "ValueLayout.JAVA_BYTE" }, float = { java = "float", plain = "float", layout = "ValueLayout.JAVA_FLOAT" }, int8_t = { java = "byte", plain = "byte", layout = "ValueLayout.JAVA_BYTE" }, int16_t = { java = "short", plain = "short", layout = "ValueLayout.JAVA_SHORT" }, int32_t = { java = "int", plain = "int", layout = "ValueLayout.JAVA_INT" }, int64_t = { java = "long", plain = "long", layout = "ValueLayout.JAVA_LONG", }, uint8_t = { java = "@Unsigned byte", plain = "byte", layout = "ValueLayout.JAVA_BYTE", unsigned = true }, uint16_t = { java = "@Unsigned short", plain = "short", layout = "ValueLayout.JAVA_SHORT", unsigned = true }, uint32_t = { java = "@Unsigned int", plain = "int", layout = "ValueLayout.JAVA_INT", unsigned = true }, uint64_t = { java = "@Unsigned long", plain = "long", layout = "ValueLayout.JAVA_LONG", unsigned = true }, uintptr_t = { java = "@Unsigned long", plain = "long", layout = "C_UINTPTR_T", uintptr = true, unsigned = true }, bgfx_view_id_t = { java = "short", plain = "short", layout = "ValueLayout.JAVA_SHORT" }, void = { java = "void", plain = "void" }, va_list = { java = "MemorySegment", plain = "MemorySegment", layout = "ValueLayout.ADDRESS", opaque = true }, } local ctype_info = {} local enum_counts = {} local tagged_handle_members = {} local generation_mode = "single" local function java_type_name(typ) if typ.enum then return typ.typename elseif typ.namespace then return (generation_mode == "single" and "Bgfx." or "") .. typ.namespace .. "." .. typ.name else return typ.name:gsub("::Enum$", "") end end for _, typ in ipairs(idl.types) do if typ.cname then local kind if typ.enum then kind = "enum" enum_counts[typ.typename] = #typ.enum elseif typ.handle then kind = "handle" elseif typ.struct then kind = "struct" elseif typ.args and typ.ret then kind = "funcptr" else kind = "opaque" end if kind ~= "opaque" or primitive_types[typ.cname] == nil then ctype_info[typ.cname] = { kind = kind, java = java_type_name(typ), typ = typ, } end end end for _, typ in ipairs(idl.types) do if typ.handle and typ.tagged then for index, source in ipairs(typ.tagged) do assert(tagged_handle_members[source] == nil, "Handle belongs to more than one tagged handle: " .. source) tagged_handle_members[source] = { parent = typ, tag = index - 1, } end end end local function normalize_ctype(ctype) ctype = ctype:gsub("%s+", " ") ctype = ctype:gsub("%s*%*%s*", "*") ctype = ctype:match("^%s*(.-)%s*$") local is_const = ctype:match("^const ") ~= nil ctype = ctype:gsub("^const%s+", "") local pointers = 0 while hasSuffix(ctype, "*") do pointers = pointers + 1 ctype = ctype:sub(1, -2) end return { base = ctype, pointers = pointers, is_const = is_const, } end local function type_details(arg, array_as_pointer) local result = normalize_ctype(arg.ctype) if arg.array and array_as_pointer then result.pointers = result.pointers + 1 end result.info = ctype_info[result.base] result.primitive = primitive_types[result.base] return result end local function java_type(arg, context) if arg.ctype == "..." then return "Object..." end local details = type_details(arg, context ~= "member") if details.pointers > 0 then if context ~= "member" and details.base == "char" and details.pointers == 1 and details.is_const then return context == "return" and "@Nullable String" or "String" elseif details.pointers == 1 and details.info and details.info.kind == "struct" then return details.info.java end return "MemorySegment" end if details.info then if details.info.kind == "funcptr" or details.info.kind == "opaque" then return "MemorySegment" end return details.info.java end return assert(details.primitive, "Unsupported C type: " .. arg.ctype).java end local function java_parameter_type(arg) if arg.ctype == "uint8_t" and arg.default == "BGFX_DISCARD_ALL" then return "@Unsigned int" end local java = java_type(arg, "arg") if arg.default == "NULL" then return "@Nullable " .. java end return java end local function layout_type(arg, array_as_pointer) local details = type_details(arg, array_as_pointer) if details.pointers > 0 then return "ValueLayout.ADDRESS" end if details.info then if details.info.kind == "enum" then return "ValueLayout.JAVA_INT" elseif details.info.kind == "funcptr" or details.info.kind == "opaque" then return "ValueLayout.ADDRESS" end return details.info.java .. ".LAYOUT" end return assert(details.primitive, "Unsupported C layout type: " .. arg.ctype).layout end local function array_length(member) local number = member.array:match("^%[%s*(%d+)%s*%]$") if number then return tonumber(number) end local enum_name, enum_item = member.array:match("^%[%s*([%w_]+)::([%w_]+)%s*%]$") assert(enum_name and enum_item == "Count", "Unsupported array expression: " .. member.array) return assert(enum_counts[enum_name], "Unknown enum in array expression: " .. enum_name) end local function member_layout(member) local layout = layout_type(member, false) if member.array then layout = string.format("MemoryLayout.sequenceLayout(%d, %s)", array_length(member), layout) end return layout .. ".withName(\"" .. member.name .. "\")" end local function camel_name(cname) local name = cname:gsub("_(.)", cname.upper) return name:gsub("%dd", name.upper) end local function method_name(func) local name = gisub(camel_name(func.cname), func.this_type.type, "") return name:gsub("^%L", string.lower) end local converter = {} local yield = coroutine.yield local gen = {} local combined local downcall_entries local variadic_entries local function reset_generator_state(mode) generation_mode = mode for _, info in pairs(ctype_info) do info.java = java_type_name(info.typ) end for _, name in ipairs(combined) do combined[name] = {} end downcall_entries = {} variadic_entries = {} end local function collect_methods() -- find the functions that have `this` first argument -- these belong to a type (struct) and we need to add them when converting structures local methods = {} for _, func in ipairs(idl["funcs"]) do if func.this ~= nil then local this_ctype = normalize_ctype(func.this_type.ctype).base if methods[this_ctype] == nil then methods[this_ctype] = {} end table.insert(methods[this_ctype], func) end end return methods end local function generate_object(what, object, funcs, root_indent) local tmp = {} local co = coroutine.create(converter[what]) while true do local ok, value = coroutine.resume(co, { obj = object, funcs = funcs, rootIndent = root_indent, }) assert(ok, debug.traceback(co, value)) if not value then break end table.insert(tmp, value) end return table.concat(tmp, "\n") end local function generate_function_section() local tmp = {} for _, object in ipairs(idl.funcs) do local body = generate_object("funcs", object) if body ~= "" then table.insert(tmp, body) end end return table.concat(tmp, "\n") end combined = { "State", "Stencil", "Buffer", "Texture", "Sampler", "Reset" } for _, v in ipairs(combined) do combined[v] = {} end local function camelcase_to_underscorecase(name) local words = {} for word in name:gmatch("[%u%d]+%l*") do table.insert(words, word:lower()) end return table.concat(words, "_") end local function enum_constant_name(typ, item) local name = item.cname if not name then name = typ.underscore and camelcase_to_underscorecase(item.name) or item.name end return name:upper() end local function javadoc_text(line) line = line or "" line = line:gsub("&", "&") line = line:gsub("<", "<") line = line:gsub(">", ">") line = line:gsub("`([^`]*)`", "{@code %1}") for _, typ in ipairs(idl.types) do if typ.enum then for _, item in ipairs(typ.enum) do line = line:gsub(typ.typename .. "::" .. item.name, typ.typename .. "." .. enum_constant_name(typ, item)) end line = line:gsub(typ.typename .. "::Count", typ.typename .. ".COUNT") end end line = line:gsub("bgfx::", "") line = line:gsub("::", ".") line = line:gsub("([%w_%.]+)%.Enum", "%1") line = line:gsub("%*/", "*/") line = line:gsub("^%s*@remarks?%s*", "Remarks: ") line = line:gsub("^%s*@attention%s*", "Attention: ") line = line:gsub("^%s*@warning%s*", "Warning: ") line = line:gsub("^%s*@note%s*", "Note: ") line = line:gsub("^%s*@returns?%s*", "Returns: ") return line end local function emit_javadoc(lines, doc_indent, params, returns) yield(doc_indent .. "/**") if lines and #lines > 0 then local emitted = false local paragraph = false for _, line in ipairs(lines) do local text = javadoc_text(line) if text == "" then paragraph = emitted else if paragraph then yield(doc_indent .. " *
") end yield(doc_indent .. " * " .. text) emitted = true paragraph = false end end end if params then for _, param in ipairs(params) do yield(doc_indent .. " * @param " .. param.name .. " " .. javadoc_text(param.text)) end end if returns and returns ~= "" then yield(doc_indent .. " * @return " .. javadoc_text(returns)) end yield(doc_indent .. " */") end local function FlagBlock(typ, root_indent) root_indent = root_indent or "" local body_indent = root_indent .. "\t" local format = "0x%08x" local enumType = "int" if typ.bits == 64 then format = "0x%016xL" enumType = "long" elseif typ.bits == 16 then format = "(short) 0x%04x" enumType = "short" end local name = typ.name .. "Flags" emit_javadoc(typ.comments or { "Constants for " .. typ.name .. " flags." }, root_indent) yield(root_indent .. "@NullMarked") yield(root_indent .. "public " .. (root_indent ~= "" and "static " or "") .. "final class " .. name .. " {") yield(body_indent .. "private " .. name .. "() {") yield(body_indent .. "}") for idx, flag in ipairs(typ.flag) do local flagName = flag.name:gsub("_", "") -- make 2d/3d upper case 2D/3D flagName = flagName:gsub("%dd", flagName.upper); if flag.comment ~= nil then if idx ~= 1 then yield("") end emit_javadoc(flag.comment, body_indent) else yield("") emit_javadoc({ typ.name .. " flag value {@code " .. flagName .. "}." }, body_indent) end yield(body_indent .. "public static final " .. enumType .. " " .. flagName .. " = " .. string.format(flag.format or format, flag.value) .. ";") end if typ.shift then emit_javadoc({ "Bit shift for this flag group." }, body_indent) yield(body_indent .. "public static final " .. enumType .. " Shift = " .. typ.shift .. ";") end -- generate Mask if typ.mask then emit_javadoc({ "Bit mask for this flag group." }, body_indent) yield(body_indent .. "public static final " .. enumType .. " Mask = " .. string.format(format, typ.mask) .. ";") end yield(root_indent .. "}") end local function should_emit_function(func) if func.cpponly then return false end return true end local function constant_name(name) return name:gsub("[^%w]", "_"):upper() end local function emit_comments(func, func_indent) local params = {} for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then table.insert(params, { name = arg.name, text = arg.comment and table.concat(arg.comment, " ") or "native function argument", }) else table.insert(params, { name = "_args", text = "variadic arguments; C default argument promotions are applied automatically", }) end end local returns if func.ret and func.ret.comment then returns = table.concat(func.ret.comment, " ") elseif java_type(func.ret, "return") ~= "void" then returns = "the native function result" end emit_javadoc(func.comments or { "Calls {@code bgfx_" .. func.cname .. "}." }, func_indent, params, returns) end local function emit_enum(typ, root_indent) root_indent = root_indent or "" local body_indent = root_indent .. "\t" emit_javadoc(typ.comments or { typ.typename .. " values." }, root_indent) yield(root_indent .. "@NullMarked") yield(root_indent .. "public enum " .. typ.typename .. " {") for _, item in ipairs(typ.enum) do local item_name = enum_constant_name(typ, item) if item.comment then emit_javadoc(item.comment, body_indent) else emit_javadoc({ typ.typename .. " value {@code " .. item_name .. "}." }, body_indent) end yield(body_indent .. item_name .. ",") end yield("") emit_javadoc({ "Number of native enum values." }, body_indent) yield(body_indent .. "COUNT;") yield("") emit_javadoc({ "Native C enum layout." }, body_indent) yield(body_indent .. "public static final ValueLayout.OfInt LAYOUT = ValueLayout.JAVA_INT;") yield(body_indent .. "private static final " .. typ.typename .. "[] VALUES = values();") yield("") emit_javadoc({ "Returns the enum constant for a native C enum value." }, body_indent, { { name = "value", text = "the native enum value" } }, "the matching enum constant") yield(body_indent .. "public static " .. typ.typename .. " fromValue(int value) {") yield(body_indent .. "\tif (value >= 0 && value < VALUES.length) {") yield(body_indent .. "\t\treturn VALUES[value];") yield(body_indent .. "\t}") yield(body_indent .. "\tthrow new IllegalArgumentException(\"Unknown " .. typ.typename .. " value: \" + value);") yield(body_indent .. "}") yield(root_indent .. "}") end local function emit_tagged_handle(typ, root_indent) root_indent = root_indent or "" local body_indent = root_indent .. "\t" local permits = {} for _, source in ipairs(typ.tagged) do table.insert(permits, source) end emit_javadoc(typ.comments or { "Native tagged bgfx handle." }, root_indent) yield(root_indent .. "@NullMarked") yield(root_indent .. "public sealed interface " .. typ.name) yield(root_indent .. "\tpermits " .. table.concat(permits, ", ") .. " {") emit_javadoc({ "Native by-value handle layout." }, body_indent) yield(body_indent .. "StructLayout LAYOUT = cStruct(\"" .. typ.cname .. "\",") yield(body_indent .. "\tValueLayout.JAVA_SHORT.withName(\"idx\"),") yield(body_indent .. "\tValueLayout.JAVA_SHORT.withName(\"type\"));") emit_javadoc({ "Native handle-index field accessor." }, body_indent) yield(body_indent .. "VarHandle VH_IDX = LAYOUT.varHandle(") yield(body_indent .. "\tMemoryLayout.PathElement.groupElement(\"idx\"));") emit_javadoc({ "Native handle-type field accessor." }, body_indent) yield(body_indent .. "VarHandle VH_TYPE = LAYOUT.varHandle(") yield(body_indent .. "\tMemoryLayout.PathElement.groupElement(\"type\"));") yield("") emit_javadoc({ "Invalid handle sentinel." }, body_indent) yield(body_indent .. typ.name .. " INVALID = " .. typ.tagged[1] .. ".INVALID;") yield("") emit_javadoc({ "Returns the native handle index." }, body_indent, nil, "the native handle index") yield(body_indent .. "short idx();") yield("") emit_javadoc({ "Returns the native handle type tag." }, body_indent, nil, "the native handle type tag") yield(body_indent .. "short type();") yield("") emit_javadoc({ "Returns whether this handle is valid." }, body_indent, nil, "{@code true} when the handle index is not {@code UINT16_MAX}") yield(body_indent .. "default boolean isValid() {") yield(body_indent .. "\treturn idx() != (short) 0xffff;") yield(body_indent .. "}") yield("") emit_javadoc({ "Allocates and writes the native tagged handle representation." }, body_indent, { { name = "allocator", text = "the destination allocator" } }, "the allocated native segment") yield(body_indent .. "default MemorySegment allocateTagged(SegmentAllocator allocator) {") yield(body_indent .. "\tMemorySegment segment = allocator.allocate(LAYOUT);") yield(body_indent .. "\twriteTagged(segment);") yield(body_indent .. "\treturn segment;") yield(body_indent .. "}") yield("") emit_javadoc({ "Writes this handle to an existing native tagged handle segment." }, body_indent, { { name = "segment", text = "the destination segment" } }) yield(body_indent .. "default void writeTagged(MemorySegment segment) {") yield(body_indent .. "\tsegment = view(segment, LAYOUT);") yield(body_indent .. "\tVH_IDX.set(segment, 0L, idx());") yield(body_indent .. "\tVH_TYPE.set(segment, 0L, type());") yield(body_indent .. "}") yield("") emit_javadoc({ "Reads a tagged handle from native memory." }, body_indent, { { name = "segment", text = "the source segment" } }, "the decoded handle") yield(body_indent .. "static " .. typ.name .. " read(MemorySegment segment) {") yield(body_indent .. "\tsegment = view(segment, LAYOUT);") yield(body_indent .. "\tshort idx = (short) VH_IDX.get(segment, 0L);") yield(body_indent .. "\tshort type = (short) VH_TYPE.get(segment, 0L);") yield(body_indent .. "\treturn switch (type) {") for index, source in ipairs(typ.tagged) do yield(string.format(body_indent .. "\t\tcase %d -> new %s(idx);", index - 1, source)) end yield(body_indent .. "\t\tdefault -> throw new IllegalArgumentException(\"Unknown " .. typ.name .. " type tag: \" + Short.toUnsignedInt(type));") yield(body_indent .. "\t};") yield(body_indent .. "}") yield(root_indent .. "}") end local function emit_handle(typ, root_indent) if typ.tagged then emit_tagged_handle(typ, root_indent) return end root_indent = root_indent or "" local body_indent = root_indent .. "\t" local tagged_member = tagged_handle_members[typ.name] local interfaces = { "AutoCloseable" } if tagged_member then table.insert(interfaces, tagged_member.parent.name) end emit_javadoc(typ.comments or { "Native bgfx handle." }, root_indent, { { name = "idx", text = "native handle index" } }) yield(root_indent .. "@NullMarked") yield(root_indent .. "public record " .. typ.name .. "(short idx) implements " .. table.concat(interfaces, ", ") .. " {") emit_javadoc({ "Native by-value handle layout." }, body_indent) yield(body_indent .. "public static final StructLayout LAYOUT = cStruct(\"" .. typ.cname .. "\",") yield(body_indent .. "\tValueLayout.JAVA_SHORT.withName(\"idx\"));") yield(body_indent .. "private static final VarHandle VH_IDX = LAYOUT.varHandle(") yield(body_indent .. "\tMemoryLayout.PathElement.groupElement(\"idx\"));") emit_javadoc({ "Invalid handle sentinel." }, body_indent) yield(body_indent .. "public static final " .. typ.name .. " INVALID = new " .. typ.name .. "((short) 0xffff);") if tagged_member then yield("") emit_javadoc({ "Returns this handle's native tagged-handle type." }, body_indent, nil, "the native tagged-handle type") yield(body_indent .. "@Override") yield(body_indent .. "public short type() {") yield(string.format(body_indent .. "\treturn (short) %d;", tagged_member.tag)) yield(body_indent .. "}") end yield("") emit_javadoc({ "Returns whether this handle is valid." }, body_indent, nil, "{@code true} when the handle index is not {@code UINT16_MAX}") yield(body_indent .. "public boolean isValid() {") yield(body_indent .. "\treturn idx != (short) 0xffff;") yield(body_indent .. "}") yield("") emit_javadoc({ "Allocates and writes the native by-value handle representation." }, body_indent, { { name = "allocator", text = "the destination allocator" } }, "the allocated native segment") yield(body_indent .. "public MemorySegment allocate(SegmentAllocator allocator) {") yield(body_indent .. "\tMemorySegment segment = allocator.allocate(LAYOUT);") yield(body_indent .. "\twrite(segment);") yield(body_indent .. "\treturn segment;") yield(body_indent .. "}") yield("") emit_javadoc({ "Writes this handle to an existing native segment." }, body_indent, { { name = "segment", text = "the destination segment" } }) yield(body_indent .. "public void write(MemorySegment segment) {") yield(body_indent .. "\tsegment = view(segment, LAYOUT);") yield(body_indent .. "\tVH_IDX.set(segment, 0L, idx);") yield(body_indent .. "}") yield("") emit_javadoc({ "Reads a by-value handle from native memory." }, body_indent, { { name = "segment", text = "the source segment" } }, "the decoded handle") yield(body_indent .. "public static " .. typ.name .. " read(MemorySegment segment) {") yield(body_indent .. "\tsegment = view(segment, LAYOUT);") yield(body_indent .. "\treturn new " .. typ.name .. "((short) VH_IDX.get(segment, 0L));") yield(body_indent .. "}") yield("") emit_javadoc({ "Destroys this native handle." }, body_indent) yield(body_indent .. "@Override") yield(body_indent .. "public void close() {") yield(body_indent .. "\tBgfx.destroy" .. typ.name:gsub("Handle$", "") .. "(this);") yield(body_indent .. "}") yield(root_indent .. "}") end local function callback_carrier_type(arg) local details = type_details(arg, true) if details.pointers > 0 then return "MemorySegment" elseif details.info then if details.info.kind == "enum" then return "int" elseif details.info.kind == "handle" or details.info.kind == "struct" or details.info.kind == "funcptr" or details.info.kind == "opaque" then return "MemorySegment" end end return assert(details.primitive, "Unsupported callback type: " .. arg.ctype).java end local function java_class_literal(java) return java .. ".class" end local function descriptor_expression(ret, args) local layouts = {} for _, arg in ipairs(args) do if arg.ctype ~= "..." then table.insert(layouts, layout_type(arg, true)) end end local suffix = #layouts == 0 and "" or ", " .. table.concat(layouts, ", ") if normalize_ctype(ret.ctype).base == "void" and normalize_ctype(ret.ctype).pointers == 0 then return "FunctionDescriptor.ofVoid(" .. table.concat(layouts, ", ") .. ")" end return "FunctionDescriptor.of(" .. layout_type(ret, false) .. suffix .. ")" end local function emit_funcptr(typ, root_indent) root_indent = root_indent or "" local body_indent = root_indent .. "\t" local args = {} local arg_types = {} local method_classes = {} local params = {} for _, arg in ipairs(typ.args) do if arg.ctype ~= "..." then local name = arg.name ~= "" and arg.name or "_arg" table.insert(args, callback_carrier_type(arg) .. " " .. name) table.insert(arg_types, arg) table.insert(method_classes, java_class_literal(callback_carrier_type(arg))) table.insert(params, { name = name, text = arg.comment and table.concat(arg.comment, " ") or "native callback argument", }) end end local ret = callback_carrier_type(typ.ret) emit_javadoc(typ.comments or { "Native callback." }, root_indent) yield(root_indent .. "@NullMarked") yield(root_indent .. "@FunctionalInterface") yield(root_indent .. "@SuppressWarnings(\"restricted\")") yield(root_indent .. "public interface " .. typ.name .. " {") emit_javadoc({ "Native callback function descriptor." }, body_indent) yield(body_indent .. "FunctionDescriptor DESCRIPTOR = " .. descriptor_expression(typ.ret, arg_types) .. ";") emit_javadoc({ "Bound callback target used to create upcall stubs." }, body_indent) yield(body_indent .. "MethodHandle TARGET = upcallTarget(") yield(body_indent .. "\t" .. typ.name .. ".class, \"invoke\", MethodType.methodType(") local class_suffix = #method_classes == 0 and "" or ", " .. table.concat(method_classes, ", ") yield(body_indent .. "\t\t" .. java_class_literal(ret) .. class_suffix .. "));") yield("") emit_javadoc({ "Invoked by native bgfx code. Implementations must not throw." }, body_indent, params) yield(body_indent .. ret .. " invoke(" .. table.concat(args, ", ") .. ");") yield("") emit_javadoc({ "Creates an upcall stub for this callback.", "The arena must remain alive until bgfx can no longer invoke the callback.", }, body_indent, { { name = "arena", text = "a caller-owned, long-lived arena" } }, "the native function pointer") yield(body_indent .. "default MemorySegment upcall(Arena arena) {") yield(body_indent .. "\tObjects.requireNonNull(arena, \"arena\");") yield(body_indent .. "\treturn LINKER.upcallStub(TARGET.bindTo(this), DESCRIPTOR, arena);") yield(body_indent .. "}") yield(root_indent .. "}") end local function emit_member_accessor(typ, member, body_indent) local details = type_details(member, false) local member_type = java_type(member, "member") local field_handle = (member.array or (details.pointers == 0 and details.info and (details.info.kind == "handle" or details.info.kind == "struct"))) and "MH_" .. constant_name(member.name) or "VH_" .. constant_name(member.name) local comments = member.comment or { "Gets the native {@code " .. member.name .. "} field." } if member.array then emit_javadoc(comments, body_indent, nil, "a segment view of the inline array") yield(body_indent .. "public MemorySegment " .. member.name .. "() {") yield(body_indent .. "\treturn slice(" .. field_handle .. ", segment());") yield(body_indent .. "}") return end local getter local setter local primitive = details.primitive if details.pointers > 0 then if details.pointers == 1 and details.info and details.info.kind == "struct" then getter = "new " .. details.info.java .. "((MemorySegment) " .. field_handle .. ".get(segment(), 0L))" setter = field_handle .. ".set(segment(), 0L, address(value))" else getter = "address((MemorySegment) " .. field_handle .. ".get(segment(), 0L))" setter = field_handle .. ".set(segment(), 0L, address(value))" end elseif details.info then if details.info.kind == "enum" then getter = details.info.java .. ".fromValue((int) " .. field_handle .. ".get(segment(), 0L))" setter = field_handle .. ".set(segment(), 0L, value.ordinal())" elseif details.info.kind == "handle" then getter = details.info.java .. ".read(slice(" .. field_handle .. ", segment()))" if details.info.typ.tagged then setter = "value.writeTagged(slice(" .. field_handle .. ", segment()))" else setter = "value.write(slice(" .. field_handle .. ", segment()))" end elseif details.info.kind == "struct" then getter = "new " .. details.info.java .. "(slice(" .. field_handle .. ", segment()))" setter = "slice(" .. field_handle .. ", segment()).copyFrom(value.segment())" else getter = "address((MemorySegment) " .. field_handle .. ".get(segment(), 0L))" setter = field_handle .. ".set(segment(), 0L, address(value))" end else assert(primitive, "Unsupported member type: " .. member.ctype) assert(not primitive.uintptr, "uintptr_t struct fields require a carrier-specific accessor") getter = "(" .. primitive.java .. ") " .. field_handle .. ".get(segment(), 0L)" setter = field_handle .. ".set(segment(), 0L, value)" end emit_javadoc(comments, body_indent, nil, "the field value") yield(body_indent .. "public " .. member_type .. " " .. member.name .. "() {") yield(body_indent .. "\treturn " .. getter .. ";") yield(body_indent .. "}") yield("") local setter_javadoc = { "Sets the native {@code " .. member.name .. "} field and returns {@code this}." }; emit_javadoc(setter_javadoc, body_indent, { { name = "value", text = "the new field value" } }) yield(body_indent .. "public " .. typ.name .. " " .. member.name .. "(" .. member_type .. " value) {") yield(body_indent .. "\t" .. setter .. ";") yield(body_indent .. "\treturn this;") yield(body_indent .. "}") if details.pointers == 0 and primitive and (primitive.plain == "byte" or primitive.plain == "short") then yield("") emit_javadoc(setter_javadoc, body_indent, { { name = "value", text = "the new field value" } }) yield(body_indent .. "public " .. typ.name .. " " .. member.name .. "(int value) {") local suffix = primitive.plain:sub(1, 1):upper() .. primitive.plain:sub(2) local fmt = primitive.unsigned and ("NativeObject.toUnsigned" .. suffix .. "(%s)") or ("(" .. primitive.java .. ")%s") yield(body_indent .. "\treturn " .. member.name .. "(" .. string.format(fmt, "value") .. ");") yield(body_indent .. "}") end end local function emit_struct_body(typ, funcs, body_indent) local opaque = typ.name == "Encoder" and #typ.struct == 0 if not opaque then emit_javadoc({ "Native C structure layout." }, body_indent) yield(body_indent .. "public static final StructLayout LAYOUT = cStruct(\"" .. typ.cname .. "\",") for index, member in ipairs(typ.struct) do local suffix = index == #typ.struct and ");" or "," yield(body_indent .. "\t" .. member_layout(member) .. suffix) end for _, member in ipairs(typ.struct) do local details = type_details(member, false) local group = member.array or (details.pointers == 0 and details.info and (details.info.kind == "handle" or details.info.kind == "struct")) local handle_type = group and "MethodHandle" or "VarHandle" local handle_prefix = group and "MH_" or "VH_" local factory = group and "sliceHandle" or "varHandle" yield(body_indent .. "private static final " .. handle_type .. " " .. handle_prefix .. constant_name(member.name) .. " = LAYOUT." .. factory .. "(") yield(body_indent .. "\tMemoryLayout.PathElement.groupElement(\"" .. member.name .. "\"));") end end emit_javadoc({ opaque and "Wraps an opaque native pointer." or "Wraps an existing native structure." }, body_indent, { { name = "segment", text = "native memory segment" } }) yield(body_indent .. "public " .. typ.name .. "(MemorySegment segment) {") if opaque then yield(body_indent .. "\tsuper(segment);") else yield(body_indent .. "\tsuper(segment, LAYOUT);") end yield(body_indent .. "}") if not opaque then yield("") emit_javadoc({ "Allocates a native structure." }, body_indent, { { name = "allocator", text = "destination allocator" } }) yield(body_indent .. "public " .. typ.name .. "(SegmentAllocator allocator) {") yield(body_indent .. "\tsuper(allocator, LAYOUT);") yield(body_indent .. "}") end for _, member in ipairs(typ.struct) do yield("") emit_member_accessor(typ, member, body_indent) end if funcs then for _, func in ipairs(funcs) do if should_emit_function(func) then yield("") converter.funcs({ obj = func, asMethod = true, methodIndent = body_indent }) end end end end function converter.types(params) local typ = params.obj local funcs = params.funcs local root_indent = params.rootIndent or "" if typ.args and typ.ret then emit_funcptr(typ, root_indent) elseif typ.handle then emit_handle(typ, root_indent) elseif typ.enum then emit_enum(typ, root_indent) elseif typ.bits ~= nil then FlagBlock(typ, root_indent) elseif typ.struct ~= nil then emit_javadoc(typ.comments or { typ.name .. " native structure." }, root_indent) yield(root_indent .. "@NullMarked") yield(root_indent .. "public " .. (root_indent ~= "" and "static " or "") .. "final class " .. typ.name .. " extends NativeObject {") emit_struct_body(typ, funcs, root_indent .. "\t") yield(root_indent .. "}") end end local function is_string(arg) local details = type_details(arg, true) return details.base == "char" and details.pointers == 1 and details.is_const end local function is_group_value(arg) local details = type_details(arg, false) return details.pointers == 0 and details.info and (details.info.kind == "handle" or details.info.kind == "struct") end local function native_argument(arg, name) local details = type_details(arg, true) if details.pointers > 0 then if is_string(arg) then return "cString(arena, " .. name .. ")" end return "address(" .. name .. ")" elseif details.info then if details.info.kind == "enum" then return name .. ".ordinal()" elseif details.info.kind == "handle" then if details.info.typ.tagged then return name .. ".allocateTagged(arena)" end return name .. ".allocate(arena)" elseif details.info.kind == "struct" then return name .. ".segment()" end return "address(" .. name .. ")" end local primitive = assert(details.primitive, "Unsupported native argument: " .. arg.ctype) if arg.ctype == "uint8_t" and arg.default == "BGFX_DISCARD_ALL" then return "NativeObject.toUnsignedByte(" .. name .. ")" elseif primitive.uintptr then return "nativeUintptr(" .. name .. ")" elseif primitive.opaque then return "address(" .. name .. ")" end return name end local function function_needs_arena(func) if func.vararg or is_group_value(func.ret) then return true end for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then local details = type_details(arg, true) if is_string(arg) or (details.pointers == 0 and details.info and details.info.kind == "handle") then return true end end end return false end local function native_call_arguments(func) local args = {} if is_group_value(func.ret) then table.insert(args, "(SegmentAllocator) arena") end if func.this then table.insert(args, "segment()") end for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then table.insert(args, native_argument(arg, arg.name)) end end return args end local function has_dynamic_carrier(arg, array_as_pointer) local details = type_details(arg, array_as_pointer) return details.pointers == 0 and details.primitive and details.primitive.uintptr end local function function_uses_exact_invoke(func) if has_dynamic_carrier(func.ret, false) then return false end for _, arg in ipairs(func.args) do if arg.ctype ~= "..." and has_dynamic_carrier(arg, true) then return false end end return true end local function native_call(func) local args = native_call_arguments(func) local suffix = #args == 0 and "" or ", " .. table.concat(args, ", ") if function_uses_exact_invoke(func) then return "MH_" .. constant_name(func.cname) .. ".invokeExact(" .. table.concat(args, ", ") .. ")" end return "invoke(MH_" .. constant_name(func.cname) .. suffix .. ")" end local function return_statement(func, call) local details = type_details(func.ret, false) if details.pointers > 0 then if details.base == "char" and details.pointers == 1 and details.is_const then return "return readString((MemorySegment) " .. call .. ");" elseif details.pointers == 1 and details.info and details.info.kind == "struct" then return "return new " .. details.info.java .. "((MemorySegment) " .. call .. ");" end return "return address((MemorySegment) " .. call .. ");" elseif details.info then if details.info.kind == "enum" then return "return " .. details.info.java .. ".fromValue((int) " .. call .. ");" elseif details.info.kind == "handle" then return "return " .. details.info.java .. ".read((MemorySegment) " .. call .. ");" elseif details.info.kind == "struct" then return "return new " .. details.info.java .. "((MemorySegment) " .. call .. ");" end return "return address((MemorySegment) " .. call .. ");" end local primitive = assert(details.primitive, "Unsupported return type: " .. func.ret.ctype) if primitive.java == "void" then return call .. ";" elseif primitive.uintptr then return "return javaUintptr(" .. call .. ");" elseif primitive.opaque then return "return address((MemorySegment) " .. call .. ");" end return "return (" .. primitive.java .. ") " .. call .. ";" end local function public_parameters(func, variadic) local args = {} for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then table.insert(args, java_parameter_type(arg) .. " " .. arg.name) end end if variadic then table.insert(args, "Object... _args") end return table.concat(args, ", ") end local function emit_regular_wrapper(func, func_indent, as_method) emit_comments(func, func_indent) local modifier = as_method and "public final " or "public static " local name = as_method and method_name(func) or camel_name(func.cname) yield(func_indent .. modifier .. java_type(func.ret, "return") .. " " .. name .. "(" .. public_parameters(func, false) .. ") {") local body_indent = func_indent .. "\t" local exact = function_uses_exact_invoke(func) if exact then yield(body_indent .. "try {") body_indent = body_indent .. "\t" end if function_needs_arena(func) then yield(body_indent .. "try (Arena arena = Arena.ofConfined()) {") yield(body_indent .. "\t" .. return_statement(func, native_call(func))) yield(body_indent .. "}") else yield(body_indent .. return_statement(func, native_call(func))) end if exact then body_indent = body_indent:sub(1, -2) yield(body_indent .. "} catch (Throwable ex) {") yield(body_indent .. "\tthrow invocationFailure(ex);") yield(body_indent .. "}") end yield(func_indent .. "}") end local function emit_variadic_wrapper(func, func_indent) emit_comments(func, func_indent) yield(func_indent .. "public static final " .. java_type(func.ret, "return") .. " " .. camel_name(func.cname) .. "(" .. public_parameters(func, true) .. ") {") local body = func_indent .. "\t" yield(body .. "try (Arena arena = Arena.ofConfined()) {") local fixed_count = 0 for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then fixed_count = fixed_count + 1 end end yield(body .. "\tObject[] nativeArgs = new Object[" .. fixed_count .. "];") local native_index = 0 for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then yield(body .. "\tnativeArgs[" .. native_index .. "] = " .. native_argument(arg, arg.name) .. ";") native_index = native_index + 1 end end local fixed_args = {} for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then table.insert(fixed_args, arg) end end local call = "invokeVariadic(VS_" .. constant_name(func.cname) .. ", " .. descriptor_expression(func.ret, fixed_args) .. ", nativeArgs, _args)" yield(body .. "\t" .. return_statement(func, call)) yield(body .. "}") yield(func_indent .. "}") end local function function_descriptor(func) local args = {} if func.this then table.insert(args, func.this_type) end for _, arg in ipairs(func.args) do if arg.ctype ~= "..." then table.insert(args, arg) end end return descriptor_expression(func.ret, args) end local function emit_native_descriptor(func) if func.vararg then local index = #variadic_entries table.insert(variadic_entries, { func = func, index = index }) else local index = #downcall_entries table.insert(downcall_entries, { func = func, index = index }) end end local function native_section() local lines = {} for _, entry in ipairs(downcall_entries) do local name = constant_name(entry.func.cname) table.insert(lines, "\tstatic final MethodHandle MH_" .. name .. " = downcall(") table.insert(lines, "\t\t\"bgfx_" .. entry.func.cname .. "\", " .. function_descriptor(entry.func) .. ");") end for _, entry in ipairs(variadic_entries) do local name = constant_name(entry.func.cname) table.insert(lines, "\tstatic final MethodHandle VS_" .. name .. " = variadicSymbol(\"bgfx_" .. entry.func.cname .. "\");") end return table.concat(lines, "\n") end function converter.funcs(params) local func = params.obj if not should_emit_function(func) then return end if params.asMethod then emit_regular_wrapper(func, params.methodIndent, true) return end if not func.this then if func.vararg then emit_variadic_wrapper(func, "\t") else emit_regular_wrapper(func, "\t", false) end yield("") end emit_native_descriptor(func) end local function combine_flag_type(target, typ, name) target.bits = typ.bits target.name = typ.name:match("(%u%l+)") local flags = target.flag or {} target.flag = flags local lookup = target.lookup or {} target.lookup = lookup for _, flag in ipairs(typ.flag) do local flag_name = name .. flag.name:gsub("_", "") local value = flag.value if value == nil then value = 0 for _, part in ipairs(flag) do value = value | assert(lookup[name .. part], part .. " is not defined for " .. flag_name) end end lookup[flag_name] = value table.insert(flags, { name = flag_name, value = value, comment = flag.comment, }) end if typ.shift then table.insert(flags, { name = name .. "Shift", value = typ.shift, format = "%d", comment = typ.comment, }) end if typ.mask then table.insert(flags, { name = name .. "Mask", value = typ.mask, comment = typ.comment, }) lookup[name .. "Mask"] = typ.mask end end local function prepared_types() for _, typ in ipairs(idl.types) do if typ.bits ~= nil then local prefix, name = typ.name:match("(%u%l+)(.*)") if combined[prefix] then combine_flag_type(combined[prefix], typ, name) end end end local result = {} local emitted = {} for _, typ in ipairs(idl.types) do local output = typ if typ.bits ~= nil then local prefix = typ.name:match("(%u%l+)") if combined[prefix] then if emitted[prefix] then output = nil else emitted[prefix] = true output = combined[prefix] end end end if output then table.insert(result, output) end end return result end local function generated_type_name(typ) if typ.args and typ.ret then return typ.name elseif typ.handle then return typ.name elseif typ.enum then return typ.typename elseif typ.bits ~= nil then return typ.name .. "Flags" elseif typ.struct ~= nil then return typ.name end end local function type_source(body) return java_header .. "\npackage " .. java_package .. ";\n" .. java_imports .. "\nimport static " .. java_package .. ".Bgfx.*;\n\n" .. body .. "\n" end local function clean_source(source) return (source:gsub("[ \t]+\n", "\n")) end local function render_types(types, methods, mode) local namespaces = {} for _, typ in ipairs(types) do if typ.namespace then local children = namespaces[typ.namespace] or {} namespaces[typ.namespace] = children table.insert(children, typ) end end local result = {} local indent = mode == "single" and "\t" or "" for _, typ in ipairs(types) do local name = generated_type_name(typ) if name and not typ.namespace then local body = generate_object("types", typ, methods[typ.cname], indent) local children = namespaces[name] if children then local nested = {} for _, child in ipairs(children) do table.insert(nested, generate_object("types", child, methods[child.cname], indent .. "\t")) end assert(body:sub(-1) == "}", "Expected type closing brace: " .. name) body = body:sub(1, -2) .. "\n\n" .. table.concat(nested, "\n\n") .. "\n" .. indent .. "}" namespaces[name] = nil end if body ~= "" then assert(result[name] == nil, "Duplicate Java type: " .. name) result[name] = body end end end assert(next(namespaces) == nil, "Java namespace has no generated owner") return result end local function generate(mode) reset_generator_state(mode) local types = prepared_types() local rendered = render_types(types, collect_methods(), mode) local sections = { funcs = generate_function_section(), native = native_section(), types = "" } if mode == "single" then local bodies = {} for _, typ in ipairs(types) do local name = generated_type_name(typ) if name and not typ.namespace then table.insert(bodies, rendered[name]) end end sections.types = table.concat(bodies, "\n\n") return clean_source((java_template:gsub("$(%l+)", sections))) end local files = {} for name, body in pairs(rendered) do files[name .. ".java"] = clean_source(type_source(body)) end files["Bgfx.java"] = clean_source((java_template:gsub("$(%l+)", sections))) return files end function gen.gen() if os.getenv("LOCAL_LUA_DEBUGGER_VSCODE") == "1" then require("lldebugger").start() end return generate(os.getenv("BGFX_JAVAGEN_MULTIFILE") ~= nil and "files" or "single") end -- Generate the multi-file layout regardless of the selected genie mode. function gen.files() return generate("files") end local function sorted_names(files) local names = {} for name in pairs(files) do table.insert(names, name) end table.sort(names) return names end function gen.write(codes, outputfile) local normalized = outputfile:gsub("\\", "/") local directory, filename = normalized:match("^(.*)/([^/]+)$") directory = directory or "." filename = filename or normalized local multiple = type(codes) == "table" local files = multiple and codes or { [filename] = codes } for _, name in ipairs(sorted_names(files)) do local target = multiple and directory .. "/" .. name or outputfile local out = assert(io.open(target, "wb")) out:write(files[name]) out:close() print("Generating: " .. target) end -- GENie provides os.matchfiles; keep direct Lua usage of gen.write available. if os.matchfiles then for _, existing in ipairs(os.matchfiles(directory .. "/*.java")) do local name = existing:gsub("\\", "/"):match("([^/]+)$") if not files[name] then local input = assert(io.open(existing, "rb")) local header = input:read(256) or "" input:close() if header:find("AUTO GENERATED! DO NOT EDIT!", 1, true) then assert(os.remove(existing)) print("Removing: " .. existing) end end end end end if (...) == nil then local codes = gen.gen() if type(codes) == "table" then for _, name in ipairs(sorted_names(codes)) do print("// " .. name) print(codes[name]) end else print(codes) end end return gen