matxscript运行时系统之对象模型

鱿鱼圈 Lv4

仓库链接:bytedance/matxscript: A high-performance, extensible Python AOT compiler.

概述

matxscript运行时系统的核心基础是其基于引用计数的对象模型。该模型提供了自动内存管理、类型安全和面向对象特性支持,为整个运行时环境奠定了坚实的基础。对象模型主要由三个核心组件构成:

  1. Object类:作为所有对象容器的基类,包含了类型索引、引用计数器和删除器等关键字段
  2. ObjectPtr模板类:一个自定义智能指针,用于管理Object实例的安全访问和生命周期控制
  3. ObjectRef类:对象引用类,提供给外部用户使用的接口

这套设计借鉴了Apache TVM的理念,实现了高效且灵活的对象管理系统,支持继承、多态和运行时类型检查等高级特性。

核心组件

Object基类

Object类(定义于include/matxscript/runtime/object.h)是整个对象系统的根基。它包含以下关键成员变量:

  • type_index_:标识对象类型的唯一索引
  • ref_counter_:引用计数器,用于跟踪有多少个智能指针正在引用此对象
  • deleter_:函数指针,在引用计数归零时调用以释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
class Object {
protected:
/*! \\brief Type index(tag) that indicates the type of the object. */
uint32_t type_index_{0};
/*! \\brief The internal reference counter */
RefCounterType ref_counter_{1};
/*!
* \\brief deleter of this object to enable customized allocation.
* If the deleter is nullptr, no deletion will be performed.
* The creator of the object must always set the deleter field properly.
*/
FDeleter deleter_ = nullptr;
};

Object类还提供了几个重要的方法:

  • type_index():获取当前对象的类型索引
  • GetTypeKey():返回对象类型的字符串表示
  • IsInstance<T>():模板方法,用于在运行时检查对象是否是指定类型的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Implementations details below
// Object reference counting.
#if MATXSCRIPT_OBJECT_ATOMIC_REF_COUNTER

inline void Object::IncRef() noexcept {
ref_counter_.fetch_add(1, std::memory_order_relaxed);
}

inline void Object::DecRef() noexcept {
if (use_count() == 1) {
if (this->deleter_ != nullptr) {
(*this->deleter_)(this);
}
} else if (ref_counter_.fetch_sub(1, std::memory_order_release) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
if (this->deleter_ != nullptr) {
(*this->deleter_)(this);
}
}
}

inline int Object::use_count() const noexcept {
return ref_counter_.load(std::memory_order_relaxed);
}

ObjectPtr智能指针

ObjectPtr<T>是一个模板类,专门用来管理继承自Object的对象实例。它通过重载运算符(如*->)提供了类似原生指针的操作体验,同时自动化处理引用计数逻辑。

关键特性包括:

  • 构造或复制时增加引用计数
  • 销毁或重新赋值时减少引用计数
  • 当引用计数降至0时自动调用对象的析构函数并释放内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
class ObjectPtr {
public:
T* operator->() const noexcept {
return get();
}

T& operator*() const noexcept {
return *get();
}

private:
/*! \\brief internal pointer field */
Object* data_{nullptr};
};

ObjectRef引用类

ObjectRef是对ObjectPtr的一层封装,主要用于向用户提供统一的对象访问接口。它隐藏了底层智能指针的具体实现细节,使得用户可以更方便地操作各种类型的对象实例。

ObjectRef最重要的功能之一就是类型转换——通过as<T>()方法可以在运行时安全地将通用对象引用转换为特定类型的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*! \brief Base class of all object reference */
class ObjectRef {
public:
/*! \return the internal object pointer */
const Object* get() const noexcept {
return data_.get();
}
/*! \return the internal object pointer */
const Object* operator->() const noexcept {
return get();
}
/*!
* \brief Try to downcast the internal Object to a
* raw pointer of a corresponding type.
*
* The function will return a nullptr if the cast failed.
*
* if (const Add *add = node_ref.As<Add>()) {
* // This is an add node
* }
* \tparam ObjectType the target type, must be a subtype of Object/
*/
template <typename ObjectType>
inline const ObjectType* as() const noexcept;
protected:
/*! \brief Internal pointer that backs the reference. */
ObjectPtr<Object> data_;
/*! \return return a mutable internal ptr, can be used by sub-classes. */
Object* get_mutable() const noexcept {
return data_.get();
}

};

