Browse Source

Integrate automatic sokol-rust bindings generation. (#811)

Andre Weissflog 2 năm trước cách đây
mục cha
commit
ddc36813b9
6 tập tin đã thay đổi với 980 bổ sung20 xóa
  1. 2 1
      .editorconfig
  2. 80 8
      .github/workflows/gen_bindings.yml
  3. 1 1
      README.md
  4. 1 0
      bindgen/.gitignore
  5. 10 10
      bindgen/gen_all.py
  6. 886 0
      bindgen/gen_rust.py

+ 2 - 1
.editorconfig

@@ -5,4 +5,5 @@ indent_size=4
 trim_trailing_whitespace=true
 insert_final_newline=true
 
-
+[*.yml]
+indent_size=2

+ 80 - 8
.github/workflows/gen_bindings.yml

@@ -4,8 +4,8 @@ on: [push, pull_request]
 
 jobs:
   test-windows:
-      runs-on: windows-latest
-      steps:
+    runs-on: windows-latest
+    steps:
       - uses: actions/checkout@v3
       - name: test_win
         run: |
@@ -14,8 +14,8 @@ jobs:
         shell: cmd
 
   test-mac:
-      runs-on: macos-latest
-      steps:
+    runs-on: macos-latest
+    steps:
       - uses: actions/checkout@v3
       - uses: seanmiddleditch/gha-setup-ninja@master
       - name: test_macos
@@ -24,8 +24,8 @@ jobs:
           ./test_macos.sh
 
   test-linux:
-      runs-on: ubuntu-latest
-      steps:
+    runs-on: ubuntu-latest
+    steps:
       - uses: actions/checkout@v3
       - uses: seanmiddleditch/gha-setup-ninja@master
       - name: prepare
@@ -54,6 +54,10 @@ jobs:
         with:
           repository: floooh/sokol-odin
           path: bindgen/sokol-odin
+      - uses: actions/checkout@v3
+        with:
+          repository: floooh/sokol-rust
+          path: bindgen/sokol-rust
       - name: generate
         run: |
           cd bindgen
@@ -62,21 +66,28 @@ jobs:
         uses: actions/upload-artifact@v3
         with:
           name: ignore-me-zig
-          path: bindgen/sokol-zig/src/sokol
           retention-days: 1
+          path: bindgen/sokol-zig/src/sokol
       - name: upload-nim-artifact
         uses: actions/upload-artifact@v3
         with:
           name: ignore-me-nim
-          path: bindgen/sokol-nim/src/sokol
           retention-days: 1
+          path: bindgen/sokol-nim/src/sokol
       - name: upload-odin-artifact
         uses: actions/upload-artifact@v3
         with:
           name: ignore-me-odin
+          retention-days: 1
           path: |
             bindgen/sokol-odin/sokol
             bindgen/sokol-odin/c
+      - name: upload-rust-artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: ignore-me-rust
+          retention-days: 1
+          path: bindgen/sokol-rust/src
 
   test-zig:
     needs: gen-bindings
@@ -227,6 +238,36 @@ jobs:
           windows_artifacts/odin build examples/sgl-points -debug
           windows_artifacts/odin build examples/sgl-context -debug
 
+  test-rust:
+    needs: gen-bindings
+    env:
+      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+    runs-on: ${{matrix.os}}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          repository: floooh/sokol-rust
+          submodules: recursive
+      - uses: actions/download-artifact@v3
+        with:
+          name: ignore-me-rust
+          path: src/sokol
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          override: true
+      - name: prepare-linux
+        if: runner.os == 'Linux'
+        run: |
+            sudo apt-get update
+            sudo apt-get install libglu1-mesa-dev mesa-common-dev xorg-dev libasound-dev
+      - name: Run cargo build --all-targets
+        run: cargo build
+
   # only deploy the bindings for commits on the main branch
   deploy-zig:
     needs: test-zig
@@ -289,3 +330,34 @@ jobs:
           git add -A
           git diff-index --quiet HEAD || git commit -m "updated (https://github.com/floooh/sokol/commit/${{ github.sha }})"
           git push
+
+  deploy-rust:
+    needs: test-rust
+    env:
+      CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
+    if: github.ref == 'refs/heads/master'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          repository: floooh/sokol-rust
+          submodules: recursive
+          ssh-key: ${{ secrets.GHACTIONS_RUST_PUSH }}
+      - uses: actions/download-artifact@v3
+        with:
+          name: ignore-me-rust
+          path: src/sokol
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          override: true
+      - name: "cargo fmt"
+        run: cargo fmt
+      - name: "commit and push"
+        run: |
+          git config user.email "none"
+          git config user.name "GH Action"
+          git add -A
+          git diff-index --quiet HEAD || git commit -m "updated (https://github.com/floooh/sokol/commit/${{ github.sha }})"
+          echo "FIXME: GIT PUSH"

+ 1 - 1
README.md

@@ -7,7 +7,7 @@ cross-platform libraries for C and C++, written in C.
 [**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**20-Feb-2023** a new set of functions in sokol_gfx.h
 to get a pre-filled 'desc struct' for a resource)
 
-[![Build](/../../actions/workflows/main.yml/badge.svg)](/../../actions/workflows/main.yml) [![Bindings](/../../actions/workflows/gen_bindings.yml/badge.svg)](/../../actions/workflows/gen_bindings.yml) [![build](https://github.com/floooh/sokol-zig/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-zig/actions/workflows/main.yml) [![build](https://github.com/floooh/sokol-nim/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-nim/actions/workflows/main.yml) [![Odin](https://github.com/floooh/sokol-odin/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-odin/actions/workflows/main.yml)
+[![Build](/../../actions/workflows/main.yml/badge.svg)](/../../actions/workflows/main.yml) [![Bindings](/../../actions/workflows/gen_bindings.yml/badge.svg)](/../../actions/workflows/gen_bindings.yml) [![build](https://github.com/floooh/sokol-zig/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-zig/actions/workflows/main.yml) [![build](https://github.com/floooh/sokol-nim/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-nim/actions/workflows/main.yml) [![Odin](https://github.com/floooh/sokol-odin/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-odin/actions/workflows/main.yml)[![Rust](https://github.com/floooh/sokol-rust/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-rust/actions/workflows/main.yml)
 
 ## Examples and Related Projects
 

+ 1 - 0
bindgen/.gitignore

@@ -5,3 +5,4 @@ __pycache__/
 sokol-nim/
 sokol-zig/
 sokol-odin/
+sokol-rust/

+ 10 - 10
bindgen/gen_all.py

@@ -1,4 +1,4 @@
-import os, gen_nim, gen_zig, gen_odin
+import os, gen_nim, gen_zig, gen_odin, gen_rust
 
 tasks = [
     [ '../sokol_log.h',            'slog_',     [] ],
@@ -15,23 +15,23 @@ tasks = [
 # Odin
 gen_odin.prepare()
 for task in tasks:
-    c_header_path = task[0]
-    main_prefix = task[1]
-    dep_prefixes = task[2]
+    [c_header_path, main_prefix, dep_prefixes] = task
     gen_odin.gen(c_header_path, main_prefix, dep_prefixes)
 
 # Nim
 gen_nim.prepare()
 for task in tasks:
-    c_header_path = task[0]
-    main_prefix = task[1]
-    dep_prefixes = task[2]
+    [c_header_path, main_prefix, dep_prefixes] = task
     gen_nim.gen(c_header_path, main_prefix, dep_prefixes)
 
 # Zig
 gen_zig.prepare()
 for task in tasks:
-    c_header_path = task[0]
-    main_prefix = task[1]
-    dep_prefixes = task[2]
+    [c_header_path, main_prefix, dep_prefixes] = task
     gen_zig.gen(c_header_path, main_prefix, dep_prefixes)
+
+# Rust
+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)

+ 886 - 0
bindgen/gen_rust.py

@@ -0,0 +1,886 @@
+# -------------------------------------------------------------------------------
+#   Generate rust bindings.
+#
+#   rust coding style:
+#   - types are PascalCase
+#   - otherwise snake_case
+# -------------------------------------------------------------------------------
+import gen_ir
+import os, shutil, sys
+
+import gen_util as util
+
+module_names = {
+    "slog_": "log",
+    "sg_": "gfx",
+    "sapp_": "app",
+    "stm_": "time",
+    "saudio_": "audio",
+    "sgl_": "gl",
+    "sdtx_": "debugtext",
+    "sshape_": "shape",
+    "sapp_sg": "glue",
+    "simgui_": "imgui",
+    "sg_imgui_": "gfx_imgui",
+}
+
+module_requires_rust_feature = {
+    module_names["simgui_"]: "imgui",
+    module_names["sg_imgui_"]: "imgui",
+}
+
+c_source_paths = {
+    "slog_": "sokol-rust/src/sokol/c/sokol_log.c",
+    "sg_": "sokol-rust/src/sokol/c/sokol_gfx.c",
+    "sapp_": "sokol-rust/src/sokol/c/sokol_app.c",
+    "stm_": "sokol-rust/src/sokol/c/sokol_time.c",
+    "saudio_": "sokol-rust/src/sokol/c/sokol_audio.c",
+    "sgl_": "sokol-rust/src/sokol/c/sokol_gl.c",
+    "sdtx_": "sokol-rust/src/sokol/c/sokol_debugtext.c",
+    "sshape_": "sokol-rust/src/sokol/c/sokol_shape.c",
+    "sapp_sg": "sokol-rust/src/sokol/c/sokol_glue.c",
+    "simgui_": "sokol-rust/src/sokol/c/sokol_imgui.c",
+    "sg_imgui_": "sokol-rust/src/sokol/c/sokol_gfx_imgui.c",
+}
+
+ignores = [
+    "sdtx_printf",
+    "sdtx_vprintf",
+    # "sg_install_trace_hooks",
+    # "sg_trace_hooks",
+]
+
+range_struct_name = "Range"
+
+# functions that need to be exposed as 'raw' C callbacks without a rust wrapper function
+c_callbacks = ["slog_func"]
+
+# NOTE: syntax for function results: "func_name.RESULT"
+overrides = {
+    "type": "_type",
+    "ref": "_ref",
+
+    "sg_apply_uniforms.ub_index": "uintptr_t",
+    "sg_draw.base_element": "uintptr_t",
+    "sg_draw.num_elements": "uintptr_t",
+    "sg_draw.num_instances": "uintptr_t",
+    "sshape_element_range_t.base_element": "uintptr_t",
+    "sshape_element_range_t.num_elements": "uintptr_t",
+    "sdtx_font.font_index": "uintptr_t",
+
+    "sdtx_move": "sdtx_move_cursor",
+    "sdtx_move_x": "sdtx_move_cursor_x",
+    "sdtx_move_y": "sdtx_move_cursor_y",
+
+    "sg_image_type::SG_IMAGETYPE_2D": "SG_IMAGEYPE_DIM2",
+    "sg_image_type::SG_IMAGETYPE_3D": "SG_IMAGETYPE_DIM3",
+
+    "sapp_keycode::SAPP_KEYCODE_0": "SAPP_KEYCODE_NUM0",
+    "sapp_keycode::SAPP_KEYCODE_1": "SAPP_KEYCODE_NUM1",
+    "sapp_keycode::SAPP_KEYCODE_2": "SAPP_KEYCODE_NUM2",
+    "sapp_keycode::SAPP_KEYCODE_3": "SAPP_KEYCODE_NUM3",
+    "sapp_keycode::SAPP_KEYCODE_4": "SAPP_KEYCODE_NUM4",
+    "sapp_keycode::SAPP_KEYCODE_5": "SAPP_KEYCODE_NUM5",
+    "sapp_keycode::SAPP_KEYCODE_6": "SAPP_KEYCODE_NUM6",
+    "sapp_keycode::SAPP_KEYCODE_7": "SAPP_KEYCODE_NUM7",
+    "sapp_keycode::SAPP_KEYCODE_8": "SAPP_KEYCODE_NUM8",
+    "sapp_keycode::SAPP_KEYCODE_9": "SAPP_KEYCODE_NUM9",
+
+    # "sgl_error": "sgl_get_error",  # 'error' is reserved in zig
+    # "sgl_deg": "sgl_as_degrees",
+    # "sgl_rad": "sgl_as_radians",
+    # "sg_context_desc.color_format": "int",
+    # "SGL_NO_ERROR": "SGL_ERROR_NO_ERROR",
+    # "sg_context_desc.depth_format": "int",
+}
+
+prim_types = {
+    "int": "i32",
+    "bool": "bool",
+    "char": "core::ffi::c_char",
+    "int8_t": "i8",
+    "uint8_t": "u8",
+    "int16_t": "i16",
+    "uint16_t": "u16",
+    "int32_t": "i32",
+    "uint32_t": "u32",
+    "int64_t": "i64",
+    "uint64_t": "u64",
+    "float": "f32",
+    "double": "f64",
+    "uintptr_t": "usize",
+    "intptr_t": "isize",
+    "size_t": "usize",
+}
+
+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",
+    "char": "0",
+}
+
+special_constant_types = {
+    "SG_INVALID_ID": "u32"
+}
+
+struct_types = []
+enum_types = []
+enum_items = {}
+out_lines = ""
+
+
+def reset_globals():
+    global struct_types
+    global enum_types
+    global enum_items
+    global out_lines
+    struct_types = []
+    enum_types = []
+    enum_items = {}
+    out_lines = ""
+
+
+def l(s):
+    global out_lines
+    out_lines += s + "\n"
+
+
+def as_rust_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()
+
+
+# prefix_bla_blub(_t) => (dep::)BlaBlub
+def as_rust_struct_type(s, prefix):
+    parts = s.lower().split("_")
+    outp = "" if s.startswith(prefix) else f"{parts[0]}::"
+    for part in parts[1:]:
+        # ignore '_t' type postfix
+        if part != "t":
+            outp += part.capitalize()
+    return outp
+
+
+# prefix_bla_blub(_t) => (dep::)BlaBlub
+def as_rust_enum_type(s, prefix):
+    parts = s.lower().split("_")
+    outp = "" if s.startswith(prefix) else f"{parts[0]}::"
+    for part in parts[1:]:
+        # ignore '_t' type postfix
+        if part != "t":
+            outp += part.capitalize()
+    return outp
+
+
+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_ENUM_BLA_BLA => BlaBla, _PREFIX_ENUM_BLA_BLA => BlaBla
+def as_enum_item_name(s):
+    parts = s.lstrip("_").split("_")
+    outp = ""
+    for i, part in enumerate(parts[2:]):
+        # TODO: What to do with enum fields starting with numbers?
+        outp += part.capitalize()
+
+    return outp
+
+
+def enum_default_item(enum_name):
+    return enum_items[enum_name][0]
+
+
+def is_prim_type(s):
+    return s in prim_types
+
+
+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 is_struct_ptr(s):
+    for struct_type in struct_types:
+        if s == f"{struct_type} *":
+            return True
+    return False
+
+
+def type_default_value(s):
+    return prim_defaults[s]
+
+
+def as_c_arg_type(arg_prefix, arg_type, prefix):
+    # NOTE: if arg_prefix is None, the result is used as return value
+    pre = "" if arg_prefix is None else arg_prefix
+
+    if arg_type == "void":
+        return ""
+    elif is_prim_type(arg_type):
+        return pre + as_rust_prim_type(arg_type)
+    elif is_struct_type(arg_type):
+        return pre + as_rust_struct_type(arg_type, prefix)
+    elif is_enum_type(arg_type):
+        return pre + as_rust_enum_type(arg_type, prefix)
+    elif util.is_void_ptr(arg_type):
+        return pre + "*mut core::ffi::c_void"
+    elif util.is_const_void_ptr(arg_type):
+        return pre + "*const core::ffi::c_void"
+    elif util.is_string_ptr(arg_type):
+        return pre + "*const core::ffi::c_char"
+    elif is_const_struct_ptr(arg_type):
+        return pre + f"*const {as_rust_struct_type(util.extract_ptr_type(arg_type), prefix)}"
+    elif is_struct_ptr(arg_type):
+        return pre + f"*mut {as_rust_struct_type(util.extract_ptr_type(arg_type), prefix)}"
+    elif is_prim_ptr(arg_type):
+        return pre + f"*mut {as_rust_prim_type(util.extract_ptr_type(arg_type))}"
+    elif is_const_prim_ptr(arg_type):
+        return pre + f"*const {as_rust_prim_type(util.extract_ptr_type(arg_type))}"
+    else:
+        sys.exit(f"ERROR as_c_arg_type(): {arg_type}")
+
+
+def as_rust_arg_type(arg_prefix, arg_type, prefix):
+    # NOTE: if arg_prefix is None, the result is used as return value
+    pre = "" if arg_prefix is None else arg_prefix
+
+    if arg_type == "void":
+        return ""
+    elif is_prim_type(arg_type):
+        return pre + as_rust_prim_type(arg_type)
+    elif is_struct_type(arg_type):
+        return pre + as_rust_struct_type(arg_type, prefix)
+    elif is_enum_type(arg_type):
+        return pre + as_rust_enum_type(arg_type, prefix)
+    elif util.is_void_ptr(arg_type):
+        return pre + "*mut core::ffi::c_void"
+    elif util.is_const_void_ptr(arg_type):
+        return pre + "*const core::ffi::c_void"
+    elif util.is_string_ptr(arg_type):
+        return pre + "&str"
+    elif is_const_struct_ptr(arg_type):
+        return pre + f"&{as_rust_struct_type(util.extract_ptr_type(arg_type), prefix)}"
+    elif is_struct_ptr(arg_type):
+        return pre + f"&mut {as_rust_struct_type(util.extract_ptr_type(arg_type), prefix)}"
+    elif is_prim_ptr(arg_type):
+        return pre + f"&mut {as_rust_prim_type(util.extract_ptr_type(arg_type))}"
+    elif is_const_prim_ptr(arg_type):
+        return pre + f"&{as_rust_prim_type(util.extract_ptr_type(arg_type))}"
+    else:
+        sys.exit(f"ERROR as_rust_arg_type(): {arg_type}")
+
+
+def is_rust_string(rust_type):
+    return rust_type == "&str" or rust_type == " -> &'static str"
+
+
+# get C-style arguments of a function pointer as string
+def funcptr_args_c(field_type, prefix):
+    tokens = field_type[field_type.index("(*)") + 4: -1].split(",")
+    s = ""
+    for token in tokens:
+        arg_type = token.strip()
+        if s != "":
+            s += ", "
+        c_arg = as_c_arg_type(None, arg_type, prefix)
+        if c_arg == "void":
+            return ""
+        else:
+            s += c_arg
+    return s
+
+
+# get C-style result of a function pointer as string
+def funcptr_result_c(field_type):
+    res_type = field_type[: field_type.index("(*)")].strip()
+    if res_type == "void":
+        return ""
+    elif util.is_const_void_ptr(res_type):
+        return " -> *const core::ffi::c_void"
+    elif util.is_void_ptr(res_type):
+        return " -> *mut core::ffi::c_void"
+    else:
+        sys.exit(f"ERROR funcptr_result_c(): {field_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"{as_c_arg_type(f'{param_name}: ', param_type, prefix)}"
+    return s
+
+
+def funcdecl_args_rust(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"{as_rust_arg_type(f'{param_name}: ', param_type, prefix)}"
+    return s
+
+
+def funcdecl_result_c(decl, prefix):
+    func_name = decl["name"]
+    decl_type = decl["type"]
+    result_type = check_override(
+        f"{func_name}.RESULT", default=decl_type[: decl_type.index("(")].strip()
+    )
+
+    it = as_c_arg_type(None, result_type, prefix)
+    if it == "()" or it == "":
+        return ""
+    else:
+        return f" -> {it}"
+
+
+def funcdecl_result_rust(decl, prefix):
+    func_name = decl["name"]
+    decl_type = decl["type"]
+    result_type = check_override(
+        f"{func_name}.RESULT", default=decl_type[: decl_type.index("(")].strip()
+    )
+    rust_res_type = as_rust_arg_type(None, result_type, prefix)
+
+    if is_rust_string(rust_res_type):
+        rust_res_type = "&'static str"
+
+    if rust_res_type == "":
+        return ""
+    else:
+        return f" -> {rust_res_type }"
+
+
+def gen_struct(decl, prefix):
+    struct_name = check_override(decl["name"])
+    rust_type = as_rust_struct_type(struct_name, prefix)
+    rust_struct_type = rust_type
+
+    struct_lines = []
+    default_lines = []
+
+    for field in decl["fields"]:
+        field_name = check_override(field["name"])
+        field_type = check_override(
+            f"{struct_name}.{field_name}", default=field["type"]
+        )
+
+        if is_prim_type(field_type):
+            struct_lines.append(
+                f"pub {field_name}: {as_rust_prim_type(field_type)}"
+            )
+            default_lines.append(
+                f"{field_name}: {type_default_value(field_type)}"
+            )
+        elif is_struct_type(field_type):
+            struct_lines.append(
+                f"pub {field_name}: {as_rust_struct_type(field_type, prefix)}"
+            )
+            default_lines.append(
+                f"{field_name}: {as_rust_struct_type(field_type, prefix)}::new()"
+            )
+        elif is_enum_type(field_type):
+            struct_lines.append(
+                f"pub {field_name}: {as_rust_enum_type(field_type, prefix)}"
+            )
+            default_lines.append(
+                f"{field_name}: {as_rust_enum_type(field_type, prefix)}::new()"
+            )
+        elif util.is_string_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *const core::ffi::c_char"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null()"
+            )
+        elif util.is_const_void_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *const core::ffi::c_void"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null()"
+            )
+        elif util.is_void_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *mut core::ffi::c_void"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null_mut()"
+            )
+        elif is_const_prim_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *const {as_rust_prim_type(util.extract_ptr_type(field_type))}"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null()"
+            )
+        elif is_prim_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *mut {as_rust_prim_type(util.extract_ptr_type(field_type))}"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null_mut()"
+            )
+        elif is_const_struct_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *const {as_rust_struct_type(util.extract_ptr_type(field_type), prefix)}"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null()"
+            )
+        elif is_struct_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: *mut {as_rust_struct_type(util.extract_ptr_type(field_type), prefix)}"
+            )
+            default_lines.append(
+                f"{field_name}: core::ptr::null_mut()"
+            )
+        elif util.is_func_ptr(field_type):
+            struct_lines.append(
+                f"pub {field_name}: Option<extern \"C\" fn({funcptr_args_c(field_type, prefix)}){funcptr_result_c(field_type)}>"
+            )
+            default_lines.append(
+                f"{field_name}: None"
+            )
+        elif util.is_1d_array_type(field_type):
+            array_type = util.extract_array_type(field_type)
+            array_sizes = util.extract_array_sizes(field_type)
+            if is_prim_type(array_type) or is_struct_type(array_type):
+                if is_prim_type(array_type):
+                    rust_type = as_rust_prim_type(array_type)
+                    def_val = type_default_value(array_type)
+                elif is_struct_type(array_type) or is_enum_type(array_type):
+                    rust_type = as_rust_struct_type(array_type, prefix)
+                    def_val = f"{rust_type}::new()"
+                else:
+                    sys.exit(f"ERROR gen_struct is_1d_array_type: {array_type}")
+                t0 = f"[{rust_type}; {array_sizes[0]}]"
+                # t1 = f"&{rust_type}"
+                struct_lines.append(
+                    f"pub {field_name}: {t0}"
+                )
+                default_lines.append(
+                    f"{field_name}: [{def_val}; {array_sizes[0]}]"
+                )
+            elif util.is_const_void_ptr(array_type):
+                struct_lines.append(
+                    f"pub {field_name}: [*const core::ffi::c_void; {array_sizes[0]}]"
+                )
+                default_lines.append(
+                    f"{field_name}: [core::ptr::null(); {array_sizes[0]}]"
+                )
+            else:
+                sys.exit(
+                    f"ERROR gen_struct: array {field_name}: {field_type} => [{array_type}: {array_sizes[0]}]"
+                )
+        elif util.is_2d_array_type(field_type):
+            array_type = util.extract_array_type(field_type)
+            array_sizes = util.extract_array_sizes(field_type)
+
+            if is_prim_type(array_type):
+                rust_type = as_rust_prim_type(array_type)
+                def_val = type_default_value(array_type)
+            elif is_struct_type(array_type):
+                rust_type = as_rust_struct_type(array_type, prefix)
+                def_val = f"{rust_type}::new()"
+            else:
+                sys.exit(f"ERROR gen_struct is_2d_array_type: {array_type}")
+
+            struct_lines.append(
+                f"pub {field_name}: [[{rust_type}; {array_sizes[1]}]; {array_sizes[0]}]"
+            )
+            default_lines.append(
+                f"{field_name}: [[{def_val}; {array_sizes[1]}]; {array_sizes[0]}]"
+            )
+        else:
+            sys.exit(f"ERROR gen_struct: {field_name}: {field_type};")
+
+    #
+    # TODO: Is this the best way to have zero-initialization with support for constants?
+    #       core::mem::zeroed() cleaner?
+    #
+
+    l("#[repr(C)]")
+    l("#[derive(Copy, Clone, Debug)]")
+    l(f"pub struct {rust_struct_type} {{")
+    for line in struct_lines:
+        l(f"    {line},")
+    l("}")
+
+    l(f"impl {rust_struct_type} {{")
+    l("    pub const fn new() -> Self {")
+    l("        Self {")
+    for line in default_lines:
+        l(f"            {line},")
+    l("        }")
+    l("    }")
+    l("}")
+
+    l(f"impl Default for {rust_struct_type} {{")
+    l("    fn default() -> Self {")
+    l("        Self::new()")
+    l("    }")
+    l("}")
+
+
+def gen_consts(decl, prefix):
+    for item in decl["items"]:
+        #
+        # TODO: What type should these constants have? Currently giving all `usize`
+        #       unless specifically overriden by `special_constant_types`
+        #
+
+        item_name = check_override(item["name"])
+        if item_name in special_constant_types:
+            special_type = special_constant_types[item_name]
+            l(f"pub const {as_upper_snake_case(item_name, prefix)}: {special_type} = {item['value']};")
+        else:
+            l(f"pub const {as_upper_snake_case(item_name, prefix)}: usize = {item['value']};")
+
+
+def gen_enum(decl, prefix):
+    enum_name = check_override(decl["name"])
+
+    names = [
+        as_enum_item_name(check_override(f"{decl['name']}::{item['name']}", item['name'])) for item in decl["items"]
+    ]
+
+    is_u32 = False
+    for name in names:
+        if name == "ForceU32":
+            is_u32 = True
+            break
+
+    l("#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]")
+    if is_u32:
+        l("#[repr(u32)]")
+    else:
+        l("#[repr(i32)]")
+
+    rust_enum_name = as_rust_enum_type(enum_name, prefix)
+
+    l(f"pub enum {rust_enum_name} {{")
+    for item_name, item in zip(names, decl["items"]):
+        if item_name != "ForceU32":
+            if "value" in item:
+                l(f"    {item_name} = {item['value']},")
+            else:
+                l(f"    {item_name},")
+    l("}")
+
+    default_item = enum_default_item(enum_name)
+    l(f"impl {rust_enum_name} {{")
+    l("    pub const fn new() -> Self {")
+    l(f"        Self::{default_item}")
+    l("    }")
+    l("}")
+
+    l(f"impl Default for {rust_enum_name} {{")
+    l("    fn default() -> Self {")
+    l(f"        Self::{default_item}")
+    l("    }")
+    l("}")
+
+
+def gen_func_c(decl, prefix):
+    l("pub extern \"C\" {")
+    l(f"    fn {decl['name']}({funcdecl_args_c(decl, prefix)}){funcdecl_result_c(decl, prefix)};")
+    l("}")
+
+
+def gen_c_funcs(funcs):
+    l("pub mod ffi {")
+    l("    #![allow(unused_imports)]")
+    l("    use super::*;")
+    l("    extern \"C\" {")
+    for decl, prefix in funcs:
+        l(f"        pub fn {decl['name']}({funcdecl_args_c(decl, prefix)}){funcdecl_result_c(decl, prefix)};")
+    l("    }")
+    l("}")
+
+
+def gen_rust_funcs(funcs):
+    for decl, prefix in funcs:
+        gen_func_rust(decl, prefix)
+
+
+def gen_func_rust(decl, prefix):
+    c_func_name = decl["name"]
+    rust_func_name = util.as_lower_snake_case(check_override(decl["name"]), prefix)
+    rust_res_type = funcdecl_result_rust(decl, prefix)
+
+    if c_func_name in c_callbacks:
+        c_res_type = funcdecl_result_c(decl, prefix)
+        l("#[inline]")
+        l(f'pub extern "C" fn {c_func_name}({funcdecl_args_c(decl, prefix)}){c_res_type} {{')
+        l("    unsafe {")
+        s = f"        ffi::{c_func_name}("
+        for i, param_decl in enumerate(decl["params"]):
+            if i > 0:
+                s += ", "
+            arg_name = param_decl["name"]
+            s += arg_name
+        s += ")"
+        l(s)
+        l("    }")
+        l("}")
+    else:
+        l("#[inline]")
+        l(f"pub fn {rust_func_name}({funcdecl_args_rust(decl, prefix)}){rust_res_type} {{")
+        for i, param_decl in enumerate(decl["params"]):
+            arg_name = param_decl["name"]
+            arg_type = param_decl["type"]
+            if util.is_string_ptr(arg_type):
+                l(f"        let tmp_{i} = std::ffi::CString::new({arg_name}).unwrap();")
+
+        l("    unsafe {")
+        if is_rust_string(rust_res_type):
+            # special case: convert C string to rust string slice
+            s = f"        c_char_ptr_to_rust_str(ffi::{c_func_name}("
+        else:
+            s = f"        ffi::{c_func_name}("
+
+        for i, param_decl in enumerate(decl["params"]):
+            if i > 0:
+                s += ", "
+            arg_name = param_decl["name"]
+            arg_type = param_decl["type"]
+
+            if util.is_string_ptr(arg_type):
+                s += f"tmp_{i}.as_ptr()"
+            else:
+                s += arg_name
+
+        if is_rust_string(rust_res_type):
+            s += ")"
+        s += ")"
+        l(s)
+        l("    }")
+        l("}")
+
+
+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 gen_imports(inp, dep_prefixes):
+    for dep_prefix in dep_prefixes:
+        dep_module_name = module_names[dep_prefix]
+        # l(f'const {dep_prefix[:-1]} = @import("{dep_module_name}.rs");')
+        l(f'use crate::{dep_module_name} as {dep_prefix[:-1]};')
+    l("")
+
+
+def gen_helpers(inp):
+    l("/// Helper function to convert a C string to a rust string slice")
+    l("#[inline]")
+    l("fn c_char_ptr_to_rust_str(c_char_ptr: *const core::ffi::c_char) -> &'static str {")
+    l("    let c_str = unsafe { core::ffi::CStr::from_ptr(c_char_ptr) };")
+    l("    c_str.to_str().expect(\"c_char_ptr contained invalid Utf8 Data\")")
+    l("}")
+    l("")
+
+    if inp['prefix'] in ['sg_', 'sdtx_', 'sshape_', 'sapp_']:
+        l("/// Helper function to cast a rust slice into a sokol Range")
+        l(f"pub fn slice_as_range<T>(data: &[T]) -> {range_struct_name} {{")
+        l(f"    {range_struct_name} {{ size: data.len() * std::mem::size_of::<T>(), ptr: data.as_ptr() as *const _ }}")
+        l("}")
+        l("/// Helper function to cast a rust reference into a sokol Range")
+        l(f"pub fn value_as_range<T>(value: &T) -> {range_struct_name} {{")
+        l(f"    {range_struct_name} {{ size: std::mem::size_of::<T>(), ptr: value as *const T as *const _ }}")
+        l("}")
+        l("")
+        l(f"impl<T> From<&[T]> for {range_struct_name} {{")
+        l("    #[inline]")
+        l("    fn from(data: &[T]) -> Self {")
+        l("        slice_as_range(data)")
+        l("    }")
+        l("}")
+        l(f"impl<T> From<&T> for {range_struct_name} {{")
+        l("    #[inline]")
+        l("    fn from(value: &T) -> Self {")
+        l("        value_as_range(value)")
+        l("    }")
+        l("}")
+        l("")
+
+    # if inp["prefix"] == "sdtx_":
+    #     l("/// std.fmt compatible Writer")
+    #     l("pub const Writer = struct {")
+    #     l("    pub const Error = error { };")
+    #     l("    pub fn writeAll(self: Writer, bytes: []const u8) Error!void {")
+    #     l("        _ = self;")
+    #     l("        for (bytes) |byte| {")
+    #     l("            putc(byte);")
+    #     l("        }")
+    #     l("    }")
+    #     l("    pub fn writeByteNTimes(self: Writer, byte: u8, n: u64) Error!void {")
+    #     l("        _ = self;")
+    #     l("        var i: u64 = 0;")
+    #     l("        while (i < n): (i += 1) {")
+    #     l("            putc(byte);")
+    #     l("        }")
+    #     l("    }")
+    #     l("};")
+    #     l("// std.fmt-style formatted print")
+    #     l("pub fn print(comptime fmt: anytype, args: anytype) void {")
+    #     l("    var writer: Writer = .{};")
+    #     l('    @import("std").fmt.format(writer, fmt, args) catch {};')
+    #     l("}")
+    #     l("")
+
+
+def gen_module(inp, dep_prefixes):
+    module = inp['module']
+    if module in module_requires_rust_feature:
+        feature = module_requires_rust_feature[module]
+        l(f"//! To use this module, enable the feature \"{feature}\"")
+
+    l("// machine generated, do not edit")
+    l("")
+
+    
+    l("#![allow(dead_code)]")
+    l("#![allow(unused_imports)]")
+    l("")
+    gen_imports(inp, dep_prefixes)
+    gen_helpers(inp)
+    pre_parse(inp)
+    prefix = inp["prefix"]
+
+    funcs = []
+
+    for decl in inp["decls"]:
+        #
+        #    HACK: gen_ir.py accidentally marks all sg_imgui_ declarations as is_dep since sg_imgui
+        #          depends on sg_a but also starts with sg_... Fix gen_ir.py to remove this hack
+        #
+        dep_hack = False
+        if module == "gfx_imgui":
+            dep_hack = "name" in decl and decl["name"].startswith("sg_imgui_")
+
+        if not decl["is_dep"] or dep_hack:
+            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)
+                elif kind == "func":
+                    funcs.append((decl, prefix))
+
+    gen_c_funcs(funcs)
+    gen_rust_funcs(funcs)
+
+
+def prepare():
+    print("=== Generating Rust bindings:")
+    if not os.path.isdir("sokol-rust/src/sokol"):
+        os.makedirs("sokol-rust/src/sokol")
+    if not os.path.isdir("sokol-rust/src/sokol/c"):
+        os.makedirs("sokol-rust/src/sokol/c")
+
+    with open("sokol-rust/src/lib.rs", "w", newline="\n") as f_outp:
+        f_outp.write("//! Automatically generated sokol bindings for Rust\n\n")
+
+
+def gen(c_header_path, c_prefix, dep_c_prefixes):
+    if c_prefix not in module_names:
+        print(f' >> warning: skipping generation for {c_prefix} prefix...')
+        return
+
+    module_name = module_names[c_prefix]
+    c_source_path = c_source_paths[c_prefix]
+    print(f'  {c_header_path} => {module_name}')
+    reset_globals()
+    c_path_in_project = f'sokol-rust/src/sokol/c/{os.path.basename(c_header_path)}'
+    shutil.copyfile(c_header_path, c_path_in_project)
+    ir = gen_ir.gen(c_header_path, c_source_path, module_name, c_prefix, dep_c_prefixes)
+    gen_module(ir, dep_c_prefixes)
+    output_path = f"sokol-rust/src/{ir['module']}.rs"
+    with open(output_path, 'w', newline='\n') as f_outp:
+        f_outp.write(out_lines)
+
+    with open("sokol-rust/src/lib.rs", "a", newline="\n") as f_outp:
+        module = ir['module']
+        if module in module_requires_rust_feature:
+            feature = module_requires_rust_feature[module]
+            f_outp.write(f"/// Enable feature \"{feature}\" to use\n")
+            f_outp.write(f"#[cfg(feature=\"{feature}\")]\n")
+        f_outp.write(f"pub mod {module};\n")