From 7ffd50bcf1212ef60c60146b1d74e2987d6ec9c6 Mon Sep 17 00:00:00 2001 From: blueloveTH Date: Mon, 4 Nov 2024 13:28:59 +0800 Subject: [PATCH] add `scripts/c_bind` --- scripts/c_bind/c_bind/__init__.py | 7 + scripts/c_bind/c_bind/converters.py | 169 +++++++++++++++++++++ scripts/c_bind/c_bind/enum.py | 12 ++ scripts/c_bind/c_bind/function.py | 47 ++++++ scripts/c_bind/c_bind/library.py | 195 +++++++++++++++++++++++++ scripts/c_bind/c_bind/meta/__init__.py | 1 + scripts/c_bind/c_bind/meta/parser.py | 159 ++++++++++++++++++++ scripts/c_bind/c_bind/meta/schema.py | 39 +++++ scripts/c_bind/c_bind/schema.py | 47 ++++++ scripts/c_bind/c_bind/struct.py | 125 ++++++++++++++++ scripts/c_bind/c_bind/types.py | 30 ++++ scripts/c_bind/c_bind/writer.py | 16 ++ scripts/c_bind/gen_box2d.py | 30 ++++ scripts/c_bind/gen_raylib.py | 15 ++ scripts/c_bind/libc_include/float.h | 0 scripts/c_bind/libc_include/math.h | 0 scripts/c_bind/libc_include/stdarg.h | 1 + scripts/c_bind/libc_include/stdbool.h | 1 + scripts/c_bind/libc_include/stddef.h | 1 + scripts/c_bind/libc_include/stdint.h | 11 ++ 20 files changed, 906 insertions(+) create mode 100644 scripts/c_bind/c_bind/__init__.py create mode 100644 scripts/c_bind/c_bind/converters.py create mode 100644 scripts/c_bind/c_bind/enum.py create mode 100644 scripts/c_bind/c_bind/function.py create mode 100644 scripts/c_bind/c_bind/library.py create mode 100644 scripts/c_bind/c_bind/meta/__init__.py create mode 100644 scripts/c_bind/c_bind/meta/parser.py create mode 100644 scripts/c_bind/c_bind/meta/schema.py create mode 100644 scripts/c_bind/c_bind/schema.py create mode 100644 scripts/c_bind/c_bind/struct.py create mode 100644 scripts/c_bind/c_bind/types.py create mode 100644 scripts/c_bind/c_bind/writer.py create mode 100644 scripts/c_bind/gen_box2d.py create mode 100644 scripts/c_bind/gen_raylib.py create mode 100644 scripts/c_bind/libc_include/float.h create mode 100644 scripts/c_bind/libc_include/math.h create mode 100644 scripts/c_bind/libc_include/stdarg.h create mode 100644 scripts/c_bind/libc_include/stdbool.h create mode 100644 scripts/c_bind/libc_include/stddef.h create mode 100644 scripts/c_bind/libc_include/stdint.h diff --git a/scripts/c_bind/c_bind/__init__.py b/scripts/c_bind/c_bind/__init__.py new file mode 100644 index 00000000..32b28cba --- /dev/null +++ b/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 \ No newline at end of file diff --git a/scripts/c_bind/c_bind/converters.py b/scripts/c_bind/c_bind/converters.py new file mode 100644 index 00000000..8d6e9848 --- /dev/null +++ b/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) diff --git a/scripts/c_bind/c_bind/enum.py b/scripts/c_bind/c_bind/enum.py new file mode 100644 index 00000000..8ab7f3e5 --- /dev/null +++ b/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') + \ No newline at end of file diff --git a/scripts/c_bind/c_bind/function.py b/scripts/c_bind/c_bind/function.py new file mode 100644 index 00000000..3d982bfc --- /dev/null +++ b/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('') \ No newline at end of file diff --git a/scripts/c_bind/c_bind/library.py b/scripts/c_bind/c_bind/library.py new file mode 100644 index 00000000..54f935d0 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/scripts/c_bind/c_bind/meta/__init__.py b/scripts/c_bind/c_bind/meta/__init__.py new file mode 100644 index 00000000..9520fc87 --- /dev/null +++ b/scripts/c_bind/c_bind/meta/__init__.py @@ -0,0 +1 @@ +from .parser import Header \ No newline at end of file diff --git a/scripts/c_bind/c_bind/meta/parser.py b/scripts/c_bind/c_bind/meta/parser.py new file mode 100644 index 00000000..64cb8ca1 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/scripts/c_bind/c_bind/meta/schema.py b/scripts/c_bind/c_bind/meta/schema.py new file mode 100644 index 00000000..9450fb0a --- /dev/null +++ b/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})" \ No newline at end of file diff --git a/scripts/c_bind/c_bind/schema.py b/scripts/c_bind/c_bind/schema.py new file mode 100644 index 00000000..aadfe427 --- /dev/null +++ b/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])})' + diff --git a/scripts/c_bind/c_bind/struct.py b/scripts/c_bind/c_bind/struct.py new file mode 100644 index 00000000..9bb4f21c --- /dev/null +++ b/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);' \ No newline at end of file diff --git a/scripts/c_bind/c_bind/types.py b/scripts/c_bind/c_bind/types.py new file mode 100644 index 00000000..02d94619 --- /dev/null +++ b/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' +] diff --git a/scripts/c_bind/c_bind/writer.py b/scripts/c_bind/c_bind/writer.py new file mode 100644 index 00000000..33c7075c --- /dev/null +++ b/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) \ No newline at end of file diff --git a/scripts/c_bind/gen_box2d.py b/scripts/c_bind/gen_box2d.py new file mode 100644 index 00000000..1df2812f --- /dev/null +++ b/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' +) diff --git a/scripts/c_bind/gen_raylib.py b/scripts/c_bind/gen_raylib.py new file mode 100644 index 00000000..f4ed88bf --- /dev/null +++ b/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' +) diff --git a/scripts/c_bind/libc_include/float.h b/scripts/c_bind/libc_include/float.h new file mode 100644 index 00000000..e69de29b diff --git a/scripts/c_bind/libc_include/math.h b/scripts/c_bind/libc_include/math.h new file mode 100644 index 00000000..e69de29b diff --git a/scripts/c_bind/libc_include/stdarg.h b/scripts/c_bind/libc_include/stdarg.h new file mode 100644 index 00000000..b7ae7efe --- /dev/null +++ b/scripts/c_bind/libc_include/stdarg.h @@ -0,0 +1 @@ +#define va_list int \ No newline at end of file diff --git a/scripts/c_bind/libc_include/stdbool.h b/scripts/c_bind/libc_include/stdbool.h new file mode 100644 index 00000000..9a5a8fc5 --- /dev/null +++ b/scripts/c_bind/libc_include/stdbool.h @@ -0,0 +1 @@ +#define bool _Bool \ No newline at end of file diff --git a/scripts/c_bind/libc_include/stddef.h b/scripts/c_bind/libc_include/stddef.h new file mode 100644 index 00000000..3c0fa2ed --- /dev/null +++ b/scripts/c_bind/libc_include/stddef.h @@ -0,0 +1 @@ +#define NULL ((void*)0) \ No newline at end of file diff --git a/scripts/c_bind/libc_include/stdint.h b/scripts/c_bind/libc_include/stdint.h new file mode 100644 index 00000000..1f048310 --- /dev/null +++ b/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 \ No newline at end of file