Skip to content

WASM Crate Architecture

Gyubong edited this page Sep 10, 2022 · 8 revisions

WASMVirtualMachine

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.

Converting from JS to Python

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

Converting from Python to JS

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