拷贝时调 IncRef(),析构/reset 时调 DecRef(),本质就是 引用计数智能指针 的标准模式。下面直接用代码路径一步步拆开,让你能在脑中“看到”计数的变化。


自定义智能指针体系

核心是三层:

  • **Object**:所有“节点对象”的基类,内置引用计数 + 删除器
  • **ObjectPtr<T>**:指向 **Object** 的智能指针,负责增减引用计数
  • **ObjectRef**:对外暴露的“句柄类型”(List、Dict、String 等都继承它),内部持有 **ObjectPtr<Object>**

对象的最小单元:Object 的计数字段

Object 里有两个关键字段:

1
2
3
4
5
class Object {
protected:
RefCounterType ref_counter_{1}; // 初始值是 1
FDeleter deleter_ = nullptr; // 删除器:决定如何 delete
};
  • 任意 Object 派生类(比如 ListNode)在构造完成时,ref_counter_ = 1
  • 后续所有“持有这个对象”的智能指针,都围绕这一个 ref_counter_ 做加减。

引用计数的核心:IncRef / DecRef

IncRef:增加引用数

1
2
3
4
5
6
7
inline void Object::IncRef() noexcept {
#if MATXSCRIPT_OBJECT_ATOMIC_REF_COUNTER
ref_counter_.fetch_add(1, std::memory_order_relaxed);
#else
++ref_counter_;
#endif
}

含义:又多了一个“拥有者”(一个新的 ObjectPtr 指向该对象)。

DecRef:减少引用数并在 0 时析构

非原子版本(逻辑最简单):

1
2
3
4
5
6
7
inline void Object::DecRef() noexcept {
if (--ref_counter_ == 0) {
if (this->deleter_ != nullptr) {
(*this->deleter_)(this);
}
}
}

含义:

  1. --ref_counter_:当前 ObjectPtr 不再持有该对象,多一个指针销毁 / reset。
  2. 如果减完后计数变为 0:说明再没有任何 ObjectPtr 指向它:
  • 调用 deleter_(this) 释放对象(类似 delete this)。

原子版本只是加了一些内存序和优化分支,语义一样:最后一个引用被销毁时才真正释放对象


ObjectPtr 的行为:在哪里调用 IncRef / DecRef

关键点:ObjectPtr<T> 是唯一对 Object::IncRef / DecRef 直接操作的类。 内部只有一个裸指针:

1
2
3
4
template <typename T>
class ObjectPtr {
private:
Object* data_{nullptr};

从原始 Object* 构造时:IncRef

1
2
3
4
5
explicit ObjectPtr(Object* data) noexcept : data_(data) {
if (data != nullptr) {
data_->IncRef(); // 这里 +1
}
}

谁会调用这个构造函数?

  • GetRef / GetObjectPtr 等内部接口;
  • make_object 之后,封装到 ObjectPtr 时。

效果:每创建一个新的智能指针“拥有者”,引用计数 +1

拷贝构造时:IncRef

1
2
ObjectPtr(const ObjectPtr<T>& other) noexcept
: ObjectPtr(other.data_) {} // 委托给上面的构造
  • 拷贝构造直接复用 ObjectPtr(Object*) 构造函数。
  • 所以每次拷贝 ObjectPtr,都会对 other.data_ 调用 IncRef()

模板版本同理(从 ObjectPtr<U> 拷贝到父类 ObjectPtr<T>),一样加计数:

1
2
3
4
5
template <typename U>
ObjectPtr(const ObjectPtr<U>& other) noexcept
: ObjectPtr(other.data_) {
static_assert(std::is_base_of<T, U>::value, ...);
}

=> “拷贝时 IncRef()” 就是在这里实现的。

析构 / reset 时:DecRef

1
2
3
4
5
6
7
8
9
10
~ObjectPtr() {
this->reset();
}

void reset() noexcept {
if (data_ != nullptr) {
data_->DecRef(); // 这里 -1,必要时释放对象
data_ = nullptr;
}
}
  • 每一个 ObjectPtr 在生命周期结束(离开作用域、被销毁)时调用 ~ObjectPtr()
  • 析构调用 reset()reset() 再调用 DecRef()

=> “析构/reset 时 DecRef()” 就在这里实现。

赋值运算符:用 copy-and-swap 保证计数平衡

1
2
3
4
5
6
7
8
9
ObjectPtr<T>& operator=(const ObjectPtr<T>& other) noexcept {
ObjectPtr(other).swap(*this); // 先拷贝 +1,再交换,旧值在临时对象析构时 -1
return *this;
}

ObjectPtr<T>& operator=(ObjectPtr<T>&& other) noexcept {
ObjectPtr(std::move(other)).swap(*this);
return *this;
}

以拷贝赋值为例说明计数变化:

1
2
3
4
ObjectPtr<T> a = ...;   // a 持有 X,ref_count(X) = 1
ObjectPtr<T> b = ...; // b 持有 Y,ref_count(Y) = 1

b = a;

执行过程:

