generational-box
主线位置:更新主线上「用户闭包写 signal」之前的那一步:闭包里的
count为什么能被move进去还照样能用。所有 signal、memo、resource、ReactiveContext本身,值都存在这一层;组件卸载时它们怎么被回收,也在这一层。首屏主线上每个组件第一次运行时建的 signal 同样放在这里。
flowchart TB
owner["Owner 一个 scope 一个"] -->|insert| slot["格子 StorageEntry"]
slot --> gen["generation 代数"]
slot --> data["data 值"]
handle["句柄 地址加代数"] -->|try_read| check{"代数相同?"}
check -->|是| okref["Ok 借用"]
check -->|否| dropped["Err Dropped"]
owner -->|drop 时 recycle| recycle["代数加一 值清空"]
recycle --> freelist["空闲格子列表"]
freelist -->|下一次 insert| slot
Rust 的闭包要捕获状态,要么借用(受生命周期约束,事件回调放不进 'static),要么拿走所有权(拿走了别处就不能用),要么包一层 Rc<RefCell<T>>(要到处 clone)。dioxus 0.5 起默认的做法是第四种(generational-box 和 dioxus-signals 在 v0.4.3 的仓库里就已经有了,0.5 的 dioxus-hooks 去掉了 use_state、use_ref,改由 use_signal 承担):值放进一块全局存储,句柄是一个 Copy 的小结构体,里面只有「哪一格」和「第几代」。generational-box 这个 crate 就是这块存储,packages/generational-box/src 共约 1850 行:lib.rs(句柄、Owner、两个 trait)、entry.rs(格子)、unsync.rs 和 sync.rs(两种存储)、error.rs、references.rs(借用守卫)。它不依赖 dioxus,dioxus 只在 core/src/generational_box.rs 和 scope_context.rs 里把「当前 scope 的 Owner」接上去。
句柄:格子地址加代数
GenerationalBox 只有一个有用的字段:
pub struct GenerationalBox<T, S: 'static = UnsyncStorage> {
raw: GenerationalPointer<S>,
_marker: PhantomData<T>,
}
GenerationalPointer 是一个 &'static S(格子本身)加一个 GenerationalLocation:
pub(crate) struct GenerationalLocation {
/// The generation this location is associated with. Using the location after this generation is invalidated will return errors.
generation: NonZeroU64,
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: &'static std::panic::Location<'static>,
}
两个字段都是 Copy,所以 impl Copy for GenerationalBox 不需要 T: Copy:复制的是地址,不是值。T 只出现在 PhantomData 里,值本身以 Box<dyn Any> 存在格子中,读的时候再 downcast_ref。debug 构建多带一个 created_at,就是创建这个值的源码位置(#[track_caller] 一路传下来),后面报错时打印的就是它。
用 std::mem::size_of 量一下(M2 Max、macOS 27、rustc 1.99.0,exp-dioxus/sig1-exp 的 sig_size):
##### debug
GenerationalBox<i32> = 24
GenerationalBox<i32, Sync> = 24
CopyValue<i32> = 32
Signal<i32> = 32
Signal<Vec<String>> = 32
Memo<i32> = 64
Option<Signal<i32>> = 32
##### --release
GenerationalBox<i32> = 16
GenerationalBox<i32, Sync> = 16
CopyValue<i32> = 24
Signal<i32> = 24
Signal<Vec<String>> = 24
Memo<i32> = 48
Option<Signal<i32>> = 24
release 下句柄是 16 字节:8 字节指针加 8 字节代数;debug 多出 8 字节的 created_at。大小和 T 无关,Signal<Vec<String>> 和 Signal<i32> 一样大。CopyValue 和 Signal 再多 8 字节,是创建它的 ScopeId(后文)。代数是 NonZeroU64、指针非空,Option<Signal<_>> 不额外占空间。
格子:泄漏出来,反复复用
UnsyncStorage 就是一个格子:
pub struct UnsyncStorage {
borrow_info: MemoryLocationBorrowInfo,
data: RefCell<StorageEntry<RefCellStorageEntryData>>,
}
StorageEntry 是「代数 + 数据」,数据是四种之一(RefCellStorageEntryData):Data(Box<dyn Any>) 普通值、Rc(..) 带引用计数的值、Reference(GenerationalPointer) 指向另一格、Empty 已回收。
格子从哪来,看 create_new:
fn create_new(
value: RefCellStorageEntryData,
#[allow(unused)] caller: &'static std::panic::Location<'static>,
) -> GenerationalPointer<Self> {
UNSYNC_RUNTIME.with(|runtime| match runtime.borrow_mut().pop() {
Some(storage) => {
let mut write = storage.data.borrow_mut();
let location = GenerationalLocation {
generation: write.generation(),
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
created_at: caller,
};
write.data = value;
GenerationalPointer { storage, location }
}
None => {
let storage: &'static Self = &*Box::leak(Box::new(Self {
borrow_info: Default::default(),
data: RefCell::new(StorageEntry::new(value)),
}));
// ...
GenerationalPointer { storage, location }
}
})
}
UNSYNC_RUNTIME 是一个线程局部的 Vec<&'static UnsyncStorage>,存放空闲的格子。有空闲就拿来用,沿用它当前的代数;没有就 Box::leak 一个新的。格子一旦泄漏出来就永远不会被释放,只会被回收进这个列表、再被下一个值复用。这正是 &'static S 能成立的原因:格子的内存永远有效,句柄里的指针不会悬空;值是否还在,由代数判断。
每次读写都先比代数(get_split_ref):
let borrow = pointer
.storage
.data
.try_borrow()
.map_err(|_| pointer.storage.borrow_info.borrow_error())?;
if !borrow.valid(&pointer.location) {
return Err(BorrowError::Dropped(ValueDroppedError::new_for_location(
pointer.location,
)));
}
valid 就是 self.generation == location.generation。try_borrow 失败是借用冲突,代数不等是值已回收:这两种错误分别对应后面的 AlreadyBorrowedMut 和 Dropped。
Owner:回收时代数加一
谁来回收?Owner 是 Arc<Mutex<OwnerInner>>,OwnerInner 只有一个 owned: Vec<GenerationalPointer<S>>。Owner::insert 建一格、把指针记进 owned、返回句柄;最后一个 Owner 克隆被 drop 时,OwnerInner 的 Drop 对每一格调 recycle:
fn recycle(pointer: GenerationalPointer<Self>) {
let mut borrow_mut = pointer.storage.data.borrow_mut();
// First check if the generation is still valid
if !borrow_mut.valid(&pointer.location) {
return;
}
borrow_mut.increment_generation();
// Then decrement the reference count or drop the value if it's the last reference
match &mut borrow_mut.data {
// If this is the original reference, drop the value
RefCellStorageEntryData::Data(_) => borrow_mut.data = RefCellStorageEntryData::Empty,
// If this is a rc, just ignore the drop
RefCellStorageEntryData::Rc(_) => {}
// If this is a reference, decrement the reference count
RefCellStorageEntryData::Reference(reference) => {
let reference = *reference;
drop(borrow_mut);
drop_ref(reference);
}
RefCellStorageEntryData::Empty => {}
}
UNSYNC_RUNTIME.with(|runtime| runtime.borrow_mut().push(pointer.storage));
}
UnsyncStorage::recycle 先把代数加一,再把值换成 Empty(这一步 drop 掉用户的值),最后把格子放回空闲列表。从此所有旧句柄手里的代数都比格子上的小一,读写一律得到 Dropped;格子被新值复用后,新句柄拿的是新代数,旧句柄依然对不上。
格子是寄存柜(UnsyncStorage),句柄是一张小票,上面印着柜号和一个流水号(GenerationalPointer 的地址和 generation)。小票可以随便复印(Copy),拿哪张都能开同一个柜子。租柜子的人(Owner)退租时,管理员清空柜子、把柜门上的流水号加一(recycle 里的 increment_generation),柜子回到空柜台账(UNSYNC_RUNTIME)。下一位顾客拿到同一个柜子、新的流水号;旧小票上的号对不上,开柜时管理员直接拒绝(ValueDroppedError)。
比方到这里为止:真实的寄存柜可以拆掉,这里的格子一旦 Box::leak 出来就永远不还给分配器,只在空柜台账里转圈;而且每次开柜都要比一次流水号,不是只在退租时检查。
下面这个程序直接用 generational-box,不经过 dioxus(sig1-exp/src/bin/gb_basic.rs):
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
fn main() {
let owner = UnsyncStorage::owner();
let a: GenerationalBox<String> = owner.insert(String::from("hello"));
let b = a; // Copy:两个句柄指向同一格
println!("a.id() = {:?}", a.id());
println!("b.read() = {}", *b.read());
*a.write() += " world";
println!("b.read() = {}", *b.read());
drop(owner); // Owner 回收它插入过的所有格子
match a.try_read() {
Ok(v) => println!("still alive: {}", *v),
Err(e) => {
println!("try_read: Err({e:?})");
println!("Display: {e}");
}
}
// 新的 Owner 再插一个值:拿到的是刚回收的那一格,代数 +1
let owner2 = UnsyncStorage::owner();
let c: GenerationalBox<i32> = owner2.insert(42);
println!("c.id() = {:?}", c.id());
println!("a.try_read().is_err() = {}", a.try_read().is_err());
// insert_rc:一个 Reference 格指向一个 Rc 格,占两格
let d: GenerationalBox<i32> = owner2.insert_rc(7);
println!("d.id() = {:?}", d.id());
println!("d.raw_ptr() = {:?}", d.raw_ptr());
// 最后:read() 一个已回收的句柄
drop(owner2);
println!("--- c.read() after owner2 dropped ---");
let _ = c.read();
}
debug 构建的输出:
a.id() = 0x1050e9a58@1
b.read() = hello
b.read() = hello world
try_read: Err(Dropped(ValueDroppedError { created_at: Location { file: "src/bin/gb_basic.rs", line: 5, column: 42 } }))
Display: Failed to borrow because the value was dropped.created_at: src/bin/gb_basic.rs:5:42
...
d.id() = 0x1050ea018@1
d.raw_ptr() = 0x1050ea018
--- c.read() after owner2 dropped ---
thread 'main' (1205136) panicked at src/bin/gb_basic.rs:35:13:
called `Result::unwrap()` on an `Err` value: Dropped(ValueDroppedError { created_at: Location { file: "src/bin/gb_basic.rs", line: 23, column: 40 } })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
id() 的 Debug 是「格子地址@代数」。b 是 a 的复制,写 a、读 b 看到的是同一个值。owner drop 之后 a 读不出来了;省略的两行(c 的 id() 和 a 此时能不能读)留给下面的动脑筋。
上面的 gb_basic 里,a 的 id() 打印 0x1050e9a58@1。drop(owner) 之后,新的 owner2.insert(42) 返回的 c,id() 打印什么?这时 a.try_read().is_err() 是 true 还是 false?(M2 Max、macOS 27、rustc 1.99.0,debug 构建。)
读一个已回收的值
错误类型在 error.rs:读得到 BorrowError(Dropped 或 AlreadyBorrowedMut),写得到 BorrowMutError(多一个 AlreadyBorrowed)。ValueDroppedError 的 Display:
impl Display for ValueDroppedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Failed to borrow because the value was dropped.")?;
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
f.write_fmt(format_args!("created_at: {}", self.created_at))?;
Ok(())
}
}
debug 构建在句号后面紧接着拼上创建位置,中间没有空格,所以上面的输出是 dropped.created_at: src/bin/gb_basic.rs:5:42。release 构建只有前半句。gb_basic 用 --release 再跑一次,变化的只有这三行:
try_read: Err(Dropped(ValueDroppedError))
Display: Failed to borrow because the value was dropped.
...
called `Result::unwrap()` on an `Err` value: Dropped(ValueDroppedError)
read() 和 write() 就是 try_read().unwrap(),所以 panic 文字是标准库 unwrap 的格式加上错误的 Debug。debug 输出里 panic 的 created_at 是 line: 23, column: 40(owner2.insert(42),c 是在那里建的);panic 发生的位置 gb_basic.rs:35:13 是 c.read() 那一行,因为 read 带 #[track_caller]。所以排查「值已被回收」时有两条线索:panic 位置告诉你在哪读的,created_at 告诉你值是在哪建的、该去看谁的 Owner。
借用冲突的三种错误也是同样的结构,debug 带位置、release 不带。一个只用 generational-box 的小程序(gb_display)分别在「两个读借用还在时写」「写借用还在时读」「写借用还在时再写」三种情况下打印 Display:
##### debug
[读+写] Failed to borrow mutably because the value was already borrowed immutably.borrowed_at: src/bin/gb_display.rs:7:17 src/bin/gb_display.rs:8:17
[写+读] Failed to borrow because the value was already borrowed mutably.borrowed_mut_at: src/bin/gb_display.rs:12:16
[写+写] Failed to borrow because the value was already borrowed mutably.borrowed_mut_at: src/bin/gb_display.rs:12:16
##### --release
[读+写] Failed to borrow mutably because the value was already borrowed immutably.
[写+读] Failed to borrow because the value was already borrowed mutably.
[写+写] Failed to borrow mutably because the value was already borrowed immutably.
debug 下读借用的位置是一个列表(同时可以有多个读者),写借用只有一个位置。这些位置记在格子的 MemoryLocationBorrowInfo 里,它整个字段在 release 下不存在。也因此 release 的 borrow_mut_error 分不清「已经有读者」还是「已经有写者」,一律返回 AlreadyBorrowed:最后一行「写+写」在 release 下说的是「already borrowed immutably」,和实际情况相反。generational-box 的 Cargo.toml 有 debug_borrows、debug_ownership 两个 feature,在 cfg 里和 debug_assertions 并列,打开后 release 也带这些位置;但各处 cfg 用的 feature 名不一致(例如 created_at 字段按 debug_ownership、给它赋值的 create_new 按 debug_borrows):在 release 下只开其中一个,generational-box 自己就编译不过(只开 debug_ownership 报 7 个错误,第一个是 error[E0560]:AlreadyBorrowedMutError 没有 borrowed_mut_at 字段),两个一起开才行(exp-dioxus/sig1-gbfeat)。
在 signal 上碰到这些错误是什么样,见 Signal 的「借用冲突」一节。
insert_rc 与指向别的格子
Owner 还有一个 insert_rc。UnsyncStorage::new_rc 建两格:一格是 Rc(真正的值加引用计数),另一格是 Reference,指向前者;返回的句柄和 Owner 记录的都是 Reference 那一格。读写时 get_split_ref 沿 Reference 一直走到 Rc 或 Data。上面 gb_basic 里 d 的地址是一个新地址,就是因为空闲列表里只有一格(被 c 用掉了),insert_rc 新泄漏了两格。
多这一层,是为了让一个句柄能改指向:point_to 把 Reference 格改指到另一个 Rc 格,引用计数加一;Reference 格被回收时只减引用计数,减到底才清空 Rc 格。dioxus 的 CopyValue 和 Signal 都用 insert_rc 建值,所以每个 signal 占两格。用到 point_to 的地方是 props 里的 ReadSignal 和回调:父组件重跑时,子组件旧 props 里的句柄改指向新值,而不是把值搬过去,见 #[component] 与 Props 末尾的冷知识。
UnsyncStorage 与 SyncStorage
两种存储实现同一对 trait:AnyStorage(借用守卫的类型、recycle、owner())和 Storage<T>(try_read、try_write、new、new_rc)。
UnsyncStorage(默认) | SyncStorage | |
|---|---|---|
| 格子里的锁 | RefCell | parking_lot::RwLock |
| 值的约束 | T: 'static | T: Send + Sync + 'static |
| 空闲列表 | 线程局部 UNSYNC_RUNTIME | 进程级 SYNC_RUNTIME(OnceLock<Arc<Mutex<Vec<..>>>>) |
| 读写冲突 | try_borrow 失败,返回错误 | read() / write() 阻塞等待 |
| dioxus 里谁用 | use_signal、Memo、CopyValue | use_signal_sync(SyncSignal)、ReactiveContext |
表里「读写冲突」一行是两者最大的区别。SyncStorage 的 get_split_ref 第一行就是 pointer.storage.data.read(),不是 try_read:别的线程正在写,就等它写完,这是跨线程共享时该有的行为。但同一个线程拿着写锁再去读,就等不到了,parking_lot 的 RwLock 不可重入。sig_borrow 的 sync 分支在组件里这样写(other 是 count 的复制,原因见 Signal):
fn sync_read_while_write() -> Element {
let mut count = use_signal_sync(|| 0);
let other = count;
let _w = count.write();
println!("got write lock, now reading...");
let _r = other.read();
println!("never printed");
rsx! {}
}
主程序另起一个线程,2 秒后还没结束就退出进程。debug 和 release 输出相同:
got write lock, now reading...
2 秒后主线程仍然卡在 read() 里,退出
同样的代码换成 use_signal,得到的是一条 panic(下一页)。也就是说,SyncSignal 上的读写冲突不会报错,只会卡死;在 wasm 里就是页面不再响应。
问: ReactiveContext 为什么要放在 SyncStorage 里?组件明明只在一个线程上跑。
答: 是类型逼出来的。SyncSignal<T> 把 SignalData<T> 放进 SyncStorage,要求它 Send + Sync;SignalData 里的订阅者集合装的是 ReactiveContext,所以 ReactiveContext 也必须 Send + Sync。句柄里存的是 &'static S:UnsyncStorage 含 RefCell,不是 Sync,&UnsyncStorage 就不是 Send;只有放在 SyncStorage 里,句柄才满足要求。同理 Inner 里的 update 是 Box<dyn FnMut() + Send + Sync>;组件的那个只往调度通道 unbounded_send 一条消息,满足这个约束不费事。结果是每个组件 scope 都有一个 Owner<SyncStorage>(建 scope 时就为它的 ReactiveContext 建了),用了 signal 的 scope 再多一个 Owner<UnsyncStorage>。
在 dioxus 里:Owner 挂在 scope 上
dioxus 不让用户自己拿 Owner。current_owner 先看线程局部的覆盖值(with_owner 设置的,props 的 setter 用它把 ReadSignal 建在 props 自己的 Owner 下),没有就取当前 scope 的:Scope::owner 在 context 表里找 Owner<S>,没有就新建一个 provide_context 进去。context 表按类型索引,所以 Owner<UnsyncStorage> 和 Owner<SyncStorage> 各有一个,第一次用到时才建。
scope 被删时,Runtime::remove_scope 依次取消任务、删掉排队的 effect、倒序 drop hooks,最后 shared_contexts.take() 清空 context 表(顺序见 Scope 与 hooks)。Owner 在这里被 drop,这个 scope 建的所有 signal、memo、ReactiveContext 一起被回收。hook 列表里存的只是句柄,drop 句柄什么也不做。
dioxus 这一侧的包装是 CopyValue:
pub struct CopyValue<T, S: 'static = UnsyncStorage> {
pub(crate) value: GenerationalBox<T, S>,
pub(crate) origin_scope: ScopeId,
}
new_with_caller 就是 current_owner().insert_rc_with_caller(value, caller),再记下 current_scope_id()。CopyValue 实现了 Readable / Writable,但它的 subscribers() 返回 Subscribers::new_noop():读它不订阅、写它不通知任何人,只是「可以 Copy 的可变值」。在它外面加上订阅者集合,就是 Signal。
origin_scope 只用于一个 debug 警告。CopyValue 的 try_read_unchecked、try_peek_unchecked、try_write_unchecked 开头都调一次 copy_value_hoisted:当前 scope 既不是 origin_scope、也不是它的后代时,打一条 tracing::warn!。它用 warnings crate 的 #[warning] 宏包着,这个宏在 release 下 enabled() 恒为假,警告只在 debug 构建出现。
实验:子组件的 signal 交给父组件
sig1-exp/src/bin/gb_dropped.rs,子组件把自己的 signal 写进父组件的槽里,父组件在事件回调里读它,然后隐藏子组件再读:
#[component]
fn Child(slot: Signal<Option<Signal<i32>>>) -> Element {
let s = use_signal(|| 7); // 第 6 行:这个 signal 属于 Child
use_hook(move || slot.set(Some(s)));
rsx! { "child" }
}
fn app() -> Element {
let slot = use_signal(|| None::<Signal<i32>>);
let mut show = use_signal(|| true);
let read = move |_| {
let s = slot.peek().unwrap();
match s.try_read() {
Ok(v) => println!("[read] Ok({})", *v),
Err(e) => println!("[read] Err: {e}"),
}
};
rsx! {
button { onclick: read, "read" }
button { onclick: move |_| show.set(false), "hide" }
button { onclick: move |_| println!("[read()] {}", slot.peek().unwrap()()), "read()" }
if show() { Child { slot } }
}
}
main 依次点 read、hide(再 render_immediate)、read、read();点击用 Runtime::handle_event 原生派发,做法同 一次点击 的实验一。装了一个只打印 WARN 的 tracing_subscriber。debug 输出(警告后面的帮助文字和两段示例代码很长,用 ... 省略,三次警告只留第一次的全文):
html: <button>read</button><button>hide</button><button>read()</button>child
--- click read (child alive)
WARN dioxus_signals::warnings::__copy_value_hoisted: A Copy Value created in ScopeId(4, "gb_dropped::Child") (at src/bin/gb_dropped.rs:6:11). It will be dropped when that scope is dropped, but it was used in ScopeId(3, "root") (at src/bin/gb_dropped.rs:16:13) which is not a descendant of the owning scope.
This may cause reads or writes to fail because the value is dropped while it still held.
Help:
Copy values (like CopyValue, Signal, Memo, and Resource) are owned by the scope they are created in. If you use the value in a scope that may be dropped after the origin scope,
it is very easy to use the value after it has been dropped. To fix this, you can move the value to the parent of all of the scopes that it is used in.
...
[read] Ok(7)
--- click hide
html: <button>read</button><button>hide</button><button>read()</button>
--- click read (child dropped)
WARN dioxus_signals::warnings::__copy_value_hoisted: A Copy Value created in ScopeId(4) (at src/bin/gb_dropped.rs:6:11). ...
[read] Err: Failed to borrow because the value was dropped.created_at: src/bin/gb_dropped.rs:6:11
--- click read() (child dropped)
WARN dioxus_signals::warnings::__copy_value_hoisted: A Copy Value created in ScopeId(4) (at src/bin/gb_dropped.rs:6:11). ...
thread 'main' (1204091) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-signals-0.7.10/src/read.rs:259:38:
called `Result::unwrap()` on an `Err` value: Dropped(ValueDroppedError { created_at: Location { file: "src/bin/gb_dropped.rs", line: 6, column: 11 } })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
release 输出:
html: <button>read</button><button>hide</button><button>read()</button>child
--- click read (child alive)
[read] Ok(7)
--- click hide
html: <button>read</button><button>hide</button><button>read()</button>
--- click read (child dropped)
[read] Err: Failed to borrow because the value was dropped.
--- click read() (child dropped)
thread 'main' (1204276) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-signals-0.7.10/src/read.rs:259:38:
called `Result::unwrap()` on an `Err` value: Dropped(ValueDroppedError)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
几件事:
- 事件回调运行时的「当前 scope」是监听器所在的组件(这里是
ScopeId(3, "root")),所以子组件还活着时读它就已经触发警告:警告看的是「可能」,不是「已经」。 - 子组件卸载后,
ScopeId(4)不再带名字(scope 已删),值被回收,try_read得到Dropped,created_at指回Child里的use_signal。 - 用
signal()语法读,panic 位置在dioxus-signals的read.rs:259,不在你的代码里;用.read()读,panic 位置是调用行。原因在 Signal 的「语法糖」一节。原生平台上事件回调里的 panic 没有被接住,整个进程退出;组件函数里的 panic 才会被run_scope的catch_unwind变成错误。 - release 下没有警告、没有位置,只剩一句「value was dropped」。
修法就是警告里给的:把值放在所有用到它的组件的共同祖先里,子组件只拿父组件的 signal 去写。
issue #2672「Warn when signals are used in a higher scope then they are created in」写道:「Moving a signal up through the tree can cause issues with infinite reruns and signals in children being dropped before the parent」,建议「Log a tracing::warn when something like this happens」,示例正是子组件 child_signal.set(Some(signal_owned_by_child))。PR #2771(「Add a warning when a copy value is used in a higher scope than it was created in」,说明只有一句「Fixes #2672」)加了 warnings.rs 里的 copy_value_hoisted 和 CopyValue.origin_scope,以及警告里引用的 docs/hoist/error.rs、fixed_list.rs 两个示例。在这之前,读一个被回收的 signal 只有 Dropped 错误,不会提前提示。仓库里第一个包含它的 tag 是 v0.6.0-alpha.2,正式版 0.6.0。
动脑筋答案
gb_basic 省略的两行,原样:
c.id() = 0x1050e9a58@2
a.try_read().is_err() = true
- 地址和
a相同:recycle把a那一格放回线程局部的UNSYNC_RUNTIME,create_new先pop()空闲列表,所以c拿到同一格。 - 代数是 2:
recycle先increment_generation()再放回列表,create_new沿用格子上当前的代数。 a仍然读不出来:a记的代数是 1,格子上是 2,valid不成立,get_split_ref返回Dropped。同一块内存被新值占用,旧句柄也读不到新值,这就是代数存在的意义。
小结
GenerationalBox是「&'static格子 +NonZeroU64代数」(debug 再加创建位置),release 16 字节、与T无关,所以Copy。- 格子
Box::leak出来后永不释放,回收时代数加一、值清空、放回空闲列表;每次读写先比代数,对不上就是ValueDroppedError。 Owner记录自己插入的格子,最后一个克隆 drop 时全部回收。dioxus 把Owner存在每个 scope 的 context 表里(Unsync和Sync各一个,按需创建),scope 删掉时一起回收。UnsyncStorage用RefCell,冲突时返回错误;SyncStorage用parking_lot::RwLock,同线程读写冲突直接死锁。- 错误文字:debug 带
created_at/borrowed_at位置(句号后不加空格),release 没有;release 的「写+写」被报成「already borrowed immutably」。 CopyValue=GenerationalBox+origin_scope,不订阅也不通知;debug 下在非后代 scope 里用它会打copy_value_hoisted警告。