diff --git a/Cargo.lock b/Cargo.lock index 707010a9..d4a8f174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.14.14" +version = "0.14.15" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.14.14" +version = "0.14.15" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.14.14" +version = "0.14.15" dependencies = [ "ansi_term", "bumpalo", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.14.14" +version = "0.14.15" dependencies = [ "lru", "oxide_bytecode", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1256,7 +1256,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.14.14" +version = "0.14.15" dependencies = [ "num-bigint", "oxc_allocator", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1297,7 +1297,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.14.14" +version = "0.14.15" dependencies = [ "bumpalo", "dashmap", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.14.14" +version = "0.14.15" dependencies = [ "tracing", "tracing-appender", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.14.14" +version = "0.14.15" dependencies = [ "num-bigint", "num-traits", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.14.14" +version = "0.14.15" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.14.14" +version = "0.14.15" dependencies = [ "bumpalo", "dashmap", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.14.14" +version = "0.14.15" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 9f5b438d..78eaccf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.14.14" +version = "0.14.15" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/message_channel.rs b/crates/oxide_builtins/src/message_channel.rs index 6a0534ea..49e143c2 100644 --- a/crates/oxide_builtins/src/message_channel.rs +++ b/crates/oxide_builtins/src/message_channel.rs @@ -5,7 +5,6 @@ //! 端口载荷盒(mpsc 双端与对端端口边)存于对象 `native_fn` 槽,向 GC 家族表 //! 暴露 edges/size/drop 三自由函数。 -use std::collections::HashSet; use std::mem; use oxide_kernel::message_queue; @@ -15,7 +14,7 @@ use oxide_types::value::JsValue; use oxide_runtime_api::{NativeResult, ProtoKind, VmHost}; -use crate::message_value::{detach_message, rehydrate_message, MessageValue}; +use crate::message_value::{detach_message, parse_transfer_list, rehydrate_message, MessageValue}; /// MessagePort 载荷盒:存于对象 `native_fn` 槽(`Box::into_raw`,realm 局部 /// 单线程)。`peer` 是发往对端的 mpsc 发送端(无 GC 边),`rx` 是收自对端的 @@ -169,37 +168,6 @@ fn make_port_object( (ptr, inner) } -/// 解析 postMessage 的 transfer 参数为 ArrayBuffer 指针集合。 -/// -/// # 步骤 -/// 1. undefined 返空集 -/// 2. ToList 迭代一切可迭代(数组 / 字符串 / 生成器 / Map / Set) -/// 3. 每元素须为 ArrayBuffer,重复元素报 DataCloneError -/// -/// # 边界 -/// - 不可迭代对象经 GetIterator 抛 TypeError(原值透传); -/// - 迭代中途抛错经 IteratorClose 后透传原异常; -/// - 非 AB 元素 / 重复元素报 DataCloneError(与 structuredClone 口径统一)。 -fn parse_transfer_list(vm: &mut H, transfer: JsValue) -> Result, JsValue> { - let mut set = HashSet::new(); - if transfer.is_undefined() { - return Ok(set); - } - crate::iterator::iterate_elements(vm, transfer, |vm, e| { - if !e.is_object() || !unsafe { &*e.as_js_object_ptr() }.is_array_buffer_obj() { - return Err(crate::structured_clone::data_clone_error( - vm, - "transfer list element is not an ArrayBuffer", - )); - } - if !set.insert(e.as_js_object_ptr() as *const JsObject) { - return Err(crate::structured_clone::data_clone_error(vm, "duplicate buffer in transfer list")); - } - Ok(()) - })?; - Ok(set) -} - /// MessagePort.postMessage(message, transfer):detach 消息、入队对端 mpsc、 /// 同步 `drain_port` 排空对端队列触发对端 onmessage。 /// diff --git a/crates/oxide_builtins/src/message_value.rs b/crates/oxide_builtins/src/message_value.rs index 6bc3cd77..1432c2f7 100644 --- a/crates/oxide_builtins/src/message_value.rs +++ b/crates/oxide_builtins/src/message_value.rs @@ -84,6 +84,37 @@ impl DetachState { } } +/// 解析 postMessage 的 transfer 参数为 ArrayBuffer 指针集合。 +/// +/// # 步骤 +/// 1. undefined 返空集 +/// 2. ToList 迭代一切可迭代(数组 / 字符串 / 生成器 / Map / Set) +/// 3. 每元素须为 ArrayBuffer,重复元素报 DataCloneError +/// +/// # 边界 +/// - 不可迭代对象经 GetIterator 抛 TypeError(原值透传); +/// - 迭代中途抛错经 IteratorClose 后透传原异常; +/// - 非 AB 元素 / 重复元素报 DataCloneError(与 structuredClone 口径统一)。 +pub fn parse_transfer_list(vm: &mut H, transfer: JsValue) -> Result, JsValue> { + let mut set = HashSet::new(); + if transfer.is_undefined() { + return Ok(set); + } + crate::iterator::iterate_elements(vm, transfer, |vm, e| { + if !e.is_object() || !unsafe { &*e.as_js_object_ptr() }.is_array_buffer_obj() { + return Err(crate::structured_clone::data_clone_error( + vm, + "transfer list element is not an ArrayBuffer", + )); + } + if !set.insert(e.as_js_object_ptr() as *const JsObject) { + return Err(crate::structured_clone::data_clone_error(vm, "duplicate buffer in transfer list")); + } + Ok(()) + })?; + Ok(set) +} + /// 源 realm 侧:遍历源值产出 `MessageValue`。 /// /// # 步骤 diff --git a/crates/oxide_vm/src/bindings/bind_worker.rs b/crates/oxide_vm/src/bindings/bind_worker.rs index 30975ca2..95069078 100644 --- a/crates/oxide_vm/src/bindings/bind_worker.rs +++ b/crates/oxide_vm/src/bindings/bind_worker.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use crate::bind_constructor; -use crate::bindings::{apply_binding_table, configure_native_constructor}; +use crate::bindings::{apply_binding_table, bind_accessor_getset, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::object::{JsObject, PropAttributes}; @@ -120,6 +120,24 @@ pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut ], ); } + + // onmessage / onerror / onmessageerror 访问器属性(getter 读注册表属性条目、 + // setter 写)。幂等守卫。 + for name in ["onmessage", "onerror", "onmessageerror"] { + let key = sf.intern(name).0; + if sh.lookup_position(proto.shape_id(), key).is_none() { + bind_accessor_getset( + core, + session, + proto, + key, + &format!("get {name}"), + &format!("set {name}"), + crate::worker::bindings::worker_event_handler_getter as *const (), + crate::worker::bindings::worker_event_handler_setter as *const (), + ); + } + } } // 全局 `Worker` 槽(bind_constructor! 自带既有槽原位更新、无槽新开)。 @@ -186,6 +204,24 @@ pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut global.set_data_meta(pos, PropAttributes::new(true, false, true)); global.bump_generation(); } + // onmessage / onmessageerror / onerror 直绑全局(访问器,worker realm 经 + // self === global 可达,self.onmessage = fn 由此成立;主 realm 为无害占位)。 + // 幂等守卫。 + for name in ["onmessage", "onmessageerror", "onerror"] { + let key = sf.intern(name).0; + if sh.lookup_position(global.shape_id(), key).is_none() { + bind_accessor_getset( + core, + session, + global, + key, + &format!("get {name}"), + &format!("set {name}"), + crate::worker::bindings::worker_event_handler_getter as *const (), + crate::worker::bindings::worker_event_handler_setter as *const (), + ); + } + } // name / location getter(幂等守卫)。 // let si_name_global = sf.intern("name").0; // if sh.lookup_position(global.shape_id(), si_name_global).is_none() { diff --git a/crates/oxide_vm/src/worker/bindings.rs b/crates/oxide_vm/src/worker/bindings.rs index 248e5b37..7178708b 100644 --- a/crates/oxide_vm/src/worker/bindings.rs +++ b/crates/oxide_vm/src/worker/bindings.rs @@ -14,10 +14,10 @@ use std::cell::{Cell, RefCell}; -use oxide_builtins::message_value::detach_message; +use oxide_builtins::message_value::{detach_message, parse_transfer_list}; use oxide_kernel::message_queue::Sender; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_runtime_api::{NativeResult, ProtoKind, VmHost}; +use oxide_runtime_api::{ListenerEntry, NativeResult, ProtoKind, VmHost}; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; @@ -65,8 +65,8 @@ pub(crate) fn worker_close_requested() -> bool { /// 3. 主线程侧读 url 文件(`std::fs::read_to_string`),失败抛 TypeError。 /// 4. `vm.spawn_worker(source)` 派生 worker,得 worker 编号。 /// 5. 建 Worker 对象:PLAIN 对象(`[[Prototype]]` → Worker.prototype)加非枚举 -/// `workerId` 数据属性(值为 worker 编号)与 `onmessage`/`onerror`/ -/// `onmessageerror` 普通 own 属性(初值 undefined)。无 native 盒、无 GC 家族。 +/// `workerId` 数据属性(值为 worker 编号)。`onmessage`/`onerror`/ +/// `onmessageerror` 不在对象上建自有属性(改原型访问器属性)。无 native 盒、无 GC 家族。 /// 6. 返回 Worker 对象。 /// /// # 边界与前提 @@ -113,20 +113,8 @@ pub(crate) fn worker_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { // SAFETY: ptr 是本函数刚 alloc_object 的对象,存活且本段无别名。 let worker_obj = unsafe { &mut *ptr }; - // onmessage / onerror / onmessageerror:普通 own 数据属性(非枚举),初值 undefined。 - let si_onmessage = vm.perm_intern("onmessage"); - let _ = - vm.define_data_property(worker_obj, si_onmessage, JsValue::undefined(), PropAttributes::new(true, false, true)); - let si_onerror = vm.perm_intern("onerror"); - let _ = - vm.define_data_property(worker_obj, si_onerror, JsValue::undefined(), PropAttributes::new(true, false, true)); - let si_onmessageerror = vm.perm_intern("onmessageerror"); - let _ = vm.define_data_property( - worker_obj, - si_onmessageerror, - JsValue::undefined(), - PropAttributes::new(true, false, true), - ); + // onmessage / onerror / onmessageerror 不在对象上建自有属性:改 Worker.prototype + // 访问器属性(getter 读注册表属性条目、setter 写),经既有 dispatchEvent 路径交付。 // workerId:非枚举数据属性(值为 worker 编号,数值表示无 GC 边)。 let si_worker_id = vm.perm_intern("workerId"); @@ -198,15 +186,17 @@ fn read_worker_id(vm: &mut Vm, this_val: JsValue) -> Result { Ok(n.max(0.0) as u64) } -/// `worker.postMessage(msg)`:读 `workerId`、detach 消息、投递到 worker。 +/// `worker.postMessage(msg, transfer)`:读 `workerId`、解析 transfer、detach 消息、投递到 worker。 /// /// # 步骤 /// 1. 读 `this`(`args[0]`)的 `workerId` 自有属性(快路径)。 -/// 2. 读 `args[1]` 为 msg,经 `detach_message` 转 `MessageValue`(空 transfer 集合)。 -/// 3. 委托 `vm.worker_post_message(id, mv)`(935.3 固有方法,内部 `send`)。 +/// 2. 读 `args[1]` 为 msg、`args[2]` 为 transfer,经 `parse_transfer_list` 解析 +/// transfer 集合(undefined 返空集、违例报 DataCloneError)。 +/// 3. 经 `detach_message` 转 `MessageValue`(transfer 命中的缓冲源 detach)。 +/// 4. 委托 `vm.worker_post_message(id, mv)`(935.3 固有方法,内部 `send`)。 /// /// # 边界 -/// - DataCloneError 经 `detach_message` 的 `Err` 臂抛出。 +/// - DataCloneError 经 `parse_transfer_list` 与 `detach_message` 的 `Err` 臂抛出。 /// - worker 通道已断开(worker 线程退出)时 `vm.worker_post_message` 返 `Err`。 pub(crate) fn worker_post_message(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); @@ -215,7 +205,11 @@ pub(crate) fn worker_post_message(vm: &mut Vm, args: &[u8]) -> NativeResult { Err(e) => return NativeResult::Err(e), }; let msg_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let transfer: std::collections::HashSet<*const JsObject> = std::collections::HashSet::new(); + let transfer_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let transfer = match parse_transfer_list(vm, transfer_val) { + Ok(s) => s, + Err(e) => return NativeResult::Err(e), + }; let mv = match detach_message(vm, msg_val, &transfer) { Ok(mv) => mv, Err(e) => return NativeResult::Err(e), @@ -242,18 +236,24 @@ pub(crate) fn worker_terminate(vm: &mut Vm, args: &[u8]) -> NativeResult { } } -/// `self.postMessage(msg)`(worker 侧):detach 消息、发回主线程。 +/// `self.postMessage(msg, transfer)`(worker 侧):解析 transfer、detach 消息、发回主线程。 /// /// # 步骤 -/// 1. 读 `args[1]` 为 msg,经 `detach_message` 转 `MessageValue`(空 transfer 集合)。 -/// 2. 经 thread-local `WORKER_OUT_TX` 发回主线程(主 realm 无通道时静默丢弃)。 +/// 1. 读 `args[1]` 为 msg、`args[2]` 为 transfer,经 `parse_transfer_list` 解析 +/// transfer 集合(undefined 返空集、违例报 DataCloneError)。 +/// 2. 经 `detach_message` 转 `MessageValue`(transfer 命中的缓冲源 detach)。 +/// 3. 经 thread-local `WORKER_OUT_TX` 发回主线程(主 realm 无通道时静默丢弃)。 /// /// # 边界 -/// - DataCloneError 经 `detach_message` 的 `Err` 臂抛出。 +/// - DataCloneError 经 `parse_transfer_list` 与 `detach_message` 的 `Err` 臂抛出。 /// - 主 realm(无 `WORKER_OUT_TX`)或主线程已 drop 接收端时静默丢弃。 pub(crate) fn self_post_message(vm: &mut Vm, args: &[u8]) -> NativeResult { let msg_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let transfer: std::collections::HashSet<*const JsObject> = std::collections::HashSet::new(); + let transfer_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let transfer = match parse_transfer_list(vm, transfer_val) { + Ok(s) => s, + Err(e) => return NativeResult::Err(e), + }; let mv = match detach_message(vm, msg_val, &transfer) { Ok(mv) => mv, Err(e) => return NativeResult::Err(e), @@ -287,6 +287,103 @@ pub(crate) fn self_location(vm: &mut Vm, _args: &[u8]) -> NativeResult { NativeResult::Ok(vm.new_string("")) } +/// 从当前函数对象的 `name` 读事件类型(`get onmessage` / `set onmessage` → +/// `"message"`)。 +/// +/// # 步骤 +/// 1. 读 callee 槽(寄存器 254)为当前函数对象(native 调用收口置位)。 +/// 2. 读函数对象 `name` 自有属性,剥 `get ` / `set ` 前缀与 `on` 前缀得事件类型串。 +/// 3. intern 事件类型串返 si。 +/// +/// # 边界 +/// - callee 非对象、无 `name` 属性、或非 `get on` / `set on` 形态时返 `None`。 +fn read_event_type_si(vm: &mut Vm) -> Option { + let callee = vm.reg(254); + if !callee.is_object() { + return None; + } + let callee_ptr = callee.as_js_object_ptr(); + // SAFETY: callee 是当前函数对象,native 调用期间存活。 + let callee_obj = unsafe { &*callee_ptr }; + let si_name = vm.perm_intern("name"); + let store = vm.get_own_property_slot(callee_obj, si_name)?; + let name_val = callee_obj.get_prop_at(store); + let name = vm.lookup_str(name_val)?; + let rest = name.strip_prefix("get ").or_else(|| name.strip_prefix("set "))?; + let type_text = rest.strip_prefix("on")?; + Some(vm.perm_intern(type_text)) +} + +/// `onmessage` / `onerror` / `onmessageerror` 访问器 getter(Worker.prototype 与 +/// worker global 共用):读注册表属性条目返回调,无则 null。 +/// +/// # 步骤 +/// 1. 读 `this`(`args[0]`)为目标对象(非对象返 null)。 +/// 2. 从函数对象 `name` 读事件类型 si。 +/// 3. 查注册表取该目标同类型属性监听器条目,返其回调(无则 null)。 +/// +/// # 边界 +/// - 注册表条目按目标对象裸指针为键(弱引用,不保活)。 +pub(crate) fn worker_event_handler_getter(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(args[0]); + if !this_val.is_object() { + return NativeResult::Ok(JsValue::null()); + } + let this_ptr = this_val.as_js_object_ptr(); + let Some(type_si) = read_event_type_si(vm) else { + return NativeResult::Ok(JsValue::null()); + }; + for entry in vm.et_lookup(this_ptr) { + if entry.is_attribute && entry.type_si == type_si { + return NativeResult::Ok(entry.callback); + } + } + NativeResult::Ok(JsValue::null()) +} + +/// `onmessage` / `onerror` / `onmessageerror` 访问器 setter(Worker.prototype 与 +/// worker global 共用):写注册表属性条目(可调用则登记、null / undefined 则移除)。 +/// +/// # 步骤 +/// 1. 读 `this`(`args[0]`)为目标对象(非对象 no-op)。 +/// 2. 从函数对象 `name` 读事件类型 si。 +/// 3. 移除该目标同类型的既有属性监听器条目(若有)。 +/// 4. 值为可调用时登记新属性监听器条目(`is_attribute` 置位);null / undefined 不登记。 +/// +/// # 副作用 +/// - 写注册表(`et_unregister` / `et_register`)。 +pub(crate) fn worker_event_handler_setter(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(args[0]); + if !this_val.is_object() { + return NativeResult::Ok(JsValue::undefined()); + } + let this_ptr = this_val.as_js_object_ptr(); + let Some(type_si) = read_event_type_si(vm) else { + return NativeResult::Ok(JsValue::undefined()); + }; + let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + // 移除该目标同类型的既有属性监听器条目(若有)。 + for entry in vm.et_lookup(this_ptr) { + if entry.is_attribute && entry.type_si == type_si { + vm.et_unregister(this_ptr, entry.type_si, entry.callback, entry.capture); + } + } + // 值为可调用时登记新属性监听器条目。 + if oxide_builtins::iterator::is_callable(value) { + vm.et_register( + this_ptr, + ListenerEntry { + type_si, + callback: value, + capture: false, + once: false, + is_attribute: true, + }, + ); + } + NativeResult::Ok(JsValue::undefined()) +} + /// 消息事件构造辅助:建真实 MessageEvent 盒(`data` + `type`,`ports` 空数组)。 /// /// 消息路径(`deliver_worker_messages` / `deliver_self_message`)经本辅助建