Skip to main content

Exotic 对象模型

谁该读本文

需要理解 obj.Member 底层如何查表、为何报错 zts: member not found 的开发者。 日常用法见 JS 调用 C#方法重载。对照 ZLua 的「元表模型」:ZTS 用 QuickJS exotic object + internal slots要求 ECMAScript Proxy

C# 类型在 JS 中以 类型对象 T 暴露静态成员;实例以 实例 exotic object 暴露实例成员。属性分派走 三表 + strict miss:未注册成员直接 throw Error回退到 C# 反射,也 返回 undefined

总体布局

flowchart TB
T[类型对象 T] --> STO[STO 静态分派]
STO --> SMT[静态三表]
T --> ByObjProto[ByObj IEO 原型]
T -.->|仅 struct| ByValProto[ByVal IEO 原型]
Inst[实例 exotic] --> ByObjProto
InstSV[struct ByVal 实例] --> ByValProto
概念作用
类型对象 T静态门面;Type.StaticFieldnew Type(...)
STO静态 [[Get]] / [[Set]] / [[Construct]]
IEO实例分派原型;ByObj(引用/装箱)与 ByVal(struct payload)可分离
三表methodTable / fieldGetterTable / fieldSetterTable(静、实例各一套)

脚本 不应依赖内部槽键名或 native 布局细节;稳定行为以 metatable 规范 为准。

三表分派

每个静态域或实例域各维护三张表:

读 ([[Get]])写 ([[Set]])
methodTable方法、dispatch、别名、add_/remove_
fieldGetterTable字段、无参 property 读
fieldSetterTable字段、无参 property 写
flowchart TD
K[访问键 key] --> R{读 or 写?}
R -->|读| M[methodTable]
M -->|命中| OK1[返回函数 / 值]
M -->|未命中| G[fieldGetterTable]
G -->|命中| OK2[返回字段/property 值]
G -->|未命中| E1["throw Error strict miss"]

R -->|写| S[fieldSetterTable]
S -->|命中| OK3[写入字段/property]
S -->|未命中| E2["throw Error strict miss"]
写路径

写路径 查 methodTable;给不存在的键赋值直接报错。

查表时序(实例读)

sequenceDiagram
participant J as QuickJS
participant I as instance Get
participant M as methodTable
participant G as fieldGetterTable

J->>I: demo.GetX 或 demo.x
I->>M: lookup "GetX" / "x"
alt method 命中
M-->>I: bridge function
else method 未命中
I->>G: lookup "x"
G-->>I: getter 或值
end
I-->>J: 结果或 Error

strict miss 示例

以下均 直接 throw,不会尝试反射查找 private 或未绑定父类成员:

const demo = new Demo();
const _ = demo.nonExistentField; // Error: zts: member not found: …
demo.nonExistentField = 1; // Error: member not found / not writable
demo.PrivateMethod(); // Error(private 未注册)

与 dispatch 的区别: 多重重载时 demo.Run(x) 在 methodTable 上可能是 单个 dispatch 函数,内部分派到具体桥接,仍算 methodTable 命中。

方法调用形态

情况行为
obj.Method(args)方法调用:自动传入 CLR this
const f = obj.Method; f(args)自动绑定 this(与常见 JS 一致)
单一 public 重载methodTable 直接桥接
多个重载dispatch 函数(运行时分派)
[TsAlias("run_i32")]额外键 run_i32 → 单桥接

签名字符串 不是 推荐的日常 JS 属性键;日常用短名 + dispatch / 别名。

Event 专用 { get, set, fire } 对象;使用 add_EventName / remove_EventName

平台实现

运行时分派实现
Mono (Editor)Exotic + 托管侧查三表 / Emit 桥
Il2Cpp (Player)native Dispatch* + MetaBinding / TypeRegistry

JS 可见语义一致;Il2Cpp 可内联字段 offset 读,仍走同一分派顺序。

与 Proxy / ZLua 的关系

方案分派载体
ZLuauserdata + Lua 元表三表
ZTSQuickJS exotic object + internal slots
ECMAScript Proxy规范 不要求;若实现内部使用,不得改变可见行为

何时读规范

问题文档
布局与内部槽01-LAYOUT
索引算法02-INDEX
注册期绑定03-BINDING
struct / enum 特殊形态04-SPECIAL-TYPES
dispatch 算法方法重载

相关文档