Files
bgfx/scripts/bindings-java.lua
Markus 1d4d025514 Added java bindings (#4016)
* Java bindings: Consolidate generated API and utilities

* Java bindings: Added multi-file output

* Java bindings: Introduced module-info.java for JPMS support

* Docs: Added link to Java API bindings in overview and README
2026-10-08 18:45:00 -07:00

1390 lines
46 KiB
Lua

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("<", "&lt;")
line = line:gsub(">", "&gt;")
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("%*/", "*&#47;")
line = line:gsub("^%s*@remarks?%s*", "<strong>Remarks:</strong> ")
line = line:gsub("^%s*@attention%s*", "<strong>Attention:</strong> ")
line = line:gsub("^%s*@warning%s*", "<strong>Warning:</strong> ")
line = line:gsub("^%s*@note%s*", "<strong>Note:</strong> ")
line = line:gsub("^%s*@returns?%s*", "<strong>Returns:</strong> ")
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 .. " * <p>")
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