فهرست منبع

add `scripts/c_bind`

blueloveTH 1 سال پیش
والد
کامیت
7ffd50bcf1

+ 7 - 0
scripts/c_bind/c_bind/__init__.py

@@ -0,0 +1,7 @@
+from .function import gen_function
+from .converters import get_converter, set_linalg_converter, set_enum_converters
+from .writer import Writer
+from .struct import gen_struct
+from .enum import gen_enum
+
+from .library import Library

+ 169 - 0
scripts/c_bind/c_bind/converters.py

@@ -0,0 +1,169 @@
+from .writer import Writer
+from .types import C_INT_TYPES, C_FLOAT_TYPES, C_BOOL_TYPES, C_STRING_TYPES, LINALG_TYPES
+
+class Converter:
+    def __init__(self, T: str):
+        self.T = T
+    def c2py(self, w: Writer, out: str, expr: str):
+        raise NotImplementedError
+    def py2c(self, w: Writer, out: str, expr: str):
+        raise NotImplementedError
+    @property
+    def py_T(self) -> str:
+        raise NotImplementedError
+    
+    def is_const(self):
+        return self.T.startswith('const ') or '[' in self.T
+    
+class _SimpleConverter(Converter):
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write(f'py_new{self.py_T}({out}, {expr});')
+    def py2c(self, w: Writer, out: str, expr: str):
+        w.write(f'if(!py_check{self.py_T}({expr})) return false;')
+        w.write(f'{out} = py_to{self.py_T}({expr});')
+    
+class IntConverter(_SimpleConverter):
+    @property
+    def py_T(self) -> str:
+        return 'int'
+    
+class BoolConverter(_SimpleConverter):
+    @property
+    def py_T(self) -> str:
+        return 'bool'
+    
+class StringConverter(_SimpleConverter):
+    @property
+    def py_T(self) -> str:
+        return 'str'
+    
+class FloatConverter(Converter):
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write(f'py_newfloat({out}, {expr});')
+    def py2c(self, w: Writer, out: str, expr: str):
+        if self.T == 'float':
+            w.write(f'if(!py_castfloat32({expr}, &{out})) return false;')
+        else:
+            w.write(f'if(!py_castfloat({expr}, &{out})) return false;')
+
+    @property
+    def py_T(self) -> str:
+        return 'float'
+
+class PointerConverter(Converter):
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write(f'py_newint({out}, (py_i64){expr});')
+    def py2c(self, w: Writer, out: str, expr: str):
+        w.write(f'if(!py_checkint({expr})) return false;')
+        w.write(f'{out} = ({self.T})py_toint({expr});')
+
+    @property
+    def py_T(self) -> str:
+        return 'intptr'
+
+class StructConverter(Converter):
+    def __init__(self, T: str, type_index: str | None):
+        super().__init__(T)
+        if type_index is None:
+            type_index = f'tp_user_{T}'
+        self.type_index = type_index
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write('do {')
+        w.indent()
+        w.write(f'{self.T}* ud = py_newobject({out}, {self.type_index}, 0, sizeof({self.T}));')
+        w.write(f'*ud = {expr};')
+        w.dedent()
+        w.write('} while(0);')
+    def py2c(self, w: Writer, out: str, expr: str):
+        w.write('do {')
+        w.indent()
+        w.write(f'if(!py_checktype({expr}, {self.type_index})) return false;')
+        w.write(f'{out} = *({self.T}*)py_touserdata({expr});')
+        w.dedent()
+        w.write('} while(0);')
+
+    @property
+    def py_T(self) -> str:
+        return self.T
+    
+class EnumConverter(Converter):
+    def __init__(self, T: str):
+        super().__init__(T)
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write(f'py_newint({out}, (py_i64){expr});')
+    def py2c(self, w: Writer, out: str, expr: str):
+        w.write(f'if(!py_checkint({expr})) return false;')
+        w.write(f'{out} = ({self.T})py_toint({expr});')
+
+    @property
+    def py_T(self) -> str:
+        return 'int'
+    
+class VoidConverter(Converter):
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write(f'py_newnone({out});')
+    def py2c(self, w: Writer, out: str, expr: str):
+        # raise NotImplementedError
+        w.write(f'? // VoidConverter.py2c is not implemented')
+    
+    @property
+    def py_T(self) -> str:
+        return 'None'
+    
+class BuiltinVectorConverter(Converter):
+    def __init__(self, T: str, py_builtin_T: str):
+        super().__init__(T)
+        self.py_builtin_T = py_builtin_T
+
+    def c2py(self, w: Writer, out: str, expr: str):
+        w.write(f'py_new{self.py_builtin_T}({out}, *(c11_{self.py_T}*)(&{expr}));')
+
+    def py2c(self, w: Writer, out: str, expr: str):
+        w.write('do {')
+        w.indent()
+        w.write(f'if(!py_checktype({expr}, tp_{self.py_T})) return false;')
+        w.write(f'c11_{self.py_T} tmp = py_to{self.py_builtin_T}({expr});')
+        # w.write(f'memcpy(&{out}, &tmp, sizeof(c11_{self.py_T}));')
+        w.write(f'{out} = *({self.T}*)(&tmp);')
+        w.dedent()
+        w.write('} while(0);')
+    
+    @property
+    def py_T(self) -> str:
+        return self.py_builtin_T
+
+
+_CONVERTERS: dict[str, Converter] = {}
+
+for t in C_INT_TYPES:
+    _CONVERTERS[t] = IntConverter(t)
+for t in C_FLOAT_TYPES:
+    _CONVERTERS[t] = FloatConverter(t)
+for t in C_BOOL_TYPES:
+    _CONVERTERS[t] = BoolConverter(t)
+for t in C_STRING_TYPES:
+    _CONVERTERS[t] = StringConverter(t)
+for t in LINALG_TYPES:
+    _CONVERTERS[t] = BuiltinVectorConverter(f'c11_{t}', t)
+
+_CONVERTERS['void'] = VoidConverter('void')
+_CONVERTERS['c11_array2d'] = StructConverter('c11_array2d', 'tp_array2d')
+
+def set_linalg_converter(T: str, py_T: str):
+    assert py_T in LINALG_TYPES
+    _CONVERTERS[T] = BuiltinVectorConverter(T, py_T)
+
+def set_enum_converters(enums: list[str]):
+    for T in enums:
+        _CONVERTERS[T] = EnumConverter(T)
+
+def get_converter(T: str) -> Converter:
+    if T in _CONVERTERS:
+        return _CONVERTERS[T]
+    if T.endswith('*'):
+        return PointerConverter(T)
+    if '[' in T:
+        return PointerConverter(T)
+    cvt = _CONVERTERS.get(T)
+    if cvt is None:
+        return StructConverter(T, None)

