feat: MessageChannel 构造器与 MessagePort 方法绑定及全局槽

This commit is contained in:
2026-10-09 04:07:22 +00:00
parent 10628928bc
commit a84f430739
5 changed files with 816 additions and 27 deletions
Generated
+19 -19
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "oxide_builtins"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"chrono",
"fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]]
name = "oxide_bytecode"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"num-bigint",
"oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "oxide_cfg"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "oxide_cli"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"ansi_term",
"bumpalo",
@@ -1226,7 +1226,7 @@ dependencies = [
[[package]]
name = "oxide_code_cache"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"lru",
"oxide_bytecode",
@@ -1235,7 +1235,7 @@ dependencies = [
[[package]]
name = "oxide_compiler"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1255,7 +1255,7 @@ dependencies = [
[[package]]
name = "oxide_dce"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1269,7 +1269,7 @@ dependencies = [
[[package]]
name = "oxide_emit"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"num-bigint",
"oxc_allocator",
@@ -1284,7 +1284,7 @@ dependencies = [
[[package]]
name = "oxide_engine"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1296,7 +1296,7 @@ dependencies = [
[[package]]
name = "oxide_ir"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_log",
@@ -1305,7 +1305,7 @@ dependencies = [
[[package]]
name = "oxide_kernel"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"bumpalo",
"dashmap",
@@ -1319,7 +1319,7 @@ dependencies = [
[[package]]
name = "oxide_liveness"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1331,7 +1331,7 @@ dependencies = [
[[package]]
name = "oxide_log"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"tracing",
"tracing-appender",
@@ -1340,7 +1340,7 @@ dependencies = [
[[package]]
name = "oxide_parser"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -1355,7 +1355,7 @@ dependencies = [
[[package]]
name = "oxide_regalloc"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1371,7 +1371,7 @@ dependencies = [
[[package]]
name = "oxide_runtime_api"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"num-bigint",
"num-traits",
@@ -1384,7 +1384,7 @@ dependencies = [
[[package]]
name = "oxide_test262"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1402,7 +1402,7 @@ dependencies = [
[[package]]
name = "oxide_types"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"bumpalo",
"dashmap",
@@ -1415,7 +1415,7 @@ dependencies = [
[[package]]
name = "oxide_vm"
version = "0.8.85"
version = "0.8.86"
dependencies = [
"bumpalo",
"chrono",
+1 -1
View File
@@ -23,7 +23,7 @@ members = [
]
[workspace.package]
version = "0.8.85"
version = "0.8.86"
edition = "2021"
license = "MIT"
rust-version = "1.80"
+661 -7
View File
@@ -1,22 +1,27 @@
//! MessagePort 载荷盒:mpsc 双端与对端端口边存于对象 `native_fn` 槽,
//! 向 GC 家族表暴露 edges/size/drop 三自由函数。
//! MessageChannel / MessagePort 行为面:`MessageChannel` 构造器(建双向 mpsc
//! 通道与两枚端口对象)、`MessagePort` 原型方法(postMessage 同步交付 /
//! close / start / readyState)与 `onmessage` 属性。
//!
//! 端口载荷盒(mpsc 双端与对端端口边)存于对象 `native_fn` 槽,向 GC 家族表
//! 暴露 edges/size/drop 三自由函数。
use std::collections::HashSet;
use std::mem;
use oxide_kernel::message_queue;
use oxide_types::object::JsObject;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes};
use oxide_types::value::JsValue;
use crate::message_value::MessageValue;
use oxide_runtime_api::{NativeResult, ProtoKind, VmHost};
use crate::message_value::{detach_message, rehydrate_message, MessageValue};
/// MessagePort 载荷盒:存于对象 `native_fn` 槽(`Box::into_raw`,realm 局部
/// 单线程)。`peer` 是发往对端的 mpsc 发送端(无 GC 边),`rx` 是收自对端的
/// mpsc 接收端(无 GC 边),`peer_port` 是对端端口对象的裸指针(双向 GC 对象
/// 边,保对端存活),`closed` 是关闭标志。
///
/// `peer` / `rx` / `closed` 当前仅单测读取,生产读取方是端口 send/recv 方法
/// (后续任务落地);字段被生产路径读取后须移除本标注。
#[allow(dead_code)]
struct MessagePortInner {
peer: message_queue::Sender<MessageValue>,
rx: message_queue::Receiver<MessageValue>,
@@ -83,6 +88,299 @@ pub fn drop_message_port_native(obj: &mut JsObject) -> u64 {
bytes
}
/// MessageChannel 构造器:建两条 mpsc 通道与两枚端口对象,回填双向
/// peer_port 边,返回 `{port1, port2}` 结果对象。
///
/// # 步骤
/// 1. 非构造形态(`!constructing_native`)抛 TypeError(HTML 规范只有
/// [[Construct]],无 [[Call]])
/// 2. 建两条 mpsc 通道(A→B 与 B→A),载荷类型 `MessageValue`
/// 3. 建两枚端口对象(proto = %MessagePort.prototype%,载荷盒入 native_fn 槽)
/// 4. 回填双向 peer_port(裸指针写,跨 JS 调用不持借用)
/// 5. 两端口各 define onmessage 数据属性 = null(非枚举)
/// 6. 建 plain 结果对象(proto = %Object.prototype%),define 可枚举
/// port1 / port2
///
/// # 副作用
/// - 两端口对象经 `alloc_object` 入对象表即成 GC 根;
/// - 双向 peer_port 边闭合环(两端口自然共亡拆环)。
///
/// # 注意事项
/// - 端口固定挂 %MessagePort.prototype%,不读 new.target(与 HTML 规范一致);
/// - 构造器不占 BuiltinWorld P 字段(绑定层就地填充占位)。
pub fn message_channel_constructor<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
if !vm.constructing_native() {
return NativeResult::Err(crate::error::create_type_error(
vm,
"MessageChannel constructor requires new",
));
}
let (a_to_b_tx, b_rx) = message_queue::channel::<MessageValue>();
let (b_to_a_tx, a_rx) = message_queue::channel::<MessageValue>();
let proto_val = JsValue::from_js_object(vm.builtin_proto(ProtoKind::MessagePortProto));
let (a_ptr, a_inner) = make_port_object(vm, proto_val, a_to_b_tx, a_rx);
let (b_ptr, b_inner) = make_port_object(vm, proto_val, b_to_a_tx, b_rx);
// 回填双向 peer_port:peer_port 空窗口的唯一来源,回填后双向 GC 边环闭合。
// SAFETY: a_inner / b_inner 是刚建的载荷盒,本段存活,只写单个裸指针字段。
unsafe {
(*a_inner).peer_port = b_ptr;
(*b_inner).peer_port = a_ptr;
}
// onmessage:普通 own 数据属性(非枚举),初值 null。
let si_onmessage = vm.perm_intern("onmessage");
// SAFETY: a_ptr / b_ptr 是本函数刚 alloc_object 的端口对象,存活且本段无别名。
unsafe {
let a_obj = &mut *a_ptr;
let _ = vm.define_data_property(
a_obj,
si_onmessage,
JsValue::null(),
PropAttributes::new(true, false, true),
);
let b_obj = &mut *b_ptr;
let _ = vm.define_data_property(
b_obj,
si_onmessage,
JsValue::null(),
PropAttributes::new(true, false, true),
);
}
// 结果对象:plain 对象(proto = %Object.prototype%),两枚可枚举 port1 / port2。
let object_proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::ObjectProto));
let result_ptr = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, object_proto));
// SAFETY: result_ptr 是本函数刚 alloc_object 的对象,存活且本段无别名。
let result_obj = unsafe { &mut *result_ptr };
let si_port1 = vm.perm_intern("port1");
let _ = vm.define_data_property(
result_obj,
si_port1,
JsValue::from_js_object(a_ptr),
PropAttributes::DEFAULT_DATA,
);
let si_port2 = vm.perm_intern("port2");
let _ = vm.define_data_property(
result_obj,
si_port2,
JsValue::from_js_object(b_ptr),
PropAttributes::DEFAULT_DATA,
);
NativeResult::Ok(JsValue::from_js_object(result_ptr))
}
/// 建单枚端口对象:空形状对象挂载荷盒,`alloc_object` 入对象表。
fn make_port_object<H: VmHost>(
vm: &mut H,
proto_val: JsValue,
peer: message_queue::Sender<MessageValue>,
rx: message_queue::Receiver<MessageValue>,
) -> (*mut JsObject, *mut MessagePortInner) {
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val);
obj.type_tag = JsObject::OBJ_TYPE_MESSAGE_PORT;
let inner = Box::into_raw(Box::new(MessagePortInner {
peer,
rx,
peer_port: std::ptr::null_mut(),
closed: false,
}));
// SAFETY: 端口对象不可调用,native_fn 存不透明 `Box<MessagePortInner>` 指针,
// 与 ArrayBuffer / RegExp 的类型化存储模式一致。
obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(inner as *const ()) }));
let ptr = vm.alloc_object(obj);
(ptr, inner)
}
/// 解析 postMessage 的 transfer 参数为 ArrayBuffer 指针集合。
///
/// # 步骤
/// 1. undefined 返空集
/// 2. ToList 迭代一切可迭代(数组 / 字符串 / 生成器 / Map / Set)
/// 3. 每元素须为 ArrayBuffer,重复元素报 DataCloneError
///
/// # 边界
/// - 不可迭代对象经 GetIterator 抛 TypeError(原值透传);
/// - 迭代中途抛错经 IteratorClose 后透传原异常;
/// - 非 AB 元素 / 重复元素报 DataCloneError(与 structuredClone 口径统一)。
fn parse_transfer_list<H: VmHost>(vm: &mut H, transfer: JsValue) -> Result<HashSet<*const JsObject>, 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。
///
/// # 步骤
/// 1. this 须为 MessagePort,closed 则 TypeError
/// 2. 解析 transfer 列表(undefined → 空集;违例报 DataCloneError)
/// 3. `detach_message` 产 `MessageValue`(失败原值透传)
/// 4. 入队对端 mpsc(SendError 理论不达,防御性报 TypeError)
/// 5. `drain_port` 排空对端队列并触发对端 onmessage
///
/// # 边界与前提
/// - 源对象由调用方寄存器保活;
/// - 同步重入:handler 内再 postMessage 同通道则嵌套 drain(try_recv 非阻塞,无死锁)。
pub fn message_port_post_message<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "postMessage called on a non-object"));
}
let this_ptr = this_val.as_js_object_ptr();
// SAFETY: is_object 保证非空指针;对象在 native 执行期间被根保持、不被回收。
let this_obj = unsafe { &*this_ptr };
let Some(inner_ptr) = message_port_payload_ptr(this_obj) else {
return NativeResult::Err(crate::error::create_type_error(vm, "postMessage called on a non-MessagePort"));
};
// SAFETY: inner_ptr 经 message_port_payload_ptr 校验为存活载荷盒;标量读取
// 短命,不跨 JS 调用。
if unsafe { (*inner_ptr).closed } {
return NativeResult::Err(crate::error::create_type_error(vm, "port is closed"));
}
let message = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let transfer = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
let transfer_set = match parse_transfer_list(vm, transfer) {
Ok(s) => s,
Err(e) => return NativeResult::Err(e),
};
let mv = match detach_message(vm, message, &transfer_set) {
Ok(m) => m,
Err(e) => return NativeResult::Err(e),
};
// SAFETY: 同 inner_ptr 校验;peer_port 构造期回填完成,指针有效。
let peer_port = unsafe { (*inner_ptr).peer_port };
if peer_port.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "port has no peer"));
}
// SAFETY: 对端 rx 由对端载荷盒保活,对端存活则接收端存活,SendError 理论
// 不达(防御性报 TypeError)。
if unsafe { (*inner_ptr).peer.send(mv) }.is_err() {
return NativeResult::Err(crate::error::create_type_error(vm, "peer port is gone"));
}
match drain_port(vm, peer_port) {
Ok(()) => NativeResult::Ok(JsValue::undefined()),
Err(e) => NativeResult::Err(e),
}
}
/// 排空对端队列:逐条 rehydrate 并触发对端 onmessage。
///
/// # 步骤
/// 1. 读对端载荷盒(对端必存活——双向 GC 边),盒缺失防御性返回 Ok
/// 2. try_recv 循环:Empty / Disconnected 退出;`rehydrate_message` 得 data;
/// `ordinary_get` 读 handler(读期用户代码窗口,异常原值透传);handler 是
/// 函数则 `call_function_sync(handler, peer, [data])`(用户异常原值透传,
/// 返回 Err 结束 drain);handler 非函数(null / undefined / 非函数)则静默丢弃该条
///
/// # 边界与前提
/// - rehydrate 新对象经 `alloc_object` 入对象表即成根,交付后无引用自然回收;
/// - 同步重入:handler 内再 postMessage 同通道则嵌套 drain(try_recv 非阻塞,无死锁)。
fn drain_port<H: VmHost>(vm: &mut H, peer_port: *mut JsObject) -> Result<(), JsValue> {
// SAFETY: peer_port 是有效端口对象(双向 GC 边保活)。
let peer_obj = unsafe { &*peer_port };
let Some(inner_ptr) = message_port_payload_ptr(peer_obj) else {
return Ok(());
};
let peer_val = JsValue::from_js_object(peer_port);
let si_onmessage = vm.perm_intern("onmessage");
// SAFETY: inner_ptr 是有效载荷盒,rx 的 try_recv 非阻塞、不跨 JS 调用。
while let Ok(mv) = unsafe { (*inner_ptr).rx.try_recv() } {
let data = rehydrate_message(vm, &mv);
let handler = match vm.ordinary_get(peer_obj, si_onmessage, peer_val) {
Ok(h) => h,
Err(e) => {
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
}
return Err(crate::error::create_type_error(vm, &e));
}
};
if !crate::iterator::is_callable(handler) {
continue;
}
if let Err(e) = vm.call_function_sync(handler, peer_val, &[data]) {
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
}
return Err(crate::iterator::engine_error(vm, &e));
}
}
Ok(())
}
/// MessagePort.close():置 closed 标志(幂等,重复关闭 no-op)。
///
/// # 边界
/// - this 须为 MessagePort;
/// - 不拆 peer_port 环(两端口自然共亡拆环);对端 close 事件延后。
pub fn message_port_close<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "close called on a non-object"));
}
let this_ptr = this_val.as_js_object_ptr();
// SAFETY: is_object 保证非空指针;对象在 native 执行期间被根保持、不被回收。
let this_obj = unsafe { &*this_ptr };
let Some(inner_ptr) = message_port_payload_ptr(this_obj) else {
return NativeResult::Err(crate::error::create_type_error(vm, "close called on a non-MessagePort"));
};
// SAFETY: inner_ptr 是有效载荷盒,标志写短命、不跨 JS 调用。
unsafe { (*inner_ptr).closed = true };
NativeResult::Ok(JsValue::undefined())
}
/// MessagePort.start():no-op(端口自动激活,无需显式启动)。
pub fn message_port_start<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "start called on a non-object"));
}
let this_ptr = this_val.as_js_object_ptr();
// SAFETY: is_object 保证非空指针;对象在 native 执行期间被根保持、不被回收。
let this_obj = unsafe { &*this_ptr };
if message_port_payload_ptr(this_obj).is_none() {
return NativeResult::Err(crate::error::create_type_error(vm, "start called on a non-MessagePort"));
}
NativeResult::Ok(JsValue::undefined())
}
/// MessagePort.readyState:读 closed 标志返 "open" / "closed"。
pub fn message_port_ready_state<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "readyState called on a non-object"));
}
let this_ptr = this_val.as_js_object_ptr();
// SAFETY: is_object 保证非空指针;对象在 native 执行期间被根保持、不被回收。
let this_obj = unsafe { &*this_ptr };
let Some(inner_ptr) = message_port_payload_ptr(this_obj) else {
return NativeResult::Err(crate::error::create_type_error(vm, "readyState called on a non-MessagePort"));
};
// SAFETY: inner_ptr 是有效载荷盒,标志读短命、不跨 JS 调用。
let closed = unsafe { (*inner_ptr).closed };
NativeResult::Ok(vm.new_string(if closed { "closed" } else { "open" }))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -163,4 +461,360 @@ mod tests {
// SAFETY: 测试构造的盒,恰好释放一次。
unsafe { drop(Box::from_raw(payload_ptr)) };
}
// ---- 端到端行为面(经 Vm 跑脚本,验证构造器与方法绑定)----
use oxide_vm::vm::Vm;
/// 编译并执行单脚本,返回 VM 与完成值。
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("parse: {:?}", e))?;
let module = oxide_compiler::compiler::Compiler::new()
.compile(&program)
.map_err(|e| format!("compile: {e}"))?;
let mut vm = Vm::new();
let result = vm.run(&std::sync::Arc::new(module))?;
Ok((vm, result))
}
/// 跑一段返回布尔的断言脚本,断言为真。
fn assert_script(source: &str) {
let (_vm, result) = eval(source).unwrap();
assert!(result.as_bool(), "script failed: {source}");
}
#[test]
fn constructor_shape_and_identity() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
return (
typeof ch.port1 === 'object' &&
typeof ch.port2 === 'object' &&
ch.port1 !== ch.port2 &&
MessageChannel.length === 0 &&
MessageChannel.name === 'MessageChannel' &&
Object.getPrototypeOf(ch.port1).constructor === MessageChannel
);
})()
"#,
);
}
#[test]
fn port_type_tag_is_message_port() {
let (_vm, result) = eval("new MessageChannel().port1").unwrap();
assert!(result.is_object());
// SAFETY: result 是脚本返回的端口对象,测试期间存活。
let obj = unsafe { &*result.as_js_object_ptr() };
assert_eq!(obj.type_tag, JsObject::OBJ_TYPE_MESSAGE_PORT);
}
#[test]
fn port_onmessage_is_own_non_enumerable() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var p = ch.port1;
var desc = Object.getOwnPropertyDescriptor(p, 'onmessage');
return (
desc !== undefined &&
desc.value === null &&
desc.writable === true &&
desc.enumerable === false &&
desc.configurable === true
);
})()
"#,
);
}
#[test]
fn basic_delivery_object() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var received = null;
ch.port2.onmessage = function(d) { received = d; };
ch.port1.postMessage({a: 1});
return received !== null && received.a === 1;
})()
"#,
);
}
#[test]
fn roundtrip_all_cloneable_types() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var got = null;
ch.port2.onmessage = function(d) { got = d; };
ch.port1.postMessage({
num: 42,
neg: -1.5,
big: 10n,
str: 'hello',
bool: true,
nil: null,
undef: undefined,
arr: [1, 2, 3],
obj: {x: 1, y: 'z'},
map: new Map([['k', 1]]),
set: new Set([1, 2]),
date: new Date(1700000000000),
re: /ab+c/g,
err: new Error('boom')
});
if (got.num !== 42 || got.neg !== -1.5 || got.big !== 10n) return false;
if (got.str !== 'hello' || got.bool !== true || got.nil !== null) return false;
if (!('undef' in got) || got.undef !== undefined) return false;
if (got.arr.length !== 3 || got.arr[1] !== 2) return false;
if (got.obj.x !== 1 || got.obj.y !== 'z') return false;
if (got.map.get('k') !== 1) return false;
if (got.set.size !== 2 || !got.set.has(2)) return false;
if (got.date.getTime() !== 1700000000000) return false;
if (got.re.source !== 'ab+c' || got.re.flags !== 'g') return false;
if (got.err.name !== 'Error' || got.err.message !== 'boom') return false;
return true;
})()
"#,
);
}
#[test]
fn transfer_detaches_source() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var ab = new ArrayBuffer(8);
var received = null;
ch.port2.onmessage = function(d) { received = d; };
ch.port1.postMessage(ab, [ab]);
return ab.byteLength === 0 && received.byteLength === 8;
})()
"#,
);
}
#[test]
fn clone_path_keeps_source_alive() {
// 未列入 transfer 的 ArrayBuffer 走克隆路径:源不 detach。
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var ab = new ArrayBuffer(4);
var received = null;
ch.port2.onmessage = function(d) { received = d; };
ch.port1.postMessage(ab);
return ab.byteLength === 4 && received.byteLength === 4 && received !== ab;
})()
"#,
);
}
#[test]
fn data_clone_error_on_function() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
try {
ch.port1.postMessage(function(){});
return false;
} catch (e) {
return e.name === 'DataCloneError';
}
})()
"#,
);
}
#[test]
fn data_clone_error_on_circular() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var a = {};
a.self = a;
try {
ch.port1.postMessage(a);
return false;
} catch (e) {
return e.name === 'DataCloneError';
}
})()
"#,
);
}
#[test]
fn data_clone_error_on_shared_array_buffer() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
try {
ch.port1.postMessage(new SharedArrayBuffer(8));
return false;
} catch (e) {
return e.name === 'DataCloneError';
}
})()
"#,
);
}
#[test]
fn transfer_violation_is_data_clone_error() {
// 非 AB 元素与重复元素均报 DataCloneError(与 structuredClone 口径统一)。
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var ab = new ArrayBuffer(4);
try {
ch.port1.postMessage(1, [ab, 'not-a-buffer']);
return false;
} catch (e) {
if (e.name !== 'DataCloneError') return false;
}
try {
ch.port1.postMessage(1, [ab, ab]);
return false;
} catch (e) {
return e.name === 'DataCloneError';
}
})()
"#,
);
}
#[test]
fn close_semantics() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var p = ch.port1;
if (p.readyState !== 'open') return false;
p.close();
if (p.readyState !== 'closed') return false;
// 重复关闭幂等 no-op。
p.close();
try {
p.postMessage(1);
return false;
} catch (e) {
return e.name === 'TypeError';
}
})()
"#,
);
}
#[test]
fn start_is_noop() {
assert_script(
r#"
(function() {
var ch = new MessageChannel();
ch.port1.start();
ch.port2.start();
return ch.port1.readyState === 'open' && ch.port2.readyState === 'open';
})()
"#,
);
}
#[test]
fn no_handler_silently_drops() {
// 未设 handler 时 postMessage 不抛错、不派发。
assert_script(
r#"
(function() {
var ch = new MessageChannel();
ch.port1.postMessage({a: 1});
ch.port1.postMessage('x');
return ch.port1.readyState === 'open';
})()
"#,
);
}
#[test]
fn non_construct_form_type_error() {
assert_script(
r#"
(function() {
try {
MessageChannel();
return false;
} catch (e) {
return e.name === 'TypeError';
}
})()
"#,
);
}
#[test]
fn non_port_receiver_type_error() {
// 对非对象 this 直调 native 入口抛 TypeError。
let (mut vm, _result) = eval("new MessageChannel()").unwrap();
vm.set_reg(0, JsValue::int(1));
vm.set_reg(1, JsValue::int(42));
let args = [0u8, 1];
match message_port_post_message(&mut vm, &args) {
NativeResult::Err(e) => {
assert!(e.is_object());
}
_ => panic!("expected TypeError"),
}
}
#[test]
fn non_port_object_type_error() {
// 对象但非 MessagePort(plain 对象)调 postMessage 抛 TypeError。
let (mut vm, plain) = eval("({})").unwrap();
vm.set_reg(0, plain);
vm.set_reg(1, JsValue::int(42));
let args = [0u8, 1];
match message_port_post_message(&mut vm, &args) {
NativeResult::Err(e) => {
assert!(e.is_object());
}
_ => panic!("expected TypeError"),
}
}
#[test]
fn gc_edge_keeps_peer_alive() {
// 仅持 port1 时 port2 不被回收:双向 GC 边保活对端,
// 强制收集后对端 handler 仍可触发。
assert_script(
r#"
(function() {
var ch = new MessageChannel();
var p1 = ch.port1;
var received = null;
(function() {
var p2 = ch.port2;
p2.onmessage = function(d) { received = d; };
})();
$262.gc();
p1.postMessage({x: 42});
return received !== null && received.x === 42;
})()
"#,
);
}
}
@@ -0,0 +1,121 @@
use std::sync::Arc;
use crate::bind_constructor;
use crate::bindings::{apply_binding_table, bind_accessor_getter, configure_native_constructor};
use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::value::JsValue;
/// 把 MessageChannel 构造器与 MessagePort 原型方法绑定到 global。
///
/// 935.1.1 的两枚 P 字段(`message_channel_constructor` / `message_port_proto`)
/// 是空对象占位,本函数就地填充(不新建对象——P 字段本身就是槽位,快照/脏检查/
/// 收尾枚举已覆盖)。
///
/// # 幂等
/// 本函数从三条路径到达:`init_kernel_builtins`(占位是空对象)、`rebind_dirty_
/// builtins` 的 `message_channel` 分支(占位是重建后的空对象)、global-dirty
/// 路径(占位可能已填满)。全部槽写经 `lookup_position` 前置守卫,有槽即跳过;
/// `bind_constructor!` 的全局槽臂自带既有槽原位更新,天然幂等。
///
/// `realm_id` 未使用(MessageChannel 无 well-known 符号键),保留签名与其他
/// 绑定函数一致。
pub fn bind_message_channel(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject, _realm_id: u32) {
let world = session.builtin_world();
let ctor_ptr = world.message_channel_constructor.as_ptr() as *mut JsObject;
let ctor = unsafe { &mut *ctor_ptr };
let proto_ptr = world.message_port_proto.as_ptr() as *mut JsObject;
let proto = unsafe { &mut *proto_ptr };
let sf = core.perm_interner().as_ref();
let sh = core.shape_forge().as_ref();
// ---- 填构造器占位 ----
ctor.set_function(true);
// prototype 槽 → 原型对象(描述符 {f,f,f})。
let si_prototype = sf.intern("prototype").0;
if sh.lookup_position(ctor.shape_id(), si_prototype).is_none() {
let shape = sh.make_shape(ctor.shape_id(), si_prototype);
ctor.set_shape_id(shape);
let pos = ctor.push_prop(JsValue::from_js_object(proto_ptr));
ctor.set_data_meta(pos, PropAttributes::new(false, false, false));
ctor.bump_generation();
}
// name 槽 → "MessageChannel"(描述符 {f,f,t})。
let si_name = sf.intern("name").0;
if sh.lookup_position(ctor.shape_id(), si_name).is_none() {
let shape = sh.make_shape(ctor.shape_id(), si_name);
ctor.set_shape_id(shape);
ctor.ensure_hash_props().push(JsValue::perm_string(sf.string_ptr(sf.intern("MessageChannel").0)));
let pos = ctor.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1);
ctor.set_data_meta(pos, PropAttributes::new(false, false, true));
ctor.bump_generation();
}
// [[Prototype]] → Function.prototype(幂等,裸写槽)。
let fn_proto_val = JsValue::from_js_object(world.function_proto.as_ptr() as *mut JsObject);
let _ = ctor.set_proto(fn_proto_val);
configure_native_constructor(
ctor,
oxide_builtins::message_channel::message_channel_constructor::<crate::vm::Vm> as *const (),
0,
);
bind_constructor!(
core,
global,
"MessageChannel",
ctor_ptr,
oxide_builtins::message_channel::message_channel_constructor::<crate::vm::Vm>,
0,
hash: true
);
// ---- 填原型占位 ----
// constructor 槽 → 构造器对象(描述符 {t,f,t})。
let si_constructor = sf.intern("constructor").0;
if sh.lookup_position(proto.shape_id(), si_constructor).is_none() {
let shape = sh.make_shape(proto.shape_id(), si_constructor);
proto.set_shape_id(shape);
let pos = proto.push_prop(JsValue::from_js_object(ctor_ptr));
proto.set_data_meta(pos, PropAttributes::new(true, false, true));
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);
// 原型方法:postMessage / close / start(每方法先 lookup_position 守卫)。
let si_post_message = sf.intern("postMessage").0;
if sh.lookup_position(proto.shape_id(), si_post_message).is_none() {
apply_binding_table(
world,
proto,
core,
&[
(
"postMessage",
oxide_builtins::message_channel::message_port_post_message::<crate::vm::Vm> as *const (),
2,
),
("close", oxide_builtins::message_channel::message_port_close::<crate::vm::Vm> as *const (), 0),
("start", oxide_builtins::message_channel::message_port_start::<crate::vm::Vm> as *const (), 0),
],
);
}
// readyState 原型访问器(set 恒 undefined),lookup_position 守卫。
let si_ready_state = sf.intern("readyState").0;
if sh.lookup_position(proto.shape_id(), si_ready_state).is_none() {
bind_accessor_getter(
core,
session,
proto,
"readyState",
oxide_builtins::message_channel::message_port_ready_state::<crate::vm::Vm> as *const (),
);
}
}
+14
View File
@@ -36,6 +36,8 @@ pub mod bind_json;
pub mod bind_map;
/// Math 单例对象及其 native 方法绑定。
pub mod bind_math;
/// MessageChannel 构造器与 MessagePort 原型方法的 native 绑定(就地填充占位)。
pub mod bind_message_channel;
/// Number 构造器与原型的 native 方法绑定(含常量属性)。
pub mod bind_number;
/// Object 构造器与原型的 native 方法绑定。
@@ -1050,6 +1052,11 @@ pub fn bind_global_builtin_slots(
oxide_builtins::symbol::symbol_constructor::<crate::vm::Vm> as *const (),
1,
);
configure_existing_ctor(
&world.message_channel_constructor,
oxide_builtins::message_channel::message_channel_constructor::<crate::vm::Vm> as *const (),
0,
);
for (name, value) in [
("Object", JsValue::from_js_object(world.object_constructor.as_ptr() as *mut JsObject)),
@@ -1073,6 +1080,10 @@ pub fn bind_global_builtin_slots(
("Function", JsValue::from_js_object(world.function_constructor.as_ptr() as *mut JsObject)),
("RegExp", JsValue::from_js_object(world.regexp_constructor.as_ptr() as *mut JsObject)),
("Symbol", JsValue::from_js_object(world.symbol_constructor.as_ptr() as *mut JsObject)),
(
"MessageChannel",
JsValue::from_js_object(world.message_channel_constructor.as_ptr() as *mut JsObject),
),
("Math", JsValue::from_js_object(world.math_object.as_ptr() as *mut JsObject)),
("JSON", JsValue::from_js_object(world.json_object.as_ptr() as *mut JsObject)),
("Temporal", JsValue::from_js_object(world.temporal_object.as_ptr() as *mut JsObject)),
@@ -1312,6 +1323,9 @@ pub fn rebind_dirty_builtins(
if dirty.map_or(true, |d| d.stubs) {
bind_bigint::bind_bigint(core, session, global, realm_id);
}
if dirty.map_or(true, |d| d.message_channel) {
bind_message_channel::bind_message_channel(core, session, global, realm_id);
}
if dirty.map_or(true, |d| d.console) {
bind_console::bind_console(core, session, global);
}