wasm: add stderr support (#8318)

Assisted-by: Codex:gpt-5.5

Signed-off-by: Jiwoo Ahn <ikwydls1314@gmail.com>
This commit is contained in:
Jiwoo Ahn authored and GitHub committed 2026-07-19 14:13:47 +09:00
1 parent 875400413f
commit 3290f287fd
7 files changed
+73 -24

No files matched your search

+1 -2
View File
@@ -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):
+3
View File
@@ -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
+3
View File
@@ -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<Object>) -> Result<JsValue, JsValue> {
run_py(source, options, Mode::Eval)
+43 -15
View File
@@ -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(())
})?
}
+8 -7
View File
@@ -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,
+1
View File
@@ -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
+14
View File
@@ -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"