+ 12 - 0
scripts/c_bind/c_bind/enum.py

@@ -0,0 +1,12 @@
+from .writer import Writer
+from .schema import Enum
+
+def gen_enum(w: Writer, pyi_w: Writer, enum: Enum):
+    for value in enum.values:
+        w.write(f'ADD_ENUM({value.name});')
+
+        if value.value is not None:
+            pyi_w.write(f'{value.name}: int = {value.value}')
+        else:
+            pyi_w.write(f'{value.name}: int')
+        

+ 47 - 0
scripts/c_bind/c_bind/function.py

@@ -0,0 +1,47 @@
+from .writer import Writer
+from .converters import get_converter
+from .schema import Function
+
+
+def gen_function(w: Writer, pyi_w: Writer, function: Function):
+    name = function.name
+    w.write(f'static bool cfunc__{name}(int argc, py_Ref argv) {{')
+    w.indent()
+
+    w.write(f'PY_CHECK_ARGC({len(function.params)});')
+
+    args_cvt = [get_converter(arg.type) for arg in function.params]
+    ret_cvt = get_converter(function.ret_type)
+
+    for i in range(len(args_cvt)):
+        w.write(f'{args_cvt[i].T} _{i};')
+        args_cvt[i].py2c(w, f'_{i}', f'py_arg({i})')
+
+    call_args = ', '.join([f'_{i}' for i in range(len(args_cvt))])
+
+    # gen retval
+    if function.ret_type == 'void':
+        w.write(f'{name}({call_args});')
+        w.write(f'py_newnone(py_retval());')
+    else:
+        w.write(f'{ret_cvt.T} res = {name}({call_args});')
+        ret_cvt.c2py(w, 'py_retval()', 'res')
+
+    w.write('return true;')
+
+    w.dedent()
+    w.write('}')
+
+    # pyi
+    py_args = []
+    # arg_names = [f'_{i}' for i in range(len(args_cvt))]
+    arg_names = [arg.name for arg in function.params]
+    for i in range(len(args_cvt)):
+        py_args.append(f'{arg_names[i]}: {args_cvt[i].py_T}')
+
+    pyi_w.write(f'def {name}({", ".join(py_args)}) -> {ret_cvt.py_T}:')
+    if function.desc:
+        pyi_w.write(f'    """Wraps `{function.signature()}`\n\n    {function.desc}"""')
+    else:
+        pyi_w.write(f'    """Wraps `{function.signature()}`"""')
+    pyi_w.write('')

