From 8c23b91bc57a9d2022873573872f3372d68b9a1e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 11 Oct 2026 10:47:05 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20worker=20=E5=85=A8=E5=B1=80=20importScr?= =?UTF-8?q?ipts=20=E9=80=90=20URL=20=E5=90=8C=E6=AD=A5=E8=AF=BB=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=BC=96=E8=AF=91=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 38 ++--- Cargo.toml | 2 +- crates/oxide_vm/src/bindings/bind_worker.rs | 20 ++- crates/oxide_vm/src/worker/bindings.rs | 70 +++++++++- crates/oxide_vm/src/worker/mod.rs | 146 ++++++++++++++++++++ 5 files changed, 252 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76e5b422..f1ef2236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.14.17" +version = "0.14.18" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.14.17" +version = "0.14.18" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.14.17" +version = "0.14.18" dependencies = [ "ansi_term", "bumpalo", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.14.17" +version = "0.14.18" dependencies = [ "lru", "oxide_bytecode", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1256,7 +1256,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.14.17" +version = "0.14.18" dependencies = [ "num-bigint", "oxc_allocator", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1297,7 +1297,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.14.17" +version = "0.14.18" dependencies = [ "bumpalo", "dashmap", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.14.17" +version = "0.14.18" dependencies = [ "tracing", "tracing-appender", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.14.17" +version = "0.14.18" dependencies = [ "num-bigint", "num-traits", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.14.17" +version = "0.14.18" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.14.17" +version = "0.14.18" dependencies = [ "bumpalo", "dashmap", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.14.17" +version = "0.14.18" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 0c6dcedb..a4e86449 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.14.17" +version = "0.14.18" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_vm/src/bindings/bind_worker.rs b/crates/oxide_vm/src/bindings/bind_worker.rs index 05b9e9f1..6a2a34f5 100644 --- a/crates/oxide_vm/src/bindings/bind_worker.rs +++ b/crates/oxide_vm/src/bindings/bind_worker.rs @@ -28,8 +28,9 @@ use oxide_types::value::JsValue; /// # 副作用 /// - 构造器与原型对象经 `Box::into_raw` 登记 world 释放登记表(session 收尾统一 /// 释放);全局 `Worker` 槽经 `bind_constructor!` 原位安装。 -/// - self 面方法(`postMessage`/`close` 方法、`name`/`location` getter)绑 global -/// (worker realm 经 `self` === global 可达,主 realm 无 `self` 时为无害占位)。 +/// - self 面方法(`postMessage`/`close` 方法、`name`/`location` getter)与 +/// `importScripts` 绑 global(worker realm 经 `self` === global 可达, +/// 主 realm 无 `self` 时为无害占位)。 /// /// `realm_id` 未使用(Worker 无 well-known 符号键),保留签名与其他绑定函数一致。 pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut JsObject, _realm_id: u32) { @@ -166,6 +167,21 @@ pub fn bind_worker(core: &Arc, session: &KernelSession, global: &mut ], ); } + // importScripts(worker 全局,逐 URL 同步读文件、编译、执行;主 realm 为无害占位)。 + // 幂等守卫。 + let si_import_scripts = sf.intern("importScripts").0; + if sh.lookup_position(global.shape_id(), si_import_scripts).is_none() { + apply_binding_table( + world, + global, + core, + &[( + "importScripts", + crate::worker::bindings::import_scripts as *const (), + 1, + )], + ); + } // EventTarget 三方法直绑全局(worker realm 经 self === global 可达;全局原型链 // 是 Object.prototype,首版不改全局原型,三方法直绑全局等价可见)。幂等守卫。 let si_self_add = sf.intern("addEventListener").0; diff --git a/crates/oxide_vm/src/worker/bindings.rs b/crates/oxide_vm/src/worker/bindings.rs index c16b199c..2633aece 100644 --- a/crates/oxide_vm/src/worker/bindings.rs +++ b/crates/oxide_vm/src/worker/bindings.rs @@ -1,6 +1,6 @@ //! Worker API 的 concrete `&mut Vm` native 函数:构造器、`postMessage`/`terminate` -//! 原型方法、`self` 面(`self.postMessage`/`self.close`/`self.name`/`self.location`) -//! 与 `MessageEvent` / 错误事件构造辅助。 +//! 原型方法、`self` 面(`self.postMessage`/`self.close`/`self.name`/`self.location`)、 +//! `importScripts` 与 `MessageEvent` / 错误事件构造辅助。 //! //! 关键约定: //! - 函数是 concrete `&mut Vm` 形态(`NativeFn`),不挂 `VmHost` trait(trait 是单 @@ -320,6 +320,72 @@ pub(crate) fn self_close(_vm: &mut Vm, _args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::undefined()) } +/// `importScripts(...urls)`(worker 全局):逐 URL 同步读文件、编译、执行。 +/// +/// # 步骤 +/// 1. 逐个 URL 实参(`args[1..]`):读文件(`std::fs::read_to_string`,失败抛 +/// TypeError)。 +/// 2. 经 `create_plain_dynamic_script` 编译(失败抛 SyntaxError)。 +/// 3. 经 `call_function_sync` 以 global 为 this 同步执行(运行异常原值透传)。 +/// 4. 全部成功后返回 undefined。 +/// +/// # 边界与前提 +/// - URL 是脚本文件路径(worker 线程侧读取,与 worker 脚本同一线程)。 +/// - 主 realm 的 Vm 未注入编译服务时编译抛「未安装编译服务」错误(无害占位, +/// 与 self 侧 postMessage/close 在主 realm 的占位同一模式)。 +/// +/// # 副作用 +/// - 每个 URL 扩展当前代际平表(动态脚本模块)并同步执行一段脚本 +/// (顶层 var/function 声明落全局对象)。 +pub(crate) fn import_scripts(vm: &mut Vm, args: &[u8]) -> NativeResult { + for i in 1..args.len() { + let url_val = vm.reg(args[i]); + let url = match vm.lookup_str(url_val) { + Some(u) => u, + None => { + return NativeResult::Err(oxide_builtins::error::create_type_error( + vm, + "importScripts url must be a string", + )) + } + }; + + // 读文件(脚本源码)。 + let source = match std::fs::read_to_string(&url) { + Ok(s) => s, + Err(e) => { + return NativeResult::Err(oxide_builtins::error::create_type_error( + vm, + &format!("failed to read importScripts script: {e}"), + )) + } + }; + + // 动态路径源契约:源码域转义形态传源(Rust String 无孤立 surrogate, + // 转义为恒等映射)。 + let units: Vec = source.encode_utf16().collect(); + let code = oxide_kernel::string_forge::source_escape(&units); + + // 编译(失败抛 SyntaxError)。 + let func = match vm.create_plain_dynamic_script(&code) { + Ok(f) => f, + Err(e) => return NativeResult::Err(oxide_builtins::error::create_syntax_error(vm, &e)), + }; + + // 以 global 为 this 同步执行(运行异常原值透传)。 + let global = JsValue::from_js_object(vm.realm.session.borrow().global_object().as_ptr() as *mut JsObject); + if let Err(e) = vm.call_function_sync(func, global, &[]) { + // 原异常值在 last_uncaught_value(Err 文本是展平形态);槽空时 + // (内部不变量违例)按文本恢复错误对象兜底。 + return NativeResult::Err( + vm.take_uncaught_value() + .unwrap_or_else(|| oxide_builtins::error::create_from_text(vm, &e)), + ); + } + } + NativeResult::Ok(JsValue::undefined()) +} + /// `self.name` getter(worker 侧):读本线程 worker 名称(未置位时返空串)。 pub(crate) fn self_name(vm: &mut Vm, _args: &[u8]) -> NativeResult { let name = WORKER_NAME.with(|slot| slot.borrow().as_deref().unwrap_or("").to_string()); diff --git a/crates/oxide_vm/src/worker/mod.rs b/crates/oxide_vm/src/worker/mod.rs index 95593819..21279616 100644 --- a/crates/oxide_vm/src/worker/mod.rs +++ b/crates/oxide_vm/src/worker/mod.rs @@ -937,6 +937,152 @@ mod tests { let _ = std::fs::remove_file(&path_b); } + /// 端到端:importScripts 同步执行文件(lib.js 设全局变量,importScripts 后 + /// 立即可读);worker 脚本内 typeof importScripts 为 "function"、 + /// self.importScripts === importScripts。 + #[test] + fn worker_e2e_import_scripts() { + let mut vm = vm_with_compiler(); + + // lib.js:设全局变量。 + let lib_path = write_worker_script("globalThis.libValue = 7;"); + // worker 脚本:顶层 importScripts,收到消息后回发读取结果。 + let worker_script = format!( + "importScripts('{lib}'); \ + self.addEventListener('message', function(e) {{ self.postMessage(\ + globalThis.libValue + '|' + (typeof importScripts) + '|' + (self.importScripts === importScripts)); }});", + lib = lib_path.display() + ); + let worker_path = write_worker_script(&worker_script); + // 主脚本:建 Worker、投递消息触发回发。 + let main_script = format!( + "var w = new Worker('{path}'); w.addEventListener('message', function(e) {{ globalThis.got = e.data; }}); w.postMessage('go');", + path = worker_path.display() + ); + vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); + + // 事件循环轮询至 globalThis.got 被设置(带截止,防 flaky)。 + let deadline = Instant::now() + Duration::from_secs(5); + loop { + vm.deliver_worker_messages(); + vm.cleanup_disconnected_workers(); + if global_value_opt(&vm, "got").is_some() { + break; + } + if Instant::now() >= deadline { + panic!("5 秒内未收到 importScripts 回发"); + } + std::thread::sleep(Duration::from_millis(10)); + } + let got = global_value_opt(&vm, "got").expect("got 应被设置"); + let got_text = vm.lookup_str(got).expect("got 应可读"); + assert_eq!( + got_text, + "7|function|true", + "lib.js 应同步执行、importScripts 应为 function 且经 self 可达" + ); + + vm.shutdown_workers(); + let _ = std::fs::remove_file(&worker_path); + let _ = std::fs::remove_file(&lib_path); + } + + /// 端到端:importScripts 多 URL 按序执行(a 先于 b)。 + #[test] + fn worker_e2e_import_scripts_multiple_urls_in_order() { + let mut vm = vm_with_compiler(); + + // a.js 与 b.js 各向 globalThis.order 追加一段。 + let path_a = write_worker_script("globalThis.order = (globalThis.order || '') + 'a';"); + let path_b = write_worker_script("globalThis.order = (globalThis.order || '') + 'b';"); + let worker_script = format!( + "importScripts('{a}', '{b}'); \ + self.addEventListener('message', function(e) {{ self.postMessage(globalThis.order); }});", + a = path_a.display(), + b = path_b.display() + ); + let worker_path = write_worker_script(&worker_script); + let main_script = format!( + "var w = new Worker('{path}'); w.addEventListener('message', function(e) {{ globalThis.got = e.data; }}); w.postMessage('go');", + path = worker_path.display() + ); + vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); + + // 事件循环轮询至 globalThis.got 被设置(带截止,防 flaky)。 + let deadline = Instant::now() + Duration::from_secs(5); + loop { + vm.deliver_worker_messages(); + vm.cleanup_disconnected_workers(); + if global_value_opt(&vm, "got").is_some() { + break; + } + if Instant::now() >= deadline { + panic!("5 秒内未收到 importScripts 回发"); + } + std::thread::sleep(Duration::from_millis(10)); + } + let got = global_value_opt(&vm, "got").expect("got 应被设置"); + let got_text = vm.lookup_str(got).expect("got 应可读"); + assert_eq!(got_text, "ab", "多 URL 应按实参顺序执行"); + + vm.shutdown_workers(); + let _ = std::fs::remove_file(&worker_path); + let _ = std::fs::remove_file(&path_a); + let _ = std::fs::remove_file(&path_b); + } + + /// 端到端:importScripts 文件不存在抛 TypeError、语法错误抛 SyntaxError、 + /// 脚本运行期异常原值透传。 + #[test] + fn worker_e2e_import_scripts_errors() { + let mut vm = vm_with_compiler(); + + // bad.js:语法错误(编译失败);throw.js:顶层抛字符串(运行期异常)。 + let bad_path = write_worker_script("function { 语法错误"); + let throw_path = write_worker_script("throw 'boom';"); + let worker_script = format!( + "var r1 = 'none'; try {{ importScripts('{missing}'); }} catch (e) {{ r1 = e.constructor.name; }} \ + var r2 = 'none'; try {{ importScripts('{bad}'); }} catch (e) {{ r2 = e.constructor.name; }} \ + var r3 = 'none'; try {{ importScripts('{throw}'); }} catch (e) {{ r3 = String(e); }} \ + self.addEventListener('message', function(e) {{ self.postMessage(r1 + '|' + r2 + '|' + r3); }});", + missing = "/nonexistent/oxide_import_scripts_missing.js", + bad = bad_path.display(), + throw = throw_path.display() + ); + let worker_path = write_worker_script(&worker_script); + let main_script = format!( + "var w = new Worker('{path}'); w.addEventListener('message', function(e) {{ globalThis.got = e.data; }}); w.postMessage('go');", + path = worker_path.display() + ); + vm.run(&Arc::new(compile_script(&main_script))).expect("主脚本应运行"); + + // 事件循环轮询至 globalThis.got 被设置(带截止,防 flaky)。 + let deadline = Instant::now() + Duration::from_secs(5); + loop { + vm.deliver_worker_messages(); + vm.cleanup_disconnected_workers(); + if global_value_opt(&vm, "got").is_some() { + break; + } + if Instant::now() >= deadline { + panic!("5 秒内未收到 importScripts 回发"); + } + std::thread::sleep(Duration::from_millis(10)); + } + let got = global_value_opt(&vm, "got").expect("got 应被设置"); + let got_text = vm.lookup_str(got).expect("got 应可读"); + assert_eq!( + got_text, + "TypeError|SyntaxError|boom", + "文件不存在应抛 TypeError、语法错误应抛 SyntaxError、运行期异常应原值透传" + ); + + vm.shutdown_workers(); + let _ = std::fs::remove_file(&worker_path); + let _ = std::fs::remove_file(&bad_path); + let _ = std::fs::remove_file(&throw_path); + } + /// onmessageerror 交付辅助:无自然触发,直接单测交付辅助(handler 被调用、 /// 事件 type 为 "messageerror")。不钉触发可达性(当前架构不可达)。 #[test]