1076 lines
55 KiB
Rust
1076 lines
55 KiB
Rust
//! 每次展示的托管上下文:同一个 Block 出现在两个位置时,有两个独立 Mount。
|
||
//! Mount 保存父子关系、出现几何和布局期间的查询上下文;MountTable 用弱索引定位对象,不靠索引强行保活业务树。
|
||
//! 同步会安排 onMount/onUnmount。布局取消时使用 Checkpoint 恢复未发布状态,避免半完成的出现关系影响下一次刷新。
|
||
//!
|
||
//! 调用路径:UiBridge 在布局前 checkpoint/synchronize(preparing=true),完成后提交几何,取消则 restore。
|
||
//! 源 measure/arrange/事件通过 Method 调用 Mount API;生命周期返回 vm_sequence 驱动,调用方安排实际执行。
|
||
//! 活动 Mount 由 desktop.mounts 强持有;MountTable 与反向层次关系仅保存弱索引。
|
||
//!
|
||
//! Managed presentation contexts. The active desktop owns the mount dictionary;
|
||
//! reverse links and the host registry are weak, so shared content cannot retain a Page.
|
||
use crate::document::{Frame, MountGeometry};
|
||
use endor_core::{
|
||
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef,
|
||
Value, Vm, VmError, WeakObjectRef,
|
||
};
|
||
use std::{
|
||
collections::HashMap,
|
||
sync::{Arc, Mutex},
|
||
};
|
||
|
||
#[derive(Debug, Clone)]
|
||
/// 一个组件在一个展示位置上的上下文。强持有 Block,反向页面/父子链接使用弱身份,避免共享组件把已关闭页面保活。
|
||
pub struct Mount {
|
||
// 组件是强引用;页面、父节点和兄弟链接是弱引用,防止反向链接形成保活环。
|
||
pub block: ObjectRef,
|
||
// 所属 Page 弱身份,用于区分换页后的出现上下文,不让共享内容反向保活页面。
|
||
pub page: Option<WeakObjectRef>,
|
||
// 父出现弱身份,源 parent() 在目标已消失时返回 Nil。
|
||
pub parent: Option<WeakObjectRef>,
|
||
// 首子出现弱身份,后续兄弟沿 next_sibling 查找。
|
||
pub first_child: Option<WeakObjectRef>,
|
||
// 同级下一出现弱身份,几何帧中的 ID 链在同步第二遍解析为此链接。
|
||
pub next_sibling: Option<WeakObjectRef>,
|
||
// 已发布几何与每次出现的滚动位置分开保存,同一 Block 的多个位置互不覆盖。
|
||
pub geometry: MountGeometry,
|
||
// 此出现的用户请求偏移;帧中 geometry.scroll 才是钳制后的有效位置与上限。
|
||
pub scroll_offset: [f32; 2],
|
||
// 最后同步的布局版本,Bounds 查询会一并返回,便于调用方识别几何属于哪轮发布。
|
||
pub version: u64,
|
||
// false 后禁止依赖当前几何的操作;外部即使还持有 Mount 对象也不能把它当活动出现。
|
||
pub mounted: bool,
|
||
// Mount 可以拥有任务;退役后任务交付还要检查 owner 是否仍有效。
|
||
pub tasks: Option<ObjectRef>,
|
||
// Private derived scratch, installed before the read-only layout call and
|
||
// cleared by geometry commit. It contains no managed references or roots.
|
||
// 布局只读协议使用的临时派生上下文;不拥有托管根,提交或退役时清空。
|
||
pub layout: Option<Arc<Mutex<crate::layout::Context>>>,
|
||
}
|
||
|
||
impl ObjectPayload for Mount {
|
||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||
fn reference_assignment_only(&self) -> bool {
|
||
true
|
||
}
|
||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||
fn kind_name(&self) -> &'static str {
|
||
"tessera-mount"
|
||
}
|
||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||
fn allows_attributes(&self) -> bool {
|
||
true
|
||
}
|
||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||
// 仅 block 和 tasks 是 payload 的强拥有边;page/parent/child/sibling 是弱边,不参与 trace。
|
||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||
visit(self.block);
|
||
// Aster GC 只额外追踪存在的任务作用域,弱页面/层次链不参与强引用追踪。
|
||
if let Some(tasks) = self.tasks {
|
||
visit(tasks);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy)]
|
||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||
enum Query {
|
||
// 读取本出现已提交或排列期可用的 Bounds;测量阶段不可提前读几何。
|
||
Bounds,
|
||
// 设置此出现的绝对滚动请求,实际范围由滚动层钳制。
|
||
ScrollTo,
|
||
// 在此出现请求偏移上累加位移,不写共享 Block 的滚动状态。
|
||
ScrollBy,
|
||
// 查询有效滚动位置和最大值,不直接返回未钳制请求。
|
||
ScrollPosition,
|
||
// 联动滚动祖先,把此出现送入有效可见区域。
|
||
ScrollIntoView,
|
||
// 用此视口有限尺寸和场景边界拟合相机。
|
||
FitView,
|
||
// 从此三维视口查询射线交点和原托管 mesh。
|
||
Pick3D,
|
||
// 本出现局部坐标经二维/面板投影变为页面视图坐标。
|
||
ToView,
|
||
// 页面视图点逆投影到本出现局部坐标,失败可返回 Nil。
|
||
ToLocal,
|
||
// 弱查询父 Mount,不制造新的父拥有关系。
|
||
Parent,
|
||
// 弱查询首子 Mount,空或失效均返回 Nil。
|
||
FirstChild,
|
||
// 弱查询下一同级出现,不按相同业务对象去重。
|
||
NextSibling,
|
||
// 弱查询所属页面,退役上下文可能仍有页面弱观察。
|
||
Page,
|
||
// 请求滚入可见并取得焦点,真正窗口发布在后续阶段完成。
|
||
Focus,
|
||
// 只查询此 Mount 的提交几何包含关系,不执行源 Block.hitTest。
|
||
HitTest,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||
struct Method {
|
||
// 本桌面名义类型构造的绑定方法契约,Bounds 返回类型也从此提取。
|
||
signature: endor_core::CallSignature,
|
||
// 方法实际绑定的 Mount,由 ContextNativeFunction captures 保活。
|
||
owner: ObjectRef,
|
||
// 方法对象拥有 Mount,但不能经桌面反向保活整个窗口;需要桌面能力时再 upgrade。
|
||
desktop: WeakObjectRef,
|
||
// 操作分派枚举,同时决定显式 arity、效果和返回契约。
|
||
query: Query,
|
||
}
|
||
|
||
impl NativeCallable for Method {
|
||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||
Some(self.signature.clone())
|
||
}
|
||
/// 适配器已绑定 Mount;坐标测试/转换与scrollTo/By接受两个数,其余层次/几何/焦点查询零参。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(
|
||
// 这些坐标/滚动方法需要两个 Number,其余操作无显式实参,receiver 已捕获。
|
||
if matches!(
|
||
self.query,
|
||
Query::HitTest
|
||
| Query::Pick3D
|
||
| Query::ToView
|
||
| Query::ToLocal
|
||
| Query::ScrollTo
|
||
| Query::ScrollBy
|
||
) {
|
||
2
|
||
} else {
|
||
0
|
||
},
|
||
))
|
||
}
|
||
|
||
/// 读取具体出现位置,先检查仍挂载与几何可用性,再执行层次、坐标、焦点、滚动或拾取操作,避免使用已退役上下文。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// VM 调用绑定方法后先复制当前 payload,避免分配返回对象或改堆时持有旧存储借用。
|
||
let mount = cx.store().payload::<Mount>(self.owner, "Mount")?.clone();
|
||
// 层次查询允许观察已经脱离的对象(通常得到 Nil);几何、焦点、滚动、拾取等必须仍挂接。
|
||
if !mount.mounted
|
||
&& matches!(
|
||
self.query,
|
||
Query::Bounds
|
||
| Query::Focus
|
||
| Query::HitTest
|
||
| Query::ScrollTo
|
||
| Query::ScrollBy
|
||
| Query::ScrollIntoView
|
||
| Query::FitView
|
||
| Query::Pick3D
|
||
| Query::ToView
|
||
| Query::ToLocal
|
||
| Query::ScrollPosition
|
||
)
|
||
{
|
||
return Err(VmError::InvalidArgument("Mount is no longer attached"));
|
||
}
|
||
// 除 VM 元数据检查外入口再核对实际参数数目;二维坐标/位移操作取两个 Number,其余零参。
|
||
let arity = if matches!(
|
||
self.query,
|
||
Query::HitTest
|
||
| Query::Pick3D
|
||
| Query::ToView
|
||
| Query::ToLocal
|
||
| Query::ScrollTo
|
||
| Query::ScrollBy
|
||
) {
|
||
2
|
||
} else {
|
||
0
|
||
};
|
||
// 入口再次核对实际参数数目,防止绕过元数据路径导致 args 下标越界。
|
||
if args.len() != arity {
|
||
return Err(VmError::InvalidArgument("incorrect Mount method arity"));
|
||
}
|
||
// 所有分支统一产生 Value,再交给 NativeResult::Return 的 VM 拥有关系协议。
|
||
let value = match self.query {
|
||
// 坐标互转都要求已经提交的几何,不能混用本轮布局临时 bounds 与旧相机映射。
|
||
Query::ToView | Query::ToLocal => {
|
||
// 布局期间几何尚未提交,坐标转换不能把临时 bounds 与上帧投影混用。
|
||
if mount.layout.is_some() {
|
||
return Err(VmError::InvalidArgument(
|
||
"coordinate conversion requires committed geometry",
|
||
));
|
||
}
|
||
// 显式输入是本地/视图坐标,按 coordinate 规则拒绝非法数值后再做空间转换。
|
||
let point = crate::geometry::Point::new(
|
||
crate::scroll::coordinate(&args[0])?,
|
||
crate::scroll::coordinate(&args[1])?,
|
||
);
|
||
// 根据查询方向决定正向或逆向映射;Option 保留不可投影/遮挡的失败结果。
|
||
let point =
|
||
// 查询方向决定先正投影局部点还是逆投影视图点;不能仅交换输出字段代替逆变换。
|
||
if matches!(self.query, Query::ToView) {
|
||
// ToView 先将局部点加节点原点,再套二维 view;若在世界面板内,还继续向外投影到页面。
|
||
// 先加本节点布局原点再套累计二维 view,之后如有 panel 再投影到外层页面。
|
||
let point = mount.geometry.view.unwrap_or_default().point(
|
||
crate::geometry::Point::new(
|
||
point.x + mount.geometry.x,
|
||
point.y + mount.geometry.y,
|
||
),
|
||
);
|
||
mount
|
||
.geometry
|
||
.panel
|
||
.as_ref()
|
||
.map_or(Some(point), |panel| panel.project_point(point))
|
||
} else {
|
||
// ToLocal 先撤销面板和视图投影,再减节点几何原点;不可逆时返回 Nil,而非虚构坐标。
|
||
mount.geometry.logical_point(point).map(|point| {
|
||
crate::geometry::Point::new(
|
||
point.x - mount.geometry.x,
|
||
point.y - mount.geometry.y,
|
||
)
|
||
})
|
||
};
|
||
// 转换成功才分配名义 Point;失败返回 Nil,不把无效数值写进结果属性。
|
||
if let Some(point) = point {
|
||
// 成功后创建名义 Point 的字段存储,不把转换结果作为未标类型的近似对象返回。
|
||
let result = cx.new_object()?;
|
||
// 需升级方法捕获的弱桌面以取得 Point 类型,桌面失效时明确报错。
|
||
let desktop = cx.upgrade(self.desktop)?.ok_or(VmError::InvalidArgument(
|
||
"coordinate conversion desktop expired",
|
||
))?;
|
||
crate::ui_bridge::attach_desktop_type(cx, result, "Point", desktop)?;
|
||
cx.set_attr(result, "x", crate::native_visual::value(point.x))?;
|
||
cx.set_attr(result, "y", crate::native_visual::value(point.y))?;
|
||
Value::Object(result)
|
||
} else {
|
||
Value::Nil
|
||
}
|
||
}
|
||
// 同一滚动入口区分绝对位置和相对增量,钳制、挂接状态与写屏障交由 scroll::set 统一执行。
|
||
Query::ScrollTo | Query::ScrollBy => {
|
||
crate::scroll::set(
|
||
cx,
|
||
self.owner,
|
||
self.desktop,
|
||
[
|
||
crate::scroll::coordinate(&args[0])?,
|
||
crate::scroll::coordinate(&args[1])?,
|
||
],
|
||
matches!(self.query, Query::ScrollBy),
|
||
)?;
|
||
Value::Nil
|
||
}
|
||
// 滚动层负责嵌套祖先、裁剪与偏移夹取,此适配器只提供具体出现身份。
|
||
Query::ScrollIntoView => {
|
||
crate::scroll::into_view(cx, self.owner, self.desktop)?;
|
||
Value::Nil
|
||
}
|
||
// 拟合以 Mount 的有限视口尺寸为准;不是全局 Page 相机重置。
|
||
Query::FitView => {
|
||
crate::viewport::fit(cx, &mount)?;
|
||
Value::Nil
|
||
}
|
||
// 射线拾取需要所属桌面构造名义命中值,先确认弱桌面仍有效。
|
||
Query::Pick3D => {
|
||
// 升级拾取所需桌面,不能通过旧 Mount 在已回收窗口环境里构造类型。
|
||
let desktop = cx
|
||
.upgrade(self.desktop)?
|
||
.ok_or(VmError::InvalidArgument("viewport desktop expired"))?;
|
||
crate::scene3d_model::pick(
|
||
cx,
|
||
desktop,
|
||
&mount,
|
||
crate::geometry::Point::new(
|
||
crate::scroll::coordinate(&args[0])?,
|
||
crate::scroll::coordinate(&args[1])?,
|
||
),
|
||
)?
|
||
}
|
||
// 返回帧中已钳制的有效位置与最大偏移,不直接暴露可能尚未投影的 scroll_offset 原值。
|
||
Query::ScrollPosition => {
|
||
// 只有 geometry.scroll 存在的出现才可查询滚动状态,普通节点明确报错。
|
||
let p = mount
|
||
.geometry
|
||
.scroll
|
||
.ok_or(VmError::InvalidArgument("Mount is not scrollable"))?;
|
||
// 滚动查询返回 x/y/maxX/maxY 的新记录,字段源于帧有效投影。
|
||
let result = cx.new_object()?;
|
||
// 逐项把有效偏移与上限写入返回记录,不暴露内部可变数组。
|
||
for (name, value) in [("x", p[0]), ("y", p[1]), ("maxX", p[2]), ("maxY", p[3])] {
|
||
cx.set_attr(result, name, Value::Number((value as f64).into()))?;
|
||
}
|
||
Value::Object(result)
|
||
}
|
||
Query::Bounds => {
|
||
// 布局只读调用中可查询当前安排阶段的临时 bounds;测量时尚无 bounds,明确报错。
|
||
// 非布局期则读取最后提交的 MountGeometry。
|
||
let geometry = if let Some(layout) = &mount.layout {
|
||
layout
|
||
.lock()
|
||
.map_err(|_| VmError::InvalidArgument("layout context poisoned"))?
|
||
.bounds
|
||
.ok_or(VmError::InvalidArgument(
|
||
"Mount geometry is unavailable during measurement",
|
||
))?
|
||
} else {
|
||
[
|
||
mount.geometry.x,
|
||
mount.geometry.y,
|
||
mount.geometry.width,
|
||
mount.geometry.height,
|
||
]
|
||
};
|
||
// 创建 Bounds 结果对象;名义定义由本方法签名保证与当前桌面一致。
|
||
let bounds = cx.new_object()?;
|
||
// 从本方法返回签名取得 Bounds 的名义类型,避免生成一个仅字段相似的普通对象。
|
||
let endor_core::CallType::Single(endor_core::AnnotationType::Nominal(definition)) =
|
||
&self.signature.returns
|
||
else {
|
||
return Err(VmError::InvalidArgument("invalid Mount bounds signature"));
|
||
};
|
||
cx.attach_type(bounds, *definition)?;
|
||
// 按 x/y/width/height 写入所选几何,另附 mount.version 作为 layoutVersion。
|
||
for (name, value) in [
|
||
("x", geometry[0]),
|
||
("y", geometry[1]),
|
||
("width", geometry[2]),
|
||
("height", geometry[3]),
|
||
] {
|
||
cx.set_attr(bounds, name, Value::Number((value as f64).into()))?;
|
||
}
|
||
cx.set_attr(
|
||
bounds,
|
||
"layoutVersion",
|
||
Value::Number((mount.version as i64).into()),
|
||
)?;
|
||
Value::Object(bounds)
|
||
}
|
||
// 请求焦点先让出现进入可见范围,再检查可聚焦资格并写入桌面请求;后续发布负责实际生效。
|
||
Query::Focus => {
|
||
crate::scroll::into_view(cx, self.owner, self.desktop)?;
|
||
// 只有可交互出现可以聚焦;滚入可见请求先由滚动层处理,焦点资格不由字符串类型名决定。
|
||
if !mount.geometry.interactive {
|
||
return Err(VmError::InvalidArgument("Mount is not focusable"));
|
||
}
|
||
// 焦点写入当前桌面的待处理请求,弱桌面过期则不能再设置。
|
||
let desktop = cx
|
||
.upgrade(self.desktop)?
|
||
.ok_or(VmError::InvalidArgument("Mount window has expired"))?;
|
||
cx.set_attr(
|
||
desktop,
|
||
"focus",
|
||
Value::Number((mount.geometry.id as i64).into()),
|
||
)?;
|
||
cx.set_attr(desktop, "focusRequested", Value::Bool(true))?;
|
||
Value::Nil
|
||
}
|
||
// 这里是 Mount 已提交几何的 contains 查询;源 Block 的 hitTest 覆盖由 source_hit 驱动另行执行。
|
||
Query::HitTest => {
|
||
// 布局临时上下文存在时 contains 不保证对应最终投影,必须拒绝源 Mount.hitTest。
|
||
if mount.layout.is_some() {
|
||
return Err(VmError::InvalidArgument(
|
||
"Mount hitTest is unavailable during layout",
|
||
));
|
||
}
|
||
// 命中坐标只要求 Number 可转有限 f32;与 scroll::coordinate 不同,此处不限制绝对值至一百万。
|
||
// hitTest 的输入只需有限 f32;此闭包刻意不用带一百万绝对值上限的滚动坐标解析。
|
||
let number = |value: &Value| -> Result<f32, VmError> {
|
||
// 只接受 Number,不把字符串或其他值隐式转为坐标。
|
||
let Value::Number(number) = value else {
|
||
return Err(VmError::InvalidArgument(
|
||
"point coordinates must be numbers",
|
||
));
|
||
};
|
||
// 解析到实际几何使用的 f32,溢出为无穷仍由下一步拒绝。
|
||
let number = number
|
||
.to_string()
|
||
.parse::<f32>()
|
||
.map_err(|_| VmError::InvalidArgument("invalid point coordinate"))?;
|
||
// NaN/无穷没有有效包含关系,不能送入 Rect 或面板射线计算。
|
||
if !number.is_finite() {
|
||
return Err(VmError::InvalidArgument("invalid point coordinate"));
|
||
}
|
||
Ok(number)
|
||
};
|
||
Value::Bool(mount.geometry.contains(crate::geometry::Point::new(
|
||
number(&args[0])?,
|
||
number(&args[1])?,
|
||
)))
|
||
}
|
||
// 其余枚举仅为弱层次查询,允许观察已脱离上下文;穷尽匹配保证不会把几何操作落到此处。
|
||
query => {
|
||
// 选择对应弱边再尝试升级,不额外保存或强连父/子/页面。
|
||
let weak = match query {
|
||
Query::Parent => mount.parent,
|
||
Query::FirstChild => mount.first_child,
|
||
Query::NextSibling => mount.next_sibling,
|
||
Query::Page => mount.page,
|
||
_ => unreachable!(),
|
||
};
|
||
// 弱链接不存在或目标已回收都返回 Nil;升级本身失败才传播 VM 错误。
|
||
weak.map(|weak| cx.upgrade(weak))
|
||
.transpose()?
|
||
.flatten()
|
||
.map(Value::Object)
|
||
.unwrap_or(Value::Nil)
|
||
}
|
||
};
|
||
Ok(NativeResult::Return(value))
|
||
}
|
||
}
|
||
|
||
/// 声明 Mount 查询方法的源类型契约,区分几何、层次与坐标操作。
|
||
fn mount_signature(
|
||
store: &endor_core::ObjectStore,
|
||
desktop: ObjectRef,
|
||
query: Query,
|
||
) -> Result<endor_core::CallSignature, VmError> {
|
||
use endor_core::{AnnotationType as T, CallEffect, CallSignature, CallType};
|
||
// 每个返回名义类型从该桌面 types 获取,不能拿另一 VM 同名类型拼接契约。
|
||
let nominal = |name| crate::ui_bridge::desktop_type(store, name, desktop).map(T::Nominal);
|
||
// 层次、拾取和坐标转换可能没有结果,使用 nullable 名义类型;变更操作返回 Unit。
|
||
let returns = match query {
|
||
// bounds 总返回 Bounds,测量期不可用时走错误而非 nullable。
|
||
Query::Bounds => nominal("Bounds")?.into(),
|
||
// 层次弱边可能为空或目标已回收,因此结果允许 Nil。
|
||
Query::Parent | Query::FirstChild | Query::NextSibling => {
|
||
CallType::nullable(nominal("Mount")?)
|
||
}
|
||
Query::Page => CallType::nullable(nominal("Page")?),
|
||
Query::HitTest => T::Bool.into(),
|
||
Query::Pick3D => CallType::nullable(nominal("RayHit3D")?),
|
||
Query::ToView | Query::ToLocal => CallType::nullable(nominal("Point")?),
|
||
// 滚动位置目前返回普通记录,契约 Any 与实现字段记录保持一致。
|
||
Query::ScrollPosition => T::Any.into(),
|
||
_ => T::Unit.into(),
|
||
};
|
||
// 滚动、相机拟合和焦点会写业务/桌面属性,声明 Mutating;其余查询允许在只读协议中调用。
|
||
let effect = if matches!(
|
||
query,
|
||
Query::ScrollTo | Query::ScrollBy | Query::ScrollIntoView | Query::FitView | Query::Focus
|
||
) {
|
||
CallEffect::Mutating
|
||
} else {
|
||
CallEffect::ReadOnly
|
||
};
|
||
// 显式参数契约与 Method.arity 保持相同分组,receiver 不计入已绑定实参。
|
||
let parameters = if matches!(
|
||
query,
|
||
Query::HitTest
|
||
| Query::Pick3D
|
||
| Query::ToView
|
||
| Query::ToLocal
|
||
| Query::ScrollTo
|
||
| Query::ScrollBy
|
||
) {
|
||
vec![T::Number, T::Number]
|
||
} else {
|
||
vec![]
|
||
};
|
||
// 先构造参数与效果,再替换精确返回契约,保留 nullable 名义类型信息。
|
||
let mut signature = CallSignature::exact(parameters, T::Any, effect);
|
||
signature.returns = returns;
|
||
Ok(signature)
|
||
}
|
||
|
||
#[derive(Debug, Default, Clone)]
|
||
/// 活动出现的弱索引。真实托管拥有关系在 desktop 的挂载字典中,索引不能充当隐藏 GC 根。
|
||
pub struct MountTable {
|
||
// Mount ID 到托管弱身份,真实强拥有者是 desktop.mounts 字典;order 单独保存原布局顺序。
|
||
pub active: HashMap<u64, WeakObjectRef>,
|
||
// 原布局先序 ID 列表,弱 HashMap 本身无序,生命周期退役需要这个确定顺序。
|
||
order: Vec<u64>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// 同步出现关系时的共享临时工作状态,保存本轮重用、建立和退役安排。
|
||
struct Sync {
|
||
// 本次同步所属桌面,由同步函数 captures 显式保活,持有活动挂载字典。
|
||
desktop: ObjectRef,
|
||
// 本次活动 Page,可为空;同步内部转弱身份写入 Mount,避免反向保活。
|
||
page: Option<ObjectRef>,
|
||
// 业务 Block 弱注册表,用它把发布帧中的身份文本解析为现存对象。
|
||
nodes: Vec<WeakObjectRef>,
|
||
// 上一活动 Mount 弱表,用于按出现 ID 尝试复用。
|
||
old: HashMap<u64, WeakObjectRef>,
|
||
// 上一帧先序出现顺序,逆序退役用于保证孩子先于父卸载。
|
||
old_order: Vec<u64>,
|
||
// 本轮拥有的原生几何帧,数据不依赖外部借用生命周期。
|
||
frame: Frame,
|
||
// true 为布局准备阶段,安装临时 Context 并保留已有滚动偏移;false 为几何提交,清 Context。
|
||
preparing: bool,
|
||
// 同步成功后交回新弱索引,宿主函数调用结束后由 MountTable 移出。
|
||
output: Arc<Mutex<HashMap<u64, WeakObjectRef>>>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// 将一个生命周期回调及参数接入同一 VM 调用序列。
|
||
struct LifecycleCall {
|
||
// 已经按 Block 名义类型解析到的生命周期源/原生方法。
|
||
function: ObjectRef,
|
||
// 调用时显式 receiver,包装 captures 保持它到序列实际执行。
|
||
block: ObjectRef,
|
||
// 当前具体出现上下文,作为第二参数传入同一个业务 Block 的钩子。
|
||
mount: ObjectRef,
|
||
// 卸载包装先取消 owner 任务;挂载包装不做取消。
|
||
unmount: bool,
|
||
}
|
||
impl NativeCallable for LifecycleCall {
|
||
/// Block、Mount 与用户钩子都已捕获,生命周期序列无参调用此适配器。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(0))
|
||
}
|
||
|
||
/// 卸载时先取消 Mount 所有任务,再以 Block/Mount 为实参请求 VM 执行用户 onMount/onUnmount,不在 Rust 中直接运行源体。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 用户 onUnmount 运行前先取消该出现拥有的任务,防止退役后继续交付旧工作。
|
||
if self.unmount {
|
||
crate::tasks::cancel_owner(cx, self.mount)?;
|
||
}
|
||
Ok(NativeResult::Invoke {
|
||
function: self.function,
|
||
arguments: vec![Value::Object(self.block), Value::Object(self.mount)],
|
||
})
|
||
}
|
||
}
|
||
|
||
/// 构造生命周期调用所需的托管函数与参数,使回调遵循同一 VM 的引用和执行约束。
|
||
fn lifecycle_call(
|
||
cx: &mut NativeContext<'_>,
|
||
mount: ObjectRef,
|
||
name: &'static str,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// Sync 为每个新增/退役出现建立包装,从 Mount 找到实际业务 receiver。
|
||
let block = cx.store().payload::<Mount>(mount, "Mount")?.block;
|
||
// 生命周期通过名义类型查找,不能被普通实例同名字段绕开协议。
|
||
let definition = cx
|
||
.store()
|
||
.type_of(block)?
|
||
.ok_or(VmError::InvalidArgument("untyped mounted Block"))?;
|
||
// 按 onMount/onUnmount 查真实方法,缺失类型/方法都表示协议未安装完整。
|
||
let function = cx
|
||
.store()
|
||
.lookup_method(definition, name)?
|
||
.ok_or(VmError::InvalidArgument("Block has no lifecycle method"))?;
|
||
// 生命周期包装显式捕获函数、Block、Mount,使序列执行到回调前这些对象不会被回收。
|
||
cx.allocate(ContextNativeFunction::new(
|
||
name,
|
||
LifecycleCall {
|
||
function,
|
||
block,
|
||
mount,
|
||
unmount: name == "onUnmount",
|
||
},
|
||
vec![function, block, mount],
|
||
))
|
||
}
|
||
|
||
impl NativeCallable for Sync {
|
||
/// 展示同步步骤从捕获的帧与弱节点表读取输入,无需外部实参。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(0))
|
||
}
|
||
|
||
/// 把弱业务身份升级为本轮对象映射,再同步帧中出现位置到托管 Mount,建立结构/几何与待通知生命周期。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 帧只含 Block 字符串身份,先将弱节点表升级为本次同步可用的实际对象映射。
|
||
let mut blocks = HashMap::new();
|
||
// 弱注册表不保活隐藏业务树;只升级当前仍存活的 Block 为本次调用临时根。
|
||
for weak in &self.nodes {
|
||
// 失效弱节点直接忽略,帧若仍引用其身份则在下面明确拒绝。
|
||
if let Some(block) = cx.upgrade(*weak)? {
|
||
blocks.insert(block.to_string(), block);
|
||
}
|
||
}
|
||
// 把捕获的活动 Page 转为弱身份写进各 Mount,用于上下文比较和源码 page()。
|
||
let page = self
|
||
.page
|
||
.map(|page| cx.store().downgrade(page))
|
||
.transpose()?;
|
||
// 新帧 ID→托管 Mount 临时映射,第一遍全部建立后再填关系。
|
||
let mut objects = HashMap::new();
|
||
// 记录真实新建出现的先序次序,最后仅这些对象安排 onMount。
|
||
let mut created = Vec::new();
|
||
// 第一遍按帧顺序为每个几何找业务 Block 并复用/分配 Mount。
|
||
for geometry in &self.frame.mounts {
|
||
// 帧 block_id 只能对应仍可升级的业务对象,不能凭字符串新建一个替代身份。
|
||
let block = *blocks
|
||
.get(&geometry.block_id)
|
||
.ok_or(VmError::InvalidArgument(
|
||
"frame references an unavailable Block",
|
||
))?;
|
||
// 先按同一 Mount ID 找旧弱对象,失效项不具备复用资格。
|
||
let old = self
|
||
.old
|
||
.get(&geometry.id)
|
||
.map(|weak| cx.upgrade(*weak))
|
||
.transpose()?
|
||
.flatten();
|
||
// Mount ID 相同仍不足以复用:必须仍挂接、页面相同、真实 Block 相同,防止身份误归属。
|
||
let reusable = if let Some(old) = old {
|
||
// 检查旧 Mount 的真实 payload,页面/业务对象不一致时即使数值 ID 相同也必须新建。
|
||
let mount = cx.store().payload::<Mount>(old, "Mount")?;
|
||
(mount.mounted && mount.page == page && mount.block == block).then_some(old)
|
||
} else {
|
||
None
|
||
};
|
||
// 通过全部资格检查才复用,保留已有任务作用域和用户滚动偏移。
|
||
let object = if let Some(old) = reusable {
|
||
old
|
||
} else {
|
||
// 新出现从无关系、零偏移、无任务和无临时 layout 开始,随后安装类型与方法。
|
||
let object = cx.allocate(Mount {
|
||
block,
|
||
page,
|
||
parent: None,
|
||
first_child: None,
|
||
next_sibling: None,
|
||
geometry: geometry.clone(),
|
||
scroll_offset: [0.0, 0.0],
|
||
version: self.frame.layout_version,
|
||
mounted: true,
|
||
tasks: None,
|
||
layout: None,
|
||
})?;
|
||
crate::ui_bridge::attach_desktop_type(cx, object, "Mount", self.desktop)?;
|
||
// 每个新 Mount 安装捕获自身的原生方法;captures 中只放 Mount,桌面仍保留弱身份。
|
||
for (name, query) in [
|
||
("bounds", Query::Bounds),
|
||
("scrollTo", Query::ScrollTo),
|
||
("scrollBy", Query::ScrollBy),
|
||
("scrollPosition", Query::ScrollPosition),
|
||
("scrollIntoView", Query::ScrollIntoView),
|
||
("fitView", Query::FitView),
|
||
("pick3D", Query::Pick3D),
|
||
("toView", Query::ToView),
|
||
("toLocal", Query::ToLocal),
|
||
("parent", Query::Parent),
|
||
("firstChild", Query::FirstChild),
|
||
("nextSibling", Query::NextSibling),
|
||
("page", Query::Page),
|
||
("requestFocus", Query::Focus),
|
||
("hitTest", Query::HitTest),
|
||
] {
|
||
// 方法函数只捕获本 Mount;弱桌面用于按需获取能力,避免方法反向保活整个窗口。
|
||
let function = cx.allocate(ContextNativeFunction::new(
|
||
name,
|
||
Method {
|
||
signature: mount_signature(cx.store(), self.desktop, query)?,
|
||
owner: object,
|
||
desktop: cx.store().downgrade(self.desktop)?,
|
||
query,
|
||
},
|
||
vec![object],
|
||
))?;
|
||
cx.set_attr(object, name, Value::Object(function))?;
|
||
}
|
||
cx.set_attr(object, "id", Value::Number((geometry.id as i64).into()))?;
|
||
cx.set_attr(object, "block", Value::Object(block))?;
|
||
created.push(object);
|
||
object
|
||
};
|
||
objects.insert(geometry.id, object);
|
||
}
|
||
// 先建立全部 Mount,再第二遍补链接,保证 parent/child/sibling 无论先后都能找到目标身份。
|
||
// 第二遍建立新的强拥有字典,桌面切换到此字典后才能释放旧出现集合。
|
||
let root = cx.new_dict()?;
|
||
// 第二遍补齐所有弱层次关系和本轮几何,并将每个活动出现强持有在新字典中。
|
||
for geometry in &self.frame.mounts {
|
||
// 所有 Mount 已先创建,parent/child/sibling 可指向帧中任意先后位置。
|
||
let object = objects[&geometry.id];
|
||
// 帧链接 ID 在完整临时映射中转弱对象身份;不让父子边额外形成强引用环。
|
||
let link = |id: Option<u64>| -> Result<Option<WeakObjectRef>, VmError> {
|
||
id.map(|id| cx.store().downgrade(objects[&id])).transpose()
|
||
};
|
||
// 组装最终本轮 payload,任务作用域保留,关系/几何/版本使用本帧数据。
|
||
let mount = Mount {
|
||
tasks: cx.store().payload::<Mount>(object, "Mount")?.tasks,
|
||
block: blocks[&geometry.block_id],
|
||
page,
|
||
parent: link(geometry.parent)?,
|
||
first_child: link(geometry.first_child)?,
|
||
next_sibling: link(geometry.next_sibling)?,
|
||
geometry: geometry.clone(),
|
||
// 准备阶段沿用用户滚动状态;提交阶段以帧中有效偏移为准,非滚动容器归零。
|
||
scroll_offset: if self.preparing {
|
||
cx.store().payload::<Mount>(object, "Mount")?.scroll_offset
|
||
} else {
|
||
geometry.scroll.map(|s| [s[0], s[1]]).unwrap_or([0.0, 0.0])
|
||
},
|
||
version: self.frame.layout_version,
|
||
mounted: true,
|
||
// 临时 Context 只有原生派生数据,无托管根;提交时置 None,表示几何可供外部查询。
|
||
layout: if self.preparing {
|
||
Some(Arc::new(Mutex::new(crate::layout::Context::default())))
|
||
} else {
|
||
None
|
||
},
|
||
};
|
||
// 与旧 payload 比较避免无变化 replace_payload 造成大量 Contents 日志。
|
||
let old = cx.store().payload::<Mount>(object, "Mount")?;
|
||
// 只在上下文、几何、版本、链接或公开挂接属性确实需更新时替换 payload,减少无关变更日志。
|
||
if self.preparing
|
||
|| old.layout.is_some()
|
||
|| !old.mounted
|
||
|| old.version != mount.version
|
||
|| old.geometry != mount.geometry
|
||
|| old.page != mount.page
|
||
|| old.parent != mount.parent
|
||
|| old.first_child != mount.first_child
|
||
|| old.next_sibling != mount.next_sibling
|
||
|| matches!(
|
||
cx.store().get_attr(object, "mounted"),
|
||
Err(VmError::AttributeNotFound(_))
|
||
)
|
||
{
|
||
cx.replace_payload(object, mount)?;
|
||
cx.set_attr(object, "mounted", Value::Bool(true))?;
|
||
cx.set_attr(
|
||
object,
|
||
"layoutVersion",
|
||
Value::Number((self.frame.layout_version as i64).into()),
|
||
)?;
|
||
}
|
||
// 新字典逐项拥有活动 Mount;弱链接本身不负责让整棵出现树存活。
|
||
cx.dict_set(
|
||
root,
|
||
Value::Text(geometry.id.to_string()),
|
||
Value::Object(object),
|
||
)?;
|
||
}
|
||
// 生命周期包装按“全部卸载→全部新挂载”顺序收集,实际源执行留给返回 driver。
|
||
let mut hooks = Vec::new();
|
||
// Layout records are preorder; reversing guarantees children unmount
|
||
// before their parents. A Page change creates fresh Mount contexts.
|
||
// 旧帧按父先子后记录,逆序退役保证孩子 onUnmount 在父之前;复用对象不退役。
|
||
for id in self.old_order.iter().rev() {
|
||
// 从保存的确定顺序取旧身份,不依赖 HashMap 随机迭代安排卸载。
|
||
let weak = self.old[id];
|
||
// 对象仍存活且没有作为同一 ID 的新对象复用时才退役,避免重复调用 onUnmount。
|
||
if let Some(object) = cx.upgrade(weak)?
|
||
&& objects.get(id) != Some(&object)
|
||
{
|
||
// 退役副本先撤去挂接及结构链接,随后才安装卸载包装,旧句柄不能继续用于几何操作。
|
||
let mut mount = cx.store().payload::<Mount>(object, "Mount")?.clone();
|
||
// 先撤销挂接和全部关系,再安排回调;onUnmount 不能继续使用失效几何或沿旧链操作。
|
||
mount.mounted = false;
|
||
mount.layout = None;
|
||
mount.parent = None;
|
||
mount.first_child = None;
|
||
mount.next_sibling = None;
|
||
cx.replace_payload(object, mount)?;
|
||
cx.set_attr(object, "mounted", Value::Bool(false))?;
|
||
hooks.push(lifecycle_call(cx, object, "onUnmount")?);
|
||
}
|
||
}
|
||
// 新建顺序沿帧先序,onMount 默认父先子后;全部旧退役回调排在新挂载回调之前。
|
||
// 按新帧先序安排挂载回调,父先于子;复用出现不重复通知。
|
||
for object in created {
|
||
hooks.push(lifecycle_call(cx, object, "onMount")?);
|
||
}
|
||
// 同步只返回生命周期序列,不在此处立即执行用户回调;调用方决定在后续阶段何时运行。
|
||
// 无钩子返回 Nil,有钩子构造 vm_sequence;同步本身不直接递归执行源函数。
|
||
let driver = if hooks.is_empty() {
|
||
None
|
||
} else {
|
||
Some(crate::vm_sequence::build(
|
||
cx,
|
||
hooks,
|
||
"<mount lifecycle>",
|
||
self.desktop,
|
||
)?)
|
||
};
|
||
// 新宿主索引仅保存弱引用,字典和生命周期包装才是实际托管拥有者。
|
||
let weak = objects
|
||
.into_iter()
|
||
.map(|(id, object)| Ok((id, cx.store().downgrade(object)?)))
|
||
.collect::<Result<HashMap<_, _>, VmError>>()?;
|
||
// 先让桌面拥有新字典,再公布原生弱索引;返回的驱动保持待退役对象直到回调结束。
|
||
cx.set_attr(self.desktop, "mounts", Value::Object(root))?;
|
||
*self
|
||
.output
|
||
.lock()
|
||
.map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))? = weak;
|
||
Ok(NativeResult::Return(
|
||
driver.map(Value::Object).unwrap_or(Value::Nil),
|
||
))
|
||
}
|
||
}
|
||
|
||
/// Explicitly owned roots protect the old presentation while preparation
|
||
/// temporarily replaces its desktop dictionary and Mount payloads.
|
||
/// 布局准备前的挂载快照与临时保留,用于取消恢复;成功提交后必须释放其保留引用。
|
||
pub struct Checkpoint {
|
||
// 旧弱表与顺序副本,restore 成功后替换回宿主表。
|
||
table: MountTable,
|
||
// 原 desktop.mounts 字典身份;由 roots 显式保活,None 表示原先没有该属性。
|
||
dictionary: Option<ObjectRef>,
|
||
// 保存 payload 与 mounted/layoutVersion 两项公开属性;None 表示原先没有属性,回滚要删除。
|
||
states: Vec<(ObjectRef, Mount, Option<Value>, Option<Value>)>,
|
||
// 检查点实际 retain/upgrade 得到的拥有引用,成功或失败退出都必须 release。
|
||
roots: Vec<ObjectRef>,
|
||
}
|
||
|
||
impl Checkpoint {
|
||
/// 释放检查点为回滚临时保留的托管根,成功提交后不再需要这些引用。
|
||
pub fn release(self, vm: &mut Vm) -> Result<(), VmError> {
|
||
// 聚合释放错误,单个根释放失败不阻止清理其他已保留根。
|
||
let mut result = Ok(());
|
||
// 逆序释放所有临时根,单项失败仍继续尝试余下根,最终返回最后一次释放错误。
|
||
for root in self.roots.into_iter().rev() {
|
||
// 保留最后一次释放错误,调用方仍知道资源清理没有完全成功。
|
||
if let Err(error) = vm.release(root) {
|
||
result = Err(error);
|
||
}
|
||
}
|
||
result
|
||
}
|
||
|
||
/// 恢复检查点中尚未发布的出现关系与布局上下文,并释放临时保留。
|
||
pub fn restore(
|
||
self,
|
||
vm: &mut Vm,
|
||
desktop: ObjectRef,
|
||
table: &mut MountTable,
|
||
) -> Result<(), VmError> {
|
||
// 先暂存恢复错误,让下方无论成败都尝试释放检查点拥有根。
|
||
let result = (|| {
|
||
// 回滚先识别本轮新增的出现并将其脱离,再恢复原有 Mount payload、公开属性和桌面拥有字典。
|
||
let old: std::collections::HashSet<_> =
|
||
self.states.iter().map(|state| state.0).collect();
|
||
// 遍历准备阶段当前出现,识别不属于旧检查点的临时新建 Mount。
|
||
for weak in table.ordered() {
|
||
// 弱升级提供临时强根,处理结果暂存后必须释放再传播错误。
|
||
if let Some(object) = vm.upgrade(weak)? {
|
||
// 捕获单个对象退役过程的结果,确保后面的 release 仍执行。
|
||
let result = (|| {
|
||
// 只把准备阶段新出现脱离,旧出现将由保存 payload 原样恢复。
|
||
if !old.contains(&object) {
|
||
// 新对象即使仍被外部源引用,也必须标为未挂接并清除临时几何上下文与弱层次。
|
||
let mut mount = vm.payload::<Mount>(object, "Mount")?.clone();
|
||
mount.mounted = false;
|
||
mount.layout = None;
|
||
mount.parent = None;
|
||
mount.first_child = None;
|
||
mount.next_sibling = None;
|
||
vm.replace_payload(object, mount)?;
|
||
vm.set_attr(object, "mounted", Value::Bool(false))?;
|
||
}
|
||
Ok::<_, VmError>(())
|
||
})();
|
||
// 保存处理结果后先释放 upgrade 根,再传播处理错误,避免失败路径遗留临时强引用。
|
||
vm.release(object)?;
|
||
result?;
|
||
}
|
||
}
|
||
// 恢复每个旧 payload 和两项公开属性,不能只改宿主索引而留下 VM 状态不一致。
|
||
for (object, mount, mounted, version) in &self.states {
|
||
vm.replace_payload(*object, mount.clone())?;
|
||
// 原属性“缺失”和“存在且为 Nil”等值状态不同,逐项按 Option 恢复。
|
||
for (name, value) in [("mounted", mounted), ("layoutVersion", version)] {
|
||
// 原属性存在则恢复旧值,不存在则 del_attr;已缺失可忽略,其余删除错误仍要传播。
|
||
if let Some(value) = value {
|
||
vm.set_attr(*object, name, value.clone())?;
|
||
} else {
|
||
// 原本不存在的属性应删除;已经缺失视为已恢复,其他错误仍传播。
|
||
match vm.del_attr(*object, name) {
|
||
Ok(()) | Err(VmError::AttributeNotFound(_)) => {}
|
||
Err(error) => return Err(error),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 恢复旧桌面字典的强拥有关系;原来没有字典时必须删去准备阶段新字典。
|
||
if let Some(dictionary) = self.dictionary {
|
||
vm.set_attr(desktop, "mounts", Value::Object(dictionary))?;
|
||
} else {
|
||
// 缺失可忽略,其他删除失败意味着恢复未完成,不能直接报告成功。
|
||
match vm.del_attr(desktop, "mounts") {
|
||
Ok(()) | Err(VmError::AttributeNotFound(_)) => {}
|
||
Err(error) => return Err(error),
|
||
}
|
||
}
|
||
*table = self.table.clone();
|
||
Ok(())
|
||
})();
|
||
// 恢复过程失败也要释放检查点根;先尝试两项操作,再按恢复错误优先的顺序返回。
|
||
// 先尝试释放全部检查点根,再优先返回恢复错误;不能因恢复失败跳过根清理。
|
||
let released = self.release(vm);
|
||
result?;
|
||
released
|
||
}
|
||
}
|
||
|
||
impl MountTable {
|
||
/// 保留布局准备前的 Mount 状态,供失败或取消路径恢复。
|
||
pub fn checkpoint(&self, vm: &mut Vm, desktop: ObjectRef) -> Result<Checkpoint, VmError> {
|
||
// 布局准备入口读取原挂载字典,保留“属性不存在”状态;非对象现值是非法内部状态。
|
||
let dictionary = match vm.get_attr(desktop, "mounts") {
|
||
Ok(Value::Object(dictionary)) => Some(dictionary),
|
||
Err(VmError::AttributeNotFound(_)) => None,
|
||
Ok(_) => return Err(VmError::InvalidArgument("invalid mount dictionary")),
|
||
Err(error) => return Err(error),
|
||
};
|
||
// 记录每次显式 retain/upgrade 的拥有引用,部分构造失败时也可统一清理。
|
||
let mut roots = Vec::new();
|
||
// 保留旧字典,避免准备阶段 desktop.mounts 换成新字典后旧出现提前回收,导致无法恢复。
|
||
if let Some(dictionary) = dictionary {
|
||
vm.retain(dictionary)?;
|
||
roots.push(dictionary);
|
||
}
|
||
// 先保存表/字典,逐项累积状态和根,不能在全部成功前交给调用方。
|
||
let mut checkpoint = Checkpoint {
|
||
table: self.clone(),
|
||
dictionary,
|
||
states: Vec::new(),
|
||
roots,
|
||
};
|
||
// 把中途读取/保留错误暂存,创建失败时仍释放已经取得的全部拥有根。
|
||
let result = (|| {
|
||
// 按旧展示顺序保存活动对象,便于恢复一致的上下文而不受 HashMap 顺序影响。
|
||
for weak in self.ordered() {
|
||
// 只有仍存活的 Mount 可保存,弱升级本身提供一个检查点拥有根。
|
||
if let Some(object) = vm.upgrade(weak)? {
|
||
// upgrade 已提供一个拥有根,直接记录待释放列表,不重复 retain;随后读取失败也会统一清理。
|
||
checkpoint.roots.push(object);
|
||
// 复制当前出现 payload,恢复时连同滚动请求、任务作用域及临时布局状态一起还原。
|
||
let mount = vm.payload::<Mount>(object, "Mount")?.clone();
|
||
// 保存 mounted/layoutVersion 两项独立源字段,不能假设它们与 payload 一定同步或类型固定。
|
||
let mut attributes = Vec::new();
|
||
// 逐项区分有值/缺失,其他读取错误不应被误当作缺失。
|
||
for name in ["mounted", "layoutVersion"] {
|
||
// 读取返回对象 Value 时还需额外 retain,Rust Value 副本本身不增加 VM 根。
|
||
let value = match vm.get_attr(object, name) {
|
||
Ok(value) => Some(value),
|
||
Err(VmError::AttributeNotFound(_)) => None,
|
||
Err(error) => return Err(error),
|
||
};
|
||
// 公开属性理论上可能持有对象,快照保存 Value 本身不会自动增加 VM 根,须额外 retain。
|
||
if let Some(Value::Object(object)) = &value {
|
||
vm.retain(*object)?;
|
||
checkpoint.roots.push(*object);
|
||
}
|
||
attributes.push(value);
|
||
}
|
||
checkpoint.states.push((
|
||
object,
|
||
mount,
|
||
attributes.remove(0),
|
||
attributes.remove(0),
|
||
));
|
||
}
|
||
}
|
||
Ok::<_, VmError>(())
|
||
})();
|
||
// 检查点创建到一半失败时释放已经取得的根,不返回一个不完整且可能泄漏的检查点。
|
||
if let Err(error) = result {
|
||
checkpoint.release(vm)?;
|
||
return Err(error);
|
||
}
|
||
Ok(checkpoint)
|
||
}
|
||
|
||
/// A faulted domain cannot run user onUnmount callbacks. Still detach every
|
||
/// context and release its owning dictionary through the normal heap barrier.
|
||
/// 退役当前出现表,撤销挂载关系并准备不再展示的生命周期状态。
|
||
pub fn retire(&mut self, vm: &mut Vm, desktop: ObjectRef) -> Result<(), VmError> {
|
||
// 故障域退役按孩子先父后清除出现,但绝不调用任何源 onUnmount。
|
||
for weak in self.ordered().into_iter().rev() {
|
||
// 当前仍存活的旧句柄要置为 detached;每次升级的强根处理后立即释放。
|
||
if let Some(object) = vm.upgrade(weak)? {
|
||
// 暂存 payload/公开属性更新结果,让故障路径也能清理弱升级拥有根。
|
||
let result = (|| {
|
||
// 清挂接、临时 layout 与父子兄弟链接,旧源句柄不得继续操作关闭的展示上下文。
|
||
let mut mount = vm.payload::<Mount>(object, "Mount")?.clone();
|
||
mount.mounted = false;
|
||
mount.layout = None;
|
||
mount.parent = None;
|
||
mount.first_child = None;
|
||
mount.next_sibling = None;
|
||
vm.replace_payload(object, mount)?;
|
||
vm.set_attr(object, "mounted", Value::Bool(false))
|
||
})();
|
||
vm.release(object)?;
|
||
result?;
|
||
}
|
||
}
|
||
// 故障退役不执行用户回调;将 desktop.mounts 换成空字典,再清除原生索引,撤销桌面拥有关系。
|
||
// 通过正常 VM 写屏障将 desktop.mounts 换为空字典,撤销实际托管强拥有边。
|
||
let dictionary = vm.new_dict()?;
|
||
// 先保存写入结果,再释放新字典的局部分配根;桌面属性成功后持有字典。
|
||
let result = vm.set_attr(desktop, "mounts", Value::Object(dictionary));
|
||
vm.release(dictionary)?;
|
||
result?;
|
||
self.active.clear();
|
||
self.order.clear();
|
||
Ok(())
|
||
}
|
||
|
||
/// 按原始展示顺序返回弱 Mount 身份,供事件和协议驱动器保持稳定顺序。
|
||
pub fn ordered(&self) -> Vec<WeakObjectRef> {
|
||
// 过滤仍在 active 的 ID 并保留旧帧顺序,事件/生命周期调用方不直接遍历 HashMap。
|
||
self.order
|
||
.iter()
|
||
.filter_map(|id| self.active.get(id).copied())
|
||
.collect()
|
||
}
|
||
|
||
/// 根据新结构复用或建立每次出现的 Mount,连接父子关系并准备生命周期调用。
|
||
pub fn synchronize(
|
||
&mut self,
|
||
vm: &mut Vm,
|
||
desktop: ObjectRef,
|
||
page: Option<ObjectRef>,
|
||
nodes: Vec<WeakObjectRef>,
|
||
frame: &Frame,
|
||
preparing: bool,
|
||
) -> Result<Option<ObjectRef>, VmError> {
|
||
// 同步函数与宿主表之间共享一次性输出弱表;Mutex 只传递索引,不承担托管根。
|
||
let output = Arc::new(Mutex::new(HashMap::new()));
|
||
// 同步入口显式保活桌面与可选页面;旧表和节点表本身仍是弱索引,不增加隐式根。
|
||
let mut captures = vec![desktop];
|
||
captures.extend(page);
|
||
// 构造无参 native 同步入口,显式 captures 保活桌面和可选页面,其他输入为原生值/弱观察。
|
||
let function = vm.allocate(ContextNativeFunction::new(
|
||
"mount-sync",
|
||
Sync {
|
||
desktop,
|
||
page,
|
||
nodes,
|
||
old: self.active.clone(),
|
||
old_order: self.order.clone(),
|
||
frame: frame.clone(),
|
||
preparing,
|
||
output: output.clone(),
|
||
},
|
||
captures,
|
||
))?;
|
||
// 先保存调用结果再释放同步入口,错误时同样清理;只有成功后才替换活动索引与顺序。
|
||
// 让所有分配/写入在所属 VM 的 NativeContext 中执行,返回值可能是待执行生命周期驱动。
|
||
let result = vm.call(function, &[]);
|
||
vm.release(function)?;
|
||
// 先释放入口函数后再传播调用错误;失败不安装半完成的宿主 active/order。
|
||
let result = result?;
|
||
// 从共享输出移动弱表而不是克隆;生命周期驱动若存在则作为拥有结果交给调用方释放。
|
||
self.active = std::mem::take(
|
||
&mut *output
|
||
.lock()
|
||
.map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))?,
|
||
);
|
||
self.order = frame.mounts.iter().map(|mount| mount.id).collect();
|
||
// 仅 Object 驱动或 Nil 是合法同步返回;其他值先释放再报告内部协议错误。
|
||
match result {
|
||
Value::Object(hooks) => Ok(Some(hooks)),
|
||
Value::Nil => Ok(None),
|
||
// 非预期返回值也统一经 release_value 清理,再报告同步返回协议错误。
|
||
value => {
|
||
vm.release_value(value)?;
|
||
Err(VmError::InvalidArgument("invalid lifecycle batch"))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 兼容旧 crate::mount::collection 调用路径,实际对象列表拥有/索引实现集中在 object_list 模块。
|
||
pub use crate::object_list::collection;
|