+ 195 - 0
scripts/c_bind/c_bind/library.py

@@ -0,0 +1,195 @@
+from .schema import *
+from .writer import Writer
+from .enum import gen_enum
+from .struct import gen_struct
+from .function import gen_function
+
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+    from .meta import Header
+
+class Library:
+    def __init__(self, name: str) -> None:
+        self.name = name
+        # ['defines', 'structs', 'aliases', 'enums', 'callbacks', 'functions']
+        self.structs = []   # type: list[Struct]
+        self.aliases = []   # type: list[Alias]
+        self.enums = []     # type: list[Enum]
+        self.functions = [] # type: list[Function]
+        self.callbacks = set() # type: set[str]
+
+    def set_includes(self, includes: list[str]):
+        self.user_includes.extend(includes)
+
+    def build(self, *, glue_dir='.', stub_dir='.', includes: list[str] = None):
+        self.remove_unsupported()
+
+        w, pyi_w = Writer(), Writer()
+
+        pyi_w.write('from linalg import vec2, vec3, vec2i, vec3i, mat3x3')
+        pyi_w.write('from typing import overload')
+        pyi_w.write('intptr = int')
+        pyi_w.write('')
+
+        w.write('#include "pocketpy.h"')
+        w.write(f'#include "string.h"')
+        
+        if includes:
+            for include in includes:
+                w.write(f'#include "{include}"')
+
+        w.write('')
+        w.write('#define ADD_ENUM(name) py_newint(py_emplacedict(mod, py_name(#name)), name)')
+        w.write('')
+        w.write('static bool struct__address__(int argc, py_Ref argv) {')
+        w.indent()
+        w.write('PY_CHECK_ARGC(1);')
+        w.write('void* ud = py_touserdata(argv);')
+        w.write('py_newint(py_retval(), (py_i64)ud);')
+        w.write('return true;')
+        w.dedent()
+        w.write('}')
+        w.write('')
+
+        for alias in self.aliases:
+            w.write(f'#define tp_user_{alias.name} tp_user_{alias.type}')
+        w.write('')
+
+        reg_exprs = [
+            gen_struct(w, pyi_w, struct)
+            for struct in self.structs
+        ]
+
+        w.write('/* functions */')
+        for function in self.functions:
+            gen_function(w, pyi_w, function)
+        
+        w.write(f'void py__add_module_{self.name}() {{')
+        w.indent()
+
+        w.write(f'py_GlobalRef mod = py_newmodule("{self.name}");')
+
+        w.write('/* structs */')
+        for reg_expr in reg_exprs:
+            w.write(reg_expr)
+
+        w.write('/* aliases */')
+        pyi_w.write('# aliases')
+        for alias in self.aliases:
+            w.write(f'py_setdict(mod, py_name("{alias.name}"), py_getdict(mod, py_name("{alias.type}")));')
+            pyi_w.write(f'{alias.name} = {alias.type}')
+
+        w.write('/* functions */')
+        for function in self.functions:
+            w.write(f'py_bindfunc(mod, "{function.name}", &cfunc__{function.name});')
+
+        w.write('/* enums */')
+        pyi_w.write('# enums')
+        for enum in self.enums:
+            gen_enum(w, pyi_w, enum)
+        w.dedent()
+        w.write('}')
+        with open(f'{glue_dir}/{self.name}.c', 'w') as f:
+            f.write(str(w))
+        with open(f'{stub_dir}/{self.name}.pyi', 'w') as f:
+            f.write(str(pyi_w))
+
+    def remove_unsupported(self):
+        functions = []
+        for f in self.functions:
+            if f.params and f.params[-1].type == '...':
+                print('[WARN]', f.signature(), 'is variadic')
+                continue
+            for p in f.params:
+                if p.type in self.callbacks:
+                    print('[WARN]', f.signature(), 'has callback param')
+                    break
+            else:
+                functions.append(f)
+        self.functions.clear()
+        self.functions.extend(functions)
+                
+    @staticmethod
+    def from_raylib(data: dict):
+        self = Library('raylib')
+        for struct in data['structs']:
+            self.structs.append(Struct(
+                name=struct['name'],
+                desc=struct['description'],
+                fields=[StructField(
+                    type=field['type'],
+                    name=field['name'],
+                    desc=field['description']
+                ) for field in struct['fields']]
+            ))
+        for alias in data['aliases']:
+            self.aliases.append(Alias(
+                type=alias['type'],
+                name=alias['name'],
+                desc=alias['description']
+            ))
+        for enum in data['enums']:
+            self.enums.append(Enum(
+                name=enum['name'],
+                desc=enum['description'],
+                values=[EnumValue(
+                    name=value['name'],
+                    value=value['value'],
+                    desc=value['description']
+                ) for value in enum['values']]
+            ))
+        for function in data['functions']:
+            self.functions.append(Function(
+                name=function['name'],
+                desc=function['description'],
+                params=[FunctionParam(
+                    type=param['type'],
+                    name=param['name']
+                ) for param in function['params']
+                ] if 'params' in function else [],
+                ret_type=function['returnType']
+            ))
+        for callback in data['callbacks']:
+            self.callbacks.add(callback['name'])
+        return self
+    
+    @staticmethod
+    def from_header(name: str, header: 'Header'):
+        from c_bind.meta import schema
+        self = Library(name)
+        for type in header.types:
+            if isinstance(type, schema.NamedFields):
+                if type.is_opaque():
+                    continue
+                else:
+                    self.structs.append(Struct(
+                        name=type.name,
+                        fields=[StructField(
+                            type=field_type,
+                            name=field_name
+                        ) for field_name, field_type in type.fields.items()]
+                    ))
+            elif isinstance(type, schema.Enum):
+                self.enums.append(Enum(
+                    name=type.name,
+                    values=[EnumValue(
+                        name=value,
+                        value=None
+                    ) for value in type.values]
+                ))
+        for k, v in header.type_aliases.items():
+            self.aliases.append(Alias(
+                name=k,
+                type=v
+            ))
+
+        for function in header.functions:
+            self.functions.append(Function(
+                name=function.name,
+                params=[FunctionParam(
+                    type=param,
+                    name=f'_{i}'
+                ) for i, param in enumerate(function.args)],
+                ret_type=function.ret
+            ))
+        return self

