diff --git a/Cargo.lock b/Cargo.lock index 4404904f..c4713888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.14.8" +version = "0.14.9" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.14.8" +version = "0.14.9" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.14.8" +version = "0.14.9" dependencies = [ "ansi_term", "bumpalo", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.14.8" +version = "0.14.9" dependencies = [ "lru", "oxide_bytecode", @@ -1235,7 +1235,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.14.8" +version = "0.14.9" dependencies = [ "num-bigint", "oxc_allocator", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.14.8" +version = "0.14.9" dependencies = [ "bumpalo", "dashmap", @@ -1319,7 +1319,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.14.8" +version = "0.14.9" dependencies = [ "tracing", "tracing-appender", @@ -1340,7 +1340,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1371,7 +1371,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.14.8" +version = "0.14.9" dependencies = [ "num-bigint", "num-traits", @@ -1384,7 +1384,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.14.8" +version = "0.14.9" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1402,7 +1402,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.14.8" +version = "0.14.9" dependencies = [ "bumpalo", "dashmap", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.14.8" +version = "0.14.9" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f88d069c..0de81615 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.14.8" +version = "0.14.9" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/event.rs b/crates/oxide_builtins/src/event.rs index cb49bbc4..20628d51 100644 --- a/crates/oxide_builtins/src/event.rs +++ b/crates/oxide_builtins/src/event.rs @@ -86,6 +86,56 @@ pub fn drop_event_native(obj: &mut JsObject) -> u64 { bytes } +/// 读事件类型串(GC 字符串边)。非 Event 对象或无载荷盒返回 None。 +/// +/// 供派发路径(`dispatchEvent`)取事件类型串、intern 后查监听器。 +pub fn event_type_value(obj: &JsObject) -> Option { + let ptr = event_payload_ptr(obj)?; + if ptr.is_null() { + return None; + } + // SAFETY: ptr 指向有效载荷盒(构造期写入、释放时置空),只读字段。 + Some(unsafe { &*ptr }.r#type) +} + +/// 设派发目标与阶段:`target` / `current_target` 置 `target`、`event_phase` 置 +/// `phase`。非 Event 对象或无载荷盒时 no-op。 +/// +/// 供派发路径(`dispatchEvent`)在调用监听器前设目标与 AT_TARGET 阶段。 +pub fn event_set_dispatch(obj: &mut JsObject, target: JsValue, phase: u32) { + let Some(ptr) = event_payload_ptr(obj) else { + return; + }; + if ptr.is_null() { + return; + } + // SAFETY: ptr 指向有效载荷盒,写入派发字段(无 GC 窗口)。 + let inner = unsafe { &mut *ptr }; + inner.target = target; + inner.current_target = target; + inner.event_phase = phase; +} + +/// 读 `defaultPrevented` 标志。非 Event 对象或无载荷盒返回 None。 +pub fn event_default_prevented(obj: &JsObject) -> Option { + let ptr = event_payload_ptr(obj)?; + if ptr.is_null() { + return None; + } + // SAFETY: ptr 指向有效载荷盒,只读字段。 + Some(unsafe { &*ptr }.default_prevented) +} + +/// 读 `stopImmediatePropagation` 标志。非 Event 对象或无载荷盒返回 None。 +pub fn event_immediate_stopped(obj: &JsObject) -> Option { + let ptr = event_payload_ptr(obj)?; + if ptr.is_null() { + return None; + } + // SAFETY: ptr 指向有效载荷盒,只读字段。 + Some(unsafe { &*ptr }.immediate_stopped) +} + /// 解析 this 为 Event 载荷盒共享引用:非 Event 对象或无载荷盒返回 None。 /// /// # 注意事项 @@ -235,6 +285,40 @@ pub fn event_constructor(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(this_val) } +/// 建一个 Event 载荷盒对象(type 为 `type_str`),返回对象指针。 +/// +/// 供交付路径(消息事件 / 错误事件)建真实 Event 盒,使 `dispatchEvent` 品牌守卫 +/// 通过。盒的 `target` / `currentTarget` 初始 null、`eventPhase` 初始 0(派发时设); +/// `[[Prototype]]` 指向 `Event.prototype`(`e instanceof Event` 成立)。 +/// +/// # 副作用 +/// - 新建一个 Event 盒对象(经 `alloc_object` 入对象表)。 +pub fn create_event_box(vm: &mut H, type_str: &str) -> *mut JsObject { + let type_js = vm.new_string(type_str); + let inner = Box::into_raw(Box::new(EventInner { + r#type: type_js, + target: JsValue::null(), + current_target: JsValue::null(), + event_phase: 0, + is_trusted: false, + bubbles: false, + cancelable: false, + default_prevented: false, + propagation_stopped: false, + immediate_stopped: false, + composed: false, + })); + let event_proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::EventProto)); + let obj = JsObject::new_empty(EMPTY_SHAPE_ID, event_proto); + let ptr = vm.alloc_object(obj); + // SAFETY: ptr 是本函数刚 alloc_object 的对象,存活且本段无别名;native_fn 存 + // 不透明 `Box` 指针,与 ArrayBuffer / RegExp 的类型化存储模式一致。 + let event_obj = unsafe { &mut *ptr }; + event_obj.type_tag = JsObject::OBJ_TYPE_EVENT; + event_obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(inner as *const ()) })); + ptr +} + /// `Event.prototype.type`:返回事件类型串。 pub fn event_type_getter(vm: &mut H, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); diff --git a/crates/oxide_builtins/src/event_target.rs b/crates/oxide_builtins/src/event_target.rs new file mode 100644 index 00000000..324d4806 --- /dev/null +++ b/crates/oxide_builtins/src/event_target.rs @@ -0,0 +1,255 @@ +//! EventTarget 行为面:`addEventListener` / `removeEventListener` / `dispatchEvent` +//! 三原型方法。 +//! +//! 监听器注册表是 per-realm 弱引用结构(目标对象裸指针为键、不保活),由 `VmHost` +//! 的 `et_register` / `et_unregister` / `et_lookup` 三方法操作,GC sweep 按 mark 位 +//! 剪枝、full_reset 清表。注册表值(`EventTargetState`)持监听器回调的 GC 对象边, +//! mark 期经根枚举标活(存活目标的回调随目标存活)。`ListenerEntry` / +//! `EventTargetState` 类型落 `oxide_runtime_api`(trait 所在层,本 crate 依赖之)。 + +use oxide_runtime_api::{ListenerEntry, NativeResult, VmHost}; +use oxide_types::value::JsValue; + +use crate::event; + +/// 解析 `addEventListener` / `removeEventListener` 的 options 参数:布尔(capture) +/// 或对象(once / capture,passive / signal 忽略)。返回 (capture, once)。 +/// +/// # 边界与前提 +/// - options 对象属性经 `ordinary_get` 读(getter 副作用与异常原值传播)。 +/// - 缺失或非对象时取缺省(capture=false, once=false)。 +fn parse_event_options(vm: &mut H, args: &[u8]) -> Result<(bool, bool), JsValue> { + let opt_val = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() }; + if opt_val.is_bool() { + return Ok((oxide_runtime_api::to_boolean(opt_val), false)); + } + if !opt_val.is_object() { + return Ok((false, false)); + } + let opt_ptr = opt_val.as_js_object_ptr(); + // SAFETY: opt_val 已确认是对象值,借用即时消费。 + let opt_obj = unsafe { &*opt_ptr }; + let mut capture = false; + let mut once = false; + for (name, slot) in [("capture", &mut capture), ("once", &mut once)] { + let si = vm.perm_intern(name); + match vm.ordinary_get(opt_obj, si, opt_val) { + Ok(v) => *slot = oxide_runtime_api::to_boolean(v), + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + return Err(crate::error::create_type_error(vm, "Event options property getter failed")); + } + } + } + Ok((capture, once)) +} + +/// `EventTarget.prototype.addEventListener(type, callback, options)`。 +/// +/// # 步骤 +/// 1. this 解析为目标对象(非对象抛 TypeError)。 +/// 2. type 经完整 ToString 转串、intern 得 si;空串抛 SyntaxError。 +/// 3. callback 须为函数(首版不支持对象形 handleEvent,非函数抛 TypeError)。 +/// 4. options 是布尔(capture)或对象(once / capture,passive / signal 忽略)。 +/// 5. 重复登记(同 type + callback + capture)为 no-op;否则追加进注册表。 +/// +/// # 副作用 +/// - 写注册表(`et_register`)。 +pub fn event_target_add_event_listener(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "addEventListener called on non-object")); + } + let this_ptr = this_val.as_js_object_ptr(); + + // type 经完整 ToString 转串(对象经 ToPrimitive,Symbol 抛 TypeError)。 + let type_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let type_str = match oxide_runtime_api::to_string_full(type_val, vm) { + Ok(s) => s, + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error( + vm, + "Cannot convert event type to a string", + )); + } + }; + if type_str.is_empty() { + return NativeResult::Err(crate::error::create_syntax_error(vm, "The provided type is empty")); + } + let type_si = vm.perm_intern(&type_str); + + // callback 须为函数(首版不支持对象形 handleEvent)。 + let callback = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + if !crate::iterator::is_callable(callback) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Event listener callback must be a function", + )); + } + + // options:布尔(capture)或对象(once / capture)。 + let (capture, once) = match parse_event_options(vm, args) { + Ok(co) => co, + Err(e) => return NativeResult::Err(e), + }; + + // 重复登记(同 type + callback + capture)为 no-op;否则追加。 + let entry = ListenerEntry { type_si, callback, capture, once, is_attribute: false }; + let existing = vm.et_lookup(this_ptr); + if existing.iter().any(|e| e.type_si == type_si && e.capture == capture && e.callback == callback) { + return NativeResult::Ok(JsValue::undefined()); + } + vm.et_register(this_ptr, entry); + NativeResult::Ok(JsValue::undefined()) +} + +/// `EventTarget.prototype.removeEventListener(type, callback, options)`。 +/// +/// # 步骤 +/// 1. this 解析为目标对象(非对象抛 TypeError)。 +/// 2. type 经完整 ToString 转串、intern 得 si。 +/// 3. 按同 type + callback + capture 查条目移除(不存在为 no-op)。 +/// +/// # 副作用 +/// - 写注册表(`et_unregister`)。 +pub fn event_target_remove_event_listener(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "removeEventListener called on non-object")); + } + let this_ptr = this_val.as_js_object_ptr(); + + // type 经完整 ToString 转串(与 addEventListener 同一 intern 口径)。 + let type_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let type_str = match oxide_runtime_api::to_string_full(type_val, vm) { + Ok(s) => s, + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error( + vm, + "Cannot convert event type to a string", + )); + } + }; + let type_si = vm.perm_intern(&type_str); + + let callback = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let (capture, _) = match parse_event_options(vm, args) { + Ok(co) => co, + Err(e) => return NativeResult::Err(e), + }; + vm.et_unregister(this_ptr, type_si, callback, capture); + NativeResult::Ok(JsValue::undefined()) +} + +/// `EventTarget.prototype.dispatchEvent(event)`:同步派发事件,返回 +/// `!event.defaultPrevented`。 +/// +/// # 步骤 +/// 1. this 解析为目标对象(非对象抛 TypeError)。 +/// 2. event 品牌守卫(须为 Event 系对象),设 target / currentTarget 为 this、 +/// eventPhase 为 AT_TARGET(2)。 +/// 3. 取事件类型串、intern 得 si,查注册表取本对象该类型的监听器快照。 +/// 4. 逐个监听器调用(GC 根纪律:调用前钉事件与回调进寄存器,调用后重读载荷指针); +/// `once` 条目调用后移除;`stopImmediatePropagation` 截断循环。 +/// 5. 返回 `!event.defaultPrevented`。 +/// +/// # 边界与前提 +/// - 首版派发只在目标(无冒泡、无捕获阶段),监听器按登记序调用。 +/// - 监听器回调的异常原值上抛(不吞)。 +/// +/// # 副作用 +/// - 写事件载荷盒(target / currentTarget / eventPhase);移除 `once` 条目。 +pub fn event_target_dispatch_event(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "dispatchEvent called on non-object")); + } + let this_ptr = this_val.as_js_object_ptr(); + + // event 品牌守卫:须为 Event 系对象(首版仅 Event 标签,派生类由后续任务接入)。 + let event_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !event_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "dispatchEvent requires an Event")); + } + let event_ptr = event_val.as_js_object_ptr(); + if event_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "dispatchEvent requires an Event")); + } + // SAFETY: event_val 已确认是对象值,event_ptr 指向合法 JsObject。 + let event_obj = unsafe { &*event_ptr }; + if !event_obj.is_event_obj() { + return NativeResult::Err(crate::error::create_type_error(vm, "dispatchEvent requires an Event")); + } + + // 设派发目标与阶段(AT_TARGET = 2)。 + // SAFETY: event_obj 存活,无 GC 窗口。 + unsafe { + event::event_set_dispatch(&mut *event_ptr, this_val, 2); + } + + // 取事件类型串、intern 得 si,查注册表取监听器快照。 + let type_val = match event::event_type_value(event_obj) { + Some(v) => v, + None => return NativeResult::Ok(JsValue::bool(true)), + }; + let type_text = match vm.lookup_str(type_val) { + Some(t) => t, + None => return NativeResult::Ok(JsValue::bool(true)), + }; + let type_si = vm.perm_intern(&type_text); + // 注册表按目标对象存全部监听器,此处按事件类型过滤(同型才派发)。 + let listeners: Vec = vm + .et_lookup(this_ptr) + .into_iter() + .filter(|e| e.type_si == type_si) + .collect(); + if listeners.is_empty() { + return NativeResult::Ok(JsValue::bool(true)); + } + + // 逐个监听器调用(GC 根纪律:调用前钉事件与回调进寄存器,调用后重读载荷指针)。 + for entry in &listeners { + // 钉事件对象与回调进返回寄存器(GC 根,防调用窗口内被回收)。 + vm.set_reg(0, event_val); + vm.set_reg(1, entry.callback); + // 调用监听器(this = 目标,实参 = 事件)。异常原值上抛。 + match vm.call_function_sync(entry.callback, this_val, &[event_val]) { + Ok(_) => {} + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error(vm, "Event listener threw")); + } + } + // 调用窗口后重读事件对象(窗口内可发生 GC,原地不搬移但重读守纪律)。 + let event_val = vm.reg(0); + let event_ptr = event_val.as_js_object_ptr(); + // SAFETY: event_val 是 Event 对象(品牌守卫已过),存活。 + let event_obj = unsafe { &*event_ptr }; + // once 条目调用后移除。 + if entry.once { + vm.et_unregister(this_ptr, entry.type_si, entry.callback, entry.capture); + } + // stopImmediatePropagation 截断循环。 + if event::event_immediate_stopped(event_obj) == Some(true) { + break; + } + } + + // 返回 !event.defaultPrevented(末次调用窗口后重读载荷指针)。 + let event_val = vm.reg(0); + let event_ptr = event_val.as_js_object_ptr(); + // SAFETY: event_val 是 Event 对象(品牌守卫已过),存活。 + let event_obj = unsafe { &*event_ptr }; + let default_prevented = event::event_default_prevented(event_obj).unwrap_or(false); + NativeResult::Ok(JsValue::bool(!default_prevented)) +} diff --git a/crates/oxide_builtins/src/lib.rs b/crates/oxide_builtins/src/lib.rs index f6a5349f..b2576c40 100644 --- a/crates/oxide_builtins/src/lib.rs +++ b/crates/oxide_builtins/src/lib.rs @@ -36,6 +36,9 @@ pub mod error; pub mod eval; /// Event 基类实现(构造器、原型 getter 与传播方法,载荷盒与 GC 三自由函数族)。 pub mod event; +/// EventTarget 实现(addEventListener / removeEventListener / dispatchEvent 三原型 +/// 方法与 per-realm 弱引用监听器注册表)。 +pub mod event_target; /// Function 内置对象实现(call/apply/bind/toString)。 pub mod function; /// 全局函数模块(URI 编解码与 Annex B 的 escape/unescape)。 diff --git a/crates/oxide_runtime_api/src/event_target.rs b/crates/oxide_runtime_api/src/event_target.rs new file mode 100644 index 00000000..1e1ea706 --- /dev/null +++ b/crates/oxide_runtime_api/src/event_target.rs @@ -0,0 +1,29 @@ +//! EventTarget 监听器注册表的数据类型:`ListenerEntry`(单条监听器登记)与 +//! `EventTargetState`(一个目标的监听器集合,注册表值)。 +//! +//! 本类型落 `oxide_runtime_api`(`VmHost` trait 所在层):`et_register` / +//! `et_lookup` 的签名引用 `ListenerEntry`,而 trait 不依赖 `oxide_builtins` +//! (依赖方向 `runtime_api ← builtins`),故类型须落在 trait 可引用的层。 + +use oxide_types::value::JsValue; + +/// 单条监听器登记:事件类型 si、回调函数、捕获标志、once 标志、属性 handler 标志。 +/// +/// `type_si` 是事件类型串的 perm intern 下标(同型同回调同 capture 的重复登记为 +/// no-op)。`callback` 是回调函数(GC 对象边)。`is_attribute` 区分属性 handler +/// (`onmessage` 等)与 `addEventListener` 登记(首版恒 false,属性 handler 由后续 +/// 任务接入)。 +#[derive(Clone, Debug)] +pub struct ListenerEntry { + pub type_si: u32, + pub callback: JsValue, + pub capture: bool, + pub once: bool, + pub is_attribute: bool, +} + +/// 一个 EventTarget 的监听器集合(per-realm 弱引用注册表的值)。 +#[derive(Clone, Debug, Default)] +pub struct EventTargetState { + pub listeners: Vec, +} diff --git a/crates/oxide_runtime_api/src/host.rs b/crates/oxide_runtime_api/src/host.rs index 55026d4a..6792f334 100644 --- a/crates/oxide_runtime_api/src/host.rs +++ b/crates/oxide_runtime_api/src/host.rs @@ -8,6 +8,8 @@ use oxide_types::mem::P; use oxide_types::object::{Cell, JsObject, PropAttributes}; use oxide_types::value::JsValue; +use super::event_target::ListenerEntry; + /// shape 节点只读投影:shape forge 节点字段的中立镜像(免跨 crate 引用具体类型)。 #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ShapeNode { @@ -473,4 +475,26 @@ pub trait VmHost { /// - 列表元素为弱引用裸指针,调用方不得跨用户调用持有(GC 可释放 /// 通道对象,须在下一次使用前重新查表校验)。 fn bc_lookup(&self, name: &str) -> Vec<*mut JsObject>; + + // EventTarget 监听器注册表(per-realm 弱引用,目标对象裸指针为键) + /// 把一条监听器登记追加到目标对象的注册表条目(无条目时新建)。 + /// + /// # 边界与前提 + /// - `target` 为登记时刻的目标对象指针(弱引用,不保活);重复登记 + /// (同 type + callback + capture)由调用方保证不发生。 + /// # 副作用 + /// - 写注册表(内部可变);`entry` 的 callback 是 GC 对象边,mark 期经根枚举标活。 + fn et_register(&self, target: *mut JsObject, entry: ListenerEntry); + /// 从目标对象的注册表移除同 type + callback + capture 的监听器条目; + /// 不存在时 no-op。空集合条目随移除删除(不留空壳)。 + /// + /// # 副作用 + /// - 写注册表(内部可变);幂等,重复移除不报错。 + fn et_unregister(&self, target: *mut JsObject, type_si: u32, callback: JsValue, capture: bool); + /// 查目标对象的监听器条目列表(返回克隆;无条目为空列表)。 + /// + /// # 边界与前提 + /// - 列表元素含 callback 的 GC 对象边,调用方不得跨用户调用持有 + /// (GC 可回收目标对象,须在下一次使用前重新查表校验)。 + fn et_lookup(&self, target: *mut JsObject) -> Vec; } diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 6c633274..dd8b9d10 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -14,6 +14,8 @@ mod coercion; /// 动态编译服务接口(`Vm` 经它解耦对编译器的直接依赖)。 pub mod compiler_service; +/// EventTarget 监听器注册表数据类型(`ListenerEntry` / `EventTargetState`)。 +pub mod event_target; /// builtins 依赖的 `VmHost` 能力面。 mod host; /// builtin native 函数的三态返回值。 @@ -29,5 +31,6 @@ pub use coercion::{ ToPrimitiveHint, }; pub use compiler_service::CompilerService; +pub use event_target::{EventTargetState, ListenerEntry}; pub use host::{ProtoKind, ShapeNode, VmHost}; pub use native_result::NativeResult; diff --git a/crates/oxide_vm/src/bindings/bind_broadcast_channel.rs b/crates/oxide_vm/src/bindings/bind_broadcast_channel.rs index 8a892256..ca3cf392 100644 --- a/crates/oxide_vm/src/bindings/bind_broadcast_channel.rs +++ b/crates/oxide_vm/src/bindings/bind_broadcast_channel.rs @@ -85,9 +85,9 @@ pub fn bind_broadcast_channel(core: &Arc, session: &KernelSession, g proto.bump_generation(); } - // [[Prototype]] → Object.prototype(幂等,裸写槽)。 - let object_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); - let _ = proto.set_proto(object_proto_val); + // [[Prototype]] → EventTarget.prototype(事件三方法经原型链可达;幂等,裸写槽)。 + let et_proto_val = JsValue::from_js_object(world.event_target_proto.as_ptr() as *mut JsObject); + let _ = proto.set_proto(et_proto_val); // 原型方法:postMessage / close(每方法先 lookup_position 守卫)。 let si_post_message = sf.intern("postMessage").0; diff --git a/crates/oxide_vm/src/bindings/bind_event.rs b/crates/oxide_vm/src/bindings/bind_event.rs index f0d76917..b3d5db8a 100644 --- a/crates/oxide_vm/src/bindings/bind_event.rs +++ b/crates/oxide_vm/src/bindings/bind_event.rs @@ -162,4 +162,39 @@ pub fn bind_event(core: &Arc, session: &KernelSession, global: &mut bind_accessor_getter(core, session, proto, name, fn_ptr); } } + + // ---- 填 EventTarget 原型占位(无构造器,仅暴露原型与三方法)---- + // 幂等:三方法经 lookup_position 守卫,[[Prototype]] 裸写槽幂等。 + let et_proto_ptr = world.event_target_proto.as_ptr() as *mut JsObject; + let et_proto = unsafe { &mut *et_proto_ptr }; + // [[Prototype]] → Object.prototype(幂等,裸写槽)。 + let object_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); + let _ = et_proto.set_proto(object_proto_val); + // 三方法:addEventListener / removeEventListener / dispatchEvent(每方法先 + // lookup_position 守卫)。 + let si_add = sf.intern("addEventListener").0; + if sh.lookup_position(et_proto.shape_id(), si_add).is_none() { + apply_binding_table( + world, + et_proto, + core, + &[ + ( + "addEventListener", + oxide_builtins::event_target::event_target_add_event_listener:: as *const (), + 3, + ), + ( + "removeEventListener", + oxide_builtins::event_target::event_target_remove_event_listener:: as *const (), + 3, + ), + ( + "dispatchEvent", + oxide_builtins::event_target::event_target_dispatch_event:: as *const (), + 1, + ), + ], + ); + } } diff --git a/crates/oxide_vm/src/bindings/bind_message_channel.rs b/crates/oxide_vm/src/bindings/bind_message_channel.rs index 56f79c47..b9a38d70 100644 --- a/crates/oxide_vm/src/bindings/bind_message_channel.rs +++ b/crates/oxide_vm/src/bindings/bind_message_channel.rs @@ -85,9 +85,9 @@ pub fn bind_message_channel(core: &Arc, session: &KernelSession, glo proto.bump_generation(); } - // [[Prototype]] → Object.prototype(幂等,裸写槽)。 - let object_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject); - let _ = proto.set_proto(object_proto_val); + // [[Prototype]] → EventTarget.prototype(事件三方法经原型链可达;幂等,裸写槽)。 + let et_proto_val = JsValue::from_js_object(world.event_target_proto.as_ptr() as *mut JsObject); + let _ = proto.set_proto(et_proto_val); // 原型方法:postMessage / close / start(每方法先 lookup_position 守卫)。 let si_post_message = sf.intern("postMessage").0; diff --git a/crates/oxide_vm/src/bindings/bind_worker.rs b/crates/oxide_vm/src/bindings/bind_worker.rs index 680852e3..30975ca2 100644 --- a/crates/oxide_vm/src/bindings/bind_worker.rs +++ b/crates/oxide_vm/src/bindings/bind_worker.rs @@ -75,8 +75,10 @@ pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut match sh.lookup_position(ctor.shape_id(), si_prototype) { Some(pos) if ctor.get_prop_at(pos).is_object() => ctor.get_prop_at(pos).as_js_object_ptr(), _ => { - let object_proto = world.object_proto.as_ptr() as *mut JsObject; - let proto = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); + // Worker.prototype 的 [[Prototype]] 指向 EventTarget.prototype + // (事件三方法经原型链可达;EventTarget 无构造器,仅暴露原型)。 + let et_proto = world.event_target_proto.as_ptr() as *mut JsObject; + let proto = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(et_proto)); let ptr = Box::into_raw(Box::new(proto)); world.track_leaked_object(ptr); // 回填构造器 prototype 槽(描述符 { f,f,f })。 @@ -145,6 +147,33 @@ pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut ], ); } + // EventTarget 三方法直绑全局(worker realm 经 self === global 可达;全局原型链 + // 是 Object.prototype,首版不改全局原型,三方法直绑全局等价可见)。幂等守卫。 + let si_self_add = sf.intern("addEventListener").0; + if sh.lookup_position(global.shape_id(), si_self_add).is_none() { + apply_binding_table( + world, + global, + core, + &[ + ( + "addEventListener", + oxide_builtins::event_target::event_target_add_event_listener:: as *const (), + 3, + ), + ( + "removeEventListener", + oxide_builtins::event_target::event_target_remove_event_listener:: as *const (), + 3, + ), + ( + "dispatchEvent", + oxide_builtins::event_target::event_target_dispatch_event:: as *const (), + 1, + ), + ], + ); + } // self 全局属性:值为 global 对象本身(worker realm 经 self === global 可达, // self.onmessage / self.postMessage 由此成立)。幂等守卫。 let si_self = sf.intern("self").0; diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index f99bcdaa..d4f98db4 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -706,6 +706,21 @@ impl SessionGc { .broadcast_channels .retain(|_, channels| !channels.is_empty()); + // EventTarget 监听器注册表剪枝:按 mark 位移除死目标对象(弱键,同 + // broadcast_channels 时序——死目标在随后的分流循环被释放,注册表已先 + // 移除其条目,无悬垂)。 + vm.realm + .gc + .borrow_mut() + .event_targets + .retain(|&ptr, _| { + if ptr.is_null() { + return false; + } + // SAFETY: ptr 来自 session 对象表登记,sweep 运行期间有效。 + unsafe { (*ptr).is_gc_marked() } + }); + // 按 mark 位分流:存活保留清位,死对象释放本体、堆区与 upvalue 出表。 let mut dead = 0u64; let mut freed_bytes = 0u64; diff --git a/crates/oxide_vm/src/vm/gc_hooks.rs b/crates/oxide_vm/src/vm/gc_hooks.rs index ed4e61b0..921bdd23 100644 --- a/crates/oxide_vm/src/vm/gc_hooks.rs +++ b/crates/oxide_vm/src/vm/gc_hooks.rs @@ -42,11 +42,12 @@ pub(crate) enum RootGroup { Global, WorkerObjects, DisposeStack, + EventTargets, } impl RootGroup { /// 根组总数(与变体数同源,绑定逐组计数数组的长度)。 - pub const COUNT: usize = 27; + pub const COUNT: usize = 28; } impl Vm { @@ -180,6 +181,14 @@ impl Vm { for &worker_obj in self.worker_objects.values() { f(RootGroup::WorkerObjects, worker_obj); } + // EventTarget 监听器注册表:监听器回调是 GC 根(存活目标的回调随目标存活, + // 漏根 → 回调被 sweep 释放 → 派发解引用悬垂)。弱键剪枝见 sweep;此处 + // 无条件标活全部回调(死目标的回调多活一轮收集,非泄漏,sweep 已剪其条目)。 + for state in self.realm.gc.borrow().event_targets.values() { + for entry in &state.listeners { + f(RootGroup::EventTargets, entry.callback); + } + } } /// GC 根统一枚举入口:遍历的字段清单与 `for_each_value` 相同。 diff --git a/crates/oxide_vm/src/vm/vmhost.rs b/crates/oxide_vm/src/vm/vmhost.rs index cb4b1e87..bf5588c9 100644 --- a/crates/oxide_vm/src/vm/vmhost.rs +++ b/crates/oxide_vm/src/vm/vmhost.rs @@ -4,7 +4,7 @@ use std::ffi::c_void; use std::sync::Arc; use oxide_kernel::KernelCore; -use oxide_runtime_api::{ProtoKind, ShapeNode}; +use oxide_runtime_api::{ListenerEntry, ProtoKind, ShapeNode}; use oxide_types::mem::P; use oxide_types::object::{Cell, JsObject, PropAttributes}; use oxide_types::value::JsValue; @@ -449,4 +449,27 @@ impl oxide_runtime_api::VmHost for Vm { fn bc_lookup(&self, name: &str) -> Vec<*mut JsObject> { self.realm.gc.borrow().broadcast_channels.get(name).cloned().unwrap_or_default() } + fn et_register(&self, target: *mut JsObject, entry: ListenerEntry) { + self.realm + .gc + .borrow_mut() + .event_targets + .entry(target) + .or_default() + .listeners + .push(entry); + } + fn et_unregister(&self, target: *mut JsObject, type_si: u32, callback: JsValue, capture: bool) { + let mut gc = self.realm.gc.borrow_mut(); + let should_remove = gc.event_targets.get_mut(&target).is_some_and(|state| { + state.listeners.retain(|e| !(e.type_si == type_si && e.capture == capture && e.callback == callback)); + state.listeners.is_empty() + }); + if should_remove { + gc.event_targets.remove(&target); + } + } + fn et_lookup(&self, target: *mut JsObject) -> Vec { + self.realm.gc.borrow().event_targets.get(&target).map(|s| s.listeners.clone()).unwrap_or_default() + } } diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index ad99316b..afb898a2 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -14,6 +14,7 @@ use std::collections::HashMap; use crate::session_gc::SessionGc; use crate::vm::ForInIter; use oxide_builtins::iterator::BuiltinIterKind; +use oxide_runtime_api::EventTargetState; use oxide_types::object::{Cell as UpvalueCell, JsObject, JsString}; use oxide_types::private_key::WELL_KNOWN_SYMBOL_COUNT; use oxide_types::value::JsValue; @@ -65,6 +66,11 @@ pub(crate) struct GcState { /// (弱引用,不保活)。构造器分配后登记、close 注销、GC sweep 按 mark 位 /// 剪枝、full_reset 随 session 对象一并清表。 pub(crate) broadcast_channels: HashMap>, + /// EventTarget per-realm 弱引用监听器注册表:目标对象裸指针 → 监听器集合 + /// (弱键,不保活)。addEventListener 登记、removeEventListener 注销、GC + /// sweep 按 mark 位剪枝、full_reset 随 session 对象一并清表。值持监听器 + /// 回调的 GC 对象边,mark 期经根枚举标活(存活目标的回调随目标存活)。 + pub(crate) event_targets: HashMap<*mut JsObject, EventTargetState>, } impl GcState { diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index fdf1ca49..f64cd800 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -103,6 +103,7 @@ impl Vm { pending_forced_collect: false, gc_pressure_mode: gc_pressure_mode_from_env(), broadcast_channels: std::collections::HashMap::new(), + event_targets: std::collections::HashMap::new(), }), symbols: RefCell::new(SymbolState { symbol_descriptions: Vec::new(), @@ -269,6 +270,7 @@ impl Vm { pending_forced_collect: false, gc_pressure_mode: gc_pressure_mode_from_env(), broadcast_channels: std::collections::HashMap::new(), + event_targets: std::collections::HashMap::new(), }), symbols: RefCell::new(SymbolState { symbol_descriptions: Vec::new(), @@ -490,6 +492,9 @@ impl Vm { // BroadcastChannel 注册表持 session 对象裸指针:teardown 释放全部 // session 对象前须清表,否则残留悬垂指针。 self.realm.gc.borrow_mut().broadcast_channels.clear(); + // EventTarget 监听器注册表同持 session 对象裸指针(弱键):teardown 释放 + // 全部 session 对象前须清表,否则残留悬垂指针。 + self.realm.gc.borrow_mut().event_targets.clear(); // Worker 对象注册表持 session 对象指针:teardown 释放全部 session 对象 // 前须清表,否则残留悬垂指针。 self.worker_objects.clear(); diff --git a/crates/oxide_vm/src/worker/bindings.rs b/crates/oxide_vm/src/worker/bindings.rs index 1cb3cbe3..608e29ea 100644 --- a/crates/oxide_vm/src/worker/bindings.rs +++ b/crates/oxide_vm/src/worker/bindings.rs @@ -287,42 +287,43 @@ pub(crate) fn self_location(vm: &mut Vm, _args: &[u8]) -> NativeResult { NativeResult::Ok(vm.new_string("")) } -/// 事件构造辅助:建事件对象(`data` + `type` + `message` 属性)。 +/// 事件构造辅助:建真实 Event 盒(`type` 字段)加 `data` / `message` 普通属性。 /// /// 消息事件与错误事件同型,共用本辅助:消息事件 `message` 为 undefined, /// 错误事件 `data` 为 undefined、`message` 为错误串。 /// /// # 步骤 -/// 1. 建 PLAIN 对象(`[[Prototype]]` → `%Object.prototype%`)。 -/// 2. define `data` 属性(rehydrate 后的消息值,可枚举)。 -/// 3. define `type` 属性(事件类型串,可枚举)。 -/// 4. define `message` 属性(错误消息串,可枚举;消息事件为 undefined)。 -/// 5. 返回对象。 +/// 1. 经 `create_event_box` 建真实 Event 盒(`type` 取 `type_val` 的字符串形态, +/// `[[Prototype]]` → `Event.prototype`),使 `dispatchEvent` 品牌守卫通过。 +/// 2. define `data` 属性(rehydrate 后的消息值,可枚举;GC 边)。 +/// 3. `message` 非 undefined 时 define `message` 属性(错误消息串,可枚举)。 +/// 4. 返回对象。 /// /// # 边界与前提 -/// - `data` 是 rehydrate 后的消息值(GC 边,经 `alloc_object` 入对象表即成根)。 +/// - `data` 是 rehydrate 后的消息值(GC 边,经属性区入 mark)。 /// - `type` 是事件类型串(`"message"`/`"error"`/`"messageerror"`)。 /// - `message` 是错误消息串(仅错误事件定义,消息事件为 undefined)。 +/// - Event 盒无 `data` 字段(8.4 的 MessageEvent 才补),`data` 作盒对象普通属性。 /// /// # 副作用 -/// - 新建一个事件对象(经 `alloc_object` 入对象表)。 +/// - 新建一个 Event 盒对象(经 `alloc_object` 入对象表)。 pub(crate) fn message_event_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let data_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let type_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let message_val = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() }; - let object_proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::ObjectProto)); - let obj = JsObject::new_empty(EMPTY_SHAPE_ID, object_proto); - let ptr = vm.alloc_object(obj); - // SAFETY: ptr 是本函数刚 alloc_object 的对象,存活且本段无别名。 + // 建真实 Event 盒(type 取 type_val 的字符串形态)。 + let type_text = vm.lookup_str(type_val).unwrap_or_default(); + let ptr = oxide_builtins::event::create_event_box(vm, &type_text); + // SAFETY: ptr 是 create_event_box 刚 alloc_object 的对象,存活且本段无别名。 let event_obj = unsafe { &mut *ptr }; let si_data = vm.perm_intern("data"); let _ = vm.define_data_property(event_obj, si_data, data_val, PropAttributes::DEFAULT_DATA); - let si_type = vm.perm_intern("type"); - let _ = vm.define_data_property(event_obj, si_type, type_val, PropAttributes::DEFAULT_DATA); - let si_message = vm.perm_intern("message"); - let _ = vm.define_data_property(event_obj, si_message, message_val, PropAttributes::DEFAULT_DATA); + if !message_val.is_undefined() { + let si_message = vm.perm_intern("message"); + let _ = vm.define_data_property(event_obj, si_message, message_val, PropAttributes::DEFAULT_DATA); + } NativeResult::Ok(JsValue::from_js_object(ptr)) } diff --git a/crates/oxide_vm/src/worker/mod.rs b/crates/oxide_vm/src/worker/mod.rs index acb8a469..1ab38a05 100644 --- a/crates/oxide_vm/src/worker/mod.rs +++ b/crates/oxide_vm/src/worker/mod.rs @@ -20,7 +20,7 @@ use std::time::Duration; use oxide_builtins::message_value::{rehydrate_message, MessageValue}; use oxide_kernel::kernel::KernelCore; use oxide_kernel::message_queue::{channel, Receiver, Sender, Timeout}; -use oxide_runtime_api::{CompilerService, NativeResult, VmHost}; +use oxide_runtime_api::{CompilerService, NativeResult}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; @@ -345,30 +345,23 @@ impl Vm { let Some(worker_obj) = self.worker_object(id) else { continue; }; - // 错误面:逐条交付 onerror(编译 / 执行失败)。 + // 错误面:逐条经 dispatchEvent 交付 error 事件(编译 / 执行失败)。 for err in &poll.errors { if self.worker_object(id).is_none() { break; } - self.deliver_error_event(worker_obj, "onerror", "error", err); + self.deliver_error_event(worker_obj, "error", err); } - // 消息交付失败面:逐条交付 onmessageerror(当前架构不可达,前向钩子)。 + // 消息交付失败面:逐条经 dispatchEvent 交付 messageerror 事件 + // (当前架构不可达,前向钩子)。 for msg_err in &poll.message_errors { if self.worker_object(id).is_none() { break; } - self.deliver_error_event(worker_obj, "onmessageerror", "messageerror", msg_err); - } - // 数据面:读 onmessage 属性(undefined 或非可调用即静默跳过)。 - let si_onmessage = self.perm_intern("onmessage"); - let handler = match self.ordinary_get(unsafe { &*worker_obj.as_js_object_ptr() }, si_onmessage, worker_obj) - { - Ok(h) => h, - Err(_) => continue, - }; - if !is_callable(handler) { - continue; + self.deliver_error_event(worker_obj, "messageerror", msg_err); } + // 数据面:经 dispatchEvent 交付(注册表查监听器,addEventListener 与 + // 属性 handler 同权;首版属性 handler 尚不支持,8.7 完成)。 for data in poll.messages { // 交付前二次校验存活(GC 防护):worker 在事件循环中被终止 // (注册表条目移除)即停止交付。 @@ -376,40 +369,46 @@ impl Vm { break; } let event = self.build_message_event(data, "message", None); - let _ = self.execute_task(|vm| vm.call_function_sync(handler, worker_obj, &[event])); + self.dispatch_event_on(worker_obj, event); self.drain_microtasks(); } } any } - /// 交付错误事件:读 handler 属性、建事件、调用(不可调用时静默)。 + /// 经 `dispatchEvent` 交付事件到目标对象(注册表查监听器,按登记序调用)。 /// /// # 步骤 - /// 1. 读 `worker_obj` 的 `handler_name` 属性。 - /// 2. 不可调用时静默返回(浏览器语义:无 handler 即丢弃)。 - /// 3. 建错误事件(`type` = `event_type`、`message` = `message`)。 - /// 4. 经 `execute_task` + `call_function_sync` 触发,后 `drain_microtasks`。 + /// 1. 目标与事件钉入寄存器 0 / 1(GC 根,防交付窗口内被回收)。 + /// 2. 调 `event_target_dispatch_event`(this = 目标,实参 = 事件)。 /// /// # 边界与前提 - /// - `handler_name` 是 `onerror` 或 `onmessageerror`。 - /// - `event_type` 是 `"error"` 或 `"messageerror"`。 - /// - `worker_obj` 与 handler 均为 `JsValue`(Copy),在 `execute_task` 前提取, - /// 不跨 `&mut self` 持引用。 + /// - 监听器回调的异常原值上抛(不吞);无监听器时 no-op(返回 true)。 /// /// # 副作用 - /// - 触发 handler 回调与微任务 drain。 - fn deliver_error_event(&mut self, worker_obj: JsValue, handler_name: &str, event_type: &str, message: &str) { - let si_handler = self.perm_intern(handler_name); - let handler = match self.ordinary_get(unsafe { &*worker_obj.as_js_object_ptr() }, si_handler, worker_obj) { - Ok(h) => h, - Err(_) => return, - }; - if !is_callable(handler) { - return; - } + /// - 触发监听器回调与 `once` 条目移除;写事件载荷盒(target / 阶段)。 + fn dispatch_event_on(&mut self, target: JsValue, event: JsValue) { + self.set_reg(0, target); + self.set_reg(1, event); + let _ = oxide_builtins::event_target::event_target_dispatch_event(self, &[0, 1]); + } + + /// 交付错误事件:建事件、经 `dispatchEvent` 交付(注册表查监听器)。 + /// + /// # 步骤 + /// 1. 建错误事件(`type` = `event_type`、`message` = `message`、`data` = undefined)。 + /// 2. 经 `dispatch_event_on` 交付到 `worker_obj`,后 `drain_microtasks`。 + /// + /// # 边界与前提 + /// - `event_type` 是 `"error"` 或 `"messageerror"`。 + /// - `worker_obj` 是 `JsValue`(Copy),不跨 `&mut self` 持引用。 + /// - 无监听器时 `dispatchEvent` 返回 true(no-op)。 + /// + /// # 副作用 + /// - 触发监听器回调与微任务 drain。 + fn deliver_error_event(&mut self, worker_obj: JsValue, event_type: &str, message: &str) { let event = self.build_message_event(JsValue::undefined(), event_type, Some(message)); - let _ = self.execute_task(|vm| vm.call_function_sync(handler, worker_obj, &[event])); + self.dispatch_event_on(worker_obj, event); self.drain_microtasks(); } @@ -463,21 +462,20 @@ impl Vm { } } - /// worker 侧消息交付:rehydrate 进 worker realm、建 MessageEvent、交付到 - /// `self.onmessage`(worker global 的 `onmessage` 属性)。 + /// worker 侧消息交付:rehydrate 进 worker realm、建 MessageEvent、经 + /// `dispatchEvent` 交付到 worker global(注册表查监听器)。 /// /// # 步骤 /// 1. rehydrate 消息值进 worker realm。 /// 2. 建 MessageEvent(data + type "message")。 - /// 3. 读 worker global 的 `onmessage` 属性。 - /// 4. 可调用时经 `execute_task` + `call_function_sync` 触发,后 `drain_microtasks`。 + /// 3. 取 worker global 对象,经 `dispatch_event_on` 交付,后 `drain_microtasks`。 /// /// # 边界与前提 - /// - `onmessage` 缺失或非可调用时静默跳过(消息已消费,符合浏览器 + /// - 无监听器时 `dispatchEvent` 返回 true(消息已消费,符合浏览器 /// "无 handler 即丢弃"语义)。 /// /// # 副作用 - /// - 触发 `self.onmessage` 回调与微任务 drain。 + /// - 触发监听器回调与微任务 drain。 pub(crate) fn deliver_self_message(&mut self, value: MessageValue) { let data = rehydrate_message(self, &value); let event = self.build_message_event(data, "message", None); @@ -487,29 +485,13 @@ impl Vm { session.global_object().as_ptr() as *mut JsObject }; // SAFETY: global_ptr 是当前 session 的 global 对象,存活。 - let global_obj = unsafe { &*global_ptr }; - let si_onmessage = self.perm_intern("onmessage"); - let handler = match self.ordinary_get(global_obj, si_onmessage, JsValue::from_js_object(global_ptr)) { - Ok(h) => h, - Err(_) => return, - }; - if !is_callable(handler) { - return; - } - let _ = self.execute_task(|vm| vm.call_function_sync(handler, JsValue::from_js_object(global_ptr), &[event])); + let global_val = JsValue::from_js_object(global_ptr); + // 经 dispatchEvent 交付(注册表查监听器,属性 handler 与 addEventListener 同权)。 + self.dispatch_event_on(global_val, event); self.drain_microtasks(); } } -/// 判定值是否可调用(对象且函数对象,header bit 31)。 -/// -/// # 边界与前提 -/// - 非对象值恒不可调用。 -/// - 对象经 `as_js_object_ptr` 解引用读函数位(unsafe,惯例形态)。 -fn is_callable(val: JsValue) -> bool { - val.is_object() && unsafe { &*val.as_js_object_ptr() }.is_function() -} - #[cfg(test)] mod tests { use super::*; @@ -545,9 +527,9 @@ mod tests { fn worker_round_trip() { let mut vm = vm_with_compiler(); - // worker 脚本设 onmessage 回显(onmessage 语义替换早期回显语义)。 + // worker 脚本设 addEventListener 回显(注册表交付替换早期 onmessage 属性语义)。 let id = vm - .spawn_worker("self.onmessage = function(e) { self.postMessage(e.data); };") + .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });") .expect("worker 应派生成功"); assert_eq!(vm.active_workers(), vec![id], "应有唯一活跃 worker"); @@ -581,12 +563,12 @@ mod tests { fn multiple_workers_isolated() { let mut vm = vm_with_compiler(); - // 两 worker 各设 onmessage 回显(onmessage 语义)。 + // 两 worker 各设 addEventListener 回显(注册表交付语义)。 let id_a = vm - .spawn_worker("self.onmessage = function(e) { self.postMessage(e.data); };") + .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });") .expect("worker A 应派生成功"); let id_b = vm - .spawn_worker("self.onmessage = function(e) { self.postMessage(e.data); };") + .spawn_worker("self.addEventListener('message', function(e) { self.postMessage(e.data); });") .expect("worker B 应派生成功"); assert_eq!(vm.active_workers(), vec![id_a, id_b], "应有两个活跃 worker"); @@ -647,10 +629,11 @@ mod tests { let mut vm = vm_with_compiler(); // worker 脚本:收到消息翻倍后回发。 - let worker_path = write_worker_script("self.onmessage = function(e) { self.postMessage(e.data * 2); };"); - // 主脚本:建 Worker、设 onmessage、投递 21。 + let worker_path = + write_worker_script("self.addEventListener('message', function(e) { self.postMessage(e.data * 2); });"); + // 主脚本:建 Worker、设 addEventListener、投递 21。 let main_script = format!( - "var w = new Worker('{}'); w.onmessage = function(e) {{ globalThis.received = e.data; }}; w.postMessage(21);", + "var w = new Worker('{}'); w.addEventListener('message', function(e) {{ globalThis.received = e.data; }}); w.postMessage(21);", worker_path.display() ); vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); @@ -750,9 +733,9 @@ mod tests { // worker 脚本:语法错误(编译失败)。 let worker_path = write_worker_script("function { 语法错误"); - // 主脚本:建 Worker、设 onerror,记录 e.message 与 e.type 是否为 "error"。 + // 主脚本:建 Worker、设 addEventListener('error'),记录 e.message 与 e.type 是否为 "error"。 let main_script = format!( - "var w = new Worker('{}'); w.onerror = function(e) {{ globalThis.err = e.message; globalThis.errIsErrorType = (e.type === \"error\"); }};", + "var w = new Worker('{}'); w.addEventListener('error', function(e) {{ globalThis.err = e.message; globalThis.errIsErrorType = (e.type === \"error\"); }});", worker_path.display() ); vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); @@ -795,9 +778,9 @@ mod tests { // worker 脚本:顶层 throw(执行失败)。 let worker_path = write_worker_script("throw new Error('boom');"); - // 主脚本:建 Worker、设 onerror,记录 e.message。 + // 主脚本:建 Worker、设 addEventListener('error'),记录 e.message。 let main_script = format!( - "var w = new Worker('{}'); w.onerror = function(e) {{ globalThis.err = e.message; }};", + "var w = new Worker('{}'); w.addEventListener('error', function(e) {{ globalThis.err = e.message; }});", worker_path.display() ); vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); @@ -832,17 +815,17 @@ mod tests { fn onmessageerror_delivery_helper() { let mut vm = vm_with_compiler(); - // 建 Worker 对象、设 onmessageerror handler(记录 e.message 与 e.type)。 + // 建 Worker 对象、设 addEventListener('messageerror')(记录 e.message 与 e.type)。 let worker_path = write_worker_script("1"); let main_script = format!( - "var w = new Worker('{}'); w.onmessageerror = function(e) {{ globalThis.me = e.message; globalThis.meIsMessageErrorType = (e.type === \"messageerror\"); }}; globalThis.w = w;", + "var w = new Worker('{}'); w.addEventListener('messageerror', function(e) {{ globalThis.me = e.message; globalThis.meIsMessageErrorType = (e.type === \"messageerror\"); }}); globalThis.w = w;", worker_path.display() ); vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); - // 取 Worker 对象,直接调交付辅助(模拟 rehydrate 失败)。 + // 取 Worker 对象,直接调交付辅助(模拟 rehydrate 失败,经 dispatchEvent 交付)。 let worker_obj = global_value_opt(&vm, "w").expect("w 应被设置"); - vm.deliver_error_event(worker_obj, "onmessageerror", "messageerror", "合成 rehydrate 失败"); + vm.deliver_error_event(worker_obj, "messageerror", "合成 rehydrate 失败"); // 断言 handler 被调用、message 为错误串、type 为 "messageerror"。 let me = global_value_opt(&vm, "me").expect("me 应被设置"); diff --git a/crates/oxide_vm/tests/event_target_tests.rs b/crates/oxide_vm/tests/event_target_tests.rs new file mode 100644 index 00000000..8bea8508 --- /dev/null +++ b/crates/oxide_vm/tests/event_target_tests.rs @@ -0,0 +1,263 @@ +//! EventTarget 验收测试:addEventListener / removeEventListener / dispatchEvent +//! 三方法、原型链(Worker / MessagePort / BroadcastChannel 经 EventTarget.prototype)、 +//! GC 剪枝。 +//! +//! 集成测试走公共 API(`eval` 返回脚本完成值字符串),不访问私有字段。 +//! 目标对象一律用 MessagePort(EventTarget 实例)——普通对象原型链上没有 +//! EventTarget.prototype,DOM 语义下 addEventListener 也只存在于 EventTarget 实例。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_compiler::DefaultCompilerService; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +/// 编译并运行脚本,返回完成值字符串(解析 / 编译 / 运行失败时返回错误描述)。 +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + vm.set_compiler_service(Arc::new(DefaultCompilerService)); + match vm.run(&Arc::new(module)) { + // 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。 + Ok(result) => vm.lookup_str(result).unwrap_or_else(|| format!("{result}")), + Err(e) => format!("vm error: {e}"), + } +} + +/// 建一个注入编译服务的 VM 并运行首段脚本(供 GC 测试中途收集)。 +fn vm_with_script(source: &str) -> Vm { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse 应成功"); + let module = Compiler::new().compile(&program).expect("compile 应成功"); + let mut vm = Vm::new(); + vm.set_compiler_service(Arc::new(DefaultCompilerService)); + vm.run(&Arc::new(module)).expect("run 应成功"); + vm +} + +/// 在既有 VM 上运行一段脚本,返回完成值字符串。 +fn vm_eval(vm: &mut Vm, source: &str) -> String { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse 应成功"); + let module = Compiler::new().compile(&program).expect("compile 应成功"); + match vm.run(&Arc::new(module)) { + // 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。 + Ok(result) => vm.lookup_str(result).unwrap_or_else(|| format!("{result}")), + Err(e) => format!("vm error: {e}"), + } +} + +/// 建一个 MessagePort 目标的前缀(每个测试目标独立,避免跨测试串扰)。 +const TARGET: &str = "var mc = new MessageChannel(); var t = mc.port1;"; + +/// dispatchEvent(new Event('x')) 返回 true(无 preventDefault)。 +#[test] +fn dispatch_event_returns_true() { + assert_eq!( + eval(&format!("{TARGET} t.dispatchEvent(new Event('x'));")), + "true" + ); +} + +/// preventDefault() 后 dispatchEvent 返回 false(cancelable 事件)。 +#[test] +fn dispatch_event_returns_false_after_prevent_default() { + assert_eq!( + eval( + &format!( + "{TARGET} t.addEventListener('x', function(e) {{ e.preventDefault(); }}); \ + t.dispatchEvent(new Event('x', {{ cancelable: true }}));" + ) + ), + "false" + ); +} + +/// 非 cancelable 事件 preventDefault 无效,dispatchEvent 仍返回 true。 +#[test] +fn dispatch_event_non_cancelable_stays_true() { + assert_eq!( + eval( + &format!( + "{TARGET} t.addEventListener('x', function(e) {{ e.preventDefault(); }}); \ + t.dispatchEvent(new Event('x'));" + ) + ), + "true" + ); +} + +/// removeEventListener 生效:移除后派发不再调用监听器。 +#[test] +fn remove_event_listener_works() { + assert_eq!( + eval( + &format!( + "{TARGET} var calls = 0; \ + var fn = function(e) {{ calls++; }}; \ + t.addEventListener('x', fn); \ + t.removeEventListener('x', fn); \ + t.dispatchEvent(new Event('x')); \ + calls;" + ) + ), + "0" + ); +} + +/// 重复登记(同 type + callback + capture)为 no-op:派发只调用一次。 +#[test] +fn duplicate_add_event_listener_is_noop() { + assert_eq!( + eval( + &format!( + "{TARGET} var calls = 0; \ + var fn = function(e) {{ calls++; }}; \ + t.addEventListener('x', fn); \ + t.addEventListener('x', fn); \ + t.dispatchEvent(new Event('x')); \ + calls;" + ) + ), + "1" + ); +} + +/// once 选项:监听器调用一次后自动移除。 +#[test] +fn once_listener_removed_after_call() { + assert_eq!( + eval( + &format!( + "{TARGET} var calls = 0; \ + var fn = function(e) {{ calls++; }}; \ + t.addEventListener('x', fn, {{ once: true }}); \ + t.dispatchEvent(new Event('x')); \ + t.dispatchEvent(new Event('x')); \ + calls;" + ) + ), + "1" + ); +} + +/// stopImmediatePropagation 截断后续监听器。 +#[test] +fn stop_immediate_propagation_truncates() { + assert_eq!( + eval( + &format!( + "{TARGET} var calls = 0; \ + t.addEventListener('x', function(e) {{ calls++; e.stopImmediatePropagation(); }}); \ + t.addEventListener('x', function(e) {{ calls++; }}); \ + t.dispatchEvent(new Event('x')); \ + calls;" + ) + ), + "1" + ); +} + +/// 监听器回调的 this 是目标对象、实参是事件(e.type 为类型串)。 +#[test] +fn listener_receives_target_this_and_event() { + assert_eq!( + eval( + &format!( + "{TARGET} var gotThis = null; var gotType = null; \ + t.addEventListener('x', function(e) {{ gotThis = (this === t); gotType = e.type; }}); \ + t.dispatchEvent(new Event('x')); \ + gotThis + ':' + gotType;" + ) + ), + "true:x" + ); +} + +/// MessagePort 原型链经 EventTarget.prototype:port.addEventListener 生效。 +#[test] +fn port_add_event_listener_works() { + assert_eq!( + eval( + "var mc = new MessageChannel(); var calls = 0; \ + mc.port1.addEventListener('message', function(e) { calls++; }); \ + mc.port1.dispatchEvent(new Event('message')); \ + calls;" + ), + "1" + ); +} + +/// BroadcastChannel 原型链经 EventTarget.prototype:channel.addEventListener 生效。 +#[test] +fn channel_add_event_listener_works() { + assert_eq!( + eval( + "var ch = new BroadcastChannel('t'); var calls = 0; \ + ch.addEventListener('message', function(e) { calls++; }); \ + ch.dispatchEvent(new Event('message')); \ + calls;" + ), + "1" + ); +} + +/// Worker.prototype 原型链经 EventTarget.prototype:worker.addEventListener 生效。 +#[test] +fn worker_add_event_listener_works() { + let path = std::env::temp_dir().join(format!("oxide_et_{}.js", std::process::id())); + let _ = std::fs::write(&path, "1"); + let script = format!( + "var w = new Worker('{}'); var calls = 0; \ + w.addEventListener('message', function(e) {{ calls++; }}); \ + w.dispatchEvent(new Event('message')); \ + calls;", + path.display() + ); + assert_eq!(eval(&script), "1"); + let _ = std::fs::remove_file(&path); +} + +/// 监听器对象经 GC 后仍有效(目标存活时回调随目标存活,无悬垂)。 +#[test] +fn listener_survives_gc_while_target_live() { + let mut vm = vm_with_script( + "var mc = new MessageChannel(); globalThis.t = mc.port1; \ + globalThis.t.addEventListener('x', function(e) { globalThis.calls = (globalThis.calls || 0) + 1; });", + ); + // 强制完整收集:目标存活(globalThis.t),监听器回调应保活。 + vm.collect_session_gc(); + // 收集后派发:监听器仍应被调用(无悬垂)。 + assert_eq!( + vm_eval(&mut vm, "globalThis.t.dispatchEvent(new Event('x')); globalThis.calls;"), + "1" + ); +} + +/// 目标对象被回收后注册表剪枝(GC 后无悬垂指针)。 +#[test] +fn registry_pruned_after_target_collected() { + // t 是局部变量,脚本结束后无引用 → 目标死亡。 + let mut vm = vm_with_script( + "var mc = new MessageChannel(); var t = mc.port1; t.addEventListener('x', function() {});", + ); + // 强制完整收集:注册表应按 mark 位剪枝死目标条目。 + vm.collect_session_gc(); + // 剪枝后无悬垂:新建同型目标派发不触发旧监听器(旧条目已剪),无崩溃。 + assert_eq!( + vm_eval( + &mut vm, + "var mc2 = new MessageChannel(); mc2.port1.dispatchEvent(new Event('x')); 'ok';" + ), + "ok" + ); +}