feat: Worker 构造器解析 options.name 加 self.name 与 self.location

This commit is contained in:
2026-10-11 10:25:11 +00:00
parent 10caf694b5
commit d97e78921e
5 changed files with 204 additions and 63 deletions
+41 -17
View File
@@ -9,10 +9,11 @@
use std::sync::Arc;
use crate::bind_constructor;
use crate::bindings::{apply_binding_table, bind_accessor_getset, configure_native_constructor};
use crate::bindings::{apply_binding_table, bind_accessor_getset, bind_accessor_getter, configure_native_constructor};
use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID};
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::string_forge::PermInterner;
use oxide_types::value::JsValue;
/// 把 Worker 构造器(Box 分配)与 self 面方法绑定到 global。
@@ -222,19 +223,42 @@ pub fn bind_worker(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
);
}
}
// name / location getter(幂等守卫)。
// let si_name_global = sf.intern("name").0;
// if sh.lookup_position(global.shape_id(), si_name_global).is_none() {
// bind_accessor_getter(core, session, global, "name", crate::worker::bindings::self_name as *const ());
// }
// let si_location = sf.intern("location").0;
// if sh.lookup_position(global.shape_id(), si_location).is_none() {
// bind_accessor_getter(
// core,
// session,
// global,
// "location",
// crate::worker::bindings::self_location as *const (),
// );
// }
// name / location:worker realm 绑 getter(读 thread-local 真值,规范中
// WorkerGlobalScope.name 为只读访问器);主 realm 绑空串数据属性(与浏览器
// window.name 同一形状的无害占位,不挡全局 var name 赋值)。幂等守卫。
let worker_realm = crate::worker::bindings::worker_thread();
let si_name_global = sf.intern("name").0;
if sh.lookup_position(global.shape_id(), si_name_global).is_none() {
if worker_realm {
bind_accessor_getter(core, session, global, "name", crate::worker::bindings::self_name as *const ());
} else {
bind_global_empty_string_property(global, sf, sh, si_name_global);
}
}
let si_location = sf.intern("location").0;
if sh.lookup_position(global.shape_id(), si_location).is_none() {
if worker_realm {
bind_accessor_getter(
core,
session,
global,
"location",
crate::worker::bindings::self_location as *const (),
);
} else {
bind_global_empty_string_property(global, sf, sh, si_location);
}
}
}
/// 在 global 上绑一个空串数据属性(主 realm 的 name / location 占位)。
fn bind_global_empty_string_property(global: &mut JsObject, sf: &PermInterner, sh: &ShapeForge, si: u32) {
let shape = sh.make_shape(global.shape_id(), si);
global.set_shape_id(shape);
global
.ensure_hash_props()
.push(JsValue::perm_string(sf.string_ptr(sf.intern("").0)));
let pos = global.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1);
global.set_data_meta(pos, PropAttributes::new(true, false, true));
global.bump_generation();
}
+58 -13
View File
@@ -36,6 +36,16 @@ thread_local! {
static WORKER_CLOSE_REQUESTED: Cell<bool> = const { Cell::new(false) };
}
// worker 名称(构造器 options.name,worker 线程独占,主 realm 不置位)。
thread_local! {
static WORKER_NAME: RefCell<Option<String>> = const { RefCell::new(None) };
}
// worker 位置(构造器 url 字符串,worker 线程独占,主 realm 不置位)。
thread_local! {
static WORKER_LOCATION: RefCell<Option<String>> = const { RefCell::new(None) };
}
/// 注入 worker → 主线程输出通道(`worker_event_loop` 建 Vm 后调用)。
///
/// # 副作用
@@ -57,17 +67,34 @@ pub(crate) fn worker_close_requested() -> bool {
WORKER_CLOSE_REQUESTED.with(|flag| flag.get())
}
/// `new Worker(url)`:读 url 文件、派生 worker、建 Worker 对象。
/// 注入 worker 名称与位置(`worker_event_loop` 建 Vm 前调用)。
///
/// # 副作用
/// - 覆盖本线程的 `WORKER_NAME` 与 `WORKER_LOCATION`(worker 线程内恰好一次)。
/// - 建 Vm 前调用:`bind_worker` 据本线程是否置位区分 worker realm 与主 realm。
pub(crate) fn set_worker_identity(name: String, location: String) {
WORKER_NAME.with(|slot| *slot.borrow_mut() = Some(name));
WORKER_LOCATION.with(|slot| *slot.borrow_mut() = Some(location));
}
/// 查询当前线程是否 worker 线程(绑定层区分 worker realm 与主 realm)。
pub(crate) fn worker_thread() -> bool {
WORKER_NAME.with(|slot| slot.borrow().is_some())
}
/// `new Worker(url, options)`:读 url 文件与 options.name、派生 worker、建 Worker 对象。
///
/// # 步骤
/// 1. 构造形态校验(HTML 规范只有 `[[Construct]]`,非构造调用抛 TypeError)。
/// 2. 读 `args[1]` 为 url 字符串(非字符串抛 TypeError)。
/// 3. 主线程侧读 url 文件(`std::fs::read_to_string`),失败抛 TypeError。
/// 4. `vm.spawn_worker(source)` 派生 worker,得 worker 编号。
/// 5. 建 Worker 对象:PLAIN 对象(`[[Prototype]]` → Worker.prototype)加非枚举
/// 3. 读 `args[2]` 为 options 对象(缺省 undefined);是对象时经 `ordinary_get`
/// 读 `name` 属性(getter 副作用与异常原值传播),是字符串取之、非字符串取空串。
/// 4. 主线程侧读 url 文件(`std::fs::read_to_string`),失败抛 TypeError。
/// 5. `vm.spawn_worker(source, name, url)` 派生 worker,得 worker 编号。
/// 6. 建 Worker 对象:PLAIN 对象(`[[Prototype]]` → Worker.prototype)加非枚举
/// `workerId` 数据属性(值为 worker 编号)。`onmessage`/`onerror`/
/// `onmessageerror` 不在对象上建自有属性(改原型访问器属性)。无 native 盒、无 GC 家族。
/// 6. 返回 Worker 对象。
/// 7. 返回 Worker 对象。
///
/// # 边界与前提
/// - `url` 是 worker 脚本的文件路径(主线程侧读取)。
@@ -89,6 +116,24 @@ pub(crate) fn worker_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
None => return NativeResult::Err(oxide_builtins::error::create_type_error(vm, "Worker url must be a string")),
};
// 读 options 对象(args[2],缺省 undefined)。
let options_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
// name:options 是对象时经 ordinary_get 读 name 属性(getter 副作用与异常
// 原值传播),是字符串取之,非字符串或缺省取空串。
let name = if options_val.is_object() {
let opts_ptr = options_val.as_js_object_ptr();
// SAFETY: opts_ptr 是 options 对象,native 调用期间存活。
let opts_obj = unsafe { &*opts_ptr };
let si_name = vm.perm_intern("name");
let name_val = match vm.ordinary_get(opts_obj, si_name, options_val) {
Ok(v) => v,
Err(e) => return NativeResult::Err(oxide_builtins::error::create_type_error(vm, &e)),
};
vm.lookup_str(name_val).unwrap_or_default()
} else {
String::new()
};
// 主线程侧读 url 文件(worker 脚本源码)。
let source = match std::fs::read_to_string(&url) {
Ok(s) => s,
@@ -100,8 +145,8 @@ pub(crate) fn worker_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult {
}
};
// 派生 worker(建 WorkerHandle 并 spawn OS 线程)。
let id = match vm.spawn_worker(&source) {
// 派生 worker(建 WorkerHandle 并 spawn OS 线程),传 name 与 url(location)。
let id = match vm.spawn_worker(&source, &name, &url) {
Ok(id) => id,
Err(e) => return NativeResult::Err(oxide_builtins::error::create_type_error(vm, &e)),
};
@@ -275,16 +320,16 @@ pub(crate) fn self_close(_vm: &mut Vm, _args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::undefined())
}
/// `self.name` getter(worker 侧):返空串(首版占位,规范面对齐留后续)。
#[expect(dead_code)]
/// `self.name` getter(worker 侧):读本线程 worker 名称(未置位时返空串)。
pub(crate) fn self_name(vm: &mut Vm, _args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string(""))
let name = WORKER_NAME.with(|slot| slot.borrow().as_deref().unwrap_or("").to_string());
NativeResult::Ok(vm.new_string(&name))
}
/// `self.location` getter(worker 侧):返空串(首版占位,规范面对齐留后续)。
#[expect(dead_code)]
/// `self.location` getter(worker 侧):读本线程 worker 位置(未置位时返空串)。
pub(crate) fn self_location(vm: &mut Vm, _args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string(""))
let location = WORKER_LOCATION.with(|slot| slot.borrow().as_deref().unwrap_or("").to_string());
NativeResult::Ok(vm.new_string(&location))
}
/// 从当前函数对象的 `name` 读事件类型(`get onmessage` / `set onmessage` →
+85 -13
View File
@@ -81,23 +81,31 @@ pub struct WorkerHandle {
///
/// # 步骤
/// 1. 经 `Vm::with_kernel_core` 建专属 Vm(新 realm)并注入编译服务。
/// 2. 编译 worker 脚本(worker 的程序);失败则经 worker → 主线程通道错误面上报
/// 2. 注入 worker → 主线程输出通道与名称、位置(thread-local,模式同 `WORKER_OUT_TX`)。
/// 3. 编译 worker 脚本(worker 的程序);失败则经 worker → 主线程通道错误面上报
/// 错误串,不 panic,worker 继续事件循环(可被干净终止)。
/// 3. 编译成功则运行脚本(worker 的程序);运行期未捕获异常同样经错误面上报。
/// 4. 循环 `recv_timeout(100ms)`:处理 `Message` / `Error` / `Terminate` / 超时。
/// 4. 编译成功则运行脚本(worker 的程序);运行期未捕获异常同样经错误面上报。
/// 5. 循环 `recv_timeout(100ms)`:处理 `Message` / `Error` / `Terminate` / 超时。
///
/// # 边界与前提
/// - `script` 是 worker 的程序源码(普通脚本),编译与运行均在 worker 线程内完成。
/// - `name` / `location` 是 worker 的名称与位置(构造器 options.name 与 url),
/// 置入 thread-local 供 `self.name` / `self.location` getter 读取。
/// - 超时后查 `rx.is_disconnected()`:主线程全部发送端 drop 即断开,worker 退出。
///
/// # 副作用
/// - 创建并 drop 一个 Vm(退出时 drop 触发 `Drop for Vm` → `Drop for Realm`)。
/// - 置位本线程的 `WORKER_NAME` / `WORKER_LOCATION` thread-local(模式同
/// `WORKER_OUT_TX`,线程退出随 thread-local 释放)。
/// - 每条 `Message` 经 rehydrate → onmessage 交付 → drain_microtasks 处理;
/// 编译或执行失败经 `WorkerOutMail::Error` 上报主线程(主线程交付 onerror)。
fn worker_event_loop(
core: Arc<KernelCore>, compiler: Arc<dyn CompilerService>, script: String, rx: Receiver<WorkerMail>,
out_tx: Sender<WorkerOutMail>,
out_tx: Sender<WorkerOutMail>, name: String, location: String,
) {
// 注入 worker 名称与位置(建 Vm 前:bind_worker 据本线程 thread-local 区分
// worker realm 与主 realm;self.name / self.location 经 thread-local 读取)。
bindings::set_worker_identity(name, location);
let mut vm = Vm::with_kernel_core(core);
vm.set_compiler_service(Arc::clone(&compiler));
// 注入 worker → 主线程输出通道(self.postMessage 经 thread-local 发回主线程)。
@@ -157,12 +165,15 @@ impl Vm {
/// # 步骤
/// 1. 分配 worker 编号(`worker_next_id` 自增)。
/// 2. 建主线程 → worker 与 worker → 主线程两条通道。
/// 3. spawn 线程,线程内建专属 Vm 并进入事件循环。
/// 3. spawn 线程,线程内建专属 Vm 并进入事件循环(`name` / `location`
/// 物化为 `String` 随线程闭包传入,置入 worker 线程 thread-local)。
/// 4. 把 `WorkerHandle` 登记进 `worker_registry`。
///
/// # 边界与前提
/// - `script` 是 worker 的程序源码(普通脚本);编译在 worker 线程内完成,
/// 编译失败经 worker → 主线程通道上报,不 panic。
/// - `name` 是 worker 名称(构造器 options.name,缺省空串);`location`
/// 是 worker 位置(构造器 url 字符串),供 `self.name` / `self.location` 读取。
/// - realm 计数受 512 上界约束(每 worker 一个 realm),worker 数须远低于 512。
///
/// # 返回值
@@ -170,7 +181,7 @@ impl Vm {
///
/// # 副作用
/// - 派生一个 OS 线程;登记一个 `WorkerHandle`。
pub fn spawn_worker(&mut self, script: &str) -> Result<u64, String> {
pub fn spawn_worker(&mut self, script: &str, name: &str, location: &str) -> Result<u64, String> {
let id = self.worker_next_id;
self.worker_next_id += 1;
@@ -180,9 +191,11 @@ impl Vm {
let core = Arc::clone(&self.kernel_core);
let compiler = Arc::clone(&self.compiler);
let script_owned = script.to_string();
let name_owned = name.to_string();
let location_owned = location.to_string();
let handle = std::thread::spawn(move || {
worker_event_loop(core, compiler, script_owned, rx, out_tx);
worker_event_loop(core, compiler, script_owned, rx, out_tx, name_owned, location_owned);
});
let worker = WorkerHandle { id, tx, rx_out: out_rx, handle };
@@ -550,7 +563,7 @@ mod tests {
// worker 脚本设 addEventListener 回显(注册表交付替换早期 onmessage 属性语义)。
let id = vm
.spawn_worker("self.addEventListener('message', 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");
@@ -569,7 +582,7 @@ mod tests {
fn worker_script_compile_failure_reports_error() {
let mut vm = vm_with_compiler();
let id = vm.spawn_worker("function { 语法错误").expect("worker 应派生成功");
let id = vm.spawn_worker("function { 语法错误", "", "").expect("worker 应派生成功");
let poll = poll_until_mail(&mut vm, id);
assert!(!poll.errors.is_empty(), "编译失败应经错误面上报错误串");
assert!(!poll.errors[0].is_empty(), "错误串应非空");
@@ -586,10 +599,10 @@ mod tests {
// 两 worker 各设 addEventListener 回显(注册表交付语义)。
let id_a = vm
.spawn_worker("self.addEventListener('message', 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.addEventListener('message', 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");
@@ -689,7 +702,7 @@ mod tests {
fn worker_e2e_terminate() {
let mut vm = vm_with_compiler();
let id = vm.spawn_worker("1").expect("worker 应派生成功");
let id = vm.spawn_worker("1", "", "").expect("worker 应派生成功");
assert_eq!(vm.active_workers(), vec![id], "应有唯一活跃 worker");
vm.worker_terminate(id).expect("终止应成功");
@@ -707,7 +720,7 @@ mod tests {
let core = Arc::clone(vm.kernel_core());
let baseline = core.active_vms();
let id = vm.spawn_worker("1").expect("worker 应派生成功");
let id = vm.spawn_worker("1", "", "").expect("worker 应派生成功");
// worker 线程异步建 Vm:轮询至 realm 计数 +1(带截止,防 flaky)。
let deadline = Instant::now() + Duration::from_secs(5);
while core.active_vms() < baseline + 1 {
@@ -865,6 +878,65 @@ mod tests {
let _ = std::fs::remove_file(&worker_path);
}
/// 端到端:带 name 的 Worker 内 self.name 为 name、self.location 为 url 字符串;
/// 无 options 的 Worker 内 self.name 为空串。
#[test]
fn worker_e2e_name_and_location() {
let mut vm = vm_with_compiler();
// worker 脚本:收到消息后回发 self.name 与 self.location(以 | 连接)。
let worker_script =
"self.addEventListener('message', function(e) { self.postMessage(self.name + '|' + self.location); });";
let path_a = write_worker_script(worker_script);
let path_b = write_worker_script(worker_script);
// 主脚本:A 带 options.name 建 Worker、B 无 options,各自回发后记录。
let main_script = format!(
"var wa = new Worker('{a}', {{ name: 'n' }}); var wb = new Worker('{b}'); \
wa.addEventListener('message', function(e) {{ globalThis.idA = e.data; }}); \
wb.addEventListener('message', function(e) {{ globalThis.idB = e.data; }}); \
wa.postMessage('check'); wb.postMessage('check');",
a = path_a.display(),
b = path_b.display()
);
vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行");
// 事件循环轮询至两个回发都到达(带截止,防 flaky)。
let deadline = Instant::now() + Duration::from_secs(5);
loop {
vm.deliver_worker_messages();
vm.cleanup_disconnected_workers();
let a = global_value_opt(&vm, "idA");
let b = global_value_opt(&vm, "idB");
if a.is_some_and(|v| v.is_string()) && b.is_some_and(|v| v.is_string()) {
break;
}
if Instant::now() >= deadline {
panic!("5 秒内未收齐 self.name 与 self.location 回发");
}
std::thread::sleep(Duration::from_millis(10));
}
// A:self.name 为 "n"、self.location 为 url 字符串。
let id_a = global_value_opt(&vm, "idA").expect("idA 应被设置");
let id_a_text = vm.lookup_str(id_a).expect("idA 应可读");
assert_eq!(
id_a_text,
format!("n|{}", path_a.display()),
"带 name 的 worker 内 self.name 应为 \"n\"、self.location 应为 url 字符串"
);
// B:无 options 时 self.name 为空串、self.location 仍为 url 字符串。
let id_b = global_value_opt(&vm, "idB").expect("idB 应被设置");
let id_b_text = vm.lookup_str(id_b).expect("idB 应可读");
assert_eq!(
id_b_text,
format!("|{}", path_b.display()),
"无 options 的 worker 内 self.name 应为空串、self.location 应为 url 字符串"
);
vm.shutdown_workers();
let _ = std::fs::remove_file(&path_a);
let _ = std::fs::remove_file(&path_b);
}
/// onmessageerror 交付辅助:无自然触发,直接单测交付辅助(handler 被调用、
/// 事件 type 为 "messageerror")。不钉触发可达性(当前架构不可达)。
#[test]