+ 1 - 0
scripts/c_bind/c_bind/meta/__init__.py

@@ -0,0 +1 @@
+from .parser import Header

+ 159 - 0
scripts/c_bind/c_bind/meta/parser.py

@@ -0,0 +1,159 @@
+from .schema import *
+
+class UnsupportedNode(Exception):
+    def __init__(self, node: c_ast.Node):
+        self.node = node
+
+    def __str__(self):
+        return f"'{type(self.node)}' is not supported\n{self.node!r}"
+    
+
+class Header:
+    def __init__(self):
+        self.types = [] # type: list
+        self.type_aliases = {} # type: dict[str, str]
+        self.functions = [] # type: list[Function]
+
+    def remove_types(self, names: set):
+        self.types = [t for t in self.types if getattr(t, 'name', None) not in names]
+
+    def remove_functions(self, names: set):
+        self.functions = [f for f in self.functions if f.name not in names]
+
+    def build_enum(self, node: c_ast.Enum):
+        enum = Enum(node.name)
+        for item in node.values.enumerators:
+            enum.values.append(item.name)
+        self.types.append(enum)
+
+    def unalias(self, name: str):
+        while name in self.type_aliases:
+            name = self.type_aliases[name]
+        return name
+
+    def build_struct(self, node):
+        if isinstance(node, c_ast.Struct):
+            cls = Struct
+        elif isinstance(node, c_ast.Union):
+            cls = Union
+        else:
+            raise UnsupportedNode(node)
+        if node.decls is not None:
+            fields = {}
+            for decl in node.decls:
+                try:
+                    type, name = self.build_param(decl)
+                    assert name
+                    fields[name] = type
+                except UnsupportedNode:
+                    pass
+            self.types.append(cls(node.name, fields))
+        else:
+            self.types.append(cls(node.name, None))
+
+    def build_type(self, name, node):
+        if isinstance(node, c_ast.Enum):
+            self.build_enum(node)
+        elif isinstance(node, (c_ast.Struct, c_ast.Union)):
+            self.build_struct(node)
+        elif isinstance(node, c_ast.IdentifierType):
+            assert name
+            self.type_aliases[name] = node.names[0]
+        else:
+            raise UnsupportedNode(node)
+        
+    def get_type_name(self, node):
+        # convert array to const T*
+        if isinstance(node, c_ast.ArrayDecl):
+            dims = []
+            while isinstance(node, c_ast.ArrayDecl):
+                dims.append(node.dim.value)
+                node = node.type
+            base_name = self.get_type_name(node)
+            # return base_name + ''.join(f'[{dim}]' for dim in dims)
+            return f'const {base_name}*'
+
+        node, level = self.eat_pointers(node)
+
+        if isinstance(node, c_ast.FuncDecl):
+            assert level == 1
+            return 'void (*)()'
+
+        if not isinstance(node, (c_ast.TypeDecl, c_ast.Typename)):
+            raise UnsupportedNode(node)
+
+        is_const = node.quals and 'const' in node.quals
+        node = node.type
+        base_name: str
+        if isinstance(node, c_ast.IdentifierType):
+            base_name = node.names[0]
+        elif isinstance(node, c_ast.Enum):
+            base_name = 'enum ' + node.name
+        elif isinstance(node, c_ast.Struct):
+            base_name = 'struct ' + node.name
+        elif isinstance(node, c_ast.Union):
+            base_name = 'union ' + node.name
+        else:
+            base_name = self.get_type_name(node)
+
+        if is_const:
+            base_name = 'const ' + base_name
+        return base_name + '*' * level
+        
+    def build_param(self, node):
+        name = None
+        if isinstance(node, c_ast.Decl):
+            name = node.name
+            node = node.type
+        return self.get_type_name(node), name
+    
+    def eat_pointers(self, node):
+        level = 0
+        while isinstance(node, c_ast.PtrDecl):
+            node = node.type
+            level += 1
+        return node, level
+        
+    def build_function(self, node: c_ast.FuncDecl):
+        args = node.args
+        node = node.type
+        node, level = self.eat_pointers(node)
+        assert isinstance(node, c_ast.TypeDecl), type(node)
+        name = node.declname
+        ret = node.type.names[0]
+        func = Function(name, ret + '*' * level)
+        if args is not None:
+            for param in args.params:
+                if isinstance(param, c_ast.EllipsisParam):
+                    func.args.append('...')
+                else:
+                    T, name = self.build_param(param)
+                    if T != 'void':
+                        func.args.append(T)
+        self.functions.append(func)
+
+    def build(self, ast: c_ast.FileAST):
+        for _, node in ast.children():
+            if isinstance(node, c_ast.Typedef):
+                name, node = node.name, node.type
+                if isinstance(node, c_ast.TypeDecl):
+                    self.build_type(name, node.type)
+                elif isinstance(node, c_ast.PtrDecl):
+                    type_name = self.get_type_name(node)
+                    self.type_aliases[name] = type_name
+                else:
+                    # raise UnsupportedNode(node.type)
+                    # print(f"Unsupported typedef: {type(node)}")
+                    continue
+            elif isinstance(node, c_ast.Decl):
+                if isinstance(node.type, c_ast.FuncDecl):
+                    self.build_function(node.type)
+                else:
+                    try:
+                        self.build_type(None, node.type)
+                    except UnsupportedNode:
+                        pass
+            else:
+                # raise UnsupportedNode(node.type)
+                # print(f"Unsupported typedef: {type(node)}")
+                continue

