Files

8.7 KiB
Raw Permalink Blame History

EndorObject 代码说明

当前实现的是 Endor 的 VM 运行时基础对象,角色类似 CPython 的公共对象结构。宿主可显式引导托管 Object/Type 与名义继承,见类型机制;普通源码类和继承方法分派已实现,显式 NativeTyped 原生分配继承已接入,trait 尚待接入。标量仍然可以直接保存在 Value 中,不要求一切装箱。

基本对象及统一数字表示已在此基础上加入,详见 基本对象与 Number。builtins.rs 定义字符串、字典、Cell、函数和异常数据;number.rs 定义无损数字表示;store_builtins.rs 提供容器操作及关联对象校验。

这些定义全部位于独立 objects 包。ObjectStore 拥有私有 Heap,负责对象操作与引用事务;core 的 Vm 拥有一个 ObjectStore,仅委托对象操作并管理执行状态。对象存储无需 VM 即可使用,仍不提供句柄字段或裸指针构造;不同存储的句柄互不通用。VmError 也在 objects 定义以避免反向依赖,endor_core 兼容重导出现有类型和保留 Vm 方法。

代码文件及职责见 objects 源码导航,执行器见 core 源码导航,对象及执行的生命周期见 生命周期。

EndorObject 保存什么

EndorObject
  header         身份、总强引用数、外部强引用数、变更版本、可选类型引用
  payload        原生 Rust 数据及操作接口
  attributes     VM 管理的可变属性表
  payload_edges  分配时记录的载荷引用快照

ObjectHeader.type_object 是可选的、受 GC 追踪的类型对象引用,普通实例附加后不可更改。identity 是 ObjectRef,用于识别同一个对象。strong_refs 是所有强引用的总数,external_refs 统计宿主持有和执行帧持有的根引用,也是循环回收的根来源。

payload 是 Box<dyn ObjectPayload>。Box 保存原生数据,dyn trait 让运行时通过统一接口调用不同 Rust 实现。Rust 会为 trait 分发生成所需机制;其原生操作接口与托管 Type 身份分别保存。kind_name 只用于诊断,as_any 只用于原生 Rust 扩展判断自己能否处理另一份载荷。

attributes 保存字段名和 Value。对象值会被 VM 计为内部强引用;字符串和整数等直接保存。载荷可选择是否允许属性,例如 Record 允许,NativeFunction 默认不允许。

payload_edges 让原生载荷也可以持有对象。载荷的 trace 方法逐一报告这些边,分配器验证并保存快照;重复边分别计数。自定义载荷分配后不能自行修改这些边。内置字典和 Cell 通过 VM 的载荷替换协议更新:验证和保留新边,替换载荷与快照,最后释放旧边;对象 Box 保持稳定。其他可变对象关系也可放入托管属性。单纯复制一个借用句柄不算持有,也不保护它免于回收。

指针与句柄

Heap 使用 Option<Box<EndorObject>> 保存槽位,实际对象位于独立分配的 Box 中。槽位向量扩容时,移动的是 Box,存活对象本身的地址保持稳定。

公开的 ObjectRef 使用三部分定位:

heap_id       属于哪个 VM
index         位于哪个槽位
generation    这是该槽位的哪一次分配

每次访问检查归属、范围、代数和存活状态。对象释放后,复用同一槽位会更新代数,所以旧句柄不会指向后来分配的新对象。代数耗尽就停用该槽位,VM 身份也不回绕。

公共 API 不暴露裸指针解引用,也不允许调用方构造索引。句柄是进程内引用,不是数据库持久 ID。

分配、持有与释放

new_object() 创建 Record;allocate(payload) 创建指定原生载荷的对象。分配成功时,总引用数和外部引用数均为 1,返回这一份外部持有对应的句柄。

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<Value, VmError>;VM 在调用前后检查参数和返回值中的对象句柄。

旧 call 回调不能修改 VM 或分配新对象;需要这些能力的原生方法可使用 NativeCallable 与受限 NativeContext。两种原生入口都接受借用参数,Vm::call 对外统一返回一份持有式结果。上下文临时根、非递归委托与捕获规则见 原生调用。语言函数通过 wordcode 执行器 使用显式帧、参数槽、闭包和求值栈执行;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 全部垃圾。

该过程会保留可达的环,回收无人持有的自环、互引用环,以及原生载荷与托管属性混合构成的环。收集返回回收数量,未自动按阈值触发。

当前边界

已实现:统一对象结构、稳定地址、代际句柄、身份、显式引用计数、托管属性、原生操作覆盖、比较与哈希、原生调用、资源清理和显式循环回收。另有受限原生可变上下文、安全弱引用、托管 Cell 赋值及有界对象变更记录,见 原生调用。

尚未实现:Python type 兼容、trait 和描述符、任意 VM 重入调用上下文、自动/分代/增量 GC、语言终结器/复活、完整资源限额和 VM 快照;独立 frontend 已提供阅读器、lexer/parser 与 AST,基础源码执行由独立 Compiler 接入,见 源码前端;执行器已有帧数和指令数限额。数据库仅为未来特殊类提供按需连接,普通对象或宿主基础信息不自动持久化,详见 数据库特殊类。

验证范围与执行约定见 验证。

ObjectPayload::poll_await是通用非阻塞等待观察协议,默认UnsupportedOperation。AwaitPoll::Ready的对象结果是借用,调用方须在放弃awaitable前retain;协议不允许内部阻塞/IO/修改对象。完整指令和恢复契约见bytecode.md。