#![allow(dead_code)]
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use anyhow::Result;
use wasmtime::component::{Component, Linker, ResourceTable, Val};
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
pub const SPLICER_BEFORE: &str = "splicer:tier1/before@0.3.0";
pub const SPLICER_AFTER: &str = "splicer:tier1/after@0.3.0";
pub const SPLICER_BUILTIN_CONFIG_GET: &str = "splicer:builtin-config/get@0.1.0";
pub const TARGET_IFACE: &str = "wasi:http/handler@0.3.0";
pub const TARGET_FN: &str = "handle";
pub struct Host<C: Send + 'static> {
pub wasi: WasiCtx,
pub table: ResourceTable,
pub capture: Arc<Mutex<C>>,
}
impl<C: Send + 'static> WasiView for Host<C> {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView {
ctx: &mut self.wasi,
table: &mut self.table,
}
}
}
pub fn call_id_val(iface: &str, func: &str) -> Val {
Val::Record(vec![
("interface-name".into(), Val::String(iface.into())),
("function-name".into(), Val::String(func.into())),
("id".into(), Val::U64(0)),
])
}
pub fn empty_span_context() -> Val {
Val::Record(vec![
("trace-id".into(), Val::String(String::new())),
("span-id".into(), Val::String(String::new())),
("trace-flags".into(), Val::Flags(vec![])),
("is-remote".into(), Val::Bool(false)),
("trace-state".into(), Val::List(vec![])),
])
}
pub fn drive_call_cycle<C, F>(bytes: &[u8], setup: F) -> Result<Arc<Mutex<C>>>
where
C: Default + Send + 'static,
F: FnOnce(&mut Linker<Host<C>>) -> Result<()>,
{
let mut config = Config::new();
config.wasm_component_model_async(true);
config.wasm_component_model_async_stackful(true);
let engine = Engine::new(&config)?;
let component = Component::from_binary(&engine, bytes)?;
let mut linker = Linker::new(&engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
setup(&mut linker)?;
let capture = Arc::new(Mutex::new(C::default()));
let stdout = MemoryOutputPipe::new(64 * 1024);
let host = Host {
wasi: WasiCtxBuilder::new().stdout(stdout).build(),
table: ResourceTable::new(),
capture: capture.clone(),
};
let mut store = Store::new(&engine, host);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async {
let instance = linker.instantiate_async(&mut store, &component).await?;
let on_call = instance
.get_export_index(&mut store, None, SPLICER_BEFORE)
.and_then(|idx| instance.get_export_index(&mut store, Some(&idx), "on-call"))
.and_then(|idx| instance.get_func(&mut store, idx));
let on_return = instance
.get_export_index(&mut store, None, SPLICER_AFTER)
.and_then(|idx| instance.get_export_index(&mut store, Some(&idx), "on-return"))
.and_then(|idx| instance.get_func(&mut store, idx));
let cid = call_id_val(TARGET_IFACE, TARGET_FN);
if let Some(on_call) = on_call {
let mut results: Vec<Val> = vec![];
on_call
.call_async(&mut store, std::slice::from_ref(&cid), &mut results)
.await?;
}
if let Some(on_return) = on_return {
let mut results: Vec<Val> = vec![];
on_return
.call_async(&mut store, &[cid], &mut results)
.await?;
}
Ok::<_, anyhow::Error>(())
})?;
Ok(capture)
}
pub fn add_builtin_config_stub<C: Send + 'static>(linker: &mut Linker<Host<C>>) -> Result<()> {
let mut iface = linker.instance(SPLICER_BUILTIN_CONFIG_GET)?;
iface.func_new("get", |_store, _ty, _params, results| {
results[0] = Val::Option(None);
Ok(())
})?;
Ok(())
}
pub fn assert_call_attrs(attrs: &[Val]) {
let attr_map: HashMap<String, String> = attrs
.iter()
.map(|kv| {
let r = expect_record(kv);
(
expect_string(field(r, "key")).to_string(),
expect_string(field(r, "value")).to_string(),
)
})
.collect();
assert_eq!(
attr_map.get("code.namespace").map(String::as_str),
Some(format!("\"{TARGET_IFACE}\"").as_str()),
"code.namespace JSON-encoded; got {attr_map:?}"
);
assert_eq!(
attr_map.get("code.function").map(String::as_str),
Some(format!("\"{TARGET_FN}\"").as_str()),
"code.function JSON-encoded; got {attr_map:?}"
);
}
pub fn field<'a>(record: &'a [(String, Val)], name: &str) -> &'a Val {
record
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v)
.unwrap_or_else(|| panic!("field {name:?} not found in record {record:?}"))
}
pub fn expect_record(v: &Val) -> &[(String, Val)] {
if let Val::Record(fields) = v {
fields
} else {
panic!("expected record, got {v:?}")
}
}
pub fn expect_string(v: &Val) -> &str {
if let Val::String(s) = v {
s
} else {
panic!("expected string, got {v:?}")
}
}
pub fn expect_u64(v: &Val) -> u64 {
if let Val::U64(n) = v {
*n
} else {
panic!("expected u64, got {v:?}")
}
}
pub fn expect_u32(v: &Val) -> u32 {
if let Val::U32(n) = v {
*n
} else {
panic!("expected u32, got {v:?}")
}
}
pub fn expect_bool(v: &Val) -> bool {
if let Val::Bool(b) = v {
*b
} else {
panic!("expected bool, got {v:?}")
}
}
pub fn expect_list(v: &Val) -> &[Val] {
if let Val::List(items) = v {
items
} else {
panic!("expected list, got {v:?}")
}
}
pub fn expect_variant(v: &Val) -> (&str, Option<&Val>) {
if let Val::Variant(case, payload) = v {
(case.as_str(), payload.as_deref())
} else {
panic!("expected variant, got {v:?}")
}
}
pub fn expect_enum(v: &Val) -> &str {
if let Val::Enum(case) = v {
case.as_str()
} else {
panic!("expected enum, got {v:?}")
}
}
pub fn read_builtin(name: &str) -> Vec<u8> {
let dir = std::env::var_os("SPLICER_BUILTINS_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/builtins"));
let path = dir.join(format!("{name}.wasm"));
std::fs::read(&path).unwrap_or_else(|e| {
panic!(
"couldn't read {}: {e}\n\
run `make build-builtins`, or set SPLICER_BUILTINS_DIR=<dir>",
path.display()
)
})
}