- Notifications
You must be signed in to change notification settings - Fork1.3k
WASM Crate Architecture
The core of therustpython_wasm
crate is theWASMVirtualMachine
class. Allit contains a singleid
field:
structWASMVirtualMachine{id:String,}
It has a few methods, most notablyeval()
,exec()
, andexecSingle()
, whicheach takes a source string and corresponds to the different compilation modesEval
,Exec
, andSingle
. There's alsosetStdout()
, which takes a JSfunction or the string"console"
and sets theprint()
function in the Pythonbuiltin module to either call that function or print usingconsole.log()
.There'saddToScope()
, which takes an identifier string and a JS value and setsthat name to that value. Lastly, there'sinjectModule()
, which takes a modulename string and a JS object and injects that object as a module into thestdlib_inits
on the VM.
But where is the VM? All thatWASMVirtualMachine
stores is an id string. Well,what id corresponds to a key in a static[1]HashMap
STORED_VMS
.
staticSTORED_VMS:RefCell<HashMap<String,Rc<StoredVirtualMachine>>>
Yeah, four closing braces at the end, yikes. You may notice that the innermostvalue type isn't arustpython_vm::VirtualMachine
, it's aStoredVirtualMachine
. That's defined as such:
structStoredVirtualMachine{pubvm:VirtualMachine,pubscope:RefCell<Scope>,held_objects:RefCell<Vec<PyObjectRef>>,}
There's therustpython_vm::VirtualMachine
!held_rcs
I'll come back to later,but why is theScope
necessary to be stored with a WASM VM?
In RustPython, scopes are stored separately from the VM, so if we want to have apersistent acrossvm.exec()
calls, we need to store that scope somewhere. Ifwe wanted to, we could store a Vec or HashMap of scopes, and allow JS users tochoose which scope they want to execute in for a givenexec()
, but this isfine for now.
So, when we call one of the WASMVM methods from JS, it makes sure that thereis a VM for that id (in case the VM had been deleted since the JS caller gotthe VM), gets that VM, manipulates its scope or itsVirtualMachine
, andreturns.
This functionality is in the fnconvert::js_to_py
, inwasm/lib/convert.rs
.
Strings, numbers, arrays,null
,undefined
, and TypedArrays can all be mappedone-to-one to Python types. Functions are garbage collected, so all that'snecessary is to convert each of the arguments to the Python function that'sbeing called, and make the kwargs thethis
argument. Objects are sort oftricky, because in JS they can be used as maps, classes, "modules",or what haveyou, so just converting it to a Python dict as we are right now probably isn'tthe easiest way to use objects from Python
This functionality is in the fnconvert::py_to_js
, inwasm/lib/convert.rs
.
This is mostly the same as JS to Python for dicts, numbers, bytes, etc. The onespot this is more tricky is for functions. I'd recommend reading thewasm-bindgen documentation for passing closures to JSbefore continuing with this section, to give you a background on the pitfallswith this. Essentially, we have to create awasm_bindgen::closure::Closure
that contains inside a reference to thePyObjectRef
for the function and areference to a VM in order to execute that function. The first part is prettyeasy, and we use theheld_rcs
field in StoredVirtualMachine from before whilekeeping a Weak reference in order to ensure that the reference to the functionobject is only held until the VM it belongs to is deallocated. We also keep aWeak
reference to theStoredVirtualMachine
, so that if the VM is deletedfrom JS while the closure is active, and then it's called, there's no catastrophicfailure: we just throw an error and we're done.
[1]: It's actually a thread_local variable, so that the borrow checker doesn'tcomplain about VirtualMachine not beingSend + Sync
as is necessary for astatic