My wit definition file is shared between guest and host and looks like this:
interface abi {
resource plugin {
constructor();
call-me: func() -> list<string>;
}
}
world platform {
include wasi:keyvalue/imports@0.2.0-draft;
export abi;
}
The keyvalue include is from the wasi-keyvalue repository and I use wit-deps to download the 0.2.0 draft tag.
My component implements the resource and provides it to the host. It has to do some networking and I wanted to re-use the same Client instance, therefore I converted my original plugin interface to a resource.
I am building the code using cargo build --target wasm32-wasip1 and wanted to use wasm-tools component new to lift it into a p2 component. From my understanding, I cannot simply use cargo build, since the keyvalue include would mean that I have to provide an implementation at compile time. And cargo component lets me link the missing functions at runtime using wasmtime.
I get this error and I do not understand why this happens:
wasm-tools component new --skip-validation \
target/wasm32-wasip1/release/plugin.wasm -o plugin.wasm --adapt wasi_snapshot_pre
view1.reactor.wasm
error: failed to encode a component from module
Caused by:
0: failed to decode world from module
1: module was not valid
2: failed to resolve import `$root::[subtask-cancel]`
3: no top-level imported function `[subtask-cancel]` specified
This is how the guest's lib.rs file:
use exports::ugl::plugins::abi::{Guest, GuestPlugin};
use myhttpclient::MyHttpClient;
use wasi::keyvalue::{self, store::Bucket};
use wit_bindgen::generate;
generate!({
world: "platform",
path: "../wit",
async: true,
with: {
// These are host-provided.
"wasi:keyvalue/atomics@0.2.0-draft": generate,
"wasi:keyvalue/batch@0.2.0-draft": generate,
"wasi:keyvalue/store@0.2.0-draft": generate,
},
});
struct Component;
export!(Component);
impl Guest for Component {
type Plugin = Plugin;
}
pub struct Plugin {
client: MyHttpClient,
kv: Bucket,
}
impl GuestPlugin for Plugin {
#[doc = " Creates a new instance of the plugin."]
#[allow(async_fn_in_trait)]
async fn new() -> Self {
Self {
client: MyHttpClient::new(),
// SAFETY: plugins can expect to have a key-value store available.
kv: keyvalue::store::open("default".into())
.await
.unwrap_or_else(|_| panic!("Failed to open default key-value store for plugin")),
}
}
#[allow(async_fn_in_trait)]
async fn call_me(&self) -> Vec<String> {
// let result = self.client.get(...);
todo!()
}
}
Maybe I am understanding something wrong. I would appreciate any kind of help and guidance on this matter. Thank you very much!
I followed this guide by Fermyon who managed to use async on the guest side in spin:
https://www.fermyon.com/blog/looking-ahead-to-wasip3
But the same dependencies fail for me.
I found the difference! I was building my wasm binary using cargo build -p plugin --target wasm32-wasip1. Using cargo build --manifest-path plugin/Cargo.toml --target wasm32-wasip1 worked without issues.
Does anyone know why???
Is there a plugin/.cargo directory? I'm not sure if cargo will look there when using --manifest-path. Though...the only thing relevant I would expect there would be target = "wasm-waspi1" which you already have covered... :thinking:
Maybe something about wit_bindgen::generate!'s handling of relative paths? :shrug:
Last updated: Dec 06 2025 at 07:03 UTC