+ 39 - 0
scripts/c_bind/c_bind/meta/schema.py

@@ -0,0 +1,39 @@
+from pycparser import c_ast
+
+class Pointer:
+    def __init__(self, base: str, level: int):
+        super().__init__(f'{base}' + '*' * level)
+        self.base = base
+        self.level = level
+
+class NamedFields:
+    def __init__(self, name: str, fields: dict[str, str] | None):
+        self.name = name
+        self.fields = fields
+
+    def is_opaque(self):
+        return self.fields is None
+    
+    def __repr__(self):
+        cls = type(self).__name__
+        return f"{cls}('{self.name}', {self.fields!r})"
+    
+class Struct(NamedFields): pass
+class Union(NamedFields): pass
+    
+class Enum:
+    def __init__(self, name: str):
+        self.name = name
+        self.values = [] # type: list[str]
+
+    def __repr__(self):
+        return f"Enum('{self.name}', values={self.values!r})"
+
+class Function:
+    def __init__(self, name: str, ret: str):
+        self.name = name
+        self.args = [] # type: list[str]
+        self.ret = ret
+
+    def __repr__(self):
+        return f"Function('{self.name}', args={self.args!r}, ret={self.ret!r})"

+ 47 - 0
scripts/c_bind/c_bind/schema.py

