From 2dc543b176ee66fcfd4d3ae05138bb0aa212c9ae Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 9 Oct 2026 04:39:22 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20BroadcastChannel=20=E8=BD=BD=E8=8D=B7?= =?UTF-8?q?=E7=9B=92=E4=B8=8E=20GC=20=E5=AE=B6=E6=97=8F=E8=A1=A8=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 38 ++--- Cargo.toml | 2 +- .../oxide_builtins/src/broadcast_channel.rs | 130 ++++++++++++++++++ crates/oxide_builtins/src/lib.rs | 2 + crates/oxide_types/src/object.rs | 9 ++ crates/oxide_vm/src/native_box_dispatch.rs | 21 ++- crates/oxide_vm/src/session_gc.rs | 5 +- 7 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 crates/oxide_builtins/src/broadcast_channel.rs diff --git a/Cargo.lock b/Cargo.lock index 01e1eb23..73c2c0f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.87" +version = "0.8.88" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.87" +version = "0.8.88" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.87" +version = "0.8.88" dependencies = [ "ansi_term", "bumpalo", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.87" +version = "0.8.88" dependencies = [ "lru", "oxide_bytecode", @@ -1235,7 +1235,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.87" +version = "0.8.88" dependencies = [ "num-bigint", "oxc_allocator", @@ -1284,7 +1284,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.87" +version = "0.8.88" dependencies = [ "bumpalo", "dashmap", @@ -1319,7 +1319,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.87" +version = "0.8.88" dependencies = [ "tracing", "tracing-appender", @@ -1340,7 +1340,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1371,7 +1371,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.87" +version = "0.8.88" dependencies = [ "num-bigint", "num-traits", @@ -1384,7 +1384,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.87" +version = "0.8.88" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1402,7 +1402,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.87" +version = "0.8.88" dependencies = [ "bumpalo", "dashmap", @@ -1415,7 +1415,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.87" +version = "0.8.88" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 783e5215..9c9db652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.8.87" +version = "0.8.88" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/broadcast_channel.rs b/crates/oxide_builtins/src/broadcast_channel.rs new file mode 100644 index 00000000..99bd9685 --- /dev/null +++ b/crates/oxide_builtins/src/broadcast_channel.rs @@ -0,0 +1,130 @@ +//! BroadcastChannel 载荷盒:mpsc 发送/接收对与通道名存于对象 `native_fn` 槽, +//! 向 GC 家族表暴露 size/drop 两自由函数。 +//! +//! 载荷盒无对象边(mpsc 非 GC 边、通道名为 Rust `String`),家族表 `ops_for` +//! 臂三边函数全 `None`,mark 链不消费本模块。 + +use std::mem; + +use oxide_kernel::message_queue; +use oxide_types::object::JsObject; + +use crate::message_value::MessageValue; + +/// BroadcastChannel 载荷盒:存于对象 `native_fn` 槽(`Box::into_raw`,realm +/// 局部单线程)。`name` 是通道名(Rust `String`,注册表键),`tx` 是本通道的 +/// mpsc 发送端(`Send`),`rx` 是配对的 mpsc 接收端(`!Send`,realm 局部), +/// `closed` 是关闭标志。`tx` 与 `rx` 是 `channel()` 配对,`tx.send(v)` 写入本 +/// 通道 `rx`。 +/// +#[expect(dead_code)] // 字段由构造器与方法消费,载荷盒当前仅建盒与释放 +struct BroadcastChannelInner { + name: String, + tx: message_queue::Sender, + rx: message_queue::Receiver, + closed: bool, +} + +/// 取载荷盒指针:仅 BroadcastChannel 标签对象返回 `Some`,其余标签返回 `None`。 +fn broadcast_channel_payload_ptr(obj: &JsObject) -> Option<*mut BroadcastChannelInner> { + if !obj.is_broadcast_channel() { + return None; + } + obj.native_fn().map(|ptr| ptr.as_ptr() as *mut BroadcastChannelInner) +} + +/// 只读核算 BroadcastChannel 载荷盒字节(不释放)。 +/// +/// mpsc 内部缓冲在 Arc 通道内、盒不可寻址,不计入(有界轻微少计,只推迟 +/// GC 触发,不影响触发正确性)。 +pub fn broadcast_channel_native_size(obj: &JsObject) -> u64 { + let Some(ptr) = broadcast_channel_payload_ptr(obj) else { + return 0; + }; + if ptr.is_null() { + return 0; + } + mem::size_of::() as u64 +} + +/// 释放 BroadcastChannel 载荷盒(`Box`),返回释放字节数。 +/// +/// 盒 drop 时发送端与接收端各减一次 mpsc 引用计数,`name` String 随盒释放。 +/// 槽置空保证重复调用零释放(幂等)。 +pub fn drop_broadcast_channel_native(obj: &mut JsObject) -> u64 { + let bytes = broadcast_channel_native_size(obj); + if bytes == 0 { + return 0; + } + let Some(ptr) = broadcast_channel_payload_ptr(obj) else { + return 0; + }; + // SAFETY: ptr 非空(broadcast_channel_native_size 已验证),Box::from_raw 恰好释放一次。 + unsafe { drop(Box::from_raw(ptr)) }; + obj.set_native_fn(None); + bytes +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; + use oxide_types::object::NativeFnPtr; + use oxide_types::value::JsValue; + + /// 构造真实通道并把双端移入载荷盒。 + fn channel_payload() -> BroadcastChannelInner { + let (tx, rx) = message_queue::channel::(); + BroadcastChannelInner { + name: "test".to_owned(), + tx, + rx, + closed: false, + } + } + + /// 手工构造带载荷盒的 BroadcastChannel 对象(不经 Vm,只验 GC 两自由函数)。 + fn channel_object_with_payload(inner: BroadcastChannelInner) -> JsObject { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()); + obj.type_tag = JsObject::OBJ_TYPE_BROADCAST_CHANNEL; + let payload_ptr = Box::into_raw(Box::new(inner)); + // SAFETY: 载荷盒形态与 native_fn 槽存储约定一致,测试结束前恰好释放一次。 + obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(payload_ptr as *const ()) })); + obj + } + + #[test] + fn size_and_drop_idempotent() { + let mut obj = channel_object_with_payload(channel_payload()); + let struct_bytes = mem::size_of::() as u64; + assert_eq!(broadcast_channel_native_size(&obj), struct_bytes); + // 首次 drop 释放结构体;槽置空后二次 drop 零释放。 + assert_eq!(drop_broadcast_channel_native(&mut obj), struct_bytes); + assert_eq!(drop_broadcast_channel_native(&mut obj), 0); + } + + #[test] + fn channel_roundtrip_through_box() { + let mut obj = channel_object_with_payload(channel_payload()); + let ptr = broadcast_channel_payload_ptr(&obj).unwrap(); + // SAFETY: 载荷盒测试期间存活。 + unsafe { + assert!(!(*ptr).closed); + (*ptr).tx.send(MessageValue::Number(1.0)).unwrap(); + assert!(matches!((*ptr).rx.try_recv().unwrap(), MessageValue::Number(n) if n == 1.0)); + } + drop_broadcast_channel_native(&mut obj); + } + + #[test] + fn payload_ptr_brand_guard() { + // 非 BroadcastChannel 类型标签(PLAIN)对象取载荷指针恒 None。 + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()); + let payload_ptr = Box::into_raw(Box::new(channel_payload())); + // SAFETY: 同 channel_object_with_payload 的载荷盒形态。 + obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(payload_ptr as *const ()) })); + assert!(broadcast_channel_payload_ptr(&obj).is_none()); + // SAFETY: 测试构造的盒,恰好释放一次。 + unsafe { drop(Box::from_raw(payload_ptr)) }; + } +} diff --git a/crates/oxide_builtins/src/lib.rs b/crates/oxide_builtins/src/lib.rs index 26c70c15..d3c7587e 100644 --- a/crates/oxide_builtins/src/lib.rs +++ b/crates/oxide_builtins/src/lib.rs @@ -18,6 +18,8 @@ pub mod atomics; pub mod bigint; /// Boolean 内置对象实现(constructor 与 prototype 的 valueOf/toString)。 pub mod boolean; +/// BroadcastChannel 载荷盒(mpsc 发送/接收对与通道名,GC size/drop 两自由函数)。 +pub mod broadcast_channel; /// builtins 层日志宏(`builtins_error`/`builtins_info` 等),target 为 `oxide::builtins`。 pub mod builtins_log; /// Console 全局对象实现(log/warn/error/info/debug/trace 方法)。 diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 44808caa..f565ee4c 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -223,6 +223,10 @@ impl JsObject { /// MessagePort 对象:mpsc 双端(发往对端 / 收自对端)与对端端口裸指针 /// 存于 `native_fn` 槽的载荷盒(`MessagePortInner`),对端端口为双向 GC 边。 pub const OBJ_TYPE_MESSAGE_PORT: u8 = 33; + /// BroadcastChannel 对象:mpsc 发送/接收对与通道名存于 `native_fn` 槽的 + /// 载荷盒(`BroadcastChannelInner`),无对象边(mpsc 非 GC 边、通道名为 + /// Rust `String`)。 + pub const OBJ_TYPE_BROADCAST_CHANNEL: u8 = 34; /// `is_session_epoch` 字段中的 session 标记位。 pub const SESSION_EPOCH_BIT: u8 = 0x01; /// `is_session_epoch` 字段中的 GC 标记位。 @@ -305,6 +309,11 @@ impl JsObject { pub fn is_message_port(&self) -> bool { self.type_tag == Self::OBJ_TYPE_MESSAGE_PORT } + /// 是否 BroadcastChannel 对象(`native_fn` 槽存 mpsc 发送/接收对与通道名)。 + #[inline] + pub fn is_broadcast_channel(&self) -> bool { + self.type_tag == Self::OBJ_TYPE_BROADCAST_CHANNEL + } /// 是否 DataView 对象。 #[inline] pub fn is_data_view_obj(&self) -> bool { diff --git a/crates/oxide_vm/src/native_box_dispatch.rs b/crates/oxide_vm/src/native_box_dispatch.rs index 6a9f0a59..fd76b59b 100644 --- a/crates/oxide_vm/src/native_box_dispatch.rs +++ b/crates/oxide_vm/src/native_box_dispatch.rs @@ -1,7 +1,7 @@ //! native 载荷家族的单点分类与每家族边函数引用(家族表)。 //! //! 分类是复合谓词:header 位(map/set/module_ns)优先于 `type_tag`, -//! 两维互斥、一个对象至多归入一个家族。tag 维显式枚举全部 0..=33, +//! 两维互斥、一个对象至多归入一个家族。tag 维显式枚举全部 0..=34, //! 未登记 tag panic。 //! //! 关键约定: @@ -16,7 +16,7 @@ use oxide_types::value::JsValue; /// native 载荷家族。 /// -/// 14 个 native 家族 + 空家族;空家族是无 native 盒类型的显式归类结果。 +/// 15 个 native 家族 + 空家族;空家族是无 native 盒类型的显式归类结果。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum NativeBoxFamily { /// 无 native 载荷盒。 @@ -51,6 +51,8 @@ pub(crate) enum NativeBoxFamily { RegExp, /// MessagePort 端口对象(tag 33,mpsc 双端与对端端口边)。 MessagePort, + /// BroadcastChannel 通道对象(tag 34,mpsc 发送/接收对与通道名,无对象边)。 + BroadcastChannel, } /// 一个家族的边操作集(每家族函数引用,单一注册面)。 @@ -83,7 +85,7 @@ pub(crate) fn classify(obj: &JsObject) -> NativeBoxFamily { /// 按类型 tag 分类 native 载荷家族。 /// /// # 边界与前提 -/// - 0..=33 全 tag 显式枚举;未登记 tag(34 及以上)panic。 +/// - 0..=34 全 tag 显式枚举;未登记 tag(35 及以上)panic。 pub(crate) fn classify_by_tag(tag: u8) -> NativeBoxFamily { match tag { JsObject::OBJ_TYPE_ARRAY_BUFFER => NativeBoxFamily::ArrayBuffer, @@ -121,6 +123,7 @@ pub(crate) fn classify_by_tag(tag: u8) -> NativeBoxFamily { JsObject::OBJ_TYPE_HTML_DDA => NativeBoxFamily::None, JsObject::OBJ_TYPE_RAW_JSON => NativeBoxFamily::None, JsObject::OBJ_TYPE_MESSAGE_PORT => NativeBoxFamily::MessagePort, + JsObject::OBJ_TYPE_BROADCAST_CHANNEL => NativeBoxFamily::BroadcastChannel, _ => panic!("unregistered type tag: {tag}"), } } @@ -211,6 +214,13 @@ pub(crate) fn ops_for(family: NativeBoxFamily) -> NativeBoxOps { string_edges: None, cell_edges: None, }, + // BroadcastChannel 载荷盒:无对象边(mpsc 非 GC 边、通道名为 Rust String), + // 三边函数全空,仅 size/drop 链消费。 + NativeBoxFamily::BroadcastChannel => NativeBoxOps { + object_edges: None, + string_edges: None, + cell_edges: None, + }, } } @@ -241,13 +251,14 @@ mod tests { JsObject::OBJ_TYPE_SHARED_ARRAY_BUFFER => NativeBoxFamily::SharedArrayBuffer, JsObject::OBJ_TYPE_REGEXP | JsObject::OBJ_TYPE_REGEX_STUB => NativeBoxFamily::RegExp, JsObject::OBJ_TYPE_MESSAGE_PORT => NativeBoxFamily::MessagePort, + JsObject::OBJ_TYPE_BROADCAST_CHANNEL => NativeBoxFamily::BroadcastChannel, _ => NativeBoxFamily::None, } } #[test] fn tag_dimension_every_tag_hits_exactly_one_family() { - for tag in 0..=33u8 { + for tag in 0..=34u8 { let obj = obj_with_tag(tag); let family = classify(&obj); assert_eq!(family, expected_family_for_tag(tag), "tag {tag} 家族不一致"); @@ -281,7 +292,7 @@ mod tests { #[test] #[should_panic(expected = "unregistered type tag")] fn unregistered_tag_panics() { - let obj = obj_with_tag(34); + let obj = obj_with_tag(35); classify(&obj); } } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 899021b7..d329a910 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -11,7 +11,8 @@ use crate::native_box_dispatch; use crate::vm::RootGroup; use crate::vm::Vm; use oxide_builtins::{ - array_buffer, data_view, disposable_stack, map, message_channel, module, regexp, set, typed_array, weak_map, + array_buffer, broadcast_channel, data_view, disposable_stack, map, message_channel, module, regexp, set, + typed_array, weak_map, }; /// session 级 mark-sweep GC 的状态与统计。 @@ -130,6 +131,7 @@ impl SessionGc { bytes += crate::async_generator::async_generator_native_size(obj); bytes += weak_map::weak_map_native_size(obj); bytes += message_channel::message_port_native_size(obj); + bytes += broadcast_channel::broadcast_channel_native_size(obj); bytes } @@ -570,6 +572,7 @@ impl SessionGc { freed_bytes += crate::async_generator::drop_async_generator_native(obj); freed_bytes += weak_map::drop_weak_map_native(obj); freed_bytes += message_channel::drop_message_port_native(obj); + freed_bytes += broadcast_channel::drop_broadcast_channel_native(obj); freed_bytes }