Dioxus Generational Box:让任意 Rust 类型实现Copy的无unsafe状态运行时深度解析
【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus
Generational Box 是 Dioxus 全栈应用框架(web / desktop / mobile)背后一个独立、可复用的底层运行时 crate。它解决的是一个非常核心的工程难题:如何让一个本身不是Copy的 Rust 类型,也能像Copy值一样随处传递,同时又保留运行时的借用检查与确定性析构语义。本篇文章以 packages/generational-box/README.md 为骨架,结合 crate 源码、测试与 Dioxus core 中的实际使用,系统讲解它的类型体系、工作原理、两种存储后端、错误处理与调试手段,帮助你理解乃至在自己的状态库中复刻这套方案。
读完本文,你将掌握:Owner/GenerationalBox/ 存储后端三者如何协作;一个"带代数校验的内存槽回收池"是如何用零unsafe代码实现的;以及 Dioxus 是如何借助它把响应式状态(signal 等)做成纯Copy句柄的。
一、什么是 Generational Box
官方 README 的第一句话给出了准确定位:
Generational Box is a runtime for Rust that allows any static type to implement
Copy.
即:它允许任意'static类型以Copy的方式被共享访问。核心思路并不是让类型本身变成Copy,而是把一个不可Copy的值"搬进"一个运行时托管的内存槽中,由运行时返回一个轻量的、可Copy的句柄(GenerationalBox),所有后续访问都通过这个句柄进行。正如 README 所说,它可以与全局运行时组合,形成类似dioxus-signals那样符合人体工学的状态方案——而本项目(Dioxus)的 core 也确实直接复用了它(见下文第四节与 packages/core/src/runtime.rs)。
更值得注意的是 README 中的这一句承诺:
This crate doesn't have any
unsafecode.
整个 crate 不使用任何unsafe代码。在 Rust 中要做到"绕过借用检查的句柄 + 运行时回收"却不用unsafe,通常依赖两条安全路径:Box::leak(安全地制造'static内存)与std::cell::RefCell/parking_lot::RwLock的运行时借用。这正是 Generational Box 的核心把戏,后文会展开。
二、三个核心类型:Store / Owner / GenerationalBox
README 明确指出管理状态的是三个主要类型,它们职责清晰:
| 类型 | 职责 |
|---|---|
Store(对应存储后端) | 负责回收已被释放的 generational box(内存槽池);按 README 建议,应用通常应只有一个 store,或每个线程一个 store |
Owner | 负责真正 drop generational box,相当于运行时的生命周期守卫;凡是经某 owner 创建的状态,都会在该 owner 被 drop 时一并被释放 |
GenerationalBox | 核心的Copy状态句柄;当 owner 被 drop 时,它指向的值也随之被释放(之后对它的读写会报错) |
需要说明的是,在源码实现中"Store"这一角色由存储后端类型(UnsyncStorage/SyncStorage,也就是Storagetrait 的实现)承担:它们内部维护着被回收内存槽的池子,并提供了owner()等工厂方法。而GenerationalBox<T, S>与Owner<S>都以S作为泛型参数,S默认是UnsyncStorage:
GenerationalBox<T, S = UnsyncStorage>(见 lib.rs)Owner<S: AnyStorage + 'static = UnsyncStorage>(见 lib.rs)- 类型擦除的 ID:
GenerationalBoxId { data_ptr: *const (), generation: NonZeroU64 },它本身是Clone + Copy + PartialEq + Eq + Hash,可安全地作为键值(见 lib.rs)。
生命周期链条
三者的生命周期关系可以概括为一句话:
- 调用
UnsyncStorage::owner()(或S::owner())得到一个Owner<S>; - 调用
owner.insert(value)把值搬进存储,得到GenerationalBox<T, S>; - 该 box 记录的指针与代数被登记在 owner 的"名下"(
OwnerInner.owned: Vec<GenerationalPointer<S>>,见 lib.rs); - 当 owner 被 drop 时,
OwnerInner::drop会遍历owned,把每一个 location回收(recycle)回存储池(见 lib.rs)。
impl<S: AnyStorage> Drop for OwnerInner<S> { fn drop(&mut self) { for location in self.owned.drain(..) { location.recycle(); } } }这就是 README 所说"任何你用 owner 创建的状态都会在该 owner drop 时一并 drop"的源码级答案。
三、快速上手:从 README 的最小示例说起
README 提供的最小示例可以直接跑通:
use generational_box::{UnsyncStorage, AnyStorage}; // Create an owner for some state for a scope let owner = UnsyncStorage::owner(); // Create some non-copy data, move it into a owner, and work with copy data let data: String = "hello world".to_string(); let key = owner.insert(data); // The generational box can be read from and written to like a RefCell let value = key.read(); assert_eq!(*value, "hello world");这里有三个值得注意的细节:
String本来不是Copy,但示例里key = owner.insert(data)之后的key却是一个可以随意复制、跨函数传递的Copy句柄;- 读操作语义接近
RefCell:key.read()返回的是一个带Deref的引用守卫,在调试构建中还带有借用来源信息(详见第六节); - 写入同样简单:
key.write()/key.set(...)可以修改内部值。
在基本用法之上扩展
基于 lib.rs 中公开的 API,你可以进一步组合出更完整的状态操作:
use generational_box::{AnyStorage, UnsyncStorage}; let owner = UnsyncStorage::owner(); let counter = owner.insert(0_i32); // 写入:write() 返回可 DerefMut 的守卫 *counter.write() += 1; // 便捷 set:直接覆盖值 counter.set(42); // 读取 assert_eq!(*counter.read(), 42); // 判断两个句柄是否指向同一内存位置(不比较内部值) let counter2 = counter; // Copy!可以随意赋值 assert!(counter.ptr_eq(&counter2)); // 拿到可用于 HashMap/HashSet 的类型擦除 id let id = counter.id(); assert_eq!(id, counter2.id());引用计数变体:多个 owner 共享同一份值
普通insert的语义是"一个值对应唯一一个 owner"。若希望多个 owner、多个句柄指向同一份数据,README 虽未展开,但 crate 提供了引用计数(reference counting)方案,与 API 一览见 lib.rs:
owner.insert_rc(value):创建一个引用计数的数据槽,再包一层"引用";owner.insert_reference(other_box):在另一个owner 名下挂载一份对既有 box 的引用,使值一直存活到所有引用 owner 都释放;GenerationalBox::leak_reference()/point_to():在无 owner 场景下手动制造引用 / 改指向。
例如:
use generational_box::{AnyStorage, UnsyncStorage}; let owner_a = UnsyncStorage::owner(); let owner_b = UnsyncStorage::owner(); let rc = owner_a.insert_rc(String::from("shared")); // 在 owner_b 名下创建对同一数据的引用 let ref_b = owner_b.insert_reference(rc).unwrap(); drop(owner_a); // 数据此时还活着,因为 owner_b 仍持有引用 assert_eq!(*ref_b.read(), "shared");这一机制在测试中也有完整的随机化验证(见 reference_counting.rs 与 basic.rs 的fuzz_rc)。
四、它在 Dioxus 里到底解决什么问题
理解一个底层 crate 最好的方式,是看它在上层框架中的真实用法。搜索仓库可以看到generational_box被 packages/core/Cargo.toml、packages/document、packages/desktop、packages/fullstack-core 等多个上层包依赖。
其中最关键的是 Dioxus core 的 runtime.rs:
use generational_box::{AnyStorage, Owner, SyncStorage, UnsyncStorage}; // ... pub fn current_owner<S: AnyStorage>(&self) -> Owner<S> { self.get_state(self.current_scope_id()).owner() } pub fn scope_owner<S: AnyStorage>(&self, scope: ScopeId) -> Owner<S> { self.get_state(scope).owner() }也就是说,Dioxus 为每一个组件 scope 都维护了一个Owner。scope 生命周期内创建的各种上下文、状态、句柄都由这个 owner 托管:当组件作用域结束、owner 被 drop,它名下的一切 generational box 都被自动回收,相关的悬空句柄随后访问会得到"值已被释放"的明确错误,而不会触发 UB。这正是 README 中所说 "It can be combined with a global runtime to create an ergonomic state solution" 的具象体现——你可以在此基础上再叠加全局状态注册表,做出 signal 式的一等状态 API。
五、How it works:无unsafe的代数 arena
README 的 "How it works" 一节对内部机制做了高度浓缩的概括:
Internally,
generational-boxcreates an arena of generationalRefCells that are recycled when the owner is dropped. You can think of the cells as something like&'static RefCell<Box<dyn Any>>with a generational check to make recycling a cell easier to debug. ThenGenerationalBoxes areCopybecause the&'staticpointer isCopy.
把这句话拆开,恰好对应源码中的三个关键机制:
1. 永不释放的'static内存槽
在 unsync.rs 与 sync.rs 的create_new中,当池中没有可复用的槽时,会通过Box::leak把存储永久钉在堆上:
let storage: &'static Self = &*Box::leak(Box::new(Self { borrow_info: Default::default(), data: RefCell::new(StorageEntry::new(value)), }));&'static T本身就是Copy,所以基于它的句柄天然可以Copy。这不是"内存泄漏",而是内存池的有意为之——这些槽一旦创建就不会真的归还操作系统,而是在 owner 释放后被收回池子反复使用。
2. 代数(generation)校验
每个存储条目StorageEntry<T>内部维护一个NonZeroU64代数,见 entry.rs:
pub(crate) struct StorageEntry<T> { generation: NonZeroU64, pub(crate) data: T, } impl<T> StorageEntry<T> { pub fn valid(&self, location: &GenerationalLocation) -> bool { self.generation == location.generation } pub fn increment_generation(&mut self) { self.generation = self.generation.checked_add(1).unwrap(); } }配合GenerationalLocation { generation, created_at }(lib.rs),每次读写前都会先做一次valid()校验:
let borrow = pointer.storage.data.try_borrow()...; if !borrow.valid(&pointer.location) { return Err(BorrowError::Dropped(ValueDroppedError::new_for_location(pointer.location))); }于是,一个槽被回收(recycle中increment_generation())之后,旧代数对应的旧句柄再访问就会被精确拒绝。这就是"代数检查让回收更容易调试"的含义:内存槽虽然被复用了,但旧句柄绝不会误读新数据。
3. 回收与复用:代数 + 池
在UnsyncStorage::recycle(unsync.rs)中可以看到完整流程:
- 再次校验代数是否仍有效(防止双重回收);
increment_generation()使旧句柄作废;- 根据条目类型处理:普通
Data直接置为Empty(drop 值);Rc条目忽略(真正的引用计数释放走drop_ref);Reference条目则递归drop_ref递减引用计数; - 把该
&'static存储槽 push 回运行时池。
下次owner.insert(...)时,create_new先从池中pop()一个槽、把新值写进去并读取当前代数作为新GenerationalLocation,从而完成"槽复用 + 代数递增"。
流程总结图(文字版)
owner.insert(value) │ ▼ 存储池 pop 一个 &'static 槽 ──池空──▶ Box::leak 新槽(generation=MIN) │ ▼ 写入 StorageEntry { generation, data },登记进 OwnerInner.owned │ ▼ 返回 Copy 的 GenerationalBox { &'static storage, GenerationalLocation } │ ▼ owner drop ──▶ 逐个 recycle: 校验代数 → generation += 1 → 值 drop/引用计数减 → 槽 push 回池 │ ▼ 旧句柄再次 read/write ──▶ valid() 失败 ──▶ BorrowError::Dropped六、两种存储后端:单线程 vs 线程安全
crate 提供了两个开箱即用的存储实现,README 说"你的应用应有一个 store 或每线程一个 store",对应关系如下:
UnsyncStorage(默认) | SyncStorage | |
|---|---|---|
| 内部锁原语 | RefCell(std::cell) | RwLock(parking_lot) |
| 池的位置 | 线程局部thread_local!(见 unsync.rs) | 全局OnceLock<Arc<Mutex<Vec<&'static SyncStorage>>>>(见 sync.rs) |
| 存的值 | Box<dyn Any> | Box<dyn Any + Send + Sync> |
| 语义 | 快,但只能在单线程内使用 | 慢一些,但可跨线程共享 |
在 sync.rs 中对SyncStorage的注释写得很直白:
A thread safe storage. This is slower than the unsync storage, but allows you to share the value between threads.
代码上两者的差异完全由泛型 + trait 消解:impl<T: 'static> Storage<T> for UnsyncStorage与impl<T: Sync + Send + 'static> Storage<T> for SyncStorage,即SyncStorage额外要求存储值Send + Sync。你的业务代码只依赖Storage<T>/AnyStoragetrait 时,可以在两种后端间无缝切换——这也是测试大量采用泛型辅助函数同时对两种后端跑同一套断言的原因(见 basic.rs)。
Storage trait:可插拔的存储抽象
两种后端之上是统一的 trait 契约。Storage<Data>定义内存槽的创建与借用语义(见 lib.rs),而AnyStorage定义引用类型与跨后端的通用能力(见 lib.rs):
Storage::new/new_rc:创建普通 / 引用计数内存位置(会优先复用回收槽);Storage::try_read/try_write:运行时借用,返回Self::Ref/Self::Mut;new_reference/change_reference:为引用计数模型提供的引用制造与改指向操作;AnyStorage::owner():默认实现返回Owner(Arc<Mutex<OwnerInner>>);AnyStorage::map/try_map/map_mut/try_map_mut:对守卫做映射(类似Ref::map),用于精细访问结构体字段。
换句话说,如果你对线程模型或性能有特殊需求,完全可以参照UnsyncStorage实现一个自定义存储后端接入。
七、引用与守卫:GenerationalRef / GenerationalRefMut
read()/write()返回的不是裸引用,而是GenerationalRef<R>/GenerationalRefMut<W>(见 references.rs)。它们像RefCell::Ref/RefMut一样实现了Deref/DerefMut,因此你可以像操作内部值一样直接调用方法:
let owner = UnsyncStorage::owner(); let value = owner.insert(String::from("hello")); let reference = value.read(); // 直接调用 String 的方法 assert_eq!(reference.as_str(), "hello");一个容易踩的坑:match 前必须先 deref
references.rs 的文档用一个反例说明:直接match reference { Colors::Red => ... }会得到类型不匹配错误,因为reference的类型是GenerationalRef<...>而不是内部枚举。正确做法是显式解引用:
use std::ops::Deref; match reference.deref() { Colors::Red => {} Colors::Green => {} }可写引用同理使用DerefMut。此外GenerationalRef还提供了map/try_map/cloned等工具方法,方便对守卫内部做投影。
八、错误类型与调试体验
crate 把"访问一个已释放值"与"违反借用规则"都建模为类型化错误而非 UB。错误体系集中在 error.rs:
| 错误类型 | 触发场景 |
|---|---|
BorrowError::Dropped(ValueDroppedError) | 读取一个已被 owner 释放的值 |
BorrowError::AlreadyBorrowedMut | 在可变借用未结束时尝试读取 |
BorrowMutError::Dropped | 写入一个已被释放的值 |
BorrowMutError::AlreadyBorrowed | 存在不可变借用时尝试写入 |
BorrowMutError::AlreadyBorrowedMut | 存在可变借用时尝试再次写入 |
对应地,read()/write()是"panic 版本"(内部unwrap,见 lib.rs),而try_read()/try_write()返回Result,适合在需要恢复的场景使用。
调试特性的开关
为了让"值在哪创建、被谁借走"可追溯,crate 在 Cargo.toml 中定义了两个可选 feature:
[features] debug_borrows = [] debug_ownership = []debug_borrows:让AlreadyBorrowedMutError携带"借用发生处的&'static Location",AlreadyBorrowedError携带借用者位置列表(见 error.rs);debug_ownership:让ValueDroppedError携带值的创建位置,方便回答"这个值在哪创建的、为什么现在没了"。
由于 crate 全面使用了#[track_caller],在debug 构建下这些错误信息无需额外开启即可附加上下文;release 构建下created_at()恒为None(见 lib.rs),如需在 release 保留可开启相应 feature。
测试如何验证这些语义
errors.rs 精确地断言了各种错误场景,例如"写借用进行中再读会返回AlreadyBorrowedMut"、"owner 释放后再读返回Dropped",并且错误里带上了调用位置。一个有意思的细节是:可写借用未结束时再读取,SyncStorage会直接死锁(RwLock 写锁独占),因此这类用例只在UnsyncStorage上测试(源码注释// For sync storage this will deadlock)。这说明选型时"单线程UnsyncStorage借用冲突可恢复、多线程SyncStorage简单直接"各有取舍。
九、正确性验证:测试与基准
该 crate 的测试覆盖相当认真:
- basic.rs:覆盖"owner 泄漏后值仍可读(
leaking_is_ok)"、"owner 释放后读报错(drops)"、"读借用期间继续 insert 不冲突(insert_while_reading)"、"释放后read()直接 panic"(#[should_panic]),以及两个基于rand的随机树形压力测试fuzz/fuzz_rc,在任意嵌套作用域中反复创建/读取/释放 box,验证回收与代数失效在复杂路径下不出现误读; - errors.rs:逐条断言错误类型与调试信息;
- reference_counting.rs:验证引用计数 drop 的边界;
- sync.rs:多线程后端的专项测试;
- benches/lock.rs:基于 criterion 的读写锁性能基准(Cargo.toml 中声明的 bench harness)。
这些测试大多是泛型驱动、同时对UnsyncStorage与SyncStorage执行,直接印证了两种后端语义等价。
十、使用与集成方式
作为依赖引入
generational-box是 Dioxus 工作区(workspace)成员,在仓库根 Cargo.toml 中统一管理版本,包信息见 packages/generational-box/Cargo.toml:
[package] name = "generational-box" edition = "2024" description = "A box backed by a generational runtime" license = "MIT OR Apache-2.0" rust-version = "1.85.0" [dependencies] parking_lot = { workspace = true } tracing = { workspace = true }若在仓库内开发,可cargo run -p <your-app> --example ...或直接以 path 依赖引用;作为独立库使用时可从 crates.io 引入同名generational-box,并按需开启debug_borrows/debug_ownership特性辅助调试。
最小可运行代码(含写操作)
use generational_box::{AnyStorage, UnsyncStorage}; fn main() { // 一个 owner 模拟一个作用域 let owner = UnsyncStorage::owner(); // String 不是 Copy,但得到的 key 是 Copy 句柄 let key = owner.insert(String::from("hello")); let copy_of_key = key; // 直接复制,没有任何所有权问题 // 像 RefCell 一样读写 key.write().push_str(" world"); assert_eq!(&*copy_of_key.read(), "hello world"); // owner 在此处 drop,key 也随之失效 // 之后 key.read() 将 panic;key.try_read() 返回 Err(BorrowError::Dropped(_)) }总结
Generational Box 用一套"'static内存池 + 代数校验 + owner 生命周期守卫"的设计,在零unsafe的前提下让任意'static类型获得了Copy句柄,并保证了释放后的访问是类型化错误而不是内存不安全。它把内存回收从"何时归还操作系统"推迟为"何时归还代数池",通过generation递增让悬空句柄自然失效,既解决了&'static指针无法安全释放的难题,又保持了极高的调试可观测性。
对 Dioxus 而言,它直接构成了 core 运行时中 scope 状态托管的基石——每个组件作用域一个Owner,从而让响应式状态句柄可以自由复制、跨闭包传递,而生命周期依旧有迹可循。如果你正在设计自己的信号库、状态容器或事件系统,这套"代数 arena + owner"模式值得作为首选参考实现。
延伸阅读:本主题相关的源码入口为 packages/generational-box/src/lib.rs、unsync.rs、sync.rs、entry.rs 与 error.rs;上层集成可参考 packages/core/src/runtime.rs 中的current_owner/scope_owner;正确性验证可阅读 tests/basic.rs 与 tests/errors.rs。
【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考