@@ -0,0 +1,47 @@
+from dataclasses import dataclass
+
+@dataclass
+class StructField:
+    type: str
+    name: str
+    desc: str = None
+
+@dataclass
+class EnumValue:
+    name: str
+    value: int | None
+    desc: str = None
+
+@dataclass
+class Struct:
+    name: str
+    desc: str = None
+    fields: list[StructField] = None
+
+@dataclass
+class Alias:
+    type: str
+    name: str
+    desc: str = None
+
+@dataclass
+class Enum:
+    name: str
+    values: list[EnumValue]
+    desc: str = None
+
+@dataclass
+class FunctionParam:
+    type: str
+    name: str
+
+@dataclass
+class Function:
+    name: str
+    params: list[FunctionParam]
+    ret_type: str
+    desc: str = None
+
+    def signature(self) -> str:
+        return f'{self.ret_type} {self.name}({", ".join([f"{param.type} {param.name}" for param in self.params])})'
+

+ 125 - 0
scripts/c_bind/c_bind/struct.py

@@ -0,0 +1,125 @@
+from .writer import Writer
+from .converters import get_converter, Converter
+from .schema import Struct, StructField
+
+def gen_getter(w: Writer, name: str, cvt: Converter, field: StructField):
+    w.write(f'static bool {name}__get_{field.name}(int argc, py_Ref argv) {{')
+    w.indent()
+    w.write(f'PY_CHECK_ARGC(1);')
+    w.write(f'{name}* self = py_touserdata(argv);')
+    cvt.c2py(w, 'py_retval()', f'self->{field.name}')
+    w.write('return true;')
+    w.dedent()
+    w.write('}')
+
+def gen_setter(w: Writer, name: str, cvt: Converter, field: StructField):
+    w.write(f'static bool {name}__set_{field.name}(int argc, py_Ref argv) {{')
+    w.indent()
+    w.write(f'PY_CHECK_ARGC(2);')
+    w.write(f'{name}* self = py_touserdata(argv);')
+    cvt.py2c(w, f'self->{field.name}', 'py_arg(1)')
+    w.write('py_newnone(py_retval());')
+    w.write('return true;')
+    w.dedent()
+    w.write('}')
+
+def gen_struct(w: Writer, pyi_w: Writer, struct: Struct):
+    name = struct.name
+    converters = [get_converter(field.type) for field in struct.fields]
+    # default __new__
+    w.write(f'static bool {name}__new__(int argc, py_Ref argv) {{')
+    w.indent()
+    w.write(f'py_Type cls = py_totype(argv);')
+    w.write(f'py_newobject(py_retval(), cls, 0, sizeof({name}));')
+    w.write('return true;')
+    w.dedent()
+    w.write('}')
+
+    # default __init__
+    w.write(f'static bool {name}__init__(int argc, py_Ref argv) {{')
+    w.indent()
+    w.write(f'{name}* self = py_touserdata(argv);')
+    w.write(f'if(argc == 1) {{')
+    w.indent()
+    w.write(f'memset(self, 0, sizeof({name}));')
+    w.dedent()
+    w.write(f'}} else if(argc == 1 + {len(struct.fields)}) {{')
+    w.indent()
+    for i, field in enumerate(struct.fields):
+        cvt = converters[i]
+        if not cvt.is_const():
+            cvt.py2c(w, f'self->{field.name}', f'py_arg({i+1})')
+        else:
+            w.write(f'// _{i} {field.name} is read-only')
+    w.dedent()
+    w.write('} else {')
+    w.indent()
+    w.write(f'return TypeError("expected 1 or {len(struct.fields)+1} arguments");')
+    w.dedent()
+    w.write('}')
+    w.write('py_newnone(py_retval());')
+    w.write('return true;')
+    w.dedent()
+    w.write('}')
+
+    # default __copy__
+    w.write(f'static bool {name}__copy__(int argc, py_Ref argv) {{')
+    w.indent()
+    w.write(f'PY_CHECK_ARGC(1);')
+    w.write(f'{name}* self = py_touserdata(argv);')
+    w.write(f'{name}* res = py_newobject(py_retval(), py_typeof(argv), 0, sizeof({name}));')
+    w.write(f'*res = *self;')
+    w.write('return true;')
+    w.dedent()
+    w.write('}')
+
+    for field in struct.fields:
+        cvt = get_converter(field.type)
+        gen_getter(w, name, cvt, field)
+        if not cvt.is_const():
+            gen_setter(w, name, cvt, field)
+
+    w.write(f'static py_Type register__{name}(py_GlobalRef mod) {{')
+    w.indent()
+    w.write(f'py_Type type = py_newtype("{name}", tp_object, mod, NULL);')
+
+    w.write(f'py_bindmagic(type, __new__, {name}__new__);')
+    w.write(f'py_bindmagic(type, __init__, {name}__init__);')
+    w.write(f'py_bindmethod(type, "__address__", struct__address__);')
+    w.write(f'py_bindmethod(type, "copy", {name}__copy__);')
+
+    for field in struct.fields:
+        cvt = get_converter(field.type)
+        if cvt.is_const():
+            setter = 'NULL'
+        else:
+            setter = f'{name}__set_{field.name}'
+        w.write(f'py_bindproperty(type, "{field.name}", {name}__get_{field.name}, {setter});')
+
+    w.write(f'return type;')
+    w.dedent()
+    w.write('}')
+
+    # pyi
+    pyi_w.write(f'class {name}:')
+    pyi_w.indent()
+
+    py_args = []
+    for field in struct.fields:
+        cvt = get_converter(field.type)
+        desc = (field.desc or '') + f' ({field.type})'
+        py_args.append(f"{field.name}: {cvt.py_T}")
+        pyi_w.write(f"{py_args[-1]} # {desc}")
+    pyi_w.write('')
+
+    pyi_w.write(f'@overload')
+    pyi_w.write(f'def __init__(self): ...')
+    pyi_w.write(f'@overload')
+    pyi_w.write(f'def __init__(self, {", ".join(py_args)}): ...')
+    pyi_w.write(f'def __address__(self) -> int: ...')
+    pyi_w.write(f"def copy(self) -> '{name}': ...")
+    pyi_w.write('')
+    pyi_w.dedent()
+
+    w.write(f'static py_Type tp_user_{name};')
+    return f'tp_user_{name} = register__{name}(mod);'

