Skip to content

Commit f49448a

Browse files
authored
Merge pull request RustPython#2402 from RustPython/wasm-hooks
Add a way to inject modules to rustpython_wasm
2 parents 52df7fb + be1bacd commit f49448a

File tree

4 files changed

+133
-82
lines changed

4 files changed

+133
-82
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wasm/lib/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"]
1313
[features]
1414
default = ["freeze-stdlib"]
1515
freeze-stdlib = ["rustpython-vm/freeze-stdlib"]
16+
no-start-func = []
1617

1718
[dependencies]
1819
rustpython-parser = { path = "../../parser" }
@@ -24,6 +25,7 @@ wasm-bindgen-futures = "0.4"
2425
serde-wasm-bindgen = "0.1"
2526
serde = "1.0"
2627
js-sys = "0.3"
28+
console_error_panic_hook = "0.1"
2729
# make parking_lot use wasm-bingden for instant
2830
parking_lot = { version = "0.11", features = ["wasm-bindgen"] }
2931

wasm/lib/src/lib.rs

Lines changed: 107 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,106 +7,131 @@ pub mod wasm_builtins;
77
#[macro_use]
88
extern crate rustpython_vm;
99

10-
use js_sys::{Object, Reflect, TypeError};
11-
use rustpython_vm::compile::Mode;
10+
pub(crate) use vm_class::weak_vm;
11+
12+
use js_sys::{Reflect, WebAssembly::RuntimeError};
1213
use std::panic;
1314
use wasm_bindgen::prelude::*;
1415

15-
pub use crate::convert::PyError;
16-
pub use crate::vm_class::*;
17-
18-
const PY_EVAL_VM_ID: &str = "__py_eval_vm";
16+
pub use vm_class::add_init_func;
1917