  1. ObjectPtr(other):构造一个临时 tmp,其中 tmp.data_ = a.data_
  • 调用 IncRef(),所以 ref_count(X) 从 1 -> 2;
  1. tmp.swap(*this):交换 tmp.data_b.data_
  • 此时 b.data_ 指向 X,tmp.data_ 指向原来的 Y;
  1. 语句结束,tmp 析构:
  • ~ObjectPtr() 中调用 reset(),对 Y 调用 DecRef()
  • ref_count(Y) 1 -> 0,触发删除 Y。

结果:

  • X 被 a 和 b 共享,计数 = 2;
  • Y 无人再引用,被释放。

计数始终平衡:谁新持有(拷贝)就 +1,谁不再持有(析构/覆盖)就 -1。


句柄层(ObjectRef, List 等)的拷贝/析构如何影响计数

ObjectRef 内部就是一个 ObjectPtr<Object>

1
2
3
4
5
6
7
8
class ObjectRef {
protected:
ObjectPtr<Object> data_;
public:
ObjectRef() noexcept = default;
explicit ObjectRef(ObjectPtr<Object> data) noexcept : data_(std::move(data)) {}
// 拷贝构造/赋值/析构 全都是默认生成,对 data_ 做成员拷贝/析构
};

List 为例,通过宏生成的方法类似:

1
2
3
4
5
6
7
8
9
class List : public ObjectRef {
public:
List() noexcept = default;
explicit List(ObjectPtr<Object> n) noexcept : ObjectRef(std::move(n)) {}
List(const List& other) noexcept = default;
List(List&& other) noexcept = default;
List& operator=(const List& other) noexcept = default;
List& operator=(List&& other) noexcept = default;
};

行为:

  • 拷贝 List

  • 调用 List(const List&),成员 data_ 执行拷贝构造;

  • data_ObjectPtr<Object>,其拷贝构造会调用 IncRef()

  • List 析构:

  • data_ 析构,调用 ObjectPtr::~ObjectPtr() -> reset() -> DecRef()

因此,“拷贝 List -> IncRef(),销毁 List -> DecRef()”是通过 ObjectPtr 这层自动完成的。


用例:画一条完整的计数变化时间线

假设有一个底层节点 ListNode,其 ref_counter_ 初始为 1(刚创建,暂不包到 ObjectPtr 里先忽略这一步的构造逻辑,直接从第一个 ObjectPtr 开始考虑):

1
2
3
4
5
6
7
List a = some_function();  // a 内部有一个 ObjectPtr,指向节点 N
// 设此时 ref_counter(N) = 1
List b = a; // 拷贝构造 List
List c;
c = a; // 拷贝赋值
b = List(); // 赋值为一个新的空 List
// 作用域结束,a, c, 临时对象依次析构

计数变化:

  1. List a = ...;
  • 创建一个 ObjectPtr<Object> 指向 N,调用 IncRef()
  • 假设创建时从原始裸指针开始,N: 1 → 1(构造初始化已经 1,不再重复考虑这一细节),总之最终 use_count() 为 1 或 2,取决于 make_object 实现,这里简化为“此刻 ref_count(N) = 1”。
  1. List b = a;
  • 拷贝构造 Listdata_ 拷贝构造:
  • ObjectPtr 拷贝构造调用 IncRef()
  • ref_counter(N) : 1 → 2
  1. c = a;
  • c.data_ 原本是空指针,拷贝赋值等价于:

  • 构造临时 ObjectPtr tmp(a.data_)ref_counter(N) : 2 → 3

  • swap(tmp, c.data_)c.data_ 指向 N;

  • tmp 析构,因 tmp 原来持有的是 nullptr,因此这里对 N 无影响(这里简化理解;如果 c 原来持有别的对象,也会在 tmp 析构时对那个对象做 DecRef())。

  • 结果:ref_counter(N) = 3(a、b、c 都指向 N)。

