-
Notifications
You must be signed in to change notification settings - Fork 1.3k
WASM Crate Architecture
The core of the rustpython_wasm
crate is the WASMVirtualMachine
class. All
it contains a single id
field:
struct WASMVirtualMachine {
id: String,
}
It has a few methods, most notably eval()
, exec()
, and execSingle()
, which
each takes a source string and corresponds to the different compilation modes
Eval
, Exec
, and Single
. There's also setStdout()
, which takes a JS
function or the string "console"
and sets the print()
function in the Python
builtin module to either call that function or print using console.log()
.
There's addToScope()
, which takes an identifier string and a JS value and sets
that name to that value. Lastly, there's injectModule()
, which takes a module
name string and a JS object and injects that object as a module into the
stdlib_inits
on the VM.
But where is the VM? All that WASMVirtualMachine
stores is an id string. Well,
what id corresponds to a key in a static[1] HashMap
STORED_VMS
.
static STORED_VMS: RefCell<HashMap<String, Rc<StoredVirtualMachine>>>
Yeah, four closing braces at the end, yikes. You may notice that the innermost
value type isn't a rustpython_vm::VirtualMachine
, it's a
StoredVirtualMachine
. That's defined as such:
struct StoredVirtualMachine {
pub vm: VirtualMachine,
pub scope: RefCell<Scope>,
held_objects: RefCell<Vec<PyObjectRef>>,
}
There's the rustpython_vm::VirtualMachine
! held_rcs
I'll come back to later,
but why is the Scope
necessary to be stored with a WASM VM?
In RustPython, scopes are stored separately from the VM, so if we want to have a
persistent across vm.exec()
calls, we need to store that scope somewhere. If
we wanted to, we could store a Vec or HashMap of scopes, and allow JS users to
choose which scope they want to execute in for a given exec()
, but this is
fine for now.
So, when we call one of the WASMVM methods from JS, it makes sure that there
is a VM for that id (in case the VM had been deleted since the JS caller got
the VM), gets that VM, manipulates its scope or its VirtualMachine
, and
returns.
This functionality is in the fn convert::js_to_py
, in wasm/lib/convert.rs
.
Strings, numbers, arrays, null
, undefined
, and TypedArrays can all be mapped
one-to-one to Python types. Functions are garbage collected, so all that's
necessary is to convert each of the arguments to the Python function that's
being called, and make the kwargs the this
argument. Objects are sort of
tricky, because in JS they can be used as maps, classes, "modules",or what have
you, so just converting it to a Python dict as we are right now probably isn't
the easiest way to use objects from Python
This functionality is in the fn convert::py_to_js
, in wasm/lib/convert.rs
.
This is mostly the same as JS to Python for dicts, numbers, bytes, etc. The one
spot this is more tricky is for functions. I'd recommend reading the
wasm-bindgen documentation for passing closures to JS
before continuing with this section, to give you a background on the pitfalls
with this. Essentially, we have to create a wasm_bindgen::closure::Closure
that contains inside a reference to the PyObjectRef
for the function and a
reference to a VM in order to execute that function. The first part is pretty
easy, and we use the held_rcs
field in StoredVirtualMachine from before while
keeping a Weak reference in order to ensure that the reference to the function
object is only held until the VM it belongs to is deallocated. We also keep a
Weak
reference to the StoredVirtualMachine
, so that if the VM is deleted
from JS while the closure is active, and then it's called, there's no catastrophic
failure: we just throw an error and we're done.
[1]: It's actually a thread_local variable, so that the borrow checker doesn't
complain about VirtualMachine not being Send + Sync
as is necessary for a
static