Class: Lilac::CLI::WasmMrbcDriver

Inherits:
Object
  • Object
show all
Defined in:
lib/lilac/cli/build/wasm_mrbc_driver.rb

Overview

Drives mrbc-host.wasm from the host Ruby via wasmtime-rb to produce mruby bytecode in place of an external mrbc binary.

Wire-level ABI (defined in runtime/mruby-host-compile/src/host_compile.c):

compile_source(src_ptr, src_len,
             out_ptr_outp, out_len_outp,
             err_ptr_outp, err_len_outp) -> i32 status
mrbc_alloc(len) -> i32 ptr   (host writes/reads through this buffer)
mrbc_free(ptr)               (host frees both input and output)

Status codes:

0  ok           — irep bytes in out_ptr / out_len
1  compile fail — utf-8 message in err_ptr / err_len
2  no compiler  — wasm was built without mruby-compiler

Lifecycle: the engine + module + instance are created once on the first compile call and reused across subsequent calls within a single BytecodeBuilder invocation. mruby's mrb_state itself is opened/closed inside the wasm per call (see compile_source in host_compile.c) — this matches mrbc binary's process-per-invocation semantics and keeps symbol-table growth from leaking across builds.

Defined Under Namespace

Classes: CompileError, Error, WasmExportMissingError, WasmtimeMissingError

Constant Summary collapse

REQUIRED_EXPORTS =
%w[compile_source mrbc_alloc mrbc_free memory].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(wasm_path:) ⇒ WasmMrbcDriver

Returns a new instance of WasmMrbcDriver.



69
70
71
# File 'lib/lilac/cli/build/wasm_mrbc_driver.rb', line 69

def initialize(wasm_path:)
  @wasm_path = wasm_path
end

Instance Attribute Details

#wasm_pathObject (readonly)

Returns the value of attribute wasm_path.



73
74
75
# File 'lib/lilac/cli/build/wasm_mrbc_driver.rb', line 73

def wasm_path
  @wasm_path
end

Class Method Details

.available?(wasm_path:) ⇒ Boolean

Test whether a usable backend can be constructed from wasm_path. Returns true only when wasmtime-rb is loadable AND the module parses + instantiates AND all required exports are present.

Used by BytecodeBuilder#resolve_backend! to decide between the wasm-driven and binary mrbc paths; also surfaced via lilac doctor so users can see exactly why the wasm path was/wasn't picked.

wasmtime-rb's Module doesn't expose exports directly, so we actually instantiate to enumerate the exports. The cost (~200-500ms cold) is paid once per lilac build, which is acceptable for a build-time tool.

Returns:

  • (Boolean)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/lilac/cli/build/wasm_mrbc_driver.rb', line 47

def self.available?(wasm_path:)
  return false unless wasm_path && File.file?(wasm_path)

  begin
    require 'wasmtime'
  rescue LoadError
    return false
  end

  begin
    engine = Wasmtime::Engine.new(wasm_exceptions: true)
    mod = Wasmtime::Module.from_file(engine, wasm_path)
    linker = Wasmtime::Linker.new(engine)
    Wasmtime::WASI::P1.add_to_linker_sync(linker)
    store = Wasmtime::Store.new(engine, wasi_p1_config: Wasmtime::WasiConfig.new)
    instance = linker.instantiate(store, mod)
    REQUIRED_EXPORTS.all? { |name| !instance.export(name).nil? }
  rescue Wasmtime::Error, ArgumentError
    false
  end
end

Instance Method Details

#compile(ruby_source) ⇒ Object

Compile a Ruby source string and return the raw mruby bytecode (binary string, starts with "RITE" magic). Raises CompileError on syntax/semantic errors (carrying the parser's error buffer text) or WasmExportMissingError / WasmtimeMissingError on environment problems.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/lilac/cli/build/wasm_mrbc_driver.rb', line 80

def compile(ruby_source)
  ensure_instance!

  # mrbc_alloc returns 0 on size <= 0 or malloc failure. We allocate
  # six buffers per call:
  #   src_buf    — caller-written source bytes
  #   out_p/out_l — out-params written by compile_source on success
  #   err_p/err_l — out-params written by compile_source on failure
  # The three out-param pairs are 4-byte i32 each.
  src_bytes = ruby_source.b
  src_buf = @alloc.call(src_bytes.bytesize)
  raise CompileError, 'mrbc_alloc returned 0 for src buffer' if src_buf.zero?

  out_p = @alloc.call(4)
  out_l = @alloc.call(4)
  err_p = @alloc.call(4)
  err_l = @alloc.call(4)
  if [out_p, out_l, err_p, err_l].any?(&:zero?)
    [src_buf, out_p, out_l, err_p, err_l].each { |p| @free.call(p) unless p.zero? }
    raise CompileError, 'mrbc_alloc returned 0 for out-param buffers'
  end

  @memory.write(src_buf, src_bytes)

  status = @compile.call(src_buf, src_bytes.bytesize, out_p, out_l, err_p, err_l)

  irep_ptr = read_i32(out_p)
  irep_len = read_i32(out_l)
  err_ptr  = read_i32(err_p)
  err_len  = read_i32(err_l)

  case status
  when 0
    bytecode = @memory.read(irep_ptr, irep_len)
    @free.call(irep_ptr)
    bytecode
  when 1
    message = err_len.positive? ? @memory.read(err_ptr, err_len).force_encoding('UTF-8') : '(no message)'
    @free.call(err_ptr) unless err_ptr.zero?
    raise CompileError, message
  when 2
    raise WasmExportMissingError,
          'mrbc-host.wasm was built without mruby-compiler — rebuild with build_config/mrbc-host.rb'
  else
    raise CompileError, "compile_source returned unexpected status=#{status}"
  end
ensure
  # Free the input buffer and the four out-param scratch buffers.
  # irep_ptr / err_ptr were freed inside the case branches because
  # they're the wasm's own malloc'd outputs (separate from the
  # 4-byte holders the host allocated for the pointer values).
  [src_buf, out_p, out_l, err_p, err_l].compact.each do |p|
    @free.call(p) unless p.zero?
  end
end