+ 30 - 0
scripts/c_bind/c_bind/types.py

@@ -0,0 +1,30 @@
+C_INT_TYPES = [
+    'char', 'short', 'int', 'long', 'long long',
+    'signed char', 'signed short', 'signed int', 'signed long', 'signed long long',
+    'unsigned char', 'unsigned short', 'unsigned int', 'unsigned long', 'unsigned long long',
+    'int8_t', 'int16_t', 'int32_t', 'int64_t',
+    'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t',
+    'intptr_t', 'uintptr_t',
+    'ptrdiff_t', 'size_t',
+    'unsigned', 'signed',
+    'py_i64',
+]
+
+C_FLOAT_TYPES = [
+    'float', 'double',
+    'py_f64',
+]
+
+C_BOOL_TYPES = [
+    'bool', '_Bool',
+]
+
+C_STRING_TYPES = [
+    'const char*',
+    'const char *',
+]
+
+LINALG_TYPES = [
+    'vec2', 'vec3', 'vec2i', 'vec3i',
+    'mat3x3'
+]

+ 16 - 0
scripts/c_bind/c_bind/writer.py

@@ -0,0 +1,16 @@
+class Writer:
+    def __init__(self) -> None:
+        self.buffer = []
+        self.indent_level = 0
+
+    def indent(self):
+        self.indent_level += 1
+
+    def dedent(self):
+        self.indent_level -= 1
+
+    def write(self, line: str):
+        self.buffer.append('    ' * self.indent_level + line)
+
+    def __str__(self) -> str:
+        return '\n'.join(self.buffer)

