Class: Dommy::Js::Wasmtime::VM

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/js/wasmtime/vm.rb

Overview

A wasmtime-rb host for an mruby-wasm-js build (e.g. the Lilac runtime). It implements the two import modules the wasm needs:

- `js`                       the 25-function handle-table interop ABI,
                             routed to a pluggable JS engine (Engines::Quickjs
                             by default) whose values implement the bridge
                             ABI (__js_get__/__js_set__/__js_call__/__js_new__)
- `wasi_snapshot_preview1`   wasmtime's bundled WASI preview1, with fd_write
                             shadowed to capture mruby stdout/stderr

Extracted from lilac’s reference host (test/ruby_spec/mruby_wasm.rb).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(wasm:, engine: nil) ⇒ VM

Returns a new instance of VM.

Parameters:

  • wasm (String)

    path to the mruby-wasm-js .wasm (e.g. lilac.wasm)

  • engine (#global, #eval, #make_callback, #run_until_idle, ) (defaults to: nil)

    the JS engine backing the bridge. Defaults to a QuickJS VM bound to a fresh Dommy window (real JS over a real DOM).



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/dommy/js/wasmtime/vm.rb', line 31

def initialize(wasm:, engine: nil)
  @wasm_path = wasm.to_s
  @engine = engine || default_engine
  @stdout_buf = String.new(encoding: Encoding::BINARY)
  @stderr_buf = String.new(encoding: Encoding::BINARY)
  wire_console!

  # Handle table. id 0 = undefined/null sentinel; id 1 = the JS global
  # (engine.global). User values (primitives, JsRefs, Ruby Hash/Array)
  # live at ids >= 100.
  @handles = { 0 => nil, 1 => @engine.global }
  @next_handle = 100
  # A JS-side exception captured during js_call (a host callback can't
  # raise out of a wasmtime host function — it would unwind the wasm
  # runtime), handed back to mruby via the js_take_error import so it
  # surfaces as JS::Error instead of crashing the host.
  @pending_error = 0
  boot!
end

Instance Attribute Details

#engineObject (readonly)

Returns the value of attribute engine.



25
26
27
# File 'lib/dommy/js/wasmtime/vm.rb', line 25

def engine
  @engine
end

#wasm_pathObject (readonly)

Returns the value of attribute wasm_path.



25
26
27
# File 'lib/dommy/js/wasmtime/vm.rb', line 25

def wasm_path
  @wasm_path
end

Instance Method Details

#documentObject

The Dommy document/window the engine renders into — the same DOM the wasm runtime mutates. Host-side tests drive and inspect it via Dommy’s Ruby API.



53
# File 'lib/dommy/js/wasmtime/vm.rb', line 53

def document = @engine.document

#drain_async!Object

Drive the engine’s event loop to quiescence.



89
# File 'lib/dommy/js/wasmtime/vm.rb', line 89

def drain_async! = @engine.run_until_idle

#eval(source) ⇒ Object

Evaluate mruby source. Returns mruby’s exit code (0 on success, 1 on error, 2 if the compiler is absent). After the eval, drives the event loop so fibers suspended on ‘.await` settle (the Ruby-host equivalent of the browser/Node event loop unwinding the stack).



60
61
62
63
64
65
66
67
# File 'lib/dommy/js/wasmtime/vm.rb', line 60

def eval(source)
  handle = store_handle(source.b)
  rc = @js_eval_handle.call(handle, 0, 0)
  drain_async!
  rc
ensure
  @handles.delete(handle) if handle
end

#eval!(source) ⇒ Object

Like #eval but raises RubyError on a non-zero exit code.

Raises:



70
71
72
73
74
75
# File 'lib/dommy/js/wasmtime/vm.rb', line 70

def eval!(source)
  rc = eval(source)
  raise RubyError, "mruby eval failed (rc=#{rc})#{stderr_tail}" unless rc.zero?

  rc
end

#invoke_callback(callback_id, args) ⇒ Object

Fire an mruby block registered via ‘JS.callback`. `args` are Ruby values (already unwrapped by the engine); they cross as a single handle to the args array.



94
95
96
97
98
99
100
101
# File 'lib/dommy/js/wasmtime/vm.rb', line 94

def invoke_callback(callback_id, args)
  args_handle = store_handle(args)
  result_handle = @js_invoke_proc.call(callback_id, args_handle)
  @handles[result_handle]
ensure
  @handles.delete(args_handle) if args_handle
  @handles.delete(result_handle) if result_handle && result_handle >= 100
end

#load_bytecode(bytes) ⇒ Object

Load pre-compiled mrbc bytecode. Bytes flow in via the handle table as an array-like (js_load_irep_handle reads length + indexed bytes).



79
80
81
82
83
84
85
86
# File 'lib/dommy/js/wasmtime/vm.rb', line 79

def load_bytecode(bytes)
  handle = store_handle(bytes.bytes)
  rc = @js_load_irep_handle.call(handle)
  drain_async!
  rc
ensure
  @handles.delete(handle) if handle
end

#stderrObject



110
111
112
113
114
# File 'lib/dommy/js/wasmtime/vm.rb', line 110

def stderr
  out = @stderr_buf
  @stderr_buf = String.new(encoding: Encoding::BINARY)
  out
end

#stdoutObject

Captured stdout/stderr since the last read (clears the buffer).



104
105
106
107
108
# File 'lib/dommy/js/wasmtime/vm.rb', line 104

def stdout
  out = @stdout_buf
  @stdout_buf = String.new(encoding: Encoding::BINARY)
  out
end

#windowObject



54
# File 'lib/dommy/js/wasmtime/vm.rb', line 54

def window = @engine.window