Stream: wit-bindgen

Topic: Optional imports and exports with wit-bindgen and wasmtime


view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 15:28):

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:

WIT

You split each service into it's own world and do not compose them into a single plugin world.

Host

The main steps here are:

  1. Detect what worlds a WASM component implements (not sure how to tackle this at this moment).
  2. Add only those worlds to a linker (import validation).
  3. Pre-instantiate for only those worlds (export validation).

World detection

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.

Linker

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 Instantiation

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?

Guest

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:

  1. If anyone has experience with this.
  2. If this is even possible in reality (I have not gone ahead and actually implemented it yet because of the world detection issue and the work it will take).
  3. If it's worth in case it does work.
  4. If there is tracking issue for optional exports.

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 15:31):

@Ignatz you might be interested in this.

view this post on Zulip Alex Crichton (Aug 17 2026 at 18:18):

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

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 18:34):

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.

view this post on Zulip Alex Crichton (Aug 17 2026 at 18:43):

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

view this post on Zulip Alex Crichton (Aug 17 2026 at 18:44):

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

view this post on Zulip Till Schneidereit (Aug 17 2026 at 19:00):

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

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 20:03):

Alex Crichton said:

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

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.

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 20:03):

Alex Crichton said:

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

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.

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 20:04):

Not sure why it double send?

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 20:07):

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.

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 20:07):

So it's not an optional thing which may change ar runtime but gets decided at compile time of the component.

view this post on Zulip Alex Crichton (Aug 17 2026 at 20:09):

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.

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 20:13):

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?

view this post on Zulip Alex Crichton (Aug 17 2026 at 20:52):

That sounds reasonable to me yeah!

view this post on Zulip Eduard Smet (Celarye) (Aug 17 2026 at 21:18):

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);
    }
}

view this post on Zulip Celarye (Aug 21 2026 at 15:58):

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?

view this post on Zulip Celarye (Aug 21 2026 at 16:25):

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.

view this post on Zulip Celarye (Aug 27 2026 at 10:47):

For now I'll just keep duplicating the deps but maybe a configurable deps path in wit bindgen would be interesting to support?

view this post on Zulip Celarye (Aug 27 2026 at 11:15):

While implementing the above I came across an alternative solution on the host side:

Linker

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).

Pre Instantiation

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:

view this post on Zulip Alex Crichton (Aug 27 2026 at 18:44):

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).

view this post on Zulip Alex Crichton (Aug 27 2026 at 18:46):

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

view this post on Zulip Celarye (Aug 27 2026 at 18:48):

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:

view this post on Zulip Celarye (Aug 27 2026 at 18:51):

image.png
image.png

view this post on Zulip Alex Crichton (Aug 27 2026 at 18:51):

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

view this post on Zulip Celarye (Aug 27 2026 at 18:51):

Although now looking at it it doesn't say new is limited to existence checking.

view this post on Zulip Celarye (Aug 27 2026 at 18:52):

I'm not too experienced with digging into proc macro code, but I'll see if I can take a look

view this post on Zulip Alex Crichton (Aug 27 2026 at 18:53):

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

view this post on Zulip Celarye (Aug 27 2026 at 18:54):

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