Skip to content

Commit 3290f28

Browse files
authored
wasm: add stderr support (#8318)
Assisted-by: Codex:gpt-5.5 Signed-off-by: Jiwoo Ahn <ikwydls1314@gmail.com>
1 parent 8754004 commit 3290f28

7 files changed

Lines changed: 73 additions & 24 deletions

File tree

‎crates/wasm/Lib/asyncweb.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ async def _main_wrapper(coro):
6464
import traceback
6565
import sys
6666

67-
# TODO: sys.stderr on wasm
68-
traceback.print_exc(file=sys.stdout)
67+
traceback.print_exc(file=sys.stderr)
6968

7069

7170
def _resolve(prom):

‎crates/wasm/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ pyEval(code, options?);
3434
- `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the
3535
native print function, and it will be `console.log` when giving `undefined`
3636
or "console", and it will be a dumb function when giving null.
37+
- `stderr?`: `"console" | ((out: string) => void) | null`: A function to replace
38+
`sys.stderr`, and it will be `console.error` when giving `undefined` or
39+
"console".
3740
3841
## License
3942

‎crates/wasm/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub mod eval {
6666
};
6767

6868
vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?;
69+
vm.set_stderr(Reflect::get(&options, &"stderr".into())?)?;
6970

7071
if let Some(js_vars) = js_vars {
7172
vm.add_to_scope("js_vars".into(), js_vars.into())?;
@@ -93,6 +94,8 @@ pub mod eval {
9394
/// - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the
9495
/// native print native print function, and it will be `console.log` when giving
9596
/// `undefined` or "console", and it will be a dumb function when giving null.
97+
/// - `stderr?`: `"console" | ((out: string) => void) | null`: A function to replace
98+
/// `sys.stderr`, and it will be `console.error` when giving `undefined` or "console".
9699
#[wasm_bindgen(js_name = pyEval)]
97100
pub fn eval_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
98101
run_py(source, options, Mode::Eval)

‎crates/wasm/src/vm_class.rs‎

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -249,31 +249,59 @@ impl WASMVirtualMachine {
249249

250250
#[wasm_bindgen(js_name = setStdout)]
251251
pub fn set_stdout(&self, stdout: JsValue) -> Result<(), JsValue> {
252+
self.set_stdstream(
253+
"stdout",
254+
"JSStdout",
255+
stdout,
256+
wasm_builtins::sys_stdout_write_console,
257+
)
258+
}
259+
260+
#[wasm_bindgen(js_name = setStderr)]
261+
pub fn set_stderr(&self, stderr: JsValue) -> Result<(), JsValue> {
262+
self.set_stdstream(
263+
"stderr",
264+
"JSStderr",
265+
stderr,
266+
wasm_builtins::sys_stderr_write_console,
267+
)
268+
}
269+
270+
fn set_stdstream(
271+
&self,
272+
attr: &'static str,
273+
class_name: &'static str,
274+
stream: JsValue,
275+
console_write: fn(&str, &VirtualMachine) -> PyResult<()>,
276+
) -> Result<(), JsValue> {
252277
self.with_vm(|vm, _| {
253-
fn error() -> JsValue {
254-
TypeError::new("Unknown stdout option, please pass a function or 'console'").into()
278+
fn error(attr: &str) -> JsValue {
279+
TypeError::new(&format!(
280+
"Unknown {attr} option, please pass a function or 'console'"
281+
))
282+
.into()
255283
}
256-
use wasm_builtins::make_stdout_object;
257-
let stdout: PyObjectRef = if let Some(s) = stdout.as_string() {
284+
use wasm_builtins::make_stdstream_object;
285+
let stream: PyObjectRef = if let Some(s) = stream.as_string() {
258286
match s.as_str() {
259-
"console" => make_stdout_object(vm, wasm_builtins::sys_stdout_write_console),
260-
_ => return Err(error()),
287+
"console" => make_stdstream_object(vm, class_name, console_write),
288+
_ => return Err(error(attr)),
261289
}
262-
} else if stdout.is_function() {
263-
let func = js_sys::Function::from(stdout);
264-
make_stdout_object(vm, move |data, vm| {
290+
} else if stream.is_function() {
291+
let func = js_sys::Function::from(stream);
292+
make_stdstream_object(vm, class_name, move |data, vm| {
265293
func.call1(&JsValue::UNDEFINED, &data.into())
266294
.map_err(|err| convert::js_py_typeerror(vm, err))?;
267295
Ok(())
268296
})
269-
} else if stdout.is_null() {
270-
make_stdout_object(vm, |_, _| Ok(()))
271-
} else if stdout.is_undefined() {
272-
make_stdout_object(vm, wasm_builtins::sys_stdout_write_console)
297+
} else if stream.is_null() {
298+
make_stdstream_object(vm, class_name, |_, _| Ok(()))
299+
} else if stream.is_undefined() {
300+
make_stdstream_object(vm, class_name, console_write)
273301
} else {
274-
return Err(error());
302+
return Err(error(attr));
275303
};
276-
vm.sys_module.set_attr("stdout", stdout, vm).unwrap();
304+
vm.sys_module.set_attr(attr, stream, vm).unwrap();
277305
Ok(())
278306
})?
279307
}

‎crates/wasm/src/wasm_builtins.rs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,20 @@ pub fn sys_stdout_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<()
1616
Ok(())
1717
}
1818

19-
pub fn make_stdout_object(
19+
pub fn sys_stderr_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<()> {
20+
console::error_1(&data.into());
21+
Ok(())
22+
}
23+
24+
pub fn make_stdstream_object(
2025
vm: &VirtualMachine,
26+
name: &'static str,
2127
write_f: impl Fn(&str, &VirtualMachine) -> PyResult<()> + 'static,
2228
) -> PyObjectRef {
2329
let ctx = &vm.ctx;
2430
// there's not really any point to storing this class so that there's a consistent type object,
2531
// we just want a half-decent repr() output
26-
let cls = PyRef::leak(py_class!(
27-
ctx,
28-
"JSStdout",
29-
vm.ctx.types.object_type.to_owned(),
30-
{}
31-
));
32+
let cls = PyRef::leak(py_class!(ctx, name, vm.ctx.types.object_type.to_owned(), {}));
3233
let write_method = vm.new_method(
3334
"write",
3435
cls,

‎wasm/demo/src/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ function onReady() {
159159

160160
terminalVM = rp.vmStore.init('term_vm');
161161
terminalVM.setStdout((data) => readline.print(data));
162+
terminalVM.setStderr((data) => readline.print(data));
162163
readPrompts().catch((err) => console.error(err));
163164

164165
// so that the test knows that we're ready

‎wasm/tests/test_exec_mode.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,17 @@ def test_exec_single_mode(wdriver):
1919
"""
2020
)
2121
assert stdout == "2\n4\n"
22+
23+
24+
def test_exec_stderr_option(wdriver):
25+
stderr = wdriver.execute_script(
26+
"""
27+
let output = "";
28+
save_output = function(text) {
29+
output += text
30+
};
31+
window.rp.pyExec('import sys; print("err", file=sys.stderr)', {stderr: save_output});
32+
return output;
33+
"""
34+
)
35+
assert stderr == "err\n"

0 commit comments

Comments
 (0)