I've been redoing the WIT design for a plugin system I have in one of my projects (https://github.com/wpbs-rs/wpbs/pull/29). The plugin system has a dynamic list of "services" (provided as WIT dep worlds) which a plugin can optionally implement.
Meaning that they ideally only implement the worlds they want to implement. But it seems like optional imports (https://github.com/WebAssembly/component-model/issues/555) and optional exports (can not find any tracking issue for this) are still quite a way off.
For now guests implement all worlds their export functions but just call unimplemented!() for the ones they do not want to implement. This works for now and it could get abstracted away behind a crate providing macros for this but in the future we would love to provide this at the WIT level instead.
The usage of wit-bindgen is also strongly preferred for both the host and guest because of great developer experience and type safety it provides.
So @circuitsacul and I are looking into ways to support optional imports and exports with the current system (wit-bindgen and wasmtime). And we would like some feedback on the following idea:
You split each service into it's own world and do not compose them into a single plugin world.
The main steps here are:
I could have a world with an export function which needs to list all "service" worlds the plugin implements but I rather just detect it before doing any instantiation if possible.
Added a specific "service" world to the linker in case a component has it implemented:
ServiceOne::add_to_linker::<InternalRuntime, HasSelf<InternalRuntime>>(
&mut linker,
|internal_runtime| internal_runtime,
)
.unwrap();
ServiceTwo::add_to_linker::<InternalRuntime, HasSelf<InternalRuntime>>(
&mut linker,
|internal_runtime| internal_runtime,
)
.unwrap();
// etc.
This would mean that there would be a bunch of unique linkers for the used "service" combinations.
Pre Instantiate with a specific "service" world in case a component has it implemented:
struct PluginPre {
service_one_pre: Option<ServiceOnePre>,
service_two_pre: Option<ServiceTwoPre>,
// etc.
}
ServiceOnePre::new(instance_pre).unwrap();
ServiceTwoPre::new(instance_pre).unwrap();
// etc.
The question here is what the cost if of many InstancePre's per plugin, how does it scale?
A guest can then implement the traits for the service worlds they want to a Plugin struct and then add:
// Clippy pedantic is not happy with wit-bindgen 0.60.0
#[allow(clippy::same_length_and_capacity)]
mod bindgen {
use crate::Plugin;
wit_bindgen::generate!({ path: "../service-one" });
wit_bindgen::generate!({ path: "../service-two" });
// etc.
export!(Plugin);
}
So basically I would love to know:
@Ignatz you might be interested in this.
FWIW optional imports in the component model are seen as pretty high-priority and in theory going to get implemented in the next few months or so, but that's also still all a work-in-progress (e.g. no spec definition quite just yet). Otherwise though there's no plans for optional exports at this time -- that'd be more done with host side detection I think.
As for the viability of what you're thinking, there's a lot of details glossed over I think but it seems reasonable-ish to me
Alex Crichton said:
FWIW optional imports in the component model are seen as pretty high-priority and in theory going to get implemented in the next few months or so, but that's also still all a work-in-progress (e.g. no spec definition quite just yet). Otherwise though there's no plans for optional exports at this time -- that'd be more done with host side detection I think.
As for the viability of what you're thinking, there's a lot of details glossed over I think but it seems reasonable-ish to me
Is there any specific way you see host side detection working, as that seems to be the biggest hurdle for the above proposed implementation?
Iirc the WIT worlds a component implements can be checked through wasm-tools. But that’s not a library (maybe I can use the internal crate). And then I would need to parse that as well ofcourse.
As optional exports are not planned at all I’ll probably have to try the above idea eventually if I want to keep the plugin developer experience as simple as possible.
For host-side detection I'd imagine reflection over the Component type after it's loaded/compiled. That has information on imports/exports you'd be able to dispatch on
For optional exports my impression is that an export is either there or not, optionality doesn't factor in to it. For imports it's different because you may be given the import or not, but optional exports seem like it'd be some sort of conditional decision based on what you were given at runtime and that's not planned AFAIK
the Wasmtime CLI effectively already does this, fwiw: wasmtime run and wasmtime serve both support multiple different exports, and successfully run the component if at least one of them exists
Alex Crichton said:
For host-side detection I'd imagine reflection over the
Componenttype after it's loaded/compiled. That has information on imports/exports you'd be able to dispatch on
Ah yeah something like Component::get_export_index() or so could maybe do the job? What I would want was a clear list of the worlds a component implements.
Alex Crichton said:
For host-side detection I'd imagine reflection over the
Componenttype after it's loaded/compiled. That has information on imports/exports you'd be able to dispatch on
Ah yeah something like Component::get_export_index() or so could maybe do the job? What I would want was a clear list of the worlds a component implements.
Not sure why it double send?
Just to clarify this is the current WIT structure which I have:
wit
├── deps
│ ├── wpbs-services:discord
│ │ ├── types.wit
│ │ └── world.wit
│ ├── wpbs-services:job-scheduler
│ │ ├── types.wit
│ │ └── world.wit
│ ├── wpbs:core
│ │ ├── types.wit
│ │ └── world.wit
│ ├── wpbs:services
│ │ └── world.wit
│ └── wpbs:shared
│ └── types.wit
└── world.wit
Here services like "discord" or "job-scheduler" provide worlds which a component may or may not implement.
So it's not an optional thing which may change ar runtime but gets decided at compile time of the component.
Ok yeah sounds like you definitely want to reflect over the component type, so this wouldn't be a WIT feature really but rather a feature of your embedding. And yeah get_export_index is the way to go.
I think optional imports and exports was the wrong way to go about this. Rather these worlds are either implemented or not at compile time and I need to validate which are before pre-instantiating them.
Should I combine get_export_index with the ideas I send above?
That sounds reasonable to me yeah!
Could it be that the guest code snippet in my first message was wrong and that it would look like this instead?
mod bindgen {
mod service_one {
use crate::Plugin;
wit_bindgen::generate!({ path: "../service-one" });
export!(Plugin);
}
mod service_two {
use crate::Plugin;
wit_bindgen::generate!({ path: "../service-two" });
export!(Plugin);
}
}
I've gone ahead and split my separate worlds into their own subdirectories under wit. But I am not sure if this is the best way forward as I now need to duplicate the deps directory a bunch of times:
wit
├── core
│ ├── deps
│ │ └── wpbs:shared
│ │ └── types.wit
│ ├── types.wit
│ └── world.wit
├── services
│ ├── discord
│ │ ├── deps
│ │ │ └── wpbs:shared
│ │ │ └── types.wit
│ │ ├── types.wit
│ │ └── world.wit
│ └── job-scheduler
│ ├── deps
│ │ └── wpbs:shared
│ │ └── types.wit
│ ├── types.wit
│ └── world.wit
└── shared
└── types.wit
Are there any other ways I could solve this that improves upon this?
I tried doing:
mod bindings {
pub mod shared {
// Had to create an empty world for this
wasmtime::component::bindgen!({ path: "./wit/shared/" });
}
pub mod core {
wasmtime::component::bindgen!({ path: "./wit/core/", imports: { default: async }, exports: { default: async }, with: { "wpbs::shared/shared-types@0.1.0-rc.1": crate::runtime::plugins::bindings::shared::wpbs::shared::shared_types } });
}
//...
But re-reading the reference I guess that's only for interfaces which I explicitly import into a world.
For now I'll just keep duplicating the deps but maybe a configurable deps path in wit bindgen would be interesting to support?
While implementing the above I came across an alternative solution on the host side:
Stick to a single Linker which implements all service worlds as it's okay for a Linker to implement more imports than a guest (if I understand this correctly).
Instead of pre instantiating for every service world and storing a dynamic amount of {world}Pre, create a single generic InstancePre and {world}Indices (created by wit-bindgen as well).
pub struct RuntimePlugin {
pub instance_pre: InstancePre<InternalRuntime>,
pub state_pre: RuntimePluginStatePre,
pub indices: RuntimePluginIndices,
}
pub struct RuntimePluginIndices {
pub core: CoreIndices,
pub services: RuntimePluginIndicesServices,
}
pub struct RuntimePluginIndicesServices {
pub job_scheduler: Option<JobSchedulerIndices>,
pub discord: Option<DiscordIndices>,
}
With this you can then load the service specific world instance via {world}Indices::load on demand. This does do type checking of all exports on every call, which is not really needed after the first validation.
Some questions about this approach:
{world}Pre (old solution) vs a dynamic amount of {world}Indices?{world} from a generic Instance via {world}Indices::load (new solution)?It's fine to have more things in a Linker than a guest needs, yeah. {world}Pre is relatively cheap and should be more-or-less the same cost as {world}Indices, neither being too too expensive. Loading a world from an instance can be some string lookups and type-checking, so not massive but O(size-of-world).
Actually, no, loading a world from {world}Indices should be a pretty cheap operation, still O(world) but no name lookups or type-checking (that was done ahead of time hwne creating the indices). What you've sketched out here seems reasonable
The method docs does say that the load method does type checking. And I think it said that creation of the Indices only does function existence checking? :thinking:
![]()
![]()
I might be misremembering, but I thought creation of FooIndices does type-checking, and then using a FooIndices to get a Foo is some simple index lookups
Although now looking at it it doesn't say new is limited to existence checking.
I'm not too experienced with digging into proc macro code, but I'll see if I can take a look
if it helps you can do WASMTIME_DEBUG_BINDGEN=1 in your environment and that should spit out the generated code to a file somewhere which is include!'d which can work a bit better with editors/tooling sometimes
I think I can also use this: https://docs.rs/wasmtime/latest/src/wasmtime/opt/rustwide/target/debug/build/wasmtime-internal-component-macro/e620958884515fd5/out/hello-world2.rs.html#288-290
Last updated: Aug 30 2026 at 10:08 UTC