A JavaScript library for executing snapshots created by the Microvium compiler in the browser or Node.js.
npm install @microvium/runtime
Write a guest script:
// guest.mjs
const print = vmImport(1);
function sayHello(name) {
print(`Hello, ${name}!`)
}
vmExport(1, sayHello);
Compile the guest script using the Microvium CLI:
microvium guest.mjs --output-bytes
Write a host script:
// host.mjs
import Microvium from '@microvium/runtime';
function print(str) {
console.log(str);
}
// Restore the snapshot
const snapshot = [/* paste snapshot bytes here */];
const imports = { 1: print };
const vm = await Microvium.restore(snapshot, imports);
const { 1: sayHello } = vm.exports;
// Call the guest
sayHello('World');
Run the host script:
node host.mjs # prints "Hello, World!"
- As on a microcontroller, scripts running in Microvium can only use up to 64kB of RAM.
- The WebAssembly memory is pre-allocated as 256 kB (see memory usage below), no matter how small the actual script is.
- Objects and arrays passed into the VM from the host are always passed by copy, not by reference. Only plain-old-data objects can be passed this way.
- Objects and functions passed out of the VM to the host are not identity-preserving, meaning that if you pass the same object multiple times, you get a different proxy in the host each time.
- Object prototypes are not preserved when passing objects between the VM and host.
Terminology:
- Host: the program outside the Microvium VM. E.g. the node.js app or browser app.
- Guest: the program inside the Microvium VM.
const vm = Microvium.restore(snapshot, imports, opts);
Restore a given snapshot to a running VM. Does not execute any code in the VM.
Returns the VM instance.
The snapshot can be either a Uint8Array
or a plain array of bytes.
The imports object is a map of numeric function IDs to host functions. The function IDs must be in the range 0 to 0xFFFF. The host functions are called with the arguments passed by the Microvium script, and the return value is passed back to the Microvium script.
The opts
object is optional and can contain the following properties:
opts.breakpointHit
: See Debug interface below.
The returned vm
has an exports
property which has a similar structure to the imports
except contains the functions exported by the Microvium script.
vm.createSnapshot()
Returns a Uint8Array
that is suitable to pass back to Microvium.restore
or run on an embedded device.
-
vm.stopAfterNInstructions(number)
stop the VM afternumber
instructions have been executed. Each time this is called will reset the counter to the given value. Pass-1
to disable the gas counter. -
vm.getRemainingInstructions()
returns the number of instructions remaining before the VM will stop. Returns-1
if the gas counter is disabled.
engineVersion
: The version of the Microvium engine.requiredEngineVersion
: The version of the Microvium engine that the snapshot was compiled for.exports
: The exports of the Microvium script (see Restore a snapshot above).runGC()
: Run the garbage collector.getMemoryStats()
: Get the memory usage statistics.
Pass a breakpoint callback handler to the opts
provided to Microvium.restore
to enable debugging. The breakpoint handler is called when a breakpoint is hit. The breakpoint handler is called with the address of the breakpoint that was hit.
The currently running bytecode address can also be inspected with vm.currentAddress
.
Set a breakpoint with vm.setBreakpoint(address)
or remove it with vm.removeBreakpoint(address)
.
The addresses correspond to the Microvium bytecode addresses that you see if you compile a script with the option --output-disassembly
. There is no way at present to map these addresses back to the original source code (but feel free to make a PR if want to implement this for me -- it would be really useful).
Values can be passed to and from the Microvium VM as function arguments and return values. The library wrapper code does its best to convert Microvium JavaScript types to host JavaScript types and vice versa.
Primitive values are always passed by copy (by value).
Everything passed into Microvium is passed by copy, since a Microvium VM has no Proxy
type that would allow it to have mutable references to host objects.
Plain objects, arrays, and classes are passed out of Microvium by reference -- the wrapper library maintains a Proxy
of the Microvium object, so that the host may mutate the Microvium object by interacting with the proxy. The proxy does not preserve the original prototype of the object.
The passing of an object to the guest by copy means each of the plain-old-data fields are copied individually. That does not include class methods or any other fields from the prototype. For certain kinds of objects such as Promise
, Map
, and Set
, passing from the host to the guest in this manner will very likely not be what was intended, so Microvium will throw an error rather than copying all the own enumerable properties into a new guest object.
Uint8Array
is passed out of Microvium not as a host Uint8Array
but as a MicroviumUint8Array
which has methods slice
and set
to read and write to it respectively. The slice
method returns a copy of the requested data range as a host Uint8Array
.
Functions and closures are be passed out of Microvium by reference. Host functions cannot be passed into Microvium at all at runtime, but can be imported from the host at build-time using vmImport
and then satisfied by the importMap
.
As noted above, a host Promise
cannot be passed from the host to the guest. However, the guest can directly call a host async
function and the result will be a guest promise which the guest can safely await
. This allows the host to expose asynchronous APIs to a guest. Example:
// guest.js
const hostAsyncFunction = vmImport(1);
const print = vmImport(2);
vmExport(1, run);
async function run() {
const result = await hostAsyncFunction();
print(`The result is ${result}`);
}
// host.js
async function hostAsyncFunction() {
// Delay 1000ms
await new Promise(resolve => setTimeout(resolve, 1000));
// Return 42
return 42;
}
const imports = { 1: hostAsyncFunction, 2: console.log };
const vm = Microvium.restore(snapshot, imports);
const { 1: run } = vm.exports;
run();
The bundled library (dist/index.js
) is about 83kB and has no external dependencies. It's implemented as a lightweight JavaScript wrapper around a WebAssembly build of microvium.c
, to run in the browser or in Node.js.
Each Microvium instance is a fixed size and takes 4 pages of WASM memory (a total of 256kB):
- Page 0: Main RAM page for the Microvium VM and heap
- Page 1: A copy of the snapshot
- Page 2: Working memory for C runtime (C stack, .data memory, etc)
- Page 3: Reserved for future use
Page 0 is used for the Microvium heap because Microvium pointer values are internally 16-bit integers and this this allows them to map directly to WASM memory offsets without any translation, making it very efficient.
Please help me develop/maintain this!
See also ./src/developer-notes.md.