  1. b = List();
  • List() 默认构造,内部的 data_ 是一个默认构造的 ObjectPtr<Object>
  • 默认构造的 ObjectPtr 内部 data_ = nullptr,不会对任何 ObjectIncRef()
  • 因此,创建 List() 这一步对 N 完全没有影响,ref_count(N) 仍然是 3。
  • 记住这一点:右值 List() 的内部指针是 nullptr
  • 为什么?
1
2
3
4
5
6
7
8
9
explicit ObjectPtr(Object* data) noexcept : data_(data) {
if (data != nullptr) {
data_->IncRef();
}
}
// 因为 data == nullptr,所以:
//不会调用 IncRef();
//tmp.data_ 也是 nullptr;
//此时对 N 的计数没有任何影响,仍然是 3。
  • swap(tmp, b.data_):b.data_ 改指向新的(或空);
  • tmp 析构,对旧的 N 调用 DecRef()ref_counter(N) : 3 → 2
  • 综合起来其实就是 b 由指向 N → 指向其它(或空),N 的计数从 3 → 2。
  1. 作用域结束:
  • c 析构:c.data_ 析构,DecRef()ref_counter(N): 2 → 1

  • a 析构:DecRef()ref_counter(N): 1 → 0,触发 deleter_(this),释放 N;

  • 其他临时对象的析构一般不会再影响 N(已经释放了)。

  • b.data_ == nullptr,即 b 变成空引用;

  • ac 仍然指向 N;

  • N 的引用计数从 3 变为 2。

所以:

  • 每增加一个活跃的句柄 / 智能指针,计数 +1;
  • 每一个句柄 / 智能指针消失或不再指向该对象,计数 -1;
  • 当最后一个引用释放时,计数到 0,删除对象。

总结成一句话

  • ObjectPtr 是唯一操作 Object::IncRef / DecRef 的封装:

  • 拷贝构造 / 从原始 Object* 构造时:IncRef()

  • 析构 / reset() / 赋值导致旧指针被替换时:DecRef()

  • ObjectRef(以及 List 等所有容器句柄)内部只是持有一个 ObjectPtr,靠默认的拷贝/析构行为把这些加减计数“传递”到 Object 上;

  • 这样设计保证了:对象只在无人引用时自动释放,多处引用时共享同一份数据


类型系统

matxscript的类型系统允许开发者创建具有明确类型信息的对象,并能在运行时进行准确的类型识别。这一能力依赖于几个关键机制:

静态类型信息

每个具体对象类都需要声明一组静态常量来描述其类型属性:

  • _type_key:全局唯一的类型名称字符串
  • _type_index:预分配的类型索引或者TypeIndex::kDynamic(表示动态分配)
  • _type_final:标记此类是否可被进一步继承
  • _type_child_slots:预留的孩子类型数量,用于优化运行时类型检查性能

例如:

1
2
3
4
5
6
class MyObject : public Object {
public:
static constexpr const uint32_t _type_index = TypeIndex::kDynamic;
static constexpr const char* _type_key = "MyNamespace.MyObject";
MATXSCRIPT_DECLARE_FINAL_OBJECT_INFO(MyObject, Object);
};

类型注册宏

为了使新定义的对象类型能够在运行时被正确识别,需要使用相应的宏来进行注册:

