//! 属性绑定:在只读计算中收集依赖,将结果写回绑定目标。 //! 缓存记录读取版本而不是复制业务模型;依赖变化才重新计算。托管连接对象追踪其拥有的引用。 //! //! 调用路径:UI 的 Block.bind 原生方法调用 connect;UiBridge 刷新入口在 VM 序列中调用 refresh。 //! connect 建立目标拥有的连接,refresh 在发布布局前反复求值到稳定;隐藏 Block 也参与刷新。 //! //! Target-owned managed bindings. Dependency versions are borrowed identities; //! the host's weak Block registry never roots an otherwise unreachable graph. use std::{ collections::HashSet, sync::{Arc, Mutex}, }; use anyhow::{Result, bail}; use endor_core::{NativeContext, ObjectPayload, ObjectRef, Value, Vm, VmError, WeakObjectRef}; use crate::{block, event_dispatch::Subscription, ui_bridge::attach_desktop_type}; /// The output value is borrowed for equality only, including object handles. /// It never owns a reference or dereferences a possibly reclaimed cached object. #[derive(Debug, Clone)] /// 上次绑定计算的读取观察,用于判断是否需要重新执行源计算。 struct Cached { // 每项保存读取对象/属性的版本观察;刷新时向对象存储核对,而非复制业务值。 reads: Vec, // 只借用上次结果的身份做相等比较;不能解引用其中可能已被回收的对象。 output: Value, } #[derive(Debug, Clone)] /// 托管属性连接:由目标强持有,连接只强持有 compute,不反向保活目标;缓存共享只读计算的观察结果。 pub struct Binding { // 目标属性名;目标 Block 通过 bindings 列表拥有本连接。 field: String, // Some 表示连接有效且拥有计算函数;取消/替换后置 None,让 trace 停止保活函数。 pub compute: Option, // payload 克隆共享同一缓存,刷新局部副本也能更新缓存;首次计算前为 None。 reads: Arc>>, } impl ObjectPayload for Binding { /// 声明此 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-binding" } /// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。 fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { // Aster GC 调用 trace 时只追踪有效计算;reads/output 都是借用观察,不能增加长期拥有边。 if let Some(compute) = self.compute { visit(compute); } } } /// 检查绑定目标与可调用计算,建立托管连接及其引用关系。 pub fn connect( cx: &mut NativeContext<'_>, target: ObjectRef, field: &str, compute: ObjectRef, desktop: ObjectRef, ) -> Result { // 在触碰连接列表前拒绝非法名称,尤其禁止绑定运行时类型属性。 if field.is_empty() || field.len() > 128 || field == "type" { return Err(VmError::InvalidArgument("invalid binding property")); } // 沿共享赋值解析到底层 Data 身份,避免给旧别名副本建立不再生效的连接。 let (reference, data) = block::resolve(cx.store(), target)?; // 复制宿主描述以编辑 bindings;最终 replace_payload 才更新托管拥有边,不能原地绕过 VM 引用追踪。 let mut data = data.clone(); // 只允许属性存储里已存在的字段;方法名即使可读取,也不能作为绑定写入目标。 let properties = data .properties .ok_or(VmError::InvalidArgument("missing property storage"))?; cx.store().get_attr(properties, field)?; // 计算必须是 Function(含绑定函数),可构造的类型对象等 callable 不能替代绑定函数。 if !cx.store().object(compute)?.payload().is_function() { return Err(VmError::InvalidArgument( "binding compute must be a Function", )); } // 刷新统一以零参数调用 compute,因此必须有参数元数据并且接受零实参。 if !cx .store() .call_arity(compute)? .ok_or(VmError::InvalidArgument( "binding compute requires positional metadata", ))? .accepts(0) { return Err(VmError::InvalidArgument( "binding compute requires zero unbound parameters", )); } // 同一字段只保留一个有效计算。其他字段连接沿用;旧同字段连接失效并清缓存, // 使外部仍持有的 Subscription 不会继续驱动被替换的计算。 // 收集仍有效且属于其他字段的连接,随后以这份列表替换原 bindings。 let mut retained = vec![]; // 遍历目标拥有的旧连接,已失效项不再保留;同字段有效项必须取消以保证单字段单计算。 for connection in &data.bindings { // 克隆 payload 以修改 compute,缓存 Arc 仍共享原连接的观察状态。 let mut binding = cx .store() .payload::(*connection, "Binding")? .clone(); // 其他字段仍有效的连接原样保留,其计算函数继续由目标间接保活。 if binding.compute.is_some() && binding.field != field { retained.push(*connection); } else if binding.compute.is_some() { // 只取消仍有效的同字段旧连接;已失效项无需再次清理或加入 retained。 binding.compute = None; *binding .reads .lock() .map_err(|_| VmError::InvalidArgument("binding cache poisoned"))? = None; cx.replace_payload(*connection, binding)?; } } // 预算按替换后仍有效的其他字段计数,给新连接预留一个槽,避免单 Block 连接无限增长。 if retained.len() >= 128 { return Err(VmError::ExecutionLimit("128 bindings per Block")); } // 新连接初始无缓存,第一次 refresh 必须执行 compute;分配后目标的 Data 负责持有它。 let connection = cx.allocate(Binding { field: field.into(), compute: Some(compute), reads: Arc::new(Mutex::new(None)), })?; retained.push(connection); data.bindings = retained; // text 改由通用绑定负责后,清除旧 textOf 推导入口,避免两套机制竞争同一属性。 if field == "text" { data.text_binding = None; } cx.replace_payload(reference, data)?; // 返回的取消令牌仅弱引用连接;连接生命期由目标 Block 的 bindings 决定。 let token = cx.allocate(Subscription { connection: cx.store().downgrade(connection)?, })?; attach_desktop_type(cx, token, "Subscription", desktop)?; Ok(token) } /// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。 /// UiBridge::refresh_bindings 在每轮展示刷新开始时调用;compute 只负责返回值,不能直接写已有模型。 /// 本函数退出只读调用后才验证并写目标属性,这些宿主写入可以使其他绑定失效,因而按完整轮次求固定点。 pub fn refresh( vm: &mut Vm, nodes: &[WeakObjectRef], host_desktops: &HashSet, ) -> Result { // work 跨所有轮次累计。属性 A 的写入可能使已检查的 B 失效,所以不能只扫描一次。 // 返回实际执行的计算次数,UiBridge 用它统计 bindingEvaluations,而非统计扫描节点数量。 let mut work = 0; // 反复扫描弱注册表,直到一整轮没有计算;后面绑定写入可能使本轮已处理项重新失效。 loop { // 记录本轮是否至少执行过一次计算,即使输出没变也再扫一轮核对依赖。 let mut evaluated = false; // 每轮按解析后的真实 Block 身份去重,多个包装对象共享数据时只处理一次。 let mut seen = HashSet::new(); // nodes 是宿主保存的弱 Block 索引,不作为 GC 根;隐藏但仍由业务持有的对象同样会被检查。 for weak in nodes { // 升级只为本轮临时保活;已经回收的目标直接跳过,不重建它或延续其绑定。 let Some(target) = vm.upgrade(*weak)? else { continue; }; // 用闭包暂存失败,让下方 release 无论处理成功与否都能释放 upgrade 的强引用。 let result: Result<()> = (|| { // 按最终 Data 身份去重,多个包装别名不能在同轮重复评估同一组 bindings。 let (reference, data) = block::resolve(vm.object_store(), target)?; // 本轮已处理这个 Data 时跳过,目标强引用仍由外层统一 release。 if !seen.insert(reference) { return Ok(()); } // 先复制连接句柄列表以结束 Data 借用,后续 VM 调用可能修改堆或更新属性。 let connections = data.bindings.clone(); // 目标拥有这些连接;逐个检查其取消状态和缓存,避免只刷新可见快照中的属性。 for connection in connections { // 取当前 payload 快照,compute 与缓存必须来自同一连接状态。 let binding = vm.payload::(connection, "Binding")?.clone(); // 断开或被替换的连接不再执行,即使外部仍持有旧 Subscription。 let Some(compute) = binding.compute else { continue; }; // 锁只用于复制缓存,源 compute 执行期间不持锁,避免执行源代码时占着宿主缓存互斥量。 let cached = binding .reads .lock() .map_err(|_| anyhow::anyhow!("binding cache poisoned"))? .clone(); // 两项都成立才能跳过:读取依赖版本未变,且目标仍等于上次输出。 // 后一项让外部直接改写目标后,绑定仍能恢复其计算值。 if cached.as_ref().is_some_and(|cached| { cached .reads .iter() .all(|read| vm.object_store().read_dependency_valid(read)) && vm .get_attr(target, &binding.field) .is_ok_and(|value| value == cached.output) }) { continue; } // 防止相互写入的绑定持续振荡;预算跨轮累计,超限向宿主报告不稳定。 work += 1; // 已评估次数跨轮累计,超过预算就报告不收敛,而非丢弃剩余绑定继续发布。 if work > 10_000 { bail!("property bindings did not stabilize within the execution budget"); } evaluated = true; // 计算阶段禁止写入,精确记录本次实际读取;单次调用另设帧数与指令上限。 let (value, reads) = vm .call_read_only_precise_with_limits( compute, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 100_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; // 暂存应用阶段错误,让下方 release_value 无论校验/写入是否成功都释放计算返回值。 let applied: Result<()> = (|| { // 宿主桌面对象不纳入此缓存依赖;保留业务读取,避免宿主发布状态驱动重算。 let reads = reads .dependencies .into_iter() .filter(|read| !host_desktops.contains(&read.object())) .collect(); // 计算结束后重新解析目标,不能沿用调用前借用的 Data;随后只在副本上试解码视觉值。 let (_, data) = block::resolve(vm.object_store(), target)?; // 先在数据副本上验证属性类型/范围,验证通过后才写回托管属性。 block::apply_visual_property(&mut data.clone(), &binding.field, &value)?; // 相等输出不再写入,避免无意义的版本增长使其他绑定失效;对象值按身份比较。 if vm.get_attr(target, &binding.field)? != value { vm.set_attr(target, &binding.field, value.clone())?; } // 成功应用后才保存新依赖和输出身份;缓存不 retain 输出对象,也不解引用旧对象句柄。 *binding .reads .lock() .map_err(|_| anyhow::anyhow!("binding cache poisoned"))? = Some(Cached { reads, output: value.clone(), }); Ok(()) })(); // 缓存不拥有输出对象;释放调用返回值后再传播应用错误,避免失败路径漏引用。 vm.release_value(value)?; applied?; } Ok(()) })(); // 无论 result 成功还是失败,先释放弱升级带来的临时强引用,再传播错误。 vm.release(target)?; result?; } // 整轮无需计算才达到固定点;即使某次计算输出未变,也再扫描一轮确认依赖有效。 if !evaluated { return Ok(work); } } }