///|
/// The WASM engine of the target: WAMR on native, `WebAssembly` on js.
pub struct Engine {
priv features : Array[Feature]
priv bounds : Bounds
}
///|
/// The engine of the target.
pub fn Engine::default() -> Engine {
{ features: backend_features(), bounds: backend_bounds(), }
}
///|
/// The bounds that the engine enforces.
pub fn Engine::bounds(self : Engine) -> Bounds {
self.bounds
}
///|
/// The features beyond the core set that the engine supports.
pub fn Engine::features(self : Engine) -> Array[Feature] {
self.features.copy()
}
///|
fn signature_of(type_ : @binary.FuncType) -> Signature? {
let convert = (types : Array[@binary.ValType]) => {
let out : Array[ValueType] = []
for t in types {
match t {
I32 => out.push(I32)
I64 => out.push(I64)
F32 => out.push(F32)
F64 => out.push(F64)
_ => return None
}
}
Some(out)
}
match (convert(type_.params), convert(type_.results)) {
(Some(params), Some(results)) => Some({ params, results, })
_ => None
}
}
///|
/// A compiled module.
pub struct Module {
priv parsed : @binary.Module
priv handle : ModuleHandle
}
///|
/// Compiles `bytes`. A module that needs a feature that the engine does not support fails with
/// `FeatureUnsupported` and the name of the feature, before any guest code runs.
pub fn Engine::compile(
self : Engine,
bytes : Bytes,
) -> Module raise EngineError {
let parsed = @binary.Module::parse(bytes) catch {
DecodeError(at, rule) => raise Invalid("byte \{at}: \{rule}")
}
for feature in parsed.features().map(Feature::of) {
if !self.features.contains(feature) {
raise FeatureUnsupported(feature)
}
}
if parsed.imports_memory() {
raise Invalid(
"the module imports its memory; the engine gives each instance its own memory",
)
}
// Both engines compile the prepared module: its start function is an export that the instantiation
// calls with the bounds of the caller, and its memory is exported under a reserved name.
let (prepared_bytes, prepared) = (() => {
let prepared_bytes = parsed.prepare()
(prepared_bytes, @binary.Module::parse(prepared_bytes))
})() catch {
DecodeError(at, rule) => raise Invalid("byte \{at}: \{rule}")
}
{ parsed, handle: backend_compile(prepared_bytes, prepared), }
}
///|
fn extern_kind(
module_ : @binary.Module,
kind : @binary.ExportKind,
index : Int32,
) -> ExternKind {
match kind {
Func =>
Function(
match module_.function_type(index) {
Some(type_) => signature_of(type_)
None => None
},
)
Table => Table
Memory => Memory
Global => Global
Tag => Tag
}
}
///|
/// The imports of the module, in order.
pub fn Module::imports(self : Module) -> Array[ImportDescriptor] {
self.parsed.imports.map(i => {
module_: i.module_,
name: i.name,
kind: match i.type_ {
Func(type_) => Function(signature_of(type_))
Table => Table
Memory(_) => Memory
Global => Global
Tag => Tag
},
})
}
///|
/// The exports of the module, in order.
pub fn Module::exports(self : Module) -> Array[ExportDescriptor] {
self.parsed.exports.map(e => {
name: e.name,
kind: extern_kind(self.parsed, e.kind, e.index),
})
}
///|
/// The initial pages of the module's memory, or `None` when it has no memory.
pub fn Module::initial_pages(self : Module) -> Int32? {
match self.parsed.memory() {
Some(limits) => Some(limits.min.to_int())
None => None
}
}
///|
/// What an import is bound to.
priv enum Binding {
Host(Signature, (Caller, Array[Value]) -> Array[Value] raise HostError)
Export(Function)
}
///|
priv struct ImportEntry {
module_ : String
name : String
binding : Binding
}
///|
/// The host imports of one instantiation: MoonBit functions and exported functions of other
/// instances, by module name and field name.
pub struct Imports {
priv entries : Array[ImportEntry]
}
///|
pub fn Imports::new() -> Imports {
{ entries: [], }
}
///|
/// Binds the import `module_.name` to a MoonBit function with the signature `signature`. The
/// function gets the calling instance and the arguments, and gives the results.
pub fn Imports::host(
self : Imports,
module_ : String,
name : String,
signature : Signature,
function : (Caller, Array[Value]) -> Array[Value] raise HostError,
) -> Imports {
self.entries.push({ module_, name, binding: Host(signature, function), })
self
}
///|
/// Binds the import `module_.name` to an exported function of another instance, so that a guest call
/// of the import runs in that instance.
pub fn Imports::function(
self : Imports,
module_ : String,
name : String,
function : Function,
) -> Imports {
self.entries.push({ module_, name, binding: Export(function), })
self
}
///|
fn Imports::find(self : Imports, module_ : String, name : String) -> Binding? {
match
self.entries.iter().find_first(e => e.module_ == module_ && e.name == name) {
Some(entry) => Some(entry.binding)
None => None
}
}
///|
/// The binding of each function import of `module_`, in import order. Every import must be a
/// function with a binding of the same signature.
fn Module::link(
self : Module,
imports : Imports,
) -> Array[(String, String, Binding)] raise EngineError {
let bound = []
for i in self.parsed.imports {
let label = "\{i.module_}.\{i.name}"
guard i.type_ is Func(type_) else {
raise LinkError(label, "only function imports can be linked")
}
guard signature_of(type_) is Some(wanted) else {
raise LinkError(
label, "the import has a type other than i32, i64, f32 and f64",
)
}
guard imports.find(i.module_, i.name) is Some(binding) else {
raise LinkError(label, "nothing is linked for the import \{wanted}")
}
let given = match binding {
Host(signature, _) => signature
Export(function) => function.signature
}
guard given == wanted else {
raise LinkError(
label,
"the module imports \{wanted}, and the given function is \{given}",
)
}
bound.push((i.module_, i.name, binding))
}
bound
}
///|
/// An instance of a module, with its own memory.
pub struct Instance {
priv module_ : Module
priv handle : InstanceHandle
priv mut usable : Bool
/// A refused `memory.grow` that `take_refused_grow` has not given yet.
priv mut refused : Bool
/// Instances whose exports this instance calls; they live as long as this one.
priv linked : Array[Instance]
}
///|
/// Instantiates the module with `imports` and a memory ceiling of `memory_ceiling` pages. A
/// `memory.grow` past the ceiling returns -1 to the guest. The start function of the module, if it
/// has one, runs after the instance exists, as a call with `budget` and `deadline`; when it fails, the
/// instantiation fails with `Start`.
pub fn Module::instantiate(
self : Module,
imports : Imports,
memory_ceiling~ : PageCount = PageCount::default_ceiling(),
budget? : Budget,
deadline? : Deadline,
) -> Instance raise EngineError {
let ceiling = memory_ceiling.pages()
if self.initial_pages() is Some(initial) && initial > ceiling {
raise CeilingBelowInitial(ceiling, initial)
}
let bindings = self.link(imports)
let linked = bindings.filter_map(b => {
match b.2 {
Export(function) => Some(function.instance)
Host(_, _) => None
}
})
let caller : Ref[Instance?] = Ref(None)
let handle = backend_instantiate(
self.handle,
self.parsed,
bindings,
ceiling,
caller,
)
let instance = {
module_: self,
handle,
usable: true,
refused: false,
linked,
}
caller.val = Some(instance)
if self.parsed.start is Some(_) {
(instance.invoke(
@binary.start_export,
Signature::new([], []),
[],
budget,
deadline,
)
|> ignore) catch {
error => raise Start(error)
}
}
instance
}
///|
fn check_arguments(
name : String,
signature : Signature,
args : Array[Value],
) -> Unit raise CallError {
guard args.length() == signature.params.length() &&
args
.iter()
.zip(signature.params.iter())
.all(pair => pair.0.value_type() == pair.1) else {
raise ArgumentMismatch(name, "the export takes \{signature}")
}
}
///|
/// Calls the exported function `name` with `args`. With a budget, the call stops with
/// `BudgetExhausted` when it runs more instructions; with a deadline, it stops with `Interrupted`
/// when the deadline passes, where the engine enforces these bounds. After a trap, an exhausted
/// budget, an interruption, a memory limit or a host error the instance takes no more calls; a host
/// function's `Exit(0)` ends the call and leaves the instance usable.
pub fn Instance::call(
self : Instance,
name : String,
args : Array[Value],
budget? : Budget,
deadline? : Deadline,
) -> Array[Value] raise CallError {
guard self.usable else { raise Unusable }
guard self.module_.parsed.export_type(name) is Some(type_) else {
raise UnknownExport(name)
}
guard signature_of(type_) is Some(signature) else {
raise ArgumentMismatch(
name, "the export has a type other than i32, i64, f32 and f64",
)
}
check_arguments(name, signature, args)
self.invoke(name, signature, args, budget, deadline)
}
///|
/// Calls the export `name` of the compiled module, which can be an export that the engine adds, and
/// reads the refused grows of the call.
fn Instance::invoke(
self : Instance,
name : String,
signature : Signature,
args : Array[Value],
budget : Budget?,
deadline : Deadline?,
) -> Array[Value] raise CallError {
// A refusal before this call stays recorded for `take_refused_grow`; this call reads its own.
self.refused = self.refused || backend_take_refused_grow(self.handle)
let results = backend_call(
self.handle,
name,
signature,
args,
budget,
deadline,
) catch {
Host(error) => {
self.refused = self.refused || backend_take_refused_grow(self.handle)
// A clean exit ends the call and leaves the instance usable.
if !(error is Exit(0)) {
self.usable = false
}
raise Host(error)
}
error => {
self.usable = false
let refused = backend_take_refused_grow(self.handle)
self.refused = self.refused || refused
raise if refused { MemoryLimit } else { error }
}
}
self.refused = self.refused || backend_take_refused_grow(self.handle)
results
}
///|
/// Whether the instance exports a function with the name `name`. No guest code runs.
pub fn Instance::has_function(self : Instance, name : String) -> Bool {
self.module_.parsed.export_type(name) is Some(_)
}
///|
/// Whether a `memory.grow` of this instance was refused since the last call of this function. A
/// caller that links instances, such as the plugin API with its kernel, reads it for each call.
pub fn Instance::take_refused_grow(self : Instance) -> Bool {
let refused = self.refused || backend_take_refused_grow(self.handle)
self.refused = false
refused
}
///|
/// Whether the instance takes more calls: false after a trap of the instance or of an instance whose
/// exports it calls.
pub fn Instance::is_usable(self : Instance) -> Bool {
self.usable && self.linked.iter().all(i => i.is_usable())
}
///|
/// The exported function `name`, to link it as an import of another instance.
pub fn Instance::export_function(self : Instance, name : String) -> Function? {
match self.module_.parsed.export_type(name) {
Some(type_) =>
match signature_of(type_) {
Some(signature) => Some({ instance: self, name, signature, })
None => None
}
None => None
}
}
///|
/// The memory of the instance, or `None` when it has none.
pub fn Instance::memory(self : Instance) -> Memory? {
match self.module_.parsed.memory() {
Some(_) => Some({ handle: self.handle, })
None => None
}
}
///|
/// An exported function of an instance.
pub struct Function {
priv instance : Instance
priv name : String
priv signature : Signature
}
///|
pub fn Function::signature(self : Function) -> Signature {
self.signature
}
///|
/// The instance whose host function a guest calls.
pub struct Caller {
priv memory : Memory?
}
///|
/// The memory of the calling instance, or `None` when it has none.
pub fn Caller::memory(self : Caller) -> Memory? {
self.memory
}
///|
/// The linear memory of an instance.
pub struct Memory {
priv handle : InstanceHandle
}
///|
/// The size of the memory in bytes.
pub fn Memory::size(self : Memory) -> Int64 {
backend_memory_size(self.handle)
}
///|
/// The `length` bytes at `offset`. An access outside the memory raises `OutOfBounds` and reads
/// nothing.
pub fn Memory::read(
self : Memory,
offset : Int64,
length : Int32,
) -> Bytes raise MemoryError {
guard length >= 0 && offset >= 0L && offset + length.to_int64() <= self.size() else {
raise OutOfBounds(offset, length)
}
backend_memory_read(self.handle, offset, length)
}
///|
/// Writes `bytes` at `offset`. An access outside the memory raises `OutOfBounds` and writes nothing.
pub fn Memory::write(
self : Memory,
offset : Int64,
bytes : BytesView,
) -> Unit raise MemoryError {
guard offset >= 0L && offset + bytes.length().to_int64() <= self.size() else {
raise OutOfBounds(offset, bytes.length())
}
backend_memory_write(self.handle, offset, bytes)
}
///|
/// The memory of an instance is the guest memory of the WASI sandbox, on native and on js alike.
pub impl @wasi.GuestMemory for Memory with fn size(self) {
Memory::size(self)
}
///|
pub impl @wasi.GuestMemory for Memory with fn read(self, offset, length) {
Memory::read(self, offset, length) catch {
OutOfBounds(at, count) => raise @wasi.OutOfBounds(at, count)
}
}
///|
pub impl @wasi.GuestMemory for Memory with fn write(self, offset, bytes) {
Memory::write(self, offset, bytes) catch {
OutOfBounds(at, count) => raise @wasi.OutOfBounds(at, count)
}
}