  • MATXSCRIPT_DECLARE_BASE_OBJECT_INFO(TypeName, ParentType):用于声明可继承的对象类型
  • MATXSCRIPT_DECLARE_FINAL_OBJECT_INFO(TypeName, ParentType):用于声明不可继承的最终对象类型
  • MATXSCRIPT_REGISTER_OBJECT_TYPE(TypeName):在源文件中实际执行类型注册操作

这些宏会生成必要的辅助函数和初始化代码,确保类型体系能够正常工作。

运行时类型上下文

真正的类型管理是在TypeContext类(位于src/runtime/object.cc)中完成的。这个单例模式的类维护了一个全局类型表,记录着每种已知类型的信息,包括它们的名称、父类型关系、可用槽位等元数据。

当首次请求某个动态类型索引时,系统会在互斥锁保护下为其分配一个新的编号,并将其相关信息存入类型表中供后续查询使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
struct TypeInfo {
uint32_t index{0}; // 这个类型本身的索引
uint32_t parent_index{0}; // 父类型的索引

// [index, index + num_slots) 这段连续的索引区域
// 是为这个类型及其子类保留的“槽位”区间。
uint32_t num_slots{0}; // 为“自己 + 子类”预留的总槽位数
uint32_t allocated_slots{0}; // 已经使用了多少个槽位

bool child_slots_can_overflow{true}; // 槽位用完后,是否允许继续扩展
String name; // 类型名
size_t name_hash{0}; // 类型名的哈希
};
class TypeContext {
public:
// NOTE: this is a relatively slow path for child checking
// Most types are already checked by the fast-path via reserved slot checking.
bool DerivedFrom(uint32_t child_tindex, uint32_t parent_tindex) {
// invariance: child's type index is always bigger than its parent.
if (child_tindex < parent_tindex)
return false;
if (child_tindex == parent_tindex)
return true;
{
std::lock_guard<std::mutex> lock(mutex_);
MXCHECK_LT(child_tindex, type_table_.size());
while (child_tindex > parent_tindex) {
child_tindex = type_table_[child_tindex].parent_index;
}
}
return child_tindex == parent_tindex;
}

uint32_t GetOrAllocRuntimeTypeIndex(const string_view& skey,
uint32_t static_tindex,
uint32_t parent_tindex,
uint32_t num_child_slots,
bool child_slots_can_overflow) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = type_key2index_.find(skey);
if (it != type_key2index_.end()) {
return it->second;
}
// try to allocate from parent's type table.
MXCHECK_LT(parent_tindex, type_table_.size())
<< " skey= " << skey << "static_index=" << static_tindex;
TypeInfo& pinfo = type_table_[parent_tindex];
MXCHECK_EQ(pinfo.index, parent_tindex);

// if parent cannot overflow, then this class cannot.
if (!pinfo.child_slots_can_overflow) {
child_slots_can_overflow = false;
}

// total number of slots include the type itself.
uint32_t num_slots = num_child_slots + 1;
uint32_t allocated_tindex;

if (static_tindex != TypeIndex::kDynamic) {
// statically assigned type
allocated_tindex = static_tindex;
MXCHECK_LT(static_tindex, type_table_.size());
MXCHECK_EQ(type_table_[allocated_tindex].allocated_slots, 0U)
<< "Conflicting static index " << static_tindex << " between "
<< type_table_[allocated_tindex].name << " and " << skey;
} else if (pinfo.allocated_slots + num_slots <= pinfo.num_slots) {
// allocate the slot from parent's reserved pool
allocated_tindex = parent_tindex + pinfo.allocated_slots;
// update parent's state
pinfo.allocated_slots += num_slots;
} else {
MXCHECK(pinfo.child_slots_can_overflow)
<< "Reach maximum number of sub-classes for " << pinfo.name;
// allocate new entries.
allocated_tindex = type_counter_;
type_counter_ += num_slots;
MXCHECK_LE(type_table_.size(), type_counter_);
type_table_.resize(type_counter_, TypeInfo());
}
MXCHECK_GT(allocated_tindex, parent_tindex);
// initialize the slot.
type_table_[allocated_tindex].index = allocated_tindex;
type_table_[allocated_tindex].parent_index = parent_tindex;
type_table_[allocated_tindex].num_slots = num_slots;
type_table_[allocated_tindex].allocated_slots = 1;
type_table_[allocated_tindex].child_slots_can_overflow = child_slots_can_overflow;
type_table_[allocated_tindex].name = String(skey.data(), skey.size());
type_table_[allocated_tindex].name_hash = SmartHash()(skey);
// update the key2index mapping.
type_key2index_[skey] = allocated_tindex;
return allocated_tindex;
}

const String& TypeIndex2Key(uint32_t tindex) {
std::lock_guard<std::mutex> lock(mutex_);
MXCHECK(tindex < type_table_.size() && type_table_[tindex].allocated_slots != 0)
<< "Unknown type index " << tindex;
return type_table_[tindex].name;
}

bool TryTypeIndex2Key(uint32_t tindex, string_view* tkey) {
std::lock_guard<std::mutex> lock(mutex_);
if (tindex < type_table_.size() && type_table_[tindex].allocated_slots != 0) {
*tkey = type_table_[tindex].name;
return true;
} else {
return false;
}
}

size_t TypeIndex2KeyHash(uint32_t tindex) {
std::lock_guard<std::mutex> lock(mutex_);
MXCHECK(tindex < type_table_.size() && type_table_[tindex].allocated_slots != 0)
<< "Unknown type index " << tindex;
return type_table_[tindex].name_hash;
}

uint32_t TypeKey2Index(const string_view& skey) {
auto it = type_key2index_.find(skey);
MXCHECK(it != type_key2index_.end())
<< "Cannot find type " << skey
<< ". Did you forget to register the node by MATXSCRIPT_REGISTER_NODE_TYPE ?";
return it->second;
}

void Dump(int min_children_count) {
std::vector<int> num_children(type_table_.size(), 0);
// reverse accumulation so we can get total counts in a bottom-up manner.
for (auto it = type_table_.rbegin(); it != type_table_.rend(); ++it) {
if (it->index != 0) {
num_children[it->parent_index] += num_children[it->index] + 1;
}
}

for (const auto& info : type_table_) {
if (info.index != 0 && num_children[info.index] >= min_children_count) {
std::cerr << '[' << info.index << "] " << info.name
<< "\tparent=" << type_table_[info.parent_index].name
<< "\tnum_child_slots=" << info.num_slots - 1
<< "\tnum_children=" << num_children[info.index] << std::endl;
}
}
}

static TypeContext* Global() {
static TypeContext inst;
return &inst;
}

private:
TypeContext() {
type_table_.resize(TypeIndex::kStaticIndexEnd, TypeInfo());
type_table_[0].name = "runtime.Object";
}
// mutex to avoid registration from multiple threads.
std::mutex mutex_;
std::atomic<uint32_t> type_counter_{TypeIndex::kStaticIndexEnd};
std::vector<TypeInfo> type_table_;
std::unordered_map<String, uint32_t, SmartHash, SmartEqualTo> type_key2index_;
};

