From 29d98edbdc090f5407e85b68083199ebbafd3cea Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 28 Jan 2026 16:19:59 +0800 Subject: [PATCH] refactor: basically finish ExpFctsRender --- .../CodeGen/BMapBinder/ExpFctsRender/main.py | 6 + .../ExpFctsRender/template_render.py | 260 ++++++++++++++++++ .../ExpFctsRender/templates/expfcts.cs.jinja | 16 ++ .../ExpFctsRender/templates/expfcts.py.jinja | 17 ++ .../ExpFctsRender/templates/expfcts.rs.jinja | 0 5 files changed, 299 insertions(+) create mode 100644 Assets/CodeGen/BMapBinder/ExpFctsRender/template_render.py create mode 100644 Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.cs.jinja create mode 100644 Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.py.jinja create mode 100644 Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.rs.jinja diff --git a/Assets/CodeGen/BMapBinder/ExpFctsRender/main.py b/Assets/CodeGen/BMapBinder/ExpFctsRender/main.py index a3c4580..5619ac5 100644 --- a/Assets/CodeGen/BMapBinder/ExpFctsRender/main.py +++ b/Assets/CodeGen/BMapBinder/ExpFctsRender/main.py @@ -1,9 +1,15 @@ import json_loader +import template_render import utils def main(): + render = template_render.TemplateRender() fcts = json_loader.load_fcts("BMExports.json") + render.render_cs_expfcts("BMExports.cs", fcts) + render.render_py_expfcts("BMExports.py", fcts) + render.render_rs_expfcts("BMExports.rs", fcts) + print("Done") diff --git a/Assets/CodeGen/BMapBinder/ExpFctsRender/template_render.py b/Assets/CodeGen/BMapBinder/ExpFctsRender/template_render.py new file mode 100644 index 0000000..d36c697 --- /dev/null +++ b/Assets/CodeGen/BMapBinder/ExpFctsRender/template_render.py @@ -0,0 +1,260 @@ +import jinja2 +import re +import typing +from dataclasses import dataclass +from json_loader import ExpFctCollection, ExpFctParam +import utils + +CPP_PY_TYPE_MAP: dict[str, str] = { + "CKSTRING": "CKSTRING", + "CKDWORD": "CKDWORD", + "CKWORD": "CKWORD", + "CKINT": "CKINT", + "bool": "bool", + "CKFLOAT": "CKFLOAT", + "CKBYTE": "CKBYTE", + "CK_ID": "CKID", + "NakedOutputCallback": "callback", + "BMFile": "void", + "BMMeshTransition": "void", + "VxVector3": "VxVector3", + "VxVector2": "VxVector2", + "VxColor": "VxColor", + "VxMatrix": "VxMatrix", + "CK_TEXTURE_SAVEOPTIONS": "enum", + "VX_PIXELFORMAT": "enum", + "VXLIGHT_TYPE": "enum", + "VXTEXTURE_BLENDMODE": "enum", + "VXTEXTURE_FILTERMODE": "enum", + "VXTEXTURE_ADDRESSMODE": "enum", + "VXBLEND_MODE": "enum", + "VXFILL_MODE": "enum", + "VXSHADE_MODE": "enum", + "VXCMPFUNC": "enum", + "VXMESH_LITMODE": "enum", +} + +CS_ENUM_LIKE: set[str] = set(( + "CK_TEXTURE_SAVEOPTIONS", + "VX_PIXELFORMAT", + "VXLIGHT_TYPE", + "VXTEXTURE_BLENDMODE", + "VXTEXTURE_FILTERMODE", + "VXTEXTURE_ADDRESSMODE", + "VXBLEND_MODE", + "VXFILL_MODE", + "VXSHADE_MODE", + "VXCMPFUNC", + "VXMESH_LITMODE", +)) + +@dataclass(frozen=True) +class CsInteropType: + """The class represent the C# type corresponding to extracted variable type.""" + + marshal_as: str + """ + The argument of MarshalAsAttribute constructor. + In generation, this field should be used like this: "[MarshalAs(THIS)]" (for parameter), + or "[return: MarshalAs(THIS)]" (for return value). + """ + cs_type: str + """ + The C# type used in interop function declaration for corresponding parameter. + """ + + +class RenderUtils: + """Possible used functions for jinja when rendering templates""" + + @staticmethod + def get_python_type(param: ExpFctParam) -> str: + vt = param.var_type + if not param.is_input: + vt = vt.get_pointer_of_this() + + # add type prefix + sb: str = "bm_" + # try getting cpp type from base type and add it + cpp_type = CPP_PY_TYPE_MAP.get(vt.get_base_type(), None) + if cpp_type is None: + raise RuntimeError(f"unexpected type {vt.to_c_type()}") + else: + sb += cpp_type + # add pointer suffix + if vt.is_pointer(): + sb += "_" + sb += "p" * vt.get_pointer_level() + + # return built type string. + return sb + + @staticmethod + def get_cs_interop_type(param: ExpFctParam) -> CsInteropType: + # get essential variable type properties first + vt = param.var_type + vt_base_type = vt.get_base_type() + vt_pointer_level = vt.get_pointer_level() + + # declare return value + marshal_as: str | None = None + cs_type: str | None = None + + # use "match" to check variable type + match vt_base_type: + case 'CKSTRING': + # decide direction cookies + direction_cookie = 'In' if param.is_input else 'Out' + # only allow 0 and 1 pointer level for string. + match vt_pointer_level: + case 0: + marshal_as = f'UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(BMStringMarshaler), MarshalCookie = "{direction_cookie}"' + cs_type = "string" + case 1: + marshal_as = f'UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(BMStringArrayMarshaler), MarshalCookie = "{direction_cookie}"' + cs_type = "string[]" + case "CKDWORD": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.U4" + cs_type = "uint" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "CKWORD": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.U2" + cs_type = "ushort" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "CKINT": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.I4" + cs_type = "int" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "bool": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.U1" + cs_type = "bool" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "CKFLOAT": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.R4" + cs_type = "float" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "CKBYTE": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.U1" + cs_type = "byte" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "CK_ID": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.U4" + cs_type = "uint" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "NakedOutputCallback": + # callback actually is a function pointer + # so it only allow base type without any pointer level. + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.FunctionPtr" + cs_type = "OutputCallback" + case "BMFile": + # In any case, BMFile only should be raw pointer + if vt_pointer_level != 0: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "BMMeshTransition": + # In any case, BMMeshTransition only should be raw pointer + if vt_pointer_level != 0: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "VxVector3": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.Struct" + cs_type = "VxVector3" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "VxVector2": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.Struct" + cs_type = "VxVector2" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "VxColor": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.Struct" + cs_type = "VxColor" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case "VxMatrix": + if vt_pointer_level == 0: + marshal_as = "UnmanagedType.Struct" + cs_type = "VxMatrix" + else: + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + case enumlike if enumlike in CS_ENUM_LIKE: + # all enum type use the same strategy + if vt_pointer_level == 0: + # all enum type should be marshaled as its underlying type + # but we can use its name in C# directly. + marshal_as = "UnmanagedType.U4" + cs_type = vt_base_type + else: + # for pointer type, use IntPtr instead. + marshal_as = "UnmanagedType.SysInt" + cs_type = "IntPtr" + + # check whether we successfully get result + if marshal_as is None or cs_type is None: + raise RuntimeError(f'unexpected type: {vt.to_c_type()}') + + # return value + return CsInteropType(marshal_as, cs_type) + + +class TemplateRender: + """Render templates to code files""" + + __loader: jinja2.BaseLoader + __environment: jinja2.Environment + + def __init__(self) -> None: + self.__loader = jinja2.FileSystemLoader(utils.get_template_directory()) + self.__environment = jinja2.Environment(loader=self.__loader) + + def __render( + self, template_name: str, dest_filename: str, payload: ExpFctCollection + ) -> None: + # prepare template argument + template_argument: dict[str, typing.Any] = { + "payload": payload, + "utils": RenderUtils, + } + # fetch template + template = self.__environment.get_template(str(template_name)) + # render template and save + with open(utils.get_output_file_path(dest_filename), "w", encoding="utf-8") as f: + f.write(template.render(**template_argument)) + + def render_cs_expfcts(self, filename: str, fcts: ExpFctCollection) -> None: + self.__render("expfcts.cs.jinja", filename, fcts) + + def render_py_expfcts(self, filename: str, fcts: ExpFctCollection) -> None: + self.__render("expfcts.py.jinja", filename, fcts) + + def render_rs_expfcts(self, filename: str, fcts: ExpFctCollection) -> None: + self.__render("expfcts.rs.jinja", filename, fcts) diff --git a/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.cs.jinja b/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.cs.jinja new file mode 100644 index 0000000..e08acea --- /dev/null +++ b/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.cs.jinja @@ -0,0 +1,16 @@ +{%- for fct in payload.fcts %} +/// {{ fct.fct_name }} +{%- for param in fct.fct_params %} +/// Direction: {% if param.is_input -%} input {%- else -%} output {%- endif %}. C++ type: {{ param.var_type.to_c_type() }}. {{ param.var_desc }} +{%- endfor %} +/// True if no error, otherwise False. +[DllImport(g_DllName, EntryPoint = "{{ fct.fct_name }}", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)] +[return: MarshalAs(UnmanagedType.U1)] +internal static extern bool {{ fct.fct_name }}( +{%- for param in fct.fct_params -%} +{%- set param_cs_interop_type = utils.get_cs_interop_type(param) -%} +[{% if param.is_input -%} In {%- else -%} Out {%- endif %}, MarshalAs({{ param_cs_interop_type.marshal_as }})] {% if not param.is_input %}out {% endif %} {{- param_cs_interop_type.cs_type }} {{ param.var_name }} +{%- if not loop.last %}, {% endif %} +{%- endfor -%} +); +{%- endfor %} diff --git a/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.py.jinja b/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.py.jinja new file mode 100644 index 0000000..a91e614 --- /dev/null +++ b/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.py.jinja @@ -0,0 +1,17 @@ +{%- for fct in payload.fcts %} +{{ fct.fct_name }} = _create_bmap_func('{{ fct.fct_name }}', [ +{%- for param in fct.fct_params %} +{{- utils.get_python_type(param) }} +{%- if not loop.last %}, {% endif %} +{%- endfor -%} +]) +""" +{{ fct.fct_name }} +{% for param in fct.fct_params %} +:param {{ param.var_name }}: Direction: {% if param.is_input -%} input {%- else -%} output {%- endif %}. {{ param.var_desc }} +:type {{ param.var_name }}: {{ utils.get_python_type(param) }} ({{ param.var_type.to_c_type() }} in C++). {% if param.is_input -%} Use ctypes.byref() pass it. {%- endif %} +{%- endfor %} +:return: True if no error, otherwise False. +:rtype: bool +""" +{%- endfor %} diff --git a/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.rs.jinja b/Assets/CodeGen/BMapBinder/ExpFctsRender/templates/expfcts.rs.jinja new file mode 100644 index 0000000..e69de29