+ 30 - 0
scripts/c_bind/gen_box2d.py

@@ -0,0 +1,30 @@
+import pcpp
+import pycparser
+from c_bind import Library, set_linalg_converter, set_enum_converters
+from c_bind.meta import Header
+import os
+
+path = '../3rd/box2d/include/box2d/box2d.h'
+code = pcpp.CmdPreprocessor([None, path, '-o', 'tmp.h', '--line-directive', '-I', 'libc_include', '-I', '../3rd/box2d/include'])
+
+ast = pycparser.parse_file('tmp.h')
+os.remove('tmp.h')
+
+header = Header()
+header.build(ast)
+
+header.remove_types({'b2Timer', 'b2DebugDraw'})
+header.remove_functions({'b2CreateTimer', 'b2Hash', 'b2DefaultDebugDraw'})
+
+lib = Library.from_header('box2d', header)
+
+set_linalg_converter('b2Vec2', 'vec2')
+set_linalg_converter('b2Vec3', 'vec3')
+
+set_enum_converters([enum.name for enum in lib.enums])
+
+lib.build(
+    includes=['box2d/box2d.h'],
+    glue_dir='../src',
+    stub_dir='../include/typings'
+)

+ 15 - 0
scripts/c_bind/gen_raylib.py

@@ -0,0 +1,15 @@
+import json
+from c_bind import Library, set_linalg_converter
+
+with open('../3rd/raylib/parser/output/raylib_api.json') as f:
+    data = json.load(f)
+
+lib = Library.from_raylib(data)
+set_linalg_converter('Vector2', 'vec2')
+set_linalg_converter('Vector3', 'vec3')
+
+lib.build(
+    includes=['raylib.h'],
+    glue_dir='../src',
+    stub_dir='../include/typings'
+)

+ 0 - 0
scripts/c_bind/libc_include/float.h


+ 0 - 0
scripts/c_bind/libc_include/math.h


+ 1 - 0
scripts/c_bind/libc_include/stdarg.h

@@ -0,0 +1 @@
+#define va_list int

+ 1 - 0
scripts/c_bind/libc_include/stdbool.h

@@ -0,0 +1 @@
+#define bool _Bool

+ 1 - 0
scripts/c_bind/libc_include/stddef.h

@@ -0,0 +1 @@
+#define NULL ((void*)0)

+ 11 - 0
scripts/c_bind/libc_include/stdint.h

@@ -0,0 +1,11 @@
+#define size_t int
+
+#define int8_t int
+#define int16_t int
+#define int32_t int
+#define int64_t int
+
+#define uint8_t unsigned
+#define uint16_t unsigned
+#define uint32_t unsigned
+#define uint64_t unsigned