TypeContext 这个类的作用,可以概括成一句话:

它是一个全局的“类型注册与查询中心”,用来维护运行时的类型层次结构(继承关系)以及类型名和类型索引之间的映射。

下面按功能拆开讲:


维护类型层次结构(父子关系)

核心思想:每个类型在运行时都有一个 uint32_t 的类型索引(type index),并且知道自己的父类型索引,从而形成一棵类型继承树。

  • type_table_ 是一个 std::vector<TypeInfo>,每个 TypeInfo 表示一个类型的信息:

  • index:当前类型的索引

  • parent_index:父类型的索引

  • num_slots / allocated_slots:为子类型预留的“类型槽位”及已使用数量

  • child_slots_can_overflow:是否允许超过预留槽位继续扩展

  • name / name_hash:类型名称及其哈希

  • 构造函数中:

1
2
3
4
TypeContext() {
type_table_.resize(TypeIndex::kStaticIndexEnd, TypeInfo());
type_table_[0].name = "runtime.Object";
}

初始化了一个类型表,索引 0 是根类型 "runtime.Object"


判断继承关系:DerivedFrom

1
bool DerivedFrom(uint32_t child_tindex, uint32_t parent_tindex)

用途:判断一个类型(child_tindex)是否是另一个类型(parent_tindex)的子类或本身。

实现方式:

  1. 利用不变式:子类的 type index 一定大于父类
  • 如果 child_tindex < parent_tindex → 肯定不是子类,返回 false
  • 如果相等 → 是同一类型,返回 true
  1. 在互斥锁保护下,从子类型开始不断沿 parent_index 往上跳:
1
2
3
4
while (child_tindex > parent_tindex) {
child_tindex = type_table_[child_tindex].parent_index;
}
return child_tindex == parent_tindex;

最终判断能否追溯到父类型。

注释里也说了:这是一个相对慢的路径,真正高频的类型检查通常用更快速的“保留槽位”机制来判断。


注册/分配运行时类型索引:GetOrAllocRuntimeTypeIndex

1
2
3
4
5
uint32_t GetOrAllocRuntimeTypeIndex(const string_view& skey,
uint32_t static_tindex,
uint32_t parent_tindex,
uint32_t num_child_slots,
bool child_slots_can_overflow)

用途:**给一个类型名(skey)分配一个运行时类型索引,并记录它的父类、子类槽位信息等。**这个函数也负责处理“静态分配”和“动态分配”的两种情况。

步骤:

  1. 先查 type_key2index_,如果已经注册,直接返回原来的 type index。
  2. 否则,需要新注册:
  • 确认 parent_tindex 合法,拿到父类型 pinfo
  • 如果父类不能 overflow 子槽 (!pinfo.child_slots_can_overflow),那么子类也不能
  1. 计算当前类型需要占用的槽位数(自身 + 子槽):
