PUERTS Unity 生成器 Filter 详解:用 [Filter] 与 BindingMode 精确控制 StaticWrapper 生成与 JS 访问权限
【免费下载链接】puertsPUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts
本篇基于 PUERTS 仓库官方文档《the Filter of Generator》展开,系统讲解 Unity 端 StaticWrapper 生成时的 Filter 机制:如何通过[Configure]+[Filter]配置类排除导致编译错误的成员、如何用BindingMode做 JS 侧访问控制、如何在 IL2CPP 模式下跳过指令扫描,并深入到 Configure.cs、Utils.cs 等源码,说明每种 Filter 签名的真实生效路径,帮助读者写出可复制、可运行的生成控制代码。
什么时候需要 Filter
PUERTS 的静态绑定生成是以“类”为单位圈定范围的:你选定一批类生成 StaticWrapper,其下成员默认都会生成对应的 TS 声明与 C# 包装回调。但在真实项目中总有两类需求无法靠“选类”解决:
- 排除个别会编译报错的成员:某些成员在生成包装代码后无法编译(例如依赖编辑器环境的成员),需要从生成列表中剔除;
- 访问控制:禁止某些 C# 能力暴露给 JavaScript,或在反过来的场景下——默认禁止、只白名单放行少数接口。
PUERTS 为此提供了 Filter 机制:通过一个带[Filter]标签的静态方法,在生成器遍历成员时进行逐成员决策。
Filter 配置的基本规则
写 Filter 前需要满足两条硬性约束(源码文件头部注释与官方文档一致):
- 配置类必须打上
[Configure]标签; - 配置类必须放在Editor 目录下(因为 Filter 只在代码生成阶段运行,属于编辑器期逻辑)。
这一点可以直接在 Configure.cs 的头部注释中确认:
* 配置 * 1、Binding、BlittableCopy、Filter须放到一个打了Configure标签的类里; * 2、Binding、BlittableCopy、Filter均用打了相应标签的属性来表示; * 3、Binding、BlittableCopy、Filter配置须放到Editor目录下;相关的 Attribute 定义见 Configure.cs:
[AttributeUsage(AttributeTargets.Method)] public class FilterAttribute : Attribute { }FilterAttribute标注在方法上(而 Binding、BlittableCopy 等标注在属性上),且方法必须是静态方法——生成器通过Configure.GetFilters()反射扫描当前 AppDomain 中所有程序集,找出打了[Configure]的类,再收集其中打了[Filter]的静态方法(见 Configure.cs)。
场景一:过滤掉会引发编译错误的成员(bool 风格)
生成列表按类指定,但要排除某个具体函数不被生成包装时,写一个返回bool的 Filter 即可。文档给出的典型案例是排除仅在编辑器环境存在、发布后不存在的MonoBehaviour.runInEditMode:
//1. 配置类必须打上 [Configure] 标签 //2. 必须放在 Editor 目录下 [Configure] public class ExamplesCfg { [Filter] static bool FilterMethods(System.Reflection.MemberInfo mb) { // 排除 MonoBehaviour.runInEditMode,该成员只存在于编辑器环境,发布后不存在 if (mb.DeclaringType == typeof(MonoBehaviour) && mb.Name == "runInEditMode") { return true; } return false; } }返回true表示“需要过滤”。这里需要注意其底层语义:从 Utils.cs 的SetFilters可以看到,bool 风格 Filter 被包装为:
BindingModeFilters.Add((MemberInfo mbi) => { bool res = dlg(mbi); return res ? BindingMode.SlowBinding : BindingMode.FastBinding; });也就是说,bool 风格 Filter 中return true实际被映射为BindingMode.SlowBinding(走反射调用),而不是彻底不生成。如果成员只是“静态包装不好生成、但希望保留反射通道”,这种写法正合适;若希望成员彻底不可调用,应使用下文的BindingMode风格并返回DontBinding。
场景二:访问控制(BindingMode 风格)
当需要“禁止某些 C# 特性在 JS 中使用”时,Filter 可以返回BindingMode枚举,对每个成员做三档精细决策。BindingMode的完整定义在运行时 RegisterInfoManager.cs:
public enum BindingMode { FastBinding = 1024, // static wrapper LazyBinding = 128, // reflect during first call SlowBinding = 32, // reflection to call DontBinding = 2, // not able to called in runtime. Also will not generate d.ts }| 取值 | 含义 | 效果 |
|---|---|---|
FastBinding | 静态包装 | 生成静态 wrapper 回调,性能最好;等价于 bool 风格的return false |
LazyBinding | 首次调用时反射 | 懒绑定 |
SlowBinding | 反射调用 | 每次调用走反射,无静态包装 |
DontBinding | 不绑定 | 运行时不可调用,且不会生成 d.ts 声明 |
例如禁止 JS 访问System.Threading.Tasks.Task.IsCompletedSuccessfully:
static Puerts.BindingMode FilterMethods(System.Reflection.MemberInfo mb) { if (mb.DeclaringType.ToString() == "System.Threading.Tasks.Task" && mb.Name == "IsCompletedSuccessfully") { return Puerts.BindingMode.DontBinding; // 不生成 StaticWrapper,JS 中不可调用 } return Puerts.BindingMode.FastBinding; // 等价于 bool 风格的 'return false' }多个 Filter 同时存在时,生成器会取最严格的模式。这一逻辑在 Utils.cs 中:
public static BindingMode getBindingMode(MemberInfo mbi) { BindingMode strictestMode = BindingMode.FastBinding; foreach (var filter in BindingModeFilters) { var mode = filter(mbi); strictestMode = strictestMode > mode ? mode : strictestMode; } return strictestMode; }由于DontBinding = 2是枚举中数值最小的模式,只要任一 Filter 判定为DontBinding,最终结果就是DontBinding。
DontBinding成员在生成期如何处理?在 CSharpFileExporter.cs 中,导出类成员时直接过滤掉所有DontBinding成员:
.Where(m => Utils.getBindingMode(m) != Puerts.BindingMode.DontBinding);同时,文档提到“PuerTS 会在 Wrapper 中记录这些属性的信息,注册时阻止调用”——对应 RegisterInfo.cs 在生成UseBindingMode注册信息时逐成员调用Utils.getBindingMode(m).ToString(),把每个成员最终的绑定模式写入生成的注册代码,运行时注册逻辑据此拦截这些成员。
白名单模式:SetDefaultBindingMode
上面“逐个标记禁止”的方式适合只禁几个成员。反过来,如果希望默认禁止几乎所有 JS 调用、只放行极少数接口,为每个成员写 Filter 就不现实了。此时可以修改 JsEnv 的默认绑定模式:
var env = new JsEnv(); env.SetDefaultBindingMode(BindingMode.DontBinding);SetDefaultBindingMode定义在 JsEnv.cs,它把默认值写入RegisterInfoManager.DefaultBindingMode(默认值为FastBinding,见 RegisterInfoManager.cs)。在此基础上,Filter 改为“放行者”写法:
static Puerts.BindingMode FilterMethods(System.Reflection.MemberInfo mb) { if (mb.DeclaringType == typeof(UnityEngine.Vector3)) // 保留 Vector3 可用 { return Puerts.BindingMode.FastBinding; } return Puerts.BindingMode.DontBinding; }这种“默认关闭、白名单开启”的组合拳,是控制脚本侧 API 暴露面、收窄攻击/误用面时的标准做法。
IL2CPP(xIl2cpp)模式下的指令级过滤
切换到 IL2CPP 相关构建管线时,cppwrapper 的生成策略是全量生成,因此生成器会尝试读取方法体(指令)来查找涉及到的类型。对于不想参与这一“方法体类型扫描”的类型,可以写一个双参数(带FilterAction)的 Filter:
[Filter] static bool GetFilterClass(FilterAction filterAction, MemberInfo mbi) { if (filterAction == FilterAction.MethodInInstructions) return skipAssembles.Contains(mbi.DeclaringType.Assembly.GetName().Name); return false; }FilterAction枚举定义了 Filter 被询问的三种时机(Configure.cs):
public enum FilterAction { BindingMode = 1, MethodInInstructions = 2, DisallowedType = 3 }[Filter]方法支持两种签名:单参数(MemberInfo)或双参数(FilterAction, MemberInfo)。双参数签名中,生成器会以相应的FilterAction值分别回调——MethodInInstructions阶段询问“是否跳过指令扫描”,DisallowedType阶段则传入Type询问“该类型是否禁用”(后者会进入DisallowedTypeFilters,并被isDisallowedType用于递归判定基类与值类型字段,见 Utils.cs)。
事实上,PUERTS 自己就在用这套机制。内置的 InstructionsFilter.cs 就是一个[Configure]类,其中的GetFilterClass与上文示例完全同构:当filterAction == FilterAction.MethodInInstructions时,若成员所在程序集命中skipAssembles白名单(mscorlib、System.Core、UnityEngine.CoreModule等系统/引擎程序集),则跳过指令扫描。
被跳过的成员由 Utils.cs 的shouldNotGetArgumentsInInstructions汇总各 Filter 的 OR 结果;生成 C# 代码遍历成员方法体时(CSharpFileExporter.cs)据此直接return,避免对系统程序集做无意义且可能失败的方法体解析。
同一内置类中还有一个值得参考的BindingMode风格 Filter(InstructionsFilter.cs):
- 参数/返回类型是“大值类型”(字段数超过 1024,由
Utils.IsBigValueType判定)、指针、或DisallowedType判定的类型 →DontBinding; - 参数中出现
IntPtr/UIntPtr→ 降级为SlowBinding; - 一切异常兜底为
DontBinding。
这说明 Filter 机制在 PUERTS 内部本身也被当作生成安全网使用,用户 Filter 与内置 Filter 并行参与“取最严格模式”的决策。
机制总览:一个 Filter 从定义到生效的完整链路
把上述源码证据串起来,Filter 的完整生命周期是:
- 发现:生成任务启动时,CSharpFileExporter.cs 调用
Utils.SetFilters(Puerts.Configure.GetFilters()),GetFilters反射扫描所有[Configure]类中的[Filter]静态方法; - 签名适配:
SetFilters按参数个数(1 或 2)、返回类型(bool或BindingMode)、第二参数类型(MemberInfo/Type)把每个方法适配到BindingModeFilters/InstructionsFilters/DisallowedTypeFilters三个列表;bool 返回统一映射为SlowBinding/FastBinding; - 逐成员决策:导出成员时以
getBindingMode取最严格模式,DontBinding成员被整体剔除出方法、属性、字段列表; - 注册信息落地:存活成员的最终模式写入生成代码的
UseBindingMode,供运行时注册时拦截/放行; - 清理:生成结束时调用
Utils.SetFilters(null)重置(CSharpFileExporter.cs)。
实践要点小结
- Filter 配置类必须
[Configure]+ 放在 Editor 目录,方法必须静态且标[Filter]; bool风格:true等价于降级为SlowBinding,不会彻底移除成员;BindingMode风格:返回DontBinding才真正“不生成包装、不生成 d.ts、运行时不可调用”;- 多个 Filter 并存时取最严格模式,用户 Filter 不会覆盖内置 Filter 的
DontBinding判定; - “默认全禁、白名单放行”用
env.SetDefaultBindingMode(BindingMode.DontBinding)+ Filter 返回FastBinding/SlowBinding实现; - IL2CPP 全量生成场景用双参数 Filter 响应
FilterAction.MethodInInstructions,按程序集跳过方法体扫描。
配套文档可参考 wrapper.md(StaticWrapper 生成总览)、extension.md(扩展方法)与 all_attribute.md(属性体系),中文版对应 filter.md。
【免费下载链接】puertsPUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考