diff --git a/.github/workflows/gen_bindings.yml b/.github/workflows/gen_bindings.yml index 11d908c40..443520884 100644 --- a/.github/workflows/gen_bindings.yml +++ b/.github/workflows/gen_bindings.yml @@ -66,6 +66,10 @@ jobs: with: repository: colinbellino/sokol-jai path: bindgen/sokol-jai + - uses: actions/checkout@main + with: + repository: radekm/sokol-c3 + path: bindgen/sokol-c3 - name: generate run: | cd bindgen diff --git a/bindgen/.gitignore b/bindgen/.gitignore index 926ae5250..070ac1cf2 100644 --- a/bindgen/.gitignore +++ b/bindgen/.gitignore @@ -8,3 +8,4 @@ sokol-odin/ sokol-rust/ sokol-d/ sokol-jai/ +sokol-c3/ diff --git a/bindgen/README.md b/bindgen/README.md index a73155217..11b6cacf3 100644 --- a/bindgen/README.md +++ b/bindgen/README.md @@ -28,6 +28,7 @@ To update the Zig bindings: > git clone https://github.com/floooh/sokol-rust > git clone https://github.com/floooh/sokol-d > git clone https://github.com/colinbellino/sokol-jai +> git clone https://github.com/radekm/sokol-c3 > python3 gen_all.py ``` diff --git a/bindgen/gen_all.py b/bindgen/gen_all.py index 385d1f549..ed3185c2d 100644 --- a/bindgen/gen_all.py +++ b/bindgen/gen_all.py @@ -1,4 +1,4 @@ -import os, gen_nim, gen_zig, gen_odin, gen_rust, gen_d, gen_jai +import os, gen_nim, gen_zig, gen_odin, gen_rust, gen_d, gen_jai, gen_c3 tasks = [ [ '../sokol_log.h', 'slog_', [] ], @@ -56,3 +56,9 @@ for task in tasks: [c_header_path, main_prefix, dep_prefixes] = task gen_rust.gen(c_header_path, main_prefix, dep_prefixes) + +# C3 +gen_c3.prepare() +for task in tasks: + [c_header_path, main_prefix, dep_prefixes] = task + gen_c3.gen(c_header_path, main_prefix, dep_prefixes) diff --git a/bindgen/gen_c3.py b/bindgen/gen_c3.py new file mode 100644 index 000000000..587951c96 --- /dev/null +++ b/bindgen/gen_c3.py @@ -0,0 +1,442 @@ +#------------------------------------------------------------------------------- +# gen_c3.py +# +# Generate C3 bindings. +#------------------------------------------------------------------------------- +import gen_ir +import gen_util as util +import os, shutil, sys + +bindings_root = 'sokol-c3' +c_root = f'{bindings_root}/sokol/c' +module_root = f'{bindings_root}/sokol' + +# TODO: Consider chaning module names to something shorter. +# For example we could C prefixes, for example `sg` instead of current `gfx`. +module_names = { + 'slog_': 'log', + 'sg_': 'gfx', + 'sapp_': 'app', + 'stm_': 'time', + 'saudio_': 'audio', + 'sgl_': 'gl', + 'sdtx_': 'debugtext', + 'sshape_': 'shape', + 'sglue_': 'glue', +} + +c_source_names = { + 'slog_': 'sokol_log.c', + 'sg_': 'sokol_gfx.c', + 'sapp_': 'sokol_app.c', + 'sapp_sg': 'sokol_glue.c', + 'stm_': 'sokol_time.c', + 'saudio_': 'sokol_audio.c', + 'sgl_': 'sokol_gl.c', + 'sdtx_': 'sokol_debugtext.c', + 'sshape_': 'sokol_shape.c', + 'sglue_': 'sokol_glue.c', +} + +ignores = [ + 'sdtx_printf', + 'sdtx_vprintf', + 'sg_install_trace_hooks', + 'sg_trace_hooks', +] + +overrides = { + # `any` is treated specially in C3. + 'any': '_any', + # Constants must be uppercase - lowercase `x` is not allowed. + 'SG_PIXELFORMAT_ASTC_4x4_RGBA': 'SG_PIXELFORMAT_ASTC_4X4_RGBA', + 'SG_PIXELFORMAT_ASTC_4x4_SRGBA': 'SG_PIXELFORMAT_ASTC_4X4_SRGBA', +} + +prim_types = { + 'int': 'CInt', + # TODO: Check whether we should translate to `CBool` instead? + 'bool': 'bool', + 'char': 'char', + 'int8_t': 'ichar', + 'uint8_t': 'char', + 'int16_t': 'short', + 'uint16_t': 'ushort', + 'int32_t': 'int', + 'uint32_t': 'uint', + 'int64_t': 'long', + 'uint64_t': 'ulong', + 'float': 'float', + 'double': 'double', + 'uintptr_t': 'uptr', + 'intptr_t': 'iptr', + 'size_t': 'usz' +} + +prim_defaults = { + 'int': '0', + 'bool': 'false', + 'int8_t': '0', + 'uint8_t': '0', + 'int16_t': '0', + 'uint16_t': '0', + 'int32_t': '0', + 'uint32_t': '0', + 'int64_t': '0', + 'uint64_t': '0', + 'float': '0.0', + 'double': '0.0', + 'uintptr_t': '0', + 'intptr_t': '0', + 'size_t': '0' +} + +special_constant_types = { + "SG_INVALID_ID": "uint", + "SAPP_MODIFIER_SHIFT": "uint", + "SAPP_MODIFIER_CTRL": "uint", + "SAPP_MODIFIER_ALT": "uint", + "SAPP_MODIFIER_SUPER": "uint", + "SAPP_MODIFIER_LMB": "uint", + "SAPP_MODIFIER_RMB": "uint", + "SAPP_MODIFIER_MMB": "uint", +} + +# Aliases for function pointers. +# Function pointers must be aliased - we can't use them directly inside structs, +# instead we have to create an alias and use the alias in the struct. +aliases = { + # C type -> (Alias name, Right hand side of alias). + "void (*)(void *)": + ("DataCb", "fn void(void*)"), + "void *(*)(size_t, void *)": + ("AllocCb", "fn void*(usz, void*)"), + "void (*)(void *, void *)": + ("FreeCb", "fn void*(usz, void*)"), + "void (*)(const char *, uint32_t, uint32_t, const char *, uint32_t, const char *, void *)": + ("LogCb", "fn void(ZString, uint, uint, ZString, uint, ZString, void*)"), + "void (*)(void)": + ("Cb", "fn void()"), + "void (*)(const sapp_event *)": + ("EventCb", "fn void(Event*)"), + "void (*)(const sapp_event *, void *)": + ("EventDataCb", "fn void(Event*, void*)"), + "void (*)(const sapp_html5_fetch_response *)": + ("ResponseCb", "fn void(Html5FetchResponse*)"), + "void (*)(float *, int, int)": + ("StreamCb", "fn void(float*, CInt, CInt)"), + "void (*)(float *, int, int, void *)": + ("StreamDataCb", "fn void(float*, CInt, CInt, void*)"), +} + +struct_types = [] +enum_types = [] +enum_items = {} +# Which alias were used in current module. +# At the end of module we emit only used aliases. +used_aliases = [] # We shouldn't use `set()` because the order differs among runs. +out_lines = '' + +def reset_globals(): + global struct_types + global enum_types + global enum_items + global used_aliases + global out_lines + struct_types = [] + enum_types = [] + enum_items = {} + used_aliases = [] + out_lines = '' + +def l(s): + global out_lines + out_lines += s + '\n' + +def check_override(name, default=None): + if name in overrides: + return overrides[name] + elif default is None: + return name + else: + return default + +def check_ignore(name): + return name in ignores + +# PREFIX_BLA_BLUB to BLA_BLUB, prefix_bla_blub to bla_blub +def as_snake_case(s, prefix): + outp = s + if outp.lower().startswith(prefix): + outp = outp[len(prefix):] + return outp + +def get_c3_module_path(c_prefix): + return f'{module_root}' + +def get_csource_path(c_prefix): + return f'{c_root}/{c_source_names[c_prefix]}' + +def make_c3_module_directory(c_prefix): + path = get_c3_module_path(c_prefix) + if not os.path.isdir(path): + os.makedirs(path) + +def as_prim_type(s): + return prim_types[s] + +def as_upper_snake_case(s, prefix): + outp = s.lower() + if outp.startswith(prefix): + outp = outp[len(prefix):] + return outp.upper() + +def as_module_name_for_enum_type(enum_name, prefix): + parts = enum_name.lower().split('_') + parent_module = module_names[prefix] + if parts[-1] == 't': + # Ignore '_t' suffix. + module = "_".join(parts[1:-1]) + else: + module = "_".join(parts[1:]) + return f"sokol::{parent_module}::{module}" + +def as_parent_module_name_for_enum_type(enum_name, prefix): + parent_module = module_names[prefix] + return f"sokol::{parent_module}" + +# prefix_bla_blub(_t) => (dep::)BlaBlub +def as_struct_or_enum_type(s, prefix): + parts = s.lower().split('_') + outp = '' if s.startswith(prefix) else f'sokol::{module_names[parts[0]+"_"]}::' + for part in parts[1:]: + # ignore '_t' type postfix + if part != 't': + outp += part.capitalize() + return outp + +# PREFIX_ENUM_BLA_BLUB => BLA_BLUB, _PREFIX_ENUM_BLA_BLUB => BLA_BLUB +def as_enum_item_name(s): + outp = s.lstrip('_') + parts = outp.split('_')[2:] + outp = '_'.join(parts) + if outp[0].isdigit(): + outp = 'NUM_' + outp + return outp + +def is_prim_type(s): + return s in prim_types + +def is_int_type(s): + return s == "int" + +def is_struct_type(s): + return s in struct_types + +def is_enum_type(s): + return s in enum_types + +def is_const_prim_ptr(s): + for prim_type in prim_types: + if s == f"const {prim_type} *": + return True + return False + +def is_prim_ptr(s): + for prim_type in prim_types: + if s == f"{prim_type} *": + return True + return False + +def is_const_struct_ptr(s): + for struct_type in struct_types: + if s == f"const {struct_type} *": + return True + return False + +def type_default_value(s): + return prim_defaults[s] + +def map_type(type, prefix, sub_type): + if sub_type not in ['c_arg', 'struct_field']: + sys.exit(f"Error: map_type(): unknown sub_type '{sub_type}") + if type == "void": + return "" + elif is_prim_type(type): + return as_prim_type(type) + elif is_struct_type(type): + return as_struct_or_enum_type(type, prefix) + elif is_enum_type(type): + return as_struct_or_enum_type(type, prefix) + elif util.is_void_ptr(type): + return "void*" + elif util.is_const_void_ptr(type): + return "void*" + elif util.is_string_ptr(type): + return "ZString" + elif is_const_struct_ptr(type): + return f"{as_struct_or_enum_type(util.extract_ptr_type(type), prefix)}*" + elif is_prim_ptr(type): + return f"{as_prim_type(util.extract_ptr_type(type))}*" + elif is_const_prim_ptr(type): + return f"{as_prim_type(util.extract_ptr_type(type))}*" + elif util.is_1d_array_type(type): + array_type = util.extract_array_type(type) + array_sizes = util.extract_array_sizes(type) + return f"{map_type(array_type, prefix, sub_type)}[{array_sizes[0]}]" + elif util.is_2d_array_type(type): + array_type = util.extract_array_type(type) + array_sizes = util.extract_array_sizes(type) + # TODO: Check if the dimensions are in correct order. + return f"{map_type(array_type, prefix, sub_type)}[{array_sizes[0]}][{array_sizes[1]}]" + elif util.is_func_ptr(type): + if type in aliases: + alias_name, _ = aliases[type] + if type not in used_aliases: + used_aliases.append(type) + return alias_name + else: + sys.exit(f"Error map_type(): missing alias for function pointer '{type}'") + else: + sys.exit(f"Error map_type(): unknown type '{type}'") + +def funcdecl_args_c(decl, prefix): + s = '' + func_name = decl['name'] + for param_decl in decl['params']: + if s != '': + s += ', ' + param_name = param_decl['name'] + param_type = check_override(f'{func_name}.{param_name}', default=param_decl['type']) + s += f"{map_type(param_type, prefix, 'c_arg')} {param_name}" + return s + +def funcdecl_result_c(decl, prefix): + func_name = decl['name'] + decl_type = decl['type'] + res_c_type = decl_type[:decl_type.index('(')].strip() + return map_type(check_override(f'{func_name}.RESULT', default=res_c_type), prefix, 'c_arg') + +def gen_c_imports(inp, c_prefix, prefix): + prefix = inp['prefix'] + for decl in inp['decls']: + if decl['kind'] == 'func' and not decl['is_dep'] and not check_ignore(decl['name']): + args = funcdecl_args_c(decl, prefix) + res_type = funcdecl_result_c(decl, prefix) + res_str = 'void' if res_type == '' else res_type + l(f'extern fn {res_str} {check_override(as_snake_case(decl["name"], c_prefix))}({args}) @extern("{decl["name"]}");') + l('') + +def gen_consts(decl, prefix): + for item in decl["items"]: + # + # TODO: What type should these constants have? Currently giving all `usz` + # unless specifically overridden by `special_constant_types` + # + + item_name = check_override(item["name"]) + tpe = "usz" + if item_name in special_constant_types: + tpe = special_constant_types[item_name] + l(f"const {tpe} {as_upper_snake_case(item_name, prefix)} = {item['value']};") + l('') + +def gen_struct(decl, prefix): + c_struct_name = check_override(decl['name']) + struct_name = as_struct_or_enum_type(c_struct_name, prefix) + l(f'struct {struct_name}') + l('{') + for field in decl['fields']: + field_name = check_override(field['name']) + field_type = map_type(check_override(f'{c_struct_name}.{field_name}', default=field['type']), prefix, 'struct_field') + l(f' {field_type} {field_name};') + l('}') + l('') + +def gen_enum(decl, prefix): + enum_name = check_override(decl['name']) + tpe = "int" + if any(as_enum_item_name(check_override(item['name'])) == 'FORCE_U32' for item in decl['items']): + tpe = "uint" + l(f'distinct {as_struct_or_enum_type(enum_name, prefix)} = {tpe};') + # Constants are in submodule. + l(f'module {as_module_name_for_enum_type(enum_name, prefix)};') + value = "-1" + for item in decl['items']: + item_name = as_enum_item_name(check_override(item['name'])) + if item_name != 'FORCE_U32': + if 'value' in item: + value = item['value'] + else: + value = str(int(value) + 1) + l(f"const {as_struct_or_enum_type(enum_name, prefix)} {item_name} = {value};") + l(f'module {as_parent_module_name_for_enum_type(enum_name, prefix)};') + # After reopening the original module all dependencies must be reimported. + l(f'import sokol;') + l('') + +def gen_imports(dep_prefixes): + l(f'import sokol;') + l('') + +def gen_function_pointer_aliases(): + for type in used_aliases: + alias_name, right_hand_side = aliases[type] + l(f'def {alias_name} = {right_hand_side};') + l('') + +def gen_module(inp, c_prefix, dep_prefixes): + pre_parse(inp) + l('// machine generated, do not edit') + l('') + l(f"module sokol::{module_names[c_prefix]};") + gen_imports(dep_prefixes) + prefix = inp['prefix'] + gen_c_imports(inp, c_prefix, prefix) + for decl in inp['decls']: + if not decl['is_dep']: + kind = decl['kind'] + if kind == 'consts': + gen_consts(decl, prefix) + elif not check_ignore(decl['name']): + if kind == 'struct': + gen_struct(decl, prefix) + elif kind == 'enum': + gen_enum(decl, prefix) + gen_function_pointer_aliases() + +def pre_parse(inp): + global struct_types + global enum_types + for decl in inp['decls']: + kind = decl['kind'] + if kind == 'struct': + struct_types.append(decl['name']) + elif kind == 'enum': + enum_name = decl['name'] + enum_types.append(enum_name) + enum_items[enum_name] = [] + for item in decl['items']: + enum_items[enum_name].append(as_enum_item_name(item['name'])) + +def prepare(): + print('=== Generating C3 bindings:') + if not os.path.isdir(module_root): + os.makedirs(module_root) + if not os.path.isdir(c_root): + os.makedirs(c_root) + +def gen(c_header_path, c_prefix, dep_c_prefixes): + if not c_prefix in module_names: + print(f' >> warning: skipping generation for {c_prefix} prefix...') + return + reset_globals() + make_c3_module_directory(c_prefix) + print(f' {c_header_path} => {module_names[c_prefix]}') + shutil.copyfile(c_header_path, f'{c_root}/{os.path.basename(c_header_path)}') + csource_path = get_csource_path(c_prefix) + module_name = module_names[c_prefix] + ir = gen_ir.gen(c_header_path, csource_path, module_name, c_prefix, dep_c_prefixes) + gen_module(ir, c_prefix, dep_c_prefixes) + with open(f"{module_root}/{ir['module']}.c3", 'w', newline='\n') as f_outp: + f_outp.write(out_lines)