1
uint32_t num_slots = num_child_slots + 1;
  1. 决定 allocated_tindex(当前类型的索引):
  • 静态类型static_tindex != TypeIndex::kDynamic) 直接使用编译期给定的 static_tindex,并检查该位置没有被占用。

  • 动态类型:从父类的预留槽中分配:

  • 如果 pinfo.allocated_slots + num_slots <= pinfo.num_slots,就在父类的预留范围内分配

  • 否则,如果父类允许溢出(overflow),则在 type_table_ 尾部扩展一个新的区间作为新类型的槽位

  • 如果不允许 overflow,则直接报错(达到子类型最大数量)

  1. 对新类型在 type_table_ 中进行初始化:
  • 设置 index, parent_index, num_slots, allocated_slots=1
  • 设置 child_slots_can_overflow
  • 填写 namename_hash
  1. 更新 type_key2index_ 映射,返回分配到的 allocated_tindex

这部分逻辑使得系统可以:

  • 有计划地为某些类型预留固定数量的“子类型位置”(有利于快速类型判断和布局)
  • 在必要时支持扩张(overflow),不硬性限制子类型数量

类型名与索引互查

索引 → 名称

  • const String& TypeIndex2Key(uint32_t tindex)
  • bool TryTypeIndex2Key(uint32_t tindex, string_view* tkey)
  • size_t TypeIndex2KeyHash(uint32_t tindex)

功能:

  • 根据类型索引拿到类型名称或名称哈希
  • 会检查该索引是否在表内且已经分配(allocated_slots != 0),否则报错或返回 false

名称 → 索引

  • uint32_t TypeKey2Index(const string_view& skey)

