use anyhow::{anyhow, bail, Context, Result};
use std::collections::HashMap;
use wasm_encoder::{
BlockType, CodeSection, ConstExpr, DataSection, EntityType, ExportKind, ExportSection,
Function, FunctionSection, GlobalSection, GlobalType, ImportSection, MemorySection, MemoryType,
Module, TypeSection, ValType,
};
use wit_bindgen_core::abi::lift_from_memory;
use wit_component::{
decode, embed_component_metadata, ComponentEncoder, DecodedWasm, StringEncoding,
};
use wit_parser::abi::{AbiVariant, FlatTypes, WasmSignature, WasmType};
use wit_parser::{
Function as WitFunction, Handle, InterfaceId, LiftLowerAbi, Mangling, ManglingAndAbi, Resolve,
ResourceIntrinsic, SizeAlign, Type, TypeDefKind, TypeId, TypeOwner, WasmExport, WasmExportKind,
WasmImport, WorldItem, WorldKey,
};
use super::abi::WasmEncoderBindgen;
use super::indices::{DispatchIndices, FunctionIndices};
use super::mem_layout::MemoryLayoutBuilder;
pub(crate) fn build_adapter(
target_interface: &str,
has_before: bool,
has_after: bool,
has_blocking: bool,
split_bytes: &[u8],
tier1_world_wit: &str,
) -> Result<Vec<u8>> {
let mut resolve = decode_input_resolve(split_bytes)?;
let target_iface = find_target_interface(&resolve, target_interface)?;
require_supported_case(&resolve, target_iface, has_blocking)?;
resolve
.push_str("splicer-tier1.wit", tier1_world_wit)
.context("parse tier1 WIT")?;
let world_pkg = resolve
.push_str(
"splicer-adapter.wit",
&synthesize_adapter_world_wit(target_interface, has_before, has_after, has_blocking),
)
.context("parse synthesized adapter world WIT")?;
let world_id = resolve
.select_world(&[world_pkg], Some(ADAPTER_WORLD_NAME))
.context("select adapter world")?;
let mut core_module = build_dispatch_module(
&resolve,
world_id,
target_iface,
target_interface,
has_before,
has_after,
has_blocking,
);
embed_component_metadata(&mut core_module, &resolve, world_id, StringEncoding::UTF8)
.context("embed_component_metadata")?;
ComponentEncoder::default()
.validate(true)
.module(&core_module)
.context("ComponentEncoder::module")?
.encode()
.context("ComponentEncoder::encode")
}
fn decode_input_resolve(split_bytes: &[u8]) -> Result<Resolve> {
let decoded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| decode(split_bytes)))
.map_err(|_| {
anyhow!(
"wit-parser panic during component decode — likely the import + re-export \
of a resource-bearing instance (upstream issue \
https://github.com/bytecodealliance/wasm-tools/issues/2506). The new emit \
path can't proceed until that's fixed upstream."
)
})?
.context("wit_component::decode split")?;
match decoded {
DecodedWasm::Component(resolve, _world) => Ok(resolve),
DecodedWasm::WitPackage(_, _) => bail!(
"split bytes decoded to a WIT package; \
expected a component"
),
}
}
fn find_target_interface(resolve: &Resolve, target_interface: &str) -> Result<InterfaceId> {
resolve
.interfaces
.iter()
.find(|(id, _)| resolve.id_of(*id).as_deref() == Some(target_interface))
.map(|(id, _)| id)
.ok_or_else(|| {
anyhow!(
"interface `{target_interface}` not found in \
the decoded WIT; available: {:?}",
resolve
.interfaces
.iter()
.filter_map(|(id, _)| resolve.id_of(id))
.collect::<Vec<_>>()
)
})
}
fn require_supported_case(
resolve: &Resolve,
target_iface: InterfaceId,
has_blocking: bool,
) -> Result<()> {
let iface = &resolve.interfaces[target_iface];
if iface.functions.is_empty() {
bail!("interface has no functions");
}
for (ty_name, &tid) in &iface.types {
let td = &resolve.types[tid];
if matches!(td.kind, TypeDefKind::Resource)
&& matches!(td.owner, TypeOwner::Interface(owner) if owner == target_iface)
{
let iface_name = resolve
.id_of(target_iface)
.unwrap_or_else(|| iface.name.clone().unwrap_or_default());
bail!(
"interface `{iface_name}` declares resource `{ty_name}` inline. \
Splicer's wrapper-component pattern can't preserve resource \
type identity for inline resources — runtime handle traffic \
between the import side and export side will be rejected. \
Move `{ty_name}` into a sibling `types` interface and \
reference it via `use types.{{{ty_name}}}` (the wasi-style \
factored-types pattern)."
);
}
}
for (name, func) in &iface.functions {
if has_blocking && func.result.is_some() {
bail!(
"Function '{name}' returns a value but the middleware exports \
`should-block-call`. Tier-1 blocking is only supported for \
void-returning functions because the adapter cannot synthesize \
a return value when the call is blocked."
);
}
if func.kind.is_async() {
let import_sig = resolve.wasm_signature(AbiVariant::GuestImportAsync, func);
if import_sig.indirect_params {
bail!(
"async function `{name}` has params that overflow \
MAX_FLAT_ASYNC_PARAMS (4) and require lower-to-memory; \
not yet implemented"
);
}
}
}
Ok(())
}
fn synthesize_adapter_world_wit(
target_interface: &str,
has_before: bool,
has_after: bool,
has_blocking: bool,
) -> String {
use crate::contract::{
versioned_interface, TIER1_AFTER, TIER1_BEFORE, TIER1_BLOCKING, TIER1_VERSION,
};
let mut wit = format!("package {ADAPTER_WORLD_PACKAGE};\n\nworld {ADAPTER_WORLD_NAME} {{\n");
wit.push_str(&format!(" import {target_interface};\n"));
wit.push_str(&format!(" export {target_interface};\n"));
let mut import_hook = |iface: &str| {
wit.push_str(&format!(
" import {};\n",
versioned_interface(iface, TIER1_VERSION)
));
};
if has_before {
import_hook(TIER1_BEFORE);
}
if has_after {
import_hook(TIER1_AFTER);
}
if has_blocking {
import_hook(TIER1_BLOCKING);
}
wit.push_str("}\n");
wit
}
struct TaskReturnImport {
module: String,
name: String,
sig: WasmSignature,
}
struct FuncDispatch {
import_module: String,
import_field: String,
export_name: String,
is_async: bool,
export_sig: WasmSignature,
import_sig: WasmSignature,
result_ty: Option<Type>,
task_return: Option<TaskReturnImport>,
name_offset: i32,
name_len: i32,
retptr_offset: Option<i32>,
borrow_drops: Vec<(u32, TypeId)>,
}
impl FuncDispatch {
fn direct_result(&self) -> Option<ValType> {
if !self.export_sig.retptr && self.export_sig.results.len() == 1 {
Some(wasm_type_to_val(self.export_sig.results[0]))
} else {
None
}
}
}
fn val_types(types: &[WasmType]) -> Vec<ValType> {
types.iter().copied().map(wasm_type_to_val).collect()
}
fn collect_borrow_drops(resolve: &Resolve, func: &WitFunction) -> Vec<(u32, TypeId)> {
let mut out = Vec::new();
let mut flat_idx: u32 = 0;
for param in &func.params {
if let Type::Id(tid) = param.ty {
if let TypeDefKind::Handle(Handle::Borrow(rid)) = &resolve.types[tid].kind {
out.push((flat_idx, resolve_type_alias(resolve, *rid)));
flat_idx += 1;
continue;
}
}
let mut storage = vec![WasmType::I32; 32];
let mut flat = FlatTypes::new(storage.as_mut_slice());
if !resolve.push_flat(¶m.ty, &mut flat) {
return Vec::new();
}
flat_idx += flat.to_vec().len() as u32;
}
out
}
fn resolve_type_alias(resolve: &Resolve, mut tid: TypeId) -> TypeId {
while let TypeDefKind::Type(Type::Id(next)) = &resolve.types[tid].kind {
tid = *next;
}
tid
}
const BUMP_POINTER_GLOBAL: u32 = 0;
const ADAPTER_WORLD_PACKAGE: &str = "splicer:adapter";
const ADAPTER_WORLD_NAME: &str = "adapter";
const ASYNC_INTRINSIC_MODULE: &str = "$root";
const WAITABLE_SET_NEW: &str = "[waitable-set-new]";
const WAITABLE_JOIN: &str = "[waitable-join]";
const WAITABLE_SET_WAIT: &str = "[waitable-set-wait]";
const WAITABLE_SET_DROP: &str = "[waitable-set-drop]";
const SUBTASK_DROP: &str = "[subtask-drop]";
const EXPORT_MEMORY: &str = "memory";
const EXPORT_CABI_REALLOC: &str = "cabi_realloc";
const EXPORT_INITIALIZE: &str = "_initialize";
struct TypeIndices {
handler_ty: Vec<u32>,
wrapper_ty: Vec<u32>,
task_return_ty: Vec<Option<u32>>,
hook_ty: u32,
block_hook_ty: Option<u32>,
init_ty: u32,
cabi_post_ty: u32,
cabi_realloc_ty: u32,
async_runtime: Option<AsyncRuntimeTypes>,
resource_drop_ty: Option<u32>,
}
struct AsyncRuntimeTypes {
waitable_new_ty: u32,
waitable_join_ty: u32,
waitable_wait_ty: u32,
void_i32_ty: u32,
}
struct FuncIndices {
imp_handler: Vec<u32>,
imp_before: Option<u32>,
imp_after: Option<u32>,
imp_block: Option<u32>,
imp_task_return: Vec<Option<u32>>,
wrapper_base: u32,
init: u32,
cabi_post: Vec<Option<u32>>,
cabi_realloc: Option<u32>,
block_result_ptr: Option<i32>,
async_runtime: Option<AsyncRuntimeFuncs>,
resource_drop: HashMap<TypeId, u32>,
}
struct AsyncRuntimeFuncs {
waitable_new: u32,
waitable_join: u32,
waitable_wait: u32,
waitable_drop: u32,
subtask_drop: u32,
event_ptr: i32,
}
fn build_dispatch_module(
resolve: &Resolve,
world_id: wit_parser::WorldId,
target_iface: InterfaceId,
target_interface_name: &str,
has_before: bool,
has_after: bool,
has_blocking: bool,
) -> Vec<u8> {
let funcs: Vec<&WitFunction> = resolve.interfaces[target_iface]
.functions
.values()
.collect();
let any_async_target = funcs.iter().any(|f| f.kind.is_async());
let needs_async_runtime = has_before || has_after || has_blocking || any_async_target;
let mut sizes = SizeAlign::default();
sizes.fill(resolve);
let (per_func, name_blob, event_ptr, block_result_ptr, bump_start) = compute_func_dispatches(
resolve,
&sizes,
target_iface,
target_interface_name,
&funcs,
needs_async_runtime,
has_blocking,
);
let hook_imports = collect_hook_imports(resolve, world_id, has_before, has_after, has_blocking);
let mut idx = DispatchIndices::new();
let mut module = Module::new();
let type_idx = emit_type_section(&mut module, &mut idx, &per_func, &hook_imports);
let func_idx = emit_imports_section(
&mut module,
&mut idx,
&per_func,
&type_idx,
&hook_imports,
event_ptr,
block_result_ptr,
resolve,
);
let func_idx = emit_function_section(&mut module, &mut idx, &per_func, &type_idx, func_idx);
emit_memory_and_globals(&mut module, bump_start);
emit_export_section(&mut module, &per_func, &func_idx);
emit_code_section(&mut module, resolve, &sizes, &per_func, &func_idx);
emit_data_section(&mut module, &name_blob);
module.finish()
}
#[allow(clippy::too_many_arguments)]
fn compute_func_dispatches(
resolve: &Resolve,
sizes: &SizeAlign,
target_iface: InterfaceId,
target_interface_name: &str,
funcs: &[&WitFunction],
needs_async_runtime: bool,
has_blocking: bool,
) -> (Vec<FuncDispatch>, Vec<u8>, Option<i32>, Option<i32>, u32) {
let qualified_names: Vec<String> = funcs
.iter()
.map(|f| format!("{target_interface_name}#{}", f.name))
.collect();
let total_name_bytes: u32 = qualified_names.iter().map(|n| n.len() as u32).sum();
let mut layout = MemoryLayoutBuilder::new(total_name_bytes);
let mut name_blob: Vec<u8> = Vec::with_capacity(total_name_bytes as usize);
let mut per_func: Vec<FuncDispatch> = Vec::with_capacity(funcs.len());
let target_world_key = WorldKey::Interface(target_iface);
for (func, qualified_name) in funcs.iter().zip(qualified_names.iter()) {
let is_async = func.kind.is_async();
let (import_variant, export_variant) = if is_async {
(
AbiVariant::GuestImportAsync,
AbiVariant::GuestExportAsyncStackful,
)
} else {
(AbiVariant::GuestImport, AbiVariant::GuestExport)
};
let mangling = ManglingAndAbi::Legacy(if is_async {
LiftLowerAbi::AsyncStackful
} else {
LiftLowerAbi::Sync
});
let (import_module, import_field) = resolve.wasm_import_name(
mangling,
WasmImport::Func {
interface: Some(&target_world_key),
func,
},
);
let export_name = resolve.wasm_export_name(
mangling,
WasmExport::Func {
interface: Some(&target_world_key),
func,
kind: WasmExportKind::Normal,
},
);
let export_sig = resolve.wasm_signature(export_variant, func);
let import_sig = resolve.wasm_signature(import_variant, func);
let name_offset = layout.alloc_name(qualified_name.len() as u32) as i32;
name_blob.extend_from_slice(qualified_name.as_bytes());
let retptr_needed = if is_async {
import_sig.retptr
} else {
export_sig.retptr
};
let retptr_offset = retptr_needed.then(|| {
let result_ty = func
.result
.as_ref()
.expect("retptr_needed → func.result is_some()");
let size = sizes.size(result_ty).size_wasm32() as u32;
let align = sizes.align(result_ty).align_wasm32() as u32;
layout.alloc_retptr_scratch(size, align) as i32
});
let task_return = is_async.then(|| {
let (module, name, sig) =
func.task_return_import(resolve, Some(&target_world_key), Mangling::Legacy);
TaskReturnImport { module, name, sig }
});
let borrow_drops = collect_borrow_drops(resolve, func);
per_func.push(FuncDispatch {
import_module,
import_field,
export_name,
is_async,
export_sig,
import_sig,
result_ty: func.result,
task_return,
name_offset,
name_len: qualified_name.len() as i32,
retptr_offset,
borrow_drops,
});
}
let event_ptr = needs_async_runtime.then(|| layout.alloc_event_slot() as i32);
let block_result_ptr = has_blocking.then(|| layout.alloc_block_result() as i32);
let bump_start = layout.finish_as_bump_start();
(per_func, name_blob, event_ptr, block_result_ptr, bump_start)
}
fn wasm_type_to_val(wt: WasmType) -> ValType {
match wt {
WasmType::I32 | WasmType::Pointer | WasmType::Length => ValType::I32,
WasmType::I64 | WasmType::PointerOrI64 => ValType::I64,
WasmType::F32 => ValType::F32,
WasmType::F64 => ValType::F64,
}
}
struct HookImport {
module: String,
name: String,
sig: WasmSignature,
}
struct HookImports {
before: Option<HookImport>,
after: Option<HookImport>,
blocking: Option<HookImport>,
}
impl HookImports {
fn any(&self) -> bool {
self.before.is_some() || self.after.is_some() || self.blocking.is_some()
}
}
fn collect_hook_imports(
resolve: &Resolve,
world_id: wit_parser::WorldId,
has_before: bool,
has_after: bool,
has_blocking: bool,
) -> HookImports {
use crate::contract::{
versioned_interface, TIER1_AFTER, TIER1_BEFORE, TIER1_BLOCKING, TIER1_VERSION,
};
let world = &resolve.worlds[world_id];
let resolve_one = |iface_name: &str| -> Option<HookImport> {
world.imports.iter().find_map(|(key, item)| {
let WorldItem::Interface { id, .. } = item else {
return None;
};
if resolve.id_of(*id).as_deref() != Some(iface_name) {
return None;
}
let func = resolve.interfaces[*id].functions.values().next()?;
let (module, name) = resolve.wasm_import_name(
ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback),
WasmImport::Func {
interface: Some(key),
func,
},
);
let sig = resolve.wasm_signature(AbiVariant::GuestImportAsync, func);
Some(HookImport { module, name, sig })
})
};
let pick = |active: bool, iface: &str| -> Option<HookImport> {
if !active {
return None;
}
resolve_one(&versioned_interface(iface, TIER1_VERSION))
};
HookImports {
before: pick(has_before, TIER1_BEFORE),
after: pick(has_after, TIER1_AFTER),
blocking: pick(has_blocking, TIER1_BLOCKING),
}
}
fn emit_type_section(
module: &mut Module,
idx: &mut DispatchIndices,
per_func: &[FuncDispatch],
hook_imports: &HookImports,
) -> TypeIndices {
let mut types = TypeSection::new();
let mut handler_ty: Vec<u32> = Vec::with_capacity(per_func.len());
let mut wrapper_ty: Vec<u32> = Vec::with_capacity(per_func.len());
let mut task_return_ty: Vec<Option<u32>> = vec![None; per_func.len()];
for (i, fd) in per_func.iter().enumerate() {
types.ty().function(
val_types(&fd.import_sig.params),
val_types(&fd.import_sig.results),
);
handler_ty.push(idx.alloc_ty());
types.ty().function(
val_types(&fd.export_sig.params),
val_types(&fd.export_sig.results),
);
wrapper_ty.push(idx.alloc_ty());
if let Some(tr) = &fd.task_return {
types
.ty()
.function(val_types(&tr.sig.params), val_types(&tr.sig.results));
task_return_ty[i] = Some(idx.alloc_ty());
}
}
let hook_sig = hook_imports
.before
.as_ref()
.or(hook_imports.after.as_ref())
.map(|h| (val_types(&h.sig.params), val_types(&h.sig.results)))
.unwrap_or_else(|| (vec![], vec![]));
types.ty().function(hook_sig.0, hook_sig.1);
let hook_ty = idx.alloc_ty();
types.ty().function([], []);
let init_ty = idx.alloc_ty();
types.ty().function([ValType::I32], []);
let cabi_post_ty = idx.alloc_ty();
types.ty().function(
[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
[ValType::I32],
);
let cabi_realloc_ty = idx.alloc_ty();
let block_hook_ty = hook_imports.blocking.as_ref().map(|h| {
types
.ty()
.function(val_types(&h.sig.params), val_types(&h.sig.results));
idx.alloc_ty()
});
let needs_async_runtime = hook_imports.any() || per_func.iter().any(|f| f.is_async);
let async_runtime = needs_async_runtime.then(|| {
types.ty().function([], [ValType::I32]);
let waitable_new_ty = idx.alloc_ty();
types.ty().function([ValType::I32, ValType::I32], []);
let waitable_join_ty = idx.alloc_ty();
types
.ty()
.function([ValType::I32, ValType::I32], [ValType::I32]);
let waitable_wait_ty = idx.alloc_ty();
types.ty().function([ValType::I32], []);
let void_i32_ty = idx.alloc_ty();
AsyncRuntimeTypes {
waitable_new_ty,
waitable_join_ty,
waitable_wait_ty,
void_i32_ty,
}
});
let needs_resource_drop = per_func.iter().any(|f| !f.borrow_drops.is_empty());
let resource_drop_ty = needs_resource_drop.then(|| {
if let Some(art) = &async_runtime {
art.void_i32_ty
} else {
types.ty().function([ValType::I32], []);
idx.alloc_ty()
}
});
module.section(&types);
TypeIndices {
handler_ty,
wrapper_ty,
task_return_ty,
hook_ty,
block_hook_ty,
init_ty,
cabi_post_ty,
cabi_realloc_ty,
async_runtime,
resource_drop_ty,
}
}
#[allow(clippy::too_many_arguments)]
fn emit_imports_section(
module: &mut Module,
idx: &mut DispatchIndices,
per_func: &[FuncDispatch],
type_idx: &TypeIndices,
hook_imports: &HookImports,
event_ptr: Option<i32>,
block_result_ptr: Option<i32>,
resolve: &Resolve,
) -> FuncIndices {
let mut imports = ImportSection::new();
let mut imp_handler: Vec<u32> = Vec::with_capacity(per_func.len());
for (i, fd) in per_func.iter().enumerate() {
imports.import(
&fd.import_module,
&fd.import_field,
EntityType::Function(type_idx.handler_ty[i]),
);
imp_handler.push(idx.alloc_func());
}
let mut resource_drop: HashMap<TypeId, u32> = HashMap::new();
if let Some(drop_ty) = type_idx.resource_drop_ty {
let mut unique: Vec<TypeId> = per_func
.iter()
.flat_map(|f| f.borrow_drops.iter().map(|(_, rid)| *rid))
.collect();
unique.sort();
unique.dedup();
for rid in unique {
let owner_iface = match resolve.types[rid].owner {
TypeOwner::Interface(iid) => iid,
_ => continue,
};
let owner_key = WorldKey::Interface(owner_iface);
let imp = WasmImport::ResourceIntrinsic {
interface: Some(&owner_key),
resource: rid,
intrinsic: ResourceIntrinsic::ImportedDrop,
};
let (module_name, field_name) =
resolve.wasm_import_name(ManglingAndAbi::Legacy(LiftLowerAbi::Sync), imp);
imports.import(&module_name, &field_name, EntityType::Function(drop_ty));
resource_drop.insert(rid, idx.alloc_func());
}
}
let mut import_hook = |hook: &HookImport| {
imports.import(
&hook.module,
&hook.name,
EntityType::Function(type_idx.hook_ty),
);
idx.alloc_func()
};
let imp_before = hook_imports.before.as_ref().map(&mut import_hook);
let imp_after = hook_imports.after.as_ref().map(&mut import_hook);
let imp_block = hook_imports.blocking.as_ref().map(|hook| {
let ty = type_idx
.block_hook_ty
.expect("block_hook_ty allocated when blocking is active");
imports.import(&hook.module, &hook.name, EntityType::Function(ty));
idx.alloc_func()
});
let async_runtime = type_idx.async_runtime.as_ref().map(|art| {
let event_ptr = event_ptr.expect("event_ptr must be set when async_runtime is");
let mut import_intrinsic = |name: &str, ty: u32| {
imports.import(ASYNC_INTRINSIC_MODULE, name, EntityType::Function(ty));
idx.alloc_func()
};
let waitable_new = import_intrinsic(WAITABLE_SET_NEW, art.waitable_new_ty);
let waitable_join = import_intrinsic(WAITABLE_JOIN, art.waitable_join_ty);
let waitable_wait = import_intrinsic(WAITABLE_SET_WAIT, art.waitable_wait_ty);
let waitable_drop = import_intrinsic(WAITABLE_SET_DROP, art.void_i32_ty);
let subtask_drop = import_intrinsic(SUBTASK_DROP, art.void_i32_ty);
AsyncRuntimeFuncs {
waitable_new,
waitable_join,
waitable_wait,
waitable_drop,
subtask_drop,
event_ptr,
}
});
let mut imp_task_return: Vec<Option<u32>> = vec![None; per_func.len()];
for (i, fd) in per_func.iter().enumerate() {
if let Some(tr) = &fd.task_return {
let ty_idx = type_idx.task_return_ty[i].expect("task_return_ty allocated for async");
imports.import(&tr.module, &tr.name, EntityType::Function(ty_idx));
imp_task_return[i] = Some(idx.alloc_func());
}
}
module.section(&imports);
FuncIndices {
imp_handler,
imp_before,
imp_after,
imp_block,
imp_task_return,
wrapper_base: 0,
init: 0,
cabi_post: vec![None; per_func.len()],
cabi_realloc: None,
block_result_ptr,
async_runtime,
resource_drop,
}
}
fn emit_function_section(
module: &mut Module,
idx: &mut DispatchIndices,
per_func: &[FuncDispatch],
type_idx: &TypeIndices,
mut func_idx: FuncIndices,
) -> FuncIndices {
let mut fsec = FunctionSection::new();
let wrapper_base = idx.func;
for &t in &type_idx.wrapper_ty {
fsec.function(t);
}
for _ in per_func {
idx.alloc_func();
}
func_idx.wrapper_base = wrapper_base;
fsec.function(type_idx.init_ty);
func_idx.init = idx.alloc_func();
for (i, fd) in per_func.iter().enumerate() {
if fd.export_sig.retptr && !fd.is_async {
fsec.function(type_idx.cabi_post_ty);
func_idx.cabi_post[i] = Some(idx.alloc_func());
}
}
fsec.function(type_idx.cabi_realloc_ty);
func_idx.cabi_realloc = Some(idx.alloc_func());
module.section(&fsec);
func_idx
}
fn emit_memory_and_globals(module: &mut Module, bump_start: u32) {
let mut memory = MemorySection::new();
memory.memory(MemoryType {
minimum: 1,
maximum: None,
memory64: false,
shared: false,
page_size_log2: None,
});
module.section(&memory);
let mut globals = GlobalSection::new();
globals.global(
GlobalType {
val_type: ValType::I32,
mutable: true,
shared: false,
},
&ConstExpr::i32_const(bump_start as i32),
);
module.section(&globals);
}
fn emit_export_section(module: &mut Module, per_func: &[FuncDispatch], func_idx: &FuncIndices) {
let mut exports = ExportSection::new();
for (i, fd) in per_func.iter().enumerate() {
exports.export(
&fd.export_name,
ExportKind::Func,
func_idx.wrapper_base + i as u32,
);
if let Some(post_idx) = func_idx.cabi_post[i] {
let post_name = format!("cabi_post_{}", fd.export_name);
exports.export(&post_name, ExportKind::Func, post_idx);
}
}
exports.export(EXPORT_MEMORY, ExportKind::Memory, 0);
let realloc_idx = func_idx
.cabi_realloc
.expect("cabi_realloc is always emitted");
exports.export(EXPORT_CABI_REALLOC, ExportKind::Func, realloc_idx);
exports.export(EXPORT_INITIALIZE, ExportKind::Func, func_idx.init);
module.section(&exports);
}
fn emit_code_section(
module: &mut Module,
resolve: &Resolve,
sizes: &SizeAlign,
per_func: &[FuncDispatch],
func_idx: &FuncIndices,
) {
let blocking =
func_idx
.imp_block
.zip(func_idx.block_result_ptr)
.map(|(import_fn, result_ptr)| BlockingConfig {
import_fn,
result_ptr,
});
let mut code = CodeSection::new();
for (i, fd) in per_func.iter().enumerate() {
if fd.is_async {
emit_async_wrapper_body(
&mut code,
resolve,
sizes,
fd,
func_idx.imp_handler[i],
func_idx.imp_before,
func_idx.imp_after,
blocking.as_ref(),
func_idx.imp_task_return[i].expect("async func must have task.return import"),
func_idx
.async_runtime
.as_ref()
.expect("async runtime imports active when any func is async"),
&func_idx.resource_drop,
);
} else {
emit_wrapper_body(
&mut code,
fd,
func_idx.imp_handler[i],
func_idx.imp_before,
func_idx.imp_after,
blocking.as_ref(),
func_idx.async_runtime.as_ref(),
&func_idx.resource_drop,
);
}
}
code.function(&empty_function());
for fd in per_func {
if fd.export_sig.retptr && !fd.is_async {
code.function(&empty_function());
}
}
emit_cabi_realloc(&mut code);
module.section(&code);
}
fn emit_data_section(module: &mut Module, name_blob: &[u8]) {
if name_blob.is_empty() {
return;
}
let mut data = DataSection::new();
data.active(0, &ConstExpr::i32_const(0), name_blob.iter().copied());
module.section(&data);
}
struct BlockingConfig {
import_fn: u32,
result_ptr: i32,
}
#[allow(clippy::too_many_arguments)]
fn emit_wrapper_body(
code: &mut CodeSection,
fd: &FuncDispatch,
imp_handler: u32,
imp_before: Option<u32>,
imp_after: Option<u32>,
blocking: Option<&BlockingConfig>,
async_runtime: Option<&AsyncRuntimeFuncs>,
resource_drop: &HashMap<TypeId, u32>,
) {
let nparams = fd.export_sig.params.len() as u32;
let mut locals = FunctionIndices::new(nparams);
let result_local = fd.direct_result().map(|t| locals.alloc_local(t));
let wait_locals = async_runtime.map(|_| {
let st = locals.alloc_local(ValType::I32);
let ws = locals.alloc_local(ValType::I32);
(st, ws)
});
let mut f = Function::new_with_locals_types(locals.into_locals());
if let Some(idx) = imp_before {
emit_hook_call(&mut f, fd, idx, async_runtime, wait_locals);
}
if let Some(blk) = blocking {
emit_blocking_phase(&mut f, fd, blk, async_runtime, wait_locals, None);
}
for p in 0..nparams {
f.instructions().local_get(p);
}
if fd.export_sig.retptr {
f.instructions()
.i32_const(fd.retptr_offset.expect("retptr_offset set"));
}
f.instructions().call(imp_handler);
if let Some(local) = result_local {
f.instructions().local_set(local);
}
if let Some(idx) = imp_after {
emit_hook_call(&mut f, fd, idx, async_runtime, wait_locals);
}
for (flat_idx, rid) in &fd.borrow_drops {
let drop_fn = resource_drop[rid];
f.instructions().local_get(*flat_idx);
f.instructions().call(drop_fn);
}
if let Some(local) = result_local {
f.instructions().local_get(local);
} else if fd.export_sig.retptr {
f.instructions()
.i32_const(fd.retptr_offset.expect("retptr_offset set"));
}
f.instructions().end();
code.function(&f);
}
fn emit_blocking_phase(
f: &mut Function,
fd: &FuncDispatch,
blk: &BlockingConfig,
async_runtime: Option<&AsyncRuntimeFuncs>,
wait_locals: Option<(u32, u32)>,
task_return_for_async: Option<u32>,
) {
f.instructions().i32_const(fd.name_offset);
f.instructions().i32_const(fd.name_len);
f.instructions().i32_const(blk.result_ptr);
f.instructions().call(blk.import_fn);
let art = async_runtime.expect("async_runtime active when blocking is");
let (st, ws) = wait_locals.expect("wait_locals allocated alongside async_runtime");
f.instructions().local_set(st);
emit_wait_loop(f, st, ws, art);
f.instructions().i32_const(blk.result_ptr);
f.instructions().i32_load(wasm_encoder::MemArg {
offset: 0,
align: 0,
memory_index: 0,
});
f.instructions().if_(BlockType::Empty);
if let Some(tr_fn) = task_return_for_async {
f.instructions().call(tr_fn);
}
f.instructions().return_();
f.instructions().end();
}
#[allow(clippy::too_many_arguments)]
fn emit_async_wrapper_body(
code: &mut CodeSection,
resolve: &Resolve,
sizes: &SizeAlign,
fd: &FuncDispatch,
imp_handler: u32,
imp_before: Option<u32>,
imp_after: Option<u32>,
blocking: Option<&BlockingConfig>,
imp_task_return: u32,
async_runtime: &AsyncRuntimeFuncs,
resource_drop: &HashMap<TypeId, u32>,
) {
let nparams = fd.export_sig.params.len() as u32;
let mut locals = FunctionIndices::new(nparams);
let st = locals.alloc_local(ValType::I32);
let ws = locals.alloc_local(ValType::I32);
let wait_locals = Some((st, ws));
let tr_sig = &fd.task_return.as_ref().expect("async has task_return").sig;
let tr_uses_flat_loads = !tr_sig.indirect_params && fd.result_ty.is_some();
let tr_addr_local = tr_uses_flat_loads.then(|| locals.alloc_local(ValType::I32));
let task_return_loads: Option<Vec<wasm_encoder::Instruction<'static>>> =
tr_addr_local.map(|addr_local| {
let result_ty = fd.result_ty.as_ref().expect("flat loads → result_ty");
let mut bindgen = WasmEncoderBindgen::new(sizes, addr_local, &mut locals);
lift_from_memory(resolve, &mut bindgen, (), result_ty);
bindgen.into_instructions()
});
let mut f = Function::new_with_locals_types(locals.into_locals());
if let Some(idx) = imp_before {
emit_hook_call(&mut f, fd, idx, Some(async_runtime), wait_locals);
}
if let Some(blk) = blocking {
emit_blocking_phase(
&mut f,
fd,
blk,
Some(async_runtime),
wait_locals,
Some(imp_task_return),
);
}
for p in 0..nparams {
f.instructions().local_get(p);
}
if fd.import_sig.retptr {
f.instructions()
.i32_const(fd.retptr_offset.expect("retptr_offset for async retptr"));
}
f.instructions().call(imp_handler);
f.instructions().local_set(st);
emit_wait_loop(&mut f, st, ws, async_runtime);
if let Some(idx) = imp_after {
emit_hook_call(&mut f, fd, idx, Some(async_runtime), wait_locals);
}
for (flat_idx, rid) in &fd.borrow_drops {
let drop_fn = resource_drop[rid];
f.instructions().local_get(*flat_idx);
f.instructions().call(drop_fn);
}
if fd.result_ty.is_none() {
f.instructions().call(imp_task_return);
} else if tr_sig.indirect_params {
f.instructions()
.i32_const(fd.retptr_offset.expect("retptr_offset for async retptr"));
f.instructions().call(imp_task_return);
} else {
let addr_local = tr_addr_local.expect("flat loads → tr_addr_local");
f.instructions()
.i32_const(fd.retptr_offset.expect("retptr_offset for async retptr"));
f.instructions().local_set(addr_local);
for inst in task_return_loads
.as_ref()
.expect("task_return_loads built for flat loads")
{
f.instruction(inst);
}
f.instructions().call(imp_task_return);
}
f.instructions().end();
code.function(&f);
}
fn emit_hook_call(
f: &mut Function,
fd: &FuncDispatch,
hook_idx: u32,
async_runtime: Option<&AsyncRuntimeFuncs>,
wait_locals: Option<(u32, u32)>,
) {
f.instructions().i32_const(fd.name_offset);
f.instructions().i32_const(fd.name_len);
f.instructions().call(hook_idx);
let art = async_runtime.expect("async_runtime must be set when a hook is imported");
let (st, ws) = wait_locals.expect("wait_locals allocated alongside async_runtime");
f.instructions().local_set(st);
emit_wait_loop(f, st, ws, art);
}
fn emit_wait_loop(f: &mut Function, st: u32, ws: u32, art: &AsyncRuntimeFuncs) {
f.instructions().local_get(st);
f.instructions().i32_const(4);
f.instructions().i32_shr_u();
f.instructions().local_set(st);
f.instructions().local_get(st);
f.instructions().if_(BlockType::Empty);
f.instructions().call(art.waitable_new);
f.instructions().local_set(ws);
f.instructions().local_get(st);
f.instructions().local_get(ws);
f.instructions().call(art.waitable_join);
f.instructions().local_get(ws);
f.instructions().i32_const(art.event_ptr);
f.instructions().call(art.waitable_wait);
f.instructions().drop();
f.instructions().local_get(st);
f.instructions().call(art.subtask_drop);
f.instructions().local_get(ws);
f.instructions().call(art.waitable_drop);
f.instructions().end();
}
fn empty_function() -> Function {
let mut f = Function::new_with_locals_types([]);
f.instructions().end();
f
}
fn emit_cabi_realloc(code: &mut CodeSection) {
const PARAM_COUNT: u32 = 4;
const ALIGN_LOCAL: u32 = 2;
const NEW_SIZE_LOCAL: u32 = 3;
let mut locals = FunctionIndices::new(PARAM_COUNT);
let scratch = locals.alloc_local(ValType::I32);
let mut f = Function::new_with_locals_types(locals.into_locals());
f.instructions().global_get(BUMP_POINTER_GLOBAL);
f.instructions().local_get(ALIGN_LOCAL);
f.instructions().i32_const(1);
f.instructions().i32_sub();
f.instructions().i32_add();
f.instructions().local_get(ALIGN_LOCAL);
f.instructions().i32_const(1);
f.instructions().i32_sub();
f.instructions().i32_const(-1);
f.instructions().i32_xor();
f.instructions().i32_and();
f.instructions().local_set(scratch);
f.instructions().local_get(scratch);
f.instructions().local_get(NEW_SIZE_LOCAL);
f.instructions().i32_add();
f.instructions().global_set(BUMP_POINTER_GLOBAL);
f.instructions().local_get(scratch);
f.instructions().end();
code.function(&f);
}
#[cfg(test)]
mod tests {
use super::*;
fn iface_from_wit(wit: &str, pkg_name: &str, iface_name: &str) -> (Resolve, InterfaceId) {
let mut resolve = Resolve::default();
resolve.push_str("test.wit", wit).expect("parse test WIT");
let target = format!("{pkg_name}/{iface_name}");
let iface_id = resolve
.interfaces
.iter()
.find(|(id, _)| resolve.id_of(*id).as_deref() == Some(&target))
.map(|(id, _)| id)
.expect("target interface present");
(resolve, iface_id)
}
#[test]
fn require_supported_case_bails_on_inline_resource() {
let (resolve, iface_id) = iface_from_wit(
r#"
package my:shape@1.0.0;
interface api {
resource cat { constructor(); }
foo: func(x: cat) -> cat;
}
"#,
"my:shape",
"api@1.0.0",
);
let err = require_supported_case(&resolve, iface_id, false)
.expect_err("inline resource should bail");
let msg = err.to_string();
assert!(
msg.contains("declares resource `cat` inline"),
"error should call out the inline declaration; got: {msg}"
);
assert!(
msg.contains("factored-types pattern"),
"error should point at the factored-types fix; got: {msg}"
);
}
#[test]
fn require_supported_case_accepts_factored_types() {
let (resolve, iface_id) = iface_from_wit(
r#"
package my:shape@1.0.0;
interface types {
resource cat { constructor(); }
}
interface api {
use types.{cat};
foo: func(x: cat) -> cat;
}
"#,
"my:shape",
"api@1.0.0",
);
require_supported_case(&resolve, iface_id, false)
.expect("factored-types should be accepted");
}
#[test]
fn require_supported_case_accepts_value_types() {
let (resolve, iface_id) = iface_from_wit(
r#"
package my:shape@1.0.0;
interface api {
foo: func(x: u32) -> u32;
}
"#,
"my:shape",
"api@1.0.0",
);
require_supported_case(&resolve, iface_id, false)
.expect("value-type interfaces should be accepted");
}
}