Parcourir la source

Add support for C3 to bindgen

Radek Micek il y a 8 mois
Parent
commit
80a1919101
5 fichiers modifiés avec 498 ajouts et 1 suppressions
  1. 44 0
      .github/workflows/gen_bindings.yml
  2. 1 0
      bindgen/.gitignore
  3. 1 0
      bindgen/README.md
  4. 7 1
      bindgen/gen_all.py
  5. 445 0
      bindgen/gen_c3.py

+ 44 - 0
.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: floooh/sokol-c3
+          path: bindgen/sokol-c3
       - name: generate
         run: |
           cd bindgen
@@ -108,6 +112,12 @@ jobs:
           name: ignore-me-jai
           retention-days: 1
           path: bindgen/sokol-jai/sokol
+      - name: upload-c3-artifact
+        uses: actions/upload-artifact@main
+        with:
+          name: ignore-me-c3
+          retention-days: 1
+          path: bindgen/sokol-c3/sokol.c3l
 
   test-zig:
     needs: gen-bindings
@@ -323,6 +333,40 @@ jobs:
       - name: build
         run: echo "run jai here"
 
+  test-c3:
+    needs: gen-bindings
+    strategy:
+      # Continue build even if some platforms fail.
+      fail-fast: false
+      matrix:
+        include:
+          - os: macos-latest
+            arch: arm64
+          - os: ubuntu-latest
+            arch: x64
+          - os: windows-latest
+            arch: x64
+    runs-on: ${{matrix.os}}
+    steps:
+      - uses: actions/checkout@main
+        with:
+          repository: floooh/sokol-c3
+      - uses: radekm/setup-c3@v2
+        with:
+          version: v0.6.6
+      - uses: actions/download-artifact@main
+        with:
+          name: ignore-me-c3
+          path: sokol.c3l
+      - name: prepare
+        if: runner.os == 'Linux'
+        run: |
+            sudo apt-get update
+            sudo apt-get install libgl1-mesa-dev libegl1-mesa-dev mesa-common-dev xorg-dev libasound-dev
+      - name: build
+        shell: bash
+        run: ./build-examples.sh
+
   # only deploy the bindings for commits on the main branch
   deploy-zig:
     needs: test-zig

+ 1 - 0
bindgen/.gitignore

@@ -8,3 +8,4 @@ sokol-odin/
 sokol-rust/
 sokol-d/
 sokol-jai/
+sokol-c3/

+ 1 - 0
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/floooh/sokol-c3
 > python3 gen_all.py
 ```
 

+ 7 - 1
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_',     [] ],
@@ -58,3 +58,9 @@ gen_rust.prepare()
 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)

+ 445 - 0
bindgen/gen_c3.py

@@ -0,0 +1,445 @@
+#-------------------------------------------------------------------------------
+#   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.c3l/c'
+module_root = f'{bindings_root}/sokol.c3l'
+
+# TODO: Consider chaning module names to something shorter.
+#       For example we could C prefixes, for example `sg` instead of current `gfx`.
+module_names = {
+    'slog_':    'slog',
+    'sg_':      'sg',
+    'sapp_':    'sapp',
+    'stm_':     'stm',
+    'saudio_':  'saudio',
+    'sgl_':     'sgl',
+    'sdtx_':    'sdtx',
+    'sshape_':  'sshape',
+    'sglue_':   'sglue',
+}
+
+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():
+        if outp in ["2D", "3D"]:
+            outp = "TYPE_" + outp
+        else:
+            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)