功能:

  • 根据类型名查找对应的 type index
  • 如果找不到,会提示是否忘记注册该类型(通过宏 MATXSCRIPT_REGISTER_NODE_TYPE

调试输出类型树:Dump

1
void Dump(int min_children_count)

用途:方便调试,输出所有子类数量不少于 **min_children_count** 的类型,以及它们的父类、预留子槽数量等。

实现:

  1. 创建 num_children 数组,记录每个类型的所有子孙数目
  2. type_table_ 末尾往前累加(逆序遍历),实现自底向上的统计
  3. 输出符合条件的类型信息到 std::cerr

全局单例:Global

1
2
3
4
static TypeContext* Global() {
static TypeContext inst;
return &inst;
}
  • TypeContext 被设计为进程内的全局单例,所有类型注册和查询都通过 TypeContext::Global() 访问。
  • 内部用 std::mutex 保证多线程注册/查询的线程安全。

TypeContext 的主要作用是:

  1. 作为一个全局的运行时类型系统管理器
  2. 管理:
  • 类型索引(type index)
  • 类型名称
  • 父子继承关系
  • 预留子类型槽位与溢出策略
  1. 提供接口完成:
  • 注册/分配新类型索引
  • 判断继承关系(DerivedFrom
  • 类型名与索引之间的互相转换
  • 打印调试信息(类型树结构)

下面用一个简化的“伪使用示例”来说明 TypeContext 的使用流程:注册几个类型,然后做继承判断与查询。

假设有这样的类型关系:

  • runtime.Object(根,索引 0,系统已初始化好)

  • MyBase

  • MyChild

我们在 C++ 里模拟一下操作(伪代码,接近真实用法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "type_context.h"

void Example() {
TypeContext* ctx = TypeContext::Global();

// 假设 runtime.Object 已经是 0
uint32_t object_tindex = 0;

// 1. 注册 MyBase 类型
// 假设它有静态索引 10(仅作为示例),
// 预留 10 个子类槽位,并允许子类继续 overflow。
uint32_t mybase_tindex = ctx->GetOrAllocRuntimeTypeIndex(
"MyBase", // skey: 类型名
10, // static_tindex: 静态索引(非 kDynamic)
object_tindex, // parent_tindex: 父类型是 runtime.Object
10, // num_child_slots: 为 MyBase 的子类预留 10 个槽位
true // child_slots_can_overflow: 超过 10 仍可扩展
);

// 2. 注册 MyChild 类型
// 这里假设它是动态索引(没有固定静态 index),
// 不再给它预留子类槽位,只需要自身一个槽位。
uint32_t mychild_tindex = ctx->GetOrAllocRuntimeTypeIndex(
"MyChild", // skey
TypeIndex::kDynamic, // 动态分配索引
mybase_tindex, // 父类型是 MyBase
0, // num_child_slots: 不给它预留子类
false // child_slots_can_overflow: 它不能再 overflow
);

// 3. 判断继承关系
bool b1 = ctx->DerivedFrom(mychild_tindex, mybase_tindex);
// b1 == true,因为 MyChild 是 MyBase 的子类

bool b2 = ctx->DerivedFrom(mychild_tindex, object_tindex);
// b2 == true,因为 MyChild 间接继承 runtime.Object

bool b3 = ctx->DerivedFrom(mybase_tindex, mychild_tindex);
// b3 == false,父类不是子类的派生类型

// 4. 名称 -> 索引
uint32_t idx_from_name = ctx->TypeKey2Index("MyBase");
// idx_from_name == mybase_tindex

// 5. 索引 -> 名称
const String& name_from_idx = ctx->TypeIndex2Key(mychild_tindex);
// name_from_idx == "MyChild"

// 6. 安全尝试索引 -> 名称(可能失败)
string_view tkey;
if (ctx->TryTypeIndex2Key(12345, &tkey)) {
// 找到了
} else {
// 没有这个类型索引
}

// 7. 调试打印:只打印拥有至少 1 个子孙的类型
ctx->Dump(1);
}

要点说明:

  1. GetOrAllocRuntimeTypeIndex 的参数含义:
  • skey:类型的字符串标识(通常就是类型名)

  • static_tindex

  • 如果你为类型预先安排了固定索引(编译时约定),传这个值

  • 如果没有固定索引,则传 TypeIndex::kDynamic,由系统动态分配

  • parent_tindex:父类型的索引,用来建立继承关系

  • num_child_slots:为这个类型的子类预留多少槽位

  • child_slots_can_overflow:当预留槽位用完后,是否允许继续扩展

  1. 一旦类型注册成功:
  • 可以通过 TypeKey2Index / TypeIndex2Key 做名称与索引的双向转换
  • 可以通过 DerivedFrom(child, parent) 判断任何两种类型是否存在“子类/祖先”关系
  1. 实际工程中通常不会手写调用 GetOrAllocRuntimeTypeIndex,而是通过宏 MATXSCRIPT_REGISTER_NODE_TYPE(MyType) 之类的封装宏进行注册; 宏内部会调用 TypeContext::Global()->GetOrAllocRuntimeTypeIndex(...)

如果你给我看一下 TypeInfo 结构体的定义,我可以再结合它把“预留子槽”的布局画成一张小图,帮助理解索引分配策略。

内存管理

matxscript采用基于引用计数的自动内存管理模式。每当一个新的ObjectPtr指向某个对象时,该对象的引用计数就会加一;反之,当某个ObjectPtr销毁或不再指向该对象时,引用计数减一。一旦引用计数变为零,就意味着没有任何活跃的引用存在了,此时就会触发对象的清理过程。

这种方案有几个显著优点:

  • 自动化程度高,减轻程序员负担
  • 允许跨线程共享对象所有权(前提是使用原子引用计数)
  • 相比垃圾收集机制延迟更低,更加可预测

不过也需要注意循环引用问题可能导致内存泄漏,这要求开发者在设计对象间关系时格外小心。

继承与多态

借助精心设计的类型系统,matxscript很好地支持了面向对象编程中的继承和多态概念。

子类只需按照规定格式填写自己的类型信息并注册到系统中即可成为合法的对象类型。运行时可以通过IsInstance<T>()方法判断任意对象是否属于指定类型或其派生类型。

此外,配合使用Downcast<T>(ref)函数还能实现在保持类型安全性的同时进行向上转型操作。

总结

matxscript运行时的对象模型是一套完整而优雅的设计方案,兼顾了效率、灵活性和易用性。通过对引用计数技术的应用,实现了高效的内存管理;通过丰富的类型信息支持,赋予了强大的运行时类型识别能力;再加上良好的继承体系构建,极大地提升了代码复用率和模块化水平。无论是对于底层基础设施建设还是上层应用开发来说,都是极其宝贵的资产。

  • 标题: matxscript运行时系统之对象模型
  • 作者: 鱿鱼圈
  • 创建于 : 2025-11-23 23:50:00
  • 更新于 : 2026-06-12 17:14:28
  • 链接: https://yuyanqi.com/2025/11/23/matxscript对象模型/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论