mirror of
https://github.com/bkaradzic/bgfx.git
synced 2026-08-22 19:18:21 +00:00
add python bindings generator (#3915)
* add python binding generator * add idl generated python binding
This commit is contained in:
478
scripts/bindings-py.lua
Normal file
478
scripts/bindings-py.lua
Normal file
@@ -0,0 +1,478 @@
|
||||
local codegen = require "codegen"
|
||||
local idl = codegen.idl "bgfx.idl"
|
||||
|
||||
local gen = {}
|
||||
|
||||
local function has_suffix(str, suffix)
|
||||
return suffix == "" or str:sub(-#suffix) == suffix
|
||||
end
|
||||
|
||||
local function py_ident(name)
|
||||
name = name:gsub("[^%w_]", "_")
|
||||
if name:match("^%d") then
|
||||
name = "_" .. name
|
||||
end
|
||||
|
||||
local keywords = {
|
||||
["False"] = true, ["None"] = true, ["True"] = true,
|
||||
["and"] = true, ["as"] = true, ["assert"] = true, ["async"] = true,
|
||||
["await"] = true, ["break"] = true, ["class"] = true, ["continue"] = true,
|
||||
["def"] = true, ["del"] = true, ["elif"] = true, ["else"] = true,
|
||||
["except"] = true, ["finally"] = true, ["for"] = true, ["from"] = true,
|
||||
["global"] = true, ["if"] = true, ["import"] = true, ["in"] = true,
|
||||
["is"] = true, ["lambda"] = true, ["nonlocal"] = true, ["not"] = true,
|
||||
["or"] = true, ["pass"] = true, ["raise"] = true, ["return"] = true,
|
||||
["try"] = true, ["while"] = true, ["with"] = true, ["yield"] = true,
|
||||
}
|
||||
|
||||
if keywords[name] then
|
||||
name = name .. "_"
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
local function python_type_name(typ)
|
||||
if typ.enum then
|
||||
return py_ident(typ.typename)
|
||||
elseif typ.namespace then
|
||||
return py_ident(typ.namespace .. typ.name)
|
||||
else
|
||||
return py_ident(typ.name:gsub("::Enum$", ""))
|
||||
end
|
||||
end
|
||||
|
||||
local function append(out, ...)
|
||||
for _, value in ipairs({...}) do
|
||||
out[#out + 1] = value
|
||||
end
|
||||
end
|
||||
|
||||
local ctype_map = {
|
||||
["bool"] = "ctypes.c_bool",
|
||||
["char"] = "ctypes.c_char",
|
||||
["float"] = "ctypes.c_float",
|
||||
["int8_t"] = "ctypes.c_int8",
|
||||
["int16_t"] = "ctypes.c_int16",
|
||||
["int32_t"] = "ctypes.c_int32",
|
||||
["int64_t"] = "ctypes.c_int64",
|
||||
["uint8_t"] = "ctypes.c_uint8",
|
||||
["uint16_t"] = "ctypes.c_uint16",
|
||||
["uint32_t"] = "ctypes.c_uint32",
|
||||
["uint64_t"] = "ctypes.c_uint64",
|
||||
["uintptr_t"] = "ctypes.c_size_t",
|
||||
["bgfx_view_id_t"] = "ctypes.c_uint16",
|
||||
["va_list"] = "ctypes.c_void_p",
|
||||
}
|
||||
|
||||
local opaque_types = {
|
||||
["bgfx_allocator_interface_t"] = true,
|
||||
["bgfx_callback_interface_t"] = true,
|
||||
}
|
||||
|
||||
local enum_counts = {}
|
||||
|
||||
for _, typ in ipairs(idl.types) do
|
||||
if typ.enum then
|
||||
ctype_map[typ.cname] = "ctypes.c_int"
|
||||
enum_counts[typ.typename] = #typ.enum
|
||||
elseif typ.handle or typ.struct then
|
||||
ctype_map[typ.cname] = python_type_name(typ)
|
||||
elseif typ.args and typ.ret then
|
||||
ctype_map[typ.cname] = python_type_name(typ)
|
||||
end
|
||||
end
|
||||
|
||||
local function normalize_ctype(ctype)
|
||||
ctype = ctype:gsub("%s+", " ")
|
||||
ctype = ctype:gsub("%s*%*%s*", "*")
|
||||
ctype = ctype:match("^%s*(.-)%s*$")
|
||||
return ctype
|
||||
end
|
||||
|
||||
local function convert_ctype(arg, array_as_pointer)
|
||||
if arg.ctype == "..." then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ctype = normalize_ctype(arg.ctype)
|
||||
local is_const = ctype:match("^const ") ~= nil
|
||||
ctype = ctype:gsub("^const%s+", "")
|
||||
|
||||
local pointer_count = 0
|
||||
while ctype:sub(-1) == "*" do
|
||||
pointer_count = pointer_count + 1
|
||||
ctype = ctype:sub(1, -2)
|
||||
end
|
||||
|
||||
if arg.array and array_as_pointer then
|
||||
pointer_count = pointer_count + 1
|
||||
end
|
||||
|
||||
if ctype == "void" then
|
||||
if pointer_count == 0 then
|
||||
return "None"
|
||||
end
|
||||
|
||||
local result = "ctypes.c_void_p"
|
||||
for _ = 2, pointer_count do
|
||||
result = "ctypes.POINTER(" .. result .. ")"
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
if ctype == "char" and pointer_count == 1 and is_const then
|
||||
return "ctypes.c_char_p"
|
||||
end
|
||||
|
||||
if opaque_types[ctype] and pointer_count > 0 then
|
||||
local result = "ctypes.c_void_p"
|
||||
for _ = 2, pointer_count do
|
||||
result = "ctypes.POINTER(" .. result .. ")"
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local result = ctype_map[ctype]
|
||||
assert(result, "Unsupported C type: " .. arg.ctype)
|
||||
|
||||
for _ = 1, pointer_count do
|
||||
result = "ctypes.POINTER(" .. result .. ")"
|
||||
end
|
||||
|
||||
return result
|
||||
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)
|
||||
|
||||
local count = enum_counts[enum_name]
|
||||
assert(count, "Unknown enum in array expression: " .. enum_name)
|
||||
return count
|
||||
end
|
||||
|
||||
local function comment_lines(out, comments, indent)
|
||||
if comments == nil then
|
||||
return
|
||||
end
|
||||
indent = indent or ""
|
||||
for _, line in ipairs(comments) do
|
||||
append(out, indent .. "# " .. line)
|
||||
end
|
||||
end
|
||||
|
||||
local function gen_enums(out)
|
||||
for _, typ in ipairs(idl.types) do
|
||||
if typ.enum then
|
||||
comment_lines(out, typ.comments)
|
||||
append(out, "class " .. python_type_name(typ) .. "(enum.IntEnum):")
|
||||
|
||||
for index, item in ipairs(typ.enum) do
|
||||
comment_lines(out, item.comment, "\t")
|
||||
append(out, string.format("\t%s = %d", py_ident(item.name), index - 1))
|
||||
end
|
||||
|
||||
append(out, string.format("\tCount = %d", #typ.enum), "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local combined_names = {
|
||||
State = true,
|
||||
Stencil = true,
|
||||
Buffer = true,
|
||||
Texture = true,
|
||||
Sampler = true,
|
||||
Reset = true,
|
||||
}
|
||||
|
||||
local function flag_value(item, lookup)
|
||||
if item.value ~= nil then
|
||||
return item.value
|
||||
end
|
||||
|
||||
local value = 0
|
||||
for _, name in ipairs(item) do
|
||||
local part = assert(lookup[name], "Undefined combined flag " .. name)
|
||||
value = value | part
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function emit_flag_class(out, name, items)
|
||||
append(out, "class " .. py_ident(name) .. "(enum.IntFlag):")
|
||||
|
||||
if #items == 0 then
|
||||
append(out, "\tpass", "")
|
||||
return
|
||||
end
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
comment_lines(out, item.comment, "\t")
|
||||
append(out, string.format("\t%s = 0x%x", py_ident(item.name), item.value))
|
||||
end
|
||||
append(out, "")
|
||||
end
|
||||
|
||||
local function gen_flags(out)
|
||||
local combined = {}
|
||||
local standalone = {}
|
||||
|
||||
for name in pairs(combined_names) do
|
||||
combined[name] = { lookup = {}, items = {} }
|
||||
end
|
||||
|
||||
for _, typ in ipairs(idl.types) do
|
||||
if typ.flag then
|
||||
local prefix, suffix = typ.name:match("^(%u%l+)(.*)$")
|
||||
local target = prefix and combined[prefix] or nil
|
||||
|
||||
if target then
|
||||
for _, item in ipairs(typ.flag) do
|
||||
local member_name = suffix .. item.name:gsub("_", "")
|
||||
local value = item.value
|
||||
if value == nil then
|
||||
value = 0
|
||||
for _, part in ipairs(item) do
|
||||
value = value | assert(target.lookup[suffix .. part],
|
||||
part .. " is not defined for " .. member_name)
|
||||
end
|
||||
end
|
||||
|
||||
target.lookup[member_name] = value
|
||||
target.items[#target.items + 1] = {
|
||||
name = member_name,
|
||||
value = value,
|
||||
comment = item.comment,
|
||||
}
|
||||
end
|
||||
|
||||
if typ.shift ~= nil then
|
||||
local name = suffix .. "Shift"
|
||||
target.lookup[name] = typ.shift
|
||||
target.items[#target.items + 1] = {
|
||||
name = name,
|
||||
value = typ.shift,
|
||||
comment = typ.comments,
|
||||
}
|
||||
end
|
||||
|
||||
if typ.mask ~= nil then
|
||||
local name = suffix .. "Mask"
|
||||
target.lookup[name] = typ.mask
|
||||
target.items[#target.items + 1] = {
|
||||
name = name,
|
||||
value = typ.mask,
|
||||
comment = typ.comments,
|
||||
}
|
||||
end
|
||||
else
|
||||
local lookup = {}
|
||||
local items = {}
|
||||
|
||||
for _, item in ipairs(typ.flag) do
|
||||
local value = flag_value(item, lookup)
|
||||
lookup[item.name] = value
|
||||
items[#items + 1] = {
|
||||
name = item.name,
|
||||
value = value,
|
||||
comment = item.comment,
|
||||
}
|
||||
end
|
||||
|
||||
if typ.shift ~= nil then
|
||||
items[#items + 1] = {
|
||||
name = "Shift",
|
||||
value = typ.shift,
|
||||
comment = typ.comments,
|
||||
}
|
||||
end
|
||||
|
||||
if typ.mask ~= nil then
|
||||
items[#items + 1] = {
|
||||
name = "Mask",
|
||||
value = typ.mask,
|
||||
comment = typ.comments,
|
||||
}
|
||||
end
|
||||
|
||||
standalone[#standalone + 1] = {
|
||||
name = typ.name .. "Flags",
|
||||
items = items,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, name in ipairs({ "State", "Stencil", "Buffer", "Texture", "Sampler", "Reset" }) do
|
||||
emit_flag_class(out, name .. "Flags", combined[name].items)
|
||||
end
|
||||
|
||||
for _, flag in ipairs(standalone) do
|
||||
emit_flag_class(out, flag.name, flag.items)
|
||||
end
|
||||
end
|
||||
|
||||
local function gen_struct_declarations(out)
|
||||
for _, typ in ipairs(idl.types) do
|
||||
if typ.handle then
|
||||
append(out,
|
||||
"class " .. python_type_name(typ) .. "(ctypes.Structure):",
|
||||
"\t_fields_ = [(\"idx\", ctypes.c_uint16)]",
|
||||
"",
|
||||
"\t@property",
|
||||
"\tdef valid(self):",
|
||||
"\t\treturn self.idx != 0xffff",
|
||||
""
|
||||
)
|
||||
elseif typ.struct then
|
||||
comment_lines(out, typ.comments)
|
||||
append(out, "class " .. python_type_name(typ) .. "(ctypes.Structure):", "\tpass", "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function gen_funcptrs(out)
|
||||
for _, typ in ipairs(idl.types) do
|
||||
if typ.args and typ.ret then
|
||||
local args = {}
|
||||
for _, arg in ipairs(typ.args) do
|
||||
if arg.ctype ~= "..." then
|
||||
args[#args + 1] = convert_ctype(arg, true)
|
||||
end
|
||||
end
|
||||
|
||||
local ret = convert_ctype(typ.ret, true)
|
||||
local params = ""
|
||||
if #args > 0 then
|
||||
params = ", " .. table.concat(args, ", ")
|
||||
end
|
||||
|
||||
append(out,
|
||||
python_type_name(typ) .. " = ctypes.CFUNCTYPE(" .. ret .. params .. ")",
|
||||
""
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function gen_struct_fields(out)
|
||||
for _, typ in ipairs(idl.types) do
|
||||
if typ.struct and #typ.struct > 0 then
|
||||
append(out, python_type_name(typ) .. "._fields_ = [")
|
||||
|
||||
for _, member in ipairs(typ.struct) do
|
||||
local ctype = convert_ctype(member, false)
|
||||
if member.array then
|
||||
ctype = "(" .. ctype .. " * " .. array_length(member) .. ")"
|
||||
end
|
||||
|
||||
append(out, string.format("\t(%q, %s),", member.name, ctype))
|
||||
end
|
||||
|
||||
append(out, "]", "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function should_emit_function(func)
|
||||
if func.cpponly then
|
||||
return false
|
||||
end
|
||||
if func.cppinline and not func.conly then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function gen_functions(out)
|
||||
append(out,
|
||||
"_lib = None",
|
||||
"",
|
||||
"def load(path):",
|
||||
"\tglobal _lib",
|
||||
"\t_lib = ctypes.CDLL(path)",
|
||||
"\t_bind(_lib)",
|
||||
"\treturn _lib",
|
||||
"",
|
||||
"def _bind(lib):"
|
||||
)
|
||||
|
||||
local any = false
|
||||
for _, func in ipairs(idl.funcs) do
|
||||
if should_emit_function(func) and not func.vararg then
|
||||
any = true
|
||||
|
||||
local args = {}
|
||||
if func.this ~= nil then
|
||||
args[#args + 1] = convert_ctype(func.this_type, true)
|
||||
end
|
||||
|
||||
for _, arg in ipairs(func.args) do
|
||||
if arg.ctype ~= "..." then
|
||||
args[#args + 1] = convert_ctype(arg, true)
|
||||
end
|
||||
end
|
||||
|
||||
local restype = convert_ctype(func.ret, true)
|
||||
local name = "bgfx_" .. func.cname
|
||||
|
||||
append(out,
|
||||
"\tglobal " .. name,
|
||||
"\t" .. name .. " = lib." .. name,
|
||||
"\t" .. name .. ".argtypes = [" .. table.concat(args, ", ") .. "]",
|
||||
"\t" .. name .. ".restype = " .. restype
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if not any then
|
||||
append(out, "\tpass")
|
||||
end
|
||||
|
||||
append(out, "")
|
||||
end
|
||||
|
||||
function gen.gen()
|
||||
local out = {
|
||||
"# Copyright 2011-2026 Branimir Karadzic. All rights reserved.",
|
||||
"# License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE",
|
||||
"",
|
||||
"#",
|
||||
"# AUTO GENERATED! DO NOT EDIT!",
|
||||
"#",
|
||||
"",
|
||||
"import ctypes",
|
||||
"import enum",
|
||||
"",
|
||||
"ViewId = ctypes.c_uint16",
|
||||
"",
|
||||
}
|
||||
|
||||
gen_enums(out)
|
||||
gen_flags(out)
|
||||
gen_struct_declarations(out)
|
||||
gen_funcptrs(out)
|
||||
gen_struct_fields(out)
|
||||
gen_functions(out)
|
||||
|
||||
return table.concat(out, "\n")
|
||||
end
|
||||
|
||||
function gen.write(codes, outputfile)
|
||||
local out = assert(io.open(outputfile, "wb"))
|
||||
out:write(codes)
|
||||
out:close()
|
||||
print("Generating: " .. outputfile)
|
||||
end
|
||||
|
||||
if (...) == nil then
|
||||
print(gen.gen())
|
||||
end
|
||||
|
||||
return gen
|
||||
@@ -87,6 +87,9 @@ newaction {
|
||||
local c3gen = require "bindings-c3"
|
||||
c3gen.write(c3gen.gen(), "../bindings/c3/bgfx.c3")
|
||||
|
||||
local pygen = require "bindings-py"
|
||||
pygen.write(pygen.gen(), "../bindings/py/bgfx.py")
|
||||
|
||||
local docsgen = require "docs-rst"
|
||||
docsgen.write(docsgen.gen(), "../docs/bgfx.rst")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user