# EndorObject 代码说明 当前实现的是 Endor 的 VM 运行时基础对象,角色类似 CPython 的公共对象结构。这里没有语言根类、元类、继承或 Python type 系统。标量仍然可以直接保存在 Value 中,不要求一切装箱。 基本对象及统一数字表示已在此基础上加入,详见 [基本对象与 Number](builtins.md)。`builtins.rs` 定义字符串、字典、Cell、函数和异常数据;`number.rs` 定义无损数字表示;`vm_builtins.rs` 提供容器操作及关联对象校验。 代码文件及职责见 [core 源码导航](../../crates/core/src/README.md),对象及执行的生命周期见 [生命周期](lifecycle.md)。 ## EndorObject 保存什么 ```text EndorObject header 身份、总强引用数、外部强引用数 payload 原生 Rust 数据及操作接口 attributes VM 管理的可变属性表 payload_edges 分配时记录的载荷引用快照 ``` `ObjectHeader` 不保存语言类型指针。`identity` 是 ObjectRef,用于识别同一个对象。`strong_refs` 是所有强引用的总数,`external_refs` 统计宿主持有和执行帧持有的根引用,也是循环回收的根来源。 `payload` 是 `Box`。Box 保存原生数据,dyn trait 让运行时通过统一接口调用不同 Rust 实现。Rust 会为 trait 分发生成所需机制;不需要创建一个 Python 式的 Type 对象。`kind_name` 只用于诊断,`as_any` 只用于原生 Rust 扩展判断自己能否处理另一份载荷。 `attributes` 保存字段名和 Value。对象值会被 VM 计为内部强引用;字符串和整数等直接保存。载荷可选择是否允许属性,例如 Record 允许,NativeFunction 默认不允许。 `payload_edges` 让原生载荷也可以持有对象。载荷的 trace 方法逐一报告这些边,分配器验证并保存快照;重复边分别计数。自定义载荷分配后不能自行修改这些边。内置字典和 Cell 通过 VM 的载荷替换协议更新:验证和保留新边,替换载荷与快照,最后释放旧边;对象 Box 保持稳定。其他可变对象关系也可放入托管属性。单纯复制一个借用句柄不算持有,也不保护它免于回收。 ## 指针与句柄 Heap 使用 `Option>` 保存槽位,实际对象位于独立分配的 Box 中。槽位向量扩容时,移动的是 Box,存活对象本身的地址保持稳定。 公开的 ObjectRef 使用三部分定位: ```text heap_id 属于哪个 VM index 位于哪个槽位 generation 这是该槽位的哪一次分配 ``` 每次访问检查归属、范围、代数和存活状态。对象释放后,复用同一槽位会更新代数,所以旧句柄不会指向后来分配的新对象。代数耗尽就停用该槽位,VM 身份也不回绕。 公共 API 不暴露裸指针解引用,也不允许调用方构造索引。句柄是进程内引用,不是数据库持久 ID。 ## 分配、持有与释放 `new_object()` 创建 Record;`allocate(payload)` 创建指定原生载荷的对象。分配成功时,总引用数和外部引用数均为 1,返回这一份外部持有对应的句柄。 ```rust let object = vm.new_object()?; let borrowed = object; // 只有句柄复制,引用数仍为 1。 vm.retain(borrowed)?; // 再持有一份,总数和外部数变为 2。 vm.release(object)?; // 释放一份,仍然存活。 vm.release(borrowed)?; // 最后一份释放,清理对象。 ``` ObjectRef 是 Copy,但它不是自动管理持有关系的智能指针。宿主有责任为每一份外部持有配对 retain/release。没有外部持有时不能通过 release 消耗属性拥有的内部引用;此时返回 NoOwnedReference。 引用数归零后,堆先摘除对象,再迭代减少它持有的出边,最后执行 Rust Drop。若某个目标也归零,就加入工作列表继续处理。长对象链不会形成递归析构栈。载荷可实现 Drop 释放原生文件、缓冲区或其他宿主资源;这里没有语言级终结器和复活协议。 销毁整个 Vm 时,Rust 所有权会释放全部剩余对象,包括尚未显式回收的环。复制出的句柄不能让 VM 本身继续存活。 ## 默认操作和覆盖方式 ObjectPayload 提供以下默认行为,载荷可以逐项覆盖: | 操作 | 默认行为 | | --- | --- | | `repr` | 诊断名和对象身份组成的文本 | | `str` | 调用该载荷的 repr | | `is_true` | true | | `hash` | 基于对象身份计算哈希 | | `compare` | 返回未实现,由 VM 尝试反向比较;Eq/Ne 最终按身份回退,排序报错 | | `call` | 返回不支持调用的错误 | | `allows_attributes` | 不允许属性,载荷可显式允许 | | `trace` | 没有载荷持有的对象引用 | Record 使用身份语义并开启属性。NativeFunction 保存名称和 Rust 函数指针,覆盖 repr 和 call。原生函数收到 Value 参数并返回 Result;VM 在调用前后检查参数和返回值中的对象句柄。 原生回调当前不能修改 VM 或直接在该 VM 中分配新对象;其参数和原始返回对象值是借用句柄。Vm::call 会验证并 retain 对象结果,对外统一返回一份持有式结果。语言函数已通过 [wordcode 执行器](bytecode.md) 使用显式帧、参数槽、闭包和求值栈执行;call/execute 的结果完成使用后须 release_value。 compare 的反向规则是 Lt 对应 Gt、Le 对应 Ge,Eq/Ne 保持不变。它不实现 Python 的子类型优先规则。内容比较由扩展定义;如果两个对象内容相等,它们必须给出相同哈希,或者明确拒绝哈希。 ## 属性与错误原子性 `get_attr` 读取借用值;`set_attr` 写入;`del_attr` 删除。对象属性持有内部强引用,删除或覆盖时会释放旧引用。宿主若希望读取到的对象在属性删除后继续存活,必须先 retain。 写入先验证所有句柄和计数溢出,再增加新引用,最后替换并释放旧引用。因此跨 VM、失效句柄或溢出错误不会覆盖原值;把同一个对象重复赋给同一属性,也不会在替换间隙误释放它。 ## 循环回收 引用计数无法自行释放 A → B → A,所以提供显式 `collect_cycles()`: 1. 把 external_refs 大于零的对象作为根。 2. 沿托管属性和载荷边标记所有可达对象。 3. 先从槽位中摘除全部不可达对象。 4. 扣减垃圾指向幸存对象的引用,再 Drop 全部垃圾。 该过程会保留可达的环,回收无人持有的自环、互引用环,以及原生载荷与托管属性混合构成的环。收集返回回收数量,未自动按阈值触发。 ## 当前边界 已实现:统一对象结构、稳定地址、代际句柄、身份、显式引用计数、托管属性、原生操作覆盖、比较与哈希、原生调用、资源清理和显式循环回收。 尚未实现:Python type 系统、语言根类、继承和描述符、源码解析与编译器、可变 VM 调用上下文、自动/分代/增量 GC、语言终结器/复活、完整资源限额和 VM 快照;执行器已有帧数和指令数限额。数据库仅为未来特殊类提供按需连接,普通对象或宿主基础信息不自动持久化,详见 [数据库特殊类](database-objects.md)。 验证范围与执行约定见 [验证](../operations/validation.md)。