20-
fn panic_hook(info: &panic::PanicInfo) {
18+
/// Sets error info on the window object, and prints the backtrace to console
19+
pub fn panic_hook(info: &panic::PanicInfo) {
2120
// If something errors, just ignore it; we don't want to panic in the panic hook
22-
use js_sys::WebAssembly::RuntimeError;
23-
let window = match web_sys::window() {
24-
Some(win) => win,
25-
None => return,
21+
let try_set_info = || {
22+
let msg = &info.to_string();
23+
let window = match web_sys::window() {
24+
Some(win) => win,
25+
None => return,
26+
};
27+
let _ = Reflect::set(&window, &"__RUSTPYTHON_ERROR_MSG".into(), &msg.into());
28+
let error = RuntimeError::new(&msg);
29+
let _ = Reflect::set(&window, &"__RUSTPYTHON_ERROR".into(), &error);
30+
let stack = match Reflect::get(&error, &"stack".into()) {
31+
Ok(stack) => stack,
32+
Err(_) => return,
33+
};
34+
let _ = Reflect::set(&window, &"__RUSTPYTHON_ERROR_STACK".into(), &stack);
2635
};
27-
let msg = &info.to_string();
28-
let _ = Reflect::set(&window, &"__RUSTPYTHON_ERROR_MSG".into(), &msg.into());
29-
let error = RuntimeError::new(&msg);
30-
let _ = Reflect::set(&window, &"__RUSTPYTHON_ERROR".into(), &error);
31-
let stack = match Reflect::get(&error, &"stack".into()) {
32-
Ok(stack) => stack,
33-
Err(_) => return,
34-
};
35-
let _ = Reflect::set(&window, &"__RUSTPYTHON_ERROR_STACK".into(), &stack);
36+
try_set_info();
37+
console_error_panic_hook::hook(info);
3638
}
3739

40+
#[doc(hidden)]
41+
#[cfg(not(feature = "no-start-func"))]
3842
#[wasm_bindgen(start)]
39-
pub fn setup_console_error() {
43+
pub fn _setup_console_error() {
4044
std::panic::set_hook(Box::new(panic_hook));
4145
}
4246

43-
fn run_py(source: &str, options: Option<Object>, mode: Mode) -> Result<JsValue, JsValue> {
44-
let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true));
45-
let options = options.unwrap_or_else(Object::new);
46-
let js_vars = {
47-
let prop = Reflect::get(&options, &"vars".into())?;
48-
if prop.is_undefined() {
49-
None
50-
} else if prop.is_object() {
51-
Some(Object::from(prop))
52-
} else {
53-
return Err(TypeError::new("vars must be an object").into());
47+
pub mod eval {
48+
use crate::vm_class::VMStore;
49+
use js_sys::{Object, Reflect, TypeError};
50+
use rustpython_vm::compile::Mode;
51+
use wasm_bindgen::prelude::*;
52+
53+
const PY_EVAL_VM_ID: &str = "__py_eval_vm";
54+
55+
fn run_py(source: &str, options: Option<Object>, mode: Mode) -> Result<JsValue, JsValue> {
56+
let vm = VMStore::init(PY_EVAL_VM_ID.into(), Some(true));
57+
let options = options.unwrap_or_else(Object::new);
58+
let js_vars = {
59+
let prop = Reflect::get(&options, &"vars".into())?;
60+
if prop.is_undefined() {
61+
None
62+
} else if prop.is_object() {
63+
Some(Object::from(prop))
64+
} else {
65+
return Err(TypeError::new("vars must be an object").into());
66+
}
67+
};
68+
69+
vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?;
70+
71+
if let Some(js_vars) = js_vars {
72+
vm.add_to_scope("js_vars".into(), js_vars.into())?;
5473
}
55-
};
74+
vm.run(source, mode, None)
75+
}
5676

57-
vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?;
77+
/// Evaluate Python code
78+
///
79+
/// ```js
80+
/// var result = pyEval(code, options?);
81+
/// ```
82+
///
83+
/// `code`: `string`: The Python code to run in eval mode
84+
///
85+
/// `options`:
86+
///
87+
/// - `vars?`: `{ [key: string]: any }`: Variables passed to the VM that can be
88+
/// accessed in Python with the variable `js_vars`. Functions do work, and
89+
/// receive the Python kwargs as the `this` argument.
90+
/// - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the
91+
/// native print native print function, and it will be `console.log` when giving
92+
/// `undefined` or "console", and it will be a dumb function when giving null.
93+
#[wasm_bindgen(js_name = pyEval)]
94+
pub fn eval_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
95+
run_py(source, options, Mode::Eval)
96+
}
5897

59-
if let Some(js_vars) = js_vars {
60-
vm.add_to_scope("js_vars".into(), js_vars.into())?;
98+
/// Evaluate Python code
99+
///
100+
/// ```js
101+
/// pyExec(code, options?);
102+
/// ```
103+
///
104+
/// `code`: `string`: The Python code to run in exec mode
105+
///
106+
/// `options`: The options are the same as eval mode
107+
#[wasm_bindgen(js_name = pyExec)]
108+
pub fn exec_py(source: &str, options: Option<Object>) -> Result<(), JsValue> {
109+
run_py(source, options, Mode::Exec).map(drop)
61110
}
62-
vm.run(source, mode, None)
63-
}
64111

65-
/// Evaluate Python code
66-
///
67-
/// ```js
68-
/// var result = pyEval(code, options?);
69-
/// ```
70-
///
71-
/// `code`: `string`: The Python code to run in eval mode
72-
///
73-
/// `options`:
74-
///
75-
/// - `vars?`: `{ [key: string]: any }`: Variables passed to the VM that can be
76-
/// accessed in Python with the variable `js_vars`. Functions do work, and
77-
/// receive the Python kwargs as the `this` argument.
78-
/// - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the
79-
/// native print native print function, and it will be `console.log` when giving
80-
/// `undefined` or "console", and it will be a dumb function when giving null.
81-
#[wasm_bindgen(js_name = pyEval)]
82-
pub fn eval_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
83-
run_py(source, options, Mode::Eval)
112+
/// Evaluate Python code
113+
///
114+
/// ```js
115+
/// var result = pyExecSingle(code, options?);
116+
/// ```
117+
///
118+
/// `code`: `string`: The Python code to run in exec single mode
119+
///
120+
/// `options`: The options are the same as eval mode
121+
#[wasm_bindgen(js_name = pyExecSingle)]
122+
pub fn exec_single_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
123+
run_py(source, options, Mode::Single)
124+
}
84125
}
85126

86-
/// Evaluate Python code
87-
///
88-
/// ```js
89-
/// pyExec(code, options?);
90-
/// ```
91-
///
92-
/// `code`: `string`: The Python code to run in exec mode
93-
///
94-
/// `options`: The options are the same as eval mode
95-
#[wasm_bindgen(js_name = pyExec)]
96-
pub fn exec_py(source: &str, options: Option<Object>) -> Result<(), JsValue> {
97-
run_py(source, options, Mode::Exec).map(drop)
127+
/// A module containing all the wasm-bindgen exports that rustpython_wasm has
128+
/// Re-export as `pub use rustpython_wasm::exports::*;` in the root of your crate if you want your
129+
/// wasm module to mimic rustpython_wasm's API
130+
pub mod exports {
131+
pub use crate::convert::PyError;
132+
pub use crate::eval::{eval_py, exec_py, exec_single_py};
133+
pub use crate::vm_class::{VMStore, WASMVirtualMachine};
98134
}
99135

100-
/// Evaluate Python code
101-
///
102-
/// ```js
103-
/// var result = pyExecSingle(code, options?);
104-
/// ```
105-
///
106-
/// `code`: `string`: The Python code to run in exec single mode
107-
///
108-
/// `options`: The options are the same as eval mode
109-
#[wasm_bindgen(js_name = pyExecSingle)]
110-
pub fn exec_single_py(source: &str, options: Option<Object>) -> Result<JsValue, JsValue> {
111-
run_py(source, options, Mode::Single)
112-
}
136+
#[doc(hidden)]
137+
pub use exports::*;

wasm/lib/src/vm_class.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ impl StoredVirtualMachine {
4040
setup_browser_module(vm);
4141
}
4242

43+
VM_INIT_FUNCS.with(|cell| {
44+
for f in cell.borrow().iter() {
45+
f(vm)
46+
}
47+
});
48+
4349
scope = Some(vm.new_scope_with_builtins());
4450

4551
InitParameter::Internal
@@ -53,11 +59,18 @@ impl StoredVirtualMachine {
5359
}
5460
}
5561

62+
/// Add a hook to add builtins or frozen modules to the RustPython VirtualMachine while it's
63+
/// initializing.
64+
pub fn add_init_func(f: fn(&mut VirtualMachine)) {
65+
VM_INIT_FUNCS.with(|cell| cell.borrow_mut().push(f))
66+
}
67+
5668
// It's fine that it's thread local, since WASM doesn't even have threads yet. thread_local!
5769
// probably gets compiled down to a normal-ish static varible, like Atomic* types do:
5870
// https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html#atomic-instructions
5971
thread_local! {
6072
static STORED_VMS: RefCell<HashMap<String, Rc<StoredVirtualMachine>>> = RefCell::default();
73+
static VM_INIT_FUNCS: RefCell<Vec<fn(&mut VirtualMachine)>> = RefCell::default();
6174
}
6275

6376
pub fn get_vm_id(vm: &VirtualMachine) -> &str {

0 commit comments

Comments
 (0)