use anyhow::{bail, Context, Result};
use std::collections::HashMap;
use wasm_encoder::{
BlockType, CodeSection, EntityType, Function, FunctionSection, ImportSection, Module,
TypeSection, ValType,
};
use wit_bindgen_core::abi::lift_from_memory;
use wit_component::{embed_component_metadata, ComponentEncoder, StringEncoding};
use wit_parser::abi::{AbiVariant, WasmSignature};
use wit_parser::{
Function as WitFunction, InterfaceId, Mangling, Resolve, SizeAlign, Type, TypeId, WasmExport,
WasmExportKind, WasmImport, WorldKey,
};
use super::super::abi::canon_async::{self, AsyncFuncs, AsyncTypes};
use super::super::abi::emit::{
build_lower_params_to_memory, call_id_layout, collect_borrow_drops, direct_return_type,
emit_alloc_call_id, emit_borrow_drops, emit_bump_restore, emit_bump_save, emit_cabi_realloc,
emit_data_section, emit_export_section, emit_handler_call, emit_memory_and_globals,
emit_populate_call_id, emit_resource_drop_imports, emit_wrapper_return, empty_function,
find_imported_hook, require_indirect_params_supported_shape, require_no_inline_resources,
synthesize_adapter_world_wit, val_types, BlobSlice, BumpReset, CallIdLayout, GlobalIndices,
HookImport, WrapperExport,
};
use super::super::abi::WasmEncoderBindgen;
use super::super::indices::{DispatchIndices, LocalsBuilder};
use super::super::mem_layout::MemoryLayoutBuilder;
use super::super::resolve::{decode_input_resolve, dispatch_mangling, find_target_interface};
pub(crate) fn build_adapter(
target_interface: &str,
has_before: bool,
has_after: bool,
has_blocking: bool,
split_bytes: &[u8],
common_world_wit: &str,
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-common.wit", common_world_wit)
.context("parse common WIT")?;
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(
ADAPTER_WORLD_PACKAGE,
ADAPTER_WORLD_NAME,
target_interface,
&tier1_hook_imports(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 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");
}
require_no_inline_resources(resolve, target_iface)?;
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`. 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 {
require_indirect_params_supported_shape(resolve, name, func)?;
}
}
}
Ok(())
}
fn tier1_hook_imports(has_before: bool, has_after: bool, has_blocking: bool) -> Vec<String> {
use crate::contract::{
versioned_interface, TIER1_AFTER, TIER1_BEFORE, TIER1_BLOCKING, TIER1_VERSION,
};
let mut out = Vec::new();
if has_before {
out.push(versioned_interface(TIER1_BEFORE, TIER1_VERSION));
}
if has_after {
out.push(versioned_interface(TIER1_AFTER, TIER1_VERSION));
}
if has_blocking {
out.push(versioned_interface(TIER1_BLOCKING, TIER1_VERSION));
}
out
}
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>,
iface_name_offset: i32,
iface_name_len: i32,
fn_name_offset: i32,
fn_name_len: i32,
retptr_offset: Option<i32>,
params_record_offset: Option<i32>,
borrow_drops: Vec<(u32, TypeId)>,
}
const ADAPTER_WORLD_PACKAGE: &str = "splicer:adapter";
const ADAPTER_WORLD_NAME: &str = "adapter";
struct CallIdBuf {
offset: i32,
layout: CallIdLayout,
}
#[derive(Clone, Copy)]
struct CallIdWiring<'a> {
counter_global: u32,
buf: &'a CallIdBuf,
}
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<AsyncTypes>,
resource_drop_ty: Option<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<AsyncFuncs>,
resource_drop: HashMap<TypeId, u32>,
}
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,
) -> Result<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 any_hook = has_before || has_after || has_blocking;
let mut sizes = SizeAlign::default();
sizes.fill(resolve);
let callid_layout = any_hook
.then(|| call_id_layout(resolve, &sizes))
.transpose()?;
let plan = compute_func_dispatches(
resolve,
&sizes,
target_iface,
target_interface_name,
&funcs,
SlotReservations {
needs_async_runtime,
has_blocking,
callid_layout,
},
)?;
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, &plan.per_func, &hook_imports);
let func_idx = emit_imports_section(
&mut module,
&mut idx,
&plan.per_func,
&type_idx,
&hook_imports,
plan.event_ptr,
plan.block_result_ptr,
resolve,
);
let func_idx =
emit_function_section(&mut module, &mut idx, &plan.per_func, &type_idx, func_idx);
let globals = emit_memory_and_globals(&mut module, plan.bump_start);
let wrapper_exports: Vec<WrapperExport<'_>> = plan
.per_func
.iter()
.enumerate()
.map(|(i, fd)| WrapperExport {
export_name: &fd.export_name,
cabi_post_idx: func_idx.cabi_post[i],
})
.collect();
emit_export_section(
&mut module,
&wrapper_exports,
func_idx.wrapper_base,
func_idx.init,
func_idx
.cabi_realloc
.expect("cabi_realloc is always emitted"),
);
let call_id_wiring = plan.call_id_buf.as_ref().map(|buf| CallIdWiring {
counter_global: globals.call_id_counter,
buf,
});
emit_code_section(
&mut module,
resolve,
&sizes,
&plan.per_func,
&funcs,
&func_idx,
&globals,
call_id_wiring,
);
let data_segments = if plan.name_blob.is_empty() {
Vec::new()
} else {
vec![(0, plan.name_blob)]
};
emit_data_section(&mut module, &data_segments);
Ok(module.finish())
}
struct DispatchPlan {
per_func: Vec<FuncDispatch>,
name_blob: Vec<u8>,
event_ptr: Option<i32>,
block_result_ptr: Option<i32>,
call_id_buf: Option<CallIdBuf>,
bump_start: u32,
}
struct SlotReservations {
needs_async_runtime: bool,
has_blocking: bool,
callid_layout: Option<CallIdLayout>,
}
const LAYOUT_SIZE_BUDGET: u32 = i32::MAX as u32;
fn compute_func_dispatches(
resolve: &Resolve,
sizes: &SizeAlign,
target_iface: InterfaceId,
target_interface_name: &str,
funcs: &[&WitFunction],
slots: SlotReservations,
) -> Result<DispatchPlan> {
let iface_name_bytes = target_interface_name.len() as u32;
let total_fn_name_bytes: u32 = funcs.iter().map(|f| f.name.len() as u32).sum();
let total_name_bytes = iface_name_bytes + total_fn_name_bytes;
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 iface_name_offset = layout.alloc_name(iface_name_bytes) as i32;
name_blob.extend_from_slice(target_interface_name.as_bytes());
let target_world_key = WorldKey::Interface(target_iface);
for func in funcs.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 = dispatch_mangling(is_async);
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 fn_name_offset = layout.alloc_name(func.name.len() as u32) as i32;
name_blob.extend_from_slice(func.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_aligned(size, align) as i32
});
let params_record_offset = (is_async && import_sig.indirect_params).then(|| {
let param_types: Vec<Type> = func.params.iter().map(|p| p.ty).collect();
let info = sizes.record(¶m_types);
let size = info.size.size_wasm32() as u32;
let align = info.align.align_wasm32() as u32;
layout.alloc_aligned(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,
iface_name_offset,
iface_name_len: iface_name_bytes as i32,
fn_name_offset,
fn_name_len: func.name.len() as i32,
retptr_offset,
params_record_offset,
borrow_drops,
});
}
let event_ptr = slots
.needs_async_runtime
.then(|| layout.alloc_event_slot() as i32);
let block_result_ptr = slots
.has_blocking
.then(|| layout.alloc_block_result() as i32);
let call_id_buf = slots.callid_layout.map(|callid_layout| {
let offset = layout.alloc_aligned(callid_layout.size(), callid_layout.align()) as i32;
CallIdBuf {
offset,
layout: callid_layout,
}
});
let bump_start = layout.finish_as_bump_start();
if bump_start > LAYOUT_SIZE_BUDGET {
bail!("static-data layout end {bump_start} exceeds i32 budget {LAYOUT_SIZE_BUDGET}");
}
Ok(DispatchPlan {
per_func,
name_blob,
event_ptr,
block_result_ptr,
call_id_buf,
bump_start,
})
}
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 pick = |active: bool, iface: &str| -> Option<HookImport> {
active
.then(|| {
find_imported_hook(
resolve,
world_id,
&versioned_interface(iface, TIER1_VERSION),
)
})
.flatten()
};
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(|| canon_async::emit_types(&mut types, || idx.alloc_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 resource_drop = emit_resource_drop_imports(
&mut imports,
resolve,
per_func,
|f| &f.borrow_drops,
type_idx.resource_drop_ty,
|| 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");
canon_async::import_intrinsics(&mut imports, art, event_ptr, || idx.alloc_func())
});
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
}
#[allow(clippy::too_many_arguments)]
fn emit_code_section(
module: &mut Module,
resolve: &Resolve,
sizes: &SizeAlign,
per_func: &[FuncDispatch],
funcs: &[&WitFunction],
func_idx: &FuncIndices,
globals: &GlobalIndices,
call_id_wiring: Option<CallIdWiring<'_>>,
) {
debug_assert_eq!(
per_func.len(),
funcs.len(),
"FuncDispatch list and WitFunction list must be index-aligned",
);
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,
funcs[i],
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,
call_id_wiring,
globals.bump,
);
} 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,
call_id_wiring,
globals.bump,
);
}
}
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, globals.bump);
module.section(&code);
}
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<&AsyncFuncs>,
resource_drop: &HashMap<TypeId, u32>,
call_id_wiring: Option<CallIdWiring<'_>>,
bump_global: u32,
) {
let nparams = fd.export_sig.params.len() as u32;
let mut locals = LocalsBuilder::new(nparams);
let bump_reset = BumpReset {
global: bump_global,
saved_local: locals.alloc_local(ValType::I32),
};
let result_local = direct_return_type(&fd.export_sig).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 hook_site = call_id_wiring.map(|w| HookSite {
fd,
buf: w.buf,
id_local: locals.alloc_local(ValType::I64),
});
let mut f = Function::new_with_locals_types(locals.freeze().locals);
emit_bump_save(&mut f, bump_reset);
if let (Some(w), Some(site)) = (call_id_wiring, hook_site) {
emit_alloc_call_id(&mut f, w.counter_global, site.id_local);
}
if let Some(idx) = imp_before {
emit_hook_call(&mut f, idx, async_runtime, wait_locals, hook_site.unwrap());
}
if let Some(blk) = blocking {
emit_blocking_phase(
&mut f,
blk,
async_runtime,
wait_locals,
None,
hook_site.unwrap(),
bump_reset,
);
}
emit_handler_call(
&mut f,
nparams,
fd.export_sig.retptr,
fd.retptr_offset,
imp_handler,
);
if let Some(local) = result_local {
f.instructions().local_set(local);
}
if let Some(idx) = imp_after {
emit_hook_call(&mut f, idx, async_runtime, wait_locals, hook_site.unwrap());
}
emit_borrow_drops(&mut f, &fd.borrow_drops, resource_drop);
emit_bump_restore(&mut f, bump_reset);
emit_wrapper_return(&mut f, result_local, fd.export_sig.retptr, fd.retptr_offset);
f.instructions().end();
code.function(&f);
}
#[derive(Clone, Copy)]
struct HookSite<'a> {
fd: &'a FuncDispatch,
buf: &'a CallIdBuf,
id_local: u32,
}
fn emit_blocking_phase(
f: &mut Function,
blk: &BlockingConfig,
async_runtime: Option<&AsyncFuncs>,
wait_locals: Option<(u32, u32)>,
task_return_for_async: Option<u32>,
site: HookSite<'_>,
bump_reset: BumpReset,
) {
populate_hook_call_id(f, &site);
f.instructions().i32_const(site.buf.offset);
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);
canon_async::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);
}
emit_bump_restore(f, bump_reset);
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,
func: &WitFunction,
imp_handler: u32,
imp_before: Option<u32>,
imp_after: Option<u32>,
blocking: Option<&BlockingConfig>,
imp_task_return: u32,
async_runtime: &AsyncFuncs,
resource_drop: &HashMap<TypeId, u32>,
call_id_wiring: Option<CallIdWiring<'_>>,
bump_global: u32,
) {
let nparams = fd.export_sig.params.len() as u32;
let mut locals = LocalsBuilder::new(nparams);
let bump_reset = BumpReset {
global: bump_global,
saved_local: locals.alloc_local(ValType::I32),
};
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 params_lower_seq: Option<Vec<wasm_encoder::Instruction<'static>>> =
fd.import_sig.indirect_params.then(|| {
let base = fd
.params_record_offset
.expect("indirect_params → params_record_offset reserved");
build_lower_params_to_memory(resolve, sizes, &mut locals, func, base)
});
let hook_site = call_id_wiring.map(|w| HookSite {
fd,
buf: w.buf,
id_local: locals.alloc_local(ValType::I64),
});
let mut f = Function::new_with_locals_types(locals.freeze().locals);
emit_bump_save(&mut f, bump_reset);
if let (Some(w), Some(site)) = (call_id_wiring, hook_site) {
emit_alloc_call_id(&mut f, w.counter_global, site.id_local);
}
if let Some(idx) = imp_before {
emit_hook_call(
&mut f,
idx,
Some(async_runtime),
wait_locals,
hook_site.unwrap(),
);
}
if let Some(blk) = blocking {
emit_blocking_phase(
&mut f,
blk,
Some(async_runtime),
wait_locals,
Some(imp_task_return),
hook_site.unwrap(),
bump_reset,
);
}
if let Some(seq) = params_lower_seq.as_ref() {
for inst in seq {
f.instruction(inst);
}
f.instructions().i32_const(
fd.params_record_offset
.expect("indirect_params → params_record_offset"),
);
} else {
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);
canon_async::emit_wait_loop(&mut f, st, ws, async_runtime);
if let Some(idx) = imp_after {
emit_hook_call(
&mut f,
idx,
Some(async_runtime),
wait_locals,
hook_site.unwrap(),
);
}
emit_borrow_drops(&mut f, &fd.borrow_drops, resource_drop);
emit_bump_restore(&mut f, bump_reset);
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,
hook_idx: u32,
async_runtime: Option<&AsyncFuncs>,
wait_locals: Option<(u32, u32)>,
site: HookSite<'_>,
) {
populate_hook_call_id(f, &site);
f.instructions().i32_const(site.buf.offset);
let art = async_runtime.expect("async_runtime set when hook imported");
let (st, ws) = wait_locals.expect("wait_locals allocated with async_runtime");
canon_async::emit_call_and_wait(f, hook_idx, st, ws, art);
}
fn populate_hook_call_id(f: &mut Function, site: &HookSite<'_>) {
emit_populate_call_id(
f,
site.buf.offset,
0,
&site.buf.layout,
BlobSlice {
off: site.fd.iface_name_offset as u32,
len: site.fd.iface_name_len as u32,
},
BlobSlice {
off: site.fd.fn_name_offset as u32,
len: site.fd.fn_name_len as u32,
},
site.id_local,
);
}
#[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");
}
#[test]
fn require_supported_case_accepts_primitive_alias_in_indirect_params() {
let (resolve, iface_id) = iface_from_wit(
r#"
package my:shape@1.0.0;
interface api {
type my-id = u32;
many: async func(a: my-id, b: my-id, c: my-id, d: my-id, e: my-id) -> u32;
}
"#,
"my:shape",
"api@1.0.0",
);
require_supported_case(&resolve, iface_id, false)
.expect("primitive alias in indirect-params position should be accepted");
}
#[test]
fn require_supported_case_accepts_record_tuple_enum_flags_in_indirect_params() {
let (resolve, iface_id) = iface_from_wit(
r#"
package my:shape@1.0.0;
interface api {
record point { x: u32, y: u32 }
enum color { red, green, blue }
flags perms { read, write, exec }
many: async func(
p: point,
t: tuple<u32, u64>,
c: color,
f: perms,
nested: tuple<point, color>
) -> u32;
}
"#,
"my:shape",
"api@1.0.0",
);
require_supported_case(&resolve, iface_id, false)
.expect("record / tuple / enum / flags in indirect-params should be accepted");
}
#[test]
fn require_supported_case_accepts_full_value_type_space_in_indirect_params() {
let (resolve, iface_id) = iface_from_wit(
r#"
package my:shape@1.0.0;
interface api {
variant either { left(u32), right(u64), neither }
many: async func(
s: string,
l: list<u32>,
fl: list<u32, 4>,
o: option<u32>,
r: result<u32, string>,
v: either,
m: map<string, u32>,
) -> u32;
}
"#,
"my:shape",
"api@1.0.0",
);
require_supported_case(&resolve, iface_id, false)
.expect("full canonical-ABI value-type space accepted");
}
}