From 3290f287fd796dedffdefd713916bb9fdb5633f6 Mon Sep 17 00:00:00 2001 From: Jiwoo Ahn Date: Sun, 19 Jul 2026 14:13:47 +0900 Subject: [PATCH] wasm: add stderr support (#8318) Assisted-by: Codex:gpt-5.5 Signed-off-by: Jiwoo Ahn --- crates/wasm/Lib/asyncweb.py | 3 +- crates/wasm/README.md | 3 ++ crates/wasm/src/lib.rs | 3 ++ crates/wasm/src/vm_class.rs | 58 +++++++++++++++++++++++--------- crates/wasm/src/wasm_builtins.rs | 15 +++++---- wasm/demo/src/index.js | 1 + wasm/tests/test_exec_mode.py | 14 ++++++++ 7 files changed, 73 insertions(+), 24 deletions(-) diff --git a/crates/wasm/Lib/asyncweb.py b/crates/wasm/Lib/asyncweb.py index 40bd84349..f0e7983f7 100644 --- a/crates/wasm/Lib/asyncweb.py +++ b/crates/wasm/Lib/asyncweb.py @@ -64,8 +64,7 @@ async def _main_wrapper(coro): import traceback import sys - # TODO: sys.stderr on wasm - traceback.print_exc(file=sys.stdout) + traceback.print_exc(file=sys.stderr) def _resolve(prom): diff --git a/crates/wasm/README.md b/crates/wasm/README.md index 3a7550092..1878614be 100644 --- a/crates/wasm/README.md +++ b/crates/wasm/README.md @@ -34,6 +34,9 @@ pyEval(code, options?); - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the native print function, and it will be `console.log` when giving `undefined` or "console", and it will be a dumb function when giving null. +- `stderr?`: `"console" | ((out: string) => void) | null`: A function to replace + `sys.stderr`, and it will be `console.error` when giving `undefined` or + "console". ## License diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index ae4e52f21..041cb864e 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -66,6 +66,7 @@ pub mod eval { }; vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?; + vm.set_stderr(Reflect::get(&options, &"stderr".into())?)?; if let Some(js_vars) = js_vars { vm.add_to_scope("js_vars".into(), js_vars.into())?; @@ -93,6 +94,8 @@ pub mod eval { /// - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the /// native print native print function, and it will be `console.log` when giving /// `undefined` or "console", and it will be a dumb function when giving null. + /// - `stderr?`: `"console" | ((out: string) => void) | null`: A function to replace + /// `sys.stderr`, and it will be `console.error` when giving `undefined` or "console". #[wasm_bindgen(js_name = pyEval)] pub fn eval_py(source: &str, options: Option) -> Result { run_py(source, options, Mode::Eval) diff --git a/crates/wasm/src/vm_class.rs b/crates/wasm/src/vm_class.rs index cd0af10f6..5e09af0ee 100644 --- a/crates/wasm/src/vm_class.rs +++ b/crates/wasm/src/vm_class.rs @@ -249,31 +249,59 @@ impl WASMVirtualMachine { #[wasm_bindgen(js_name = setStdout)] pub fn set_stdout(&self, stdout: JsValue) -> Result<(), JsValue> { + self.set_stdstream( + "stdout", + "JSStdout", + stdout, + wasm_builtins::sys_stdout_write_console, + ) + } + + #[wasm_bindgen(js_name = setStderr)] + pub fn set_stderr(&self, stderr: JsValue) -> Result<(), JsValue> { + self.set_stdstream( + "stderr", + "JSStderr", + stderr, + wasm_builtins::sys_stderr_write_console, + ) + } + + fn set_stdstream( + &self, + attr: &'static str, + class_name: &'static str, + stream: JsValue, + console_write: fn(&str, &VirtualMachine) -> PyResult<()>, + ) -> Result<(), JsValue> { self.with_vm(|vm, _| { - fn error() -> JsValue { - TypeError::new("Unknown stdout option, please pass a function or 'console'").into() + fn error(attr: &str) -> JsValue { + TypeError::new(&format!( + "Unknown {attr} option, please pass a function or 'console'" + )) + .into() } - use wasm_builtins::make_stdout_object; - let stdout: PyObjectRef = if let Some(s) = stdout.as_string() { + use wasm_builtins::make_stdstream_object; + let stream: PyObjectRef = if let Some(s) = stream.as_string() { match s.as_str() { - "console" => make_stdout_object(vm, wasm_builtins::sys_stdout_write_console), - _ => return Err(error()), + "console" => make_stdstream_object(vm, class_name, console_write), + _ => return Err(error(attr)), } - } else if stdout.is_function() { - let func = js_sys::Function::from(stdout); - make_stdout_object(vm, move |data, vm| { + } else if stream.is_function() { + let func = js_sys::Function::from(stream); + make_stdstream_object(vm, class_name, move |data, vm| { func.call1(&JsValue::UNDEFINED, &data.into()) .map_err(|err| convert::js_py_typeerror(vm, err))?; Ok(()) }) - } else if stdout.is_null() { - make_stdout_object(vm, |_, _| Ok(())) - } else if stdout.is_undefined() { - make_stdout_object(vm, wasm_builtins::sys_stdout_write_console) + } else if stream.is_null() { + make_stdstream_object(vm, class_name, |_, _| Ok(())) + } else if stream.is_undefined() { + make_stdstream_object(vm, class_name, console_write) } else { - return Err(error()); + return Err(error(attr)); }; - vm.sys_module.set_attr("stdout", stdout, vm).unwrap(); + vm.sys_module.set_attr(attr, stream, vm).unwrap(); Ok(()) })? } diff --git a/crates/wasm/src/wasm_builtins.rs b/crates/wasm/src/wasm_builtins.rs index efbc03c39..ae2ffa63d 100644 --- a/crates/wasm/src/wasm_builtins.rs +++ b/crates/wasm/src/wasm_builtins.rs @@ -16,19 +16,20 @@ pub fn sys_stdout_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<() Ok(()) } -pub fn make_stdout_object( +pub fn sys_stderr_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<()> { + console::error_1(&data.into()); + Ok(()) +} + +pub fn make_stdstream_object( vm: &VirtualMachine, + name: &'static str, write_f: impl Fn(&str, &VirtualMachine) -> PyResult<()> + 'static, ) -> PyObjectRef { let ctx = &vm.ctx; // there's not really any point to storing this class so that there's a consistent type object, // we just want a half-decent repr() output - let cls = PyRef::leak(py_class!( - ctx, - "JSStdout", - vm.ctx.types.object_type.to_owned(), - {} - )); + let cls = PyRef::leak(py_class!(ctx, name, vm.ctx.types.object_type.to_owned(), {})); let write_method = vm.new_method( "write", cls, diff --git a/wasm/demo/src/index.js b/wasm/demo/src/index.js index 0b568fa1d..aeab8e716 100644 --- a/wasm/demo/src/index.js +++ b/wasm/demo/src/index.js @@ -159,6 +159,7 @@ function onReady() { terminalVM = rp.vmStore.init('term_vm'); terminalVM.setStdout((data) => readline.print(data)); + terminalVM.setStderr((data) => readline.print(data)); readPrompts().catch((err) => console.error(err)); // so that the test knows that we're ready diff --git a/wasm/tests/test_exec_mode.py b/wasm/tests/test_exec_mode.py index a2a55846f..28a0cea7c 100644 --- a/wasm/tests/test_exec_mode.py +++ b/wasm/tests/test_exec_mode.py @@ -19,3 +19,17 @@ def test_exec_single_mode(wdriver): """ ) assert stdout == "2\n4\n" + + +def test_exec_stderr_option(wdriver): + stderr = wdriver.execute_script( + """ + let output = ""; + save_output = function(text) { + output += text + }; + window.rp.pyExec('import sys; print("err", file=sys.stderr)', {stderr: save_output}); + return output; + """ + ) + assert stderr == "err\n"