Stream: wasm-tools

Topic: wac moves resource around


view this post on Zulip Yan Chen (Jul 30 2025 at 20:38):

I have a wac question similar to https://github.com/bytecodealliance/wasm-tools/issues/1565#issuecomment-2815677725, but not quite the same. Hope to get some suggestions about how I can fix this.

I have the following main WIT file

package component:main;

interface res {
  resource res;
}

interface main {
  use res.{res};
  test: func(x: res);
}

world root {
  import res;
  export main;
}

And I want to virtualize the res interface by implementing this WIT

world imports {
  import res;
  export res;
}

When I run wac plug, I get this final interface

package component:main {
  interface main {
    resource res;

    test: func(x: res);
  }
}

Basically resource res gets inlined into the main interface. On the host side, I'm still using the original WIT file which expects res.res, not main.res, so it caused a type mismatch when instantiating the wasm. I created a repo. Would be great if I can work around this problem via some wac script.

Hi, I'm trying to use wasi-virt with a micro service that I have written in rust. I want to give it a pre-opened file directory so that I can read a json file. I am using this command: wasi-virt --...
Contribute to chenyan2002/wac-bug-repo development by creating an account on GitHub.

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:04):

Is your goal to get a component that looks like world root { export main; } and nothing else?

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:05):

(thanks for the repro! I was able to figure out what's going on poking around there)

view this post on Zulip Yan Chen (Jul 30 2025 at 21:06):

It should look like

world root {
  import res;
  export main;
}

In my real example, import res exists in the final wasm, but I get another inlined res in the main interface as well. In the mini-repo, the import res somehow gets dead-code eliminated.

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:09):

hm ok then there's a few things going here I think, one is that your virt.wasm doesn't import res, it only does export res, so there's nothing to attach an actual host import to

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:09):

but are you thinking that the host res would be imported to virt.wasm:import res, then virt.wasm:export res is imported into main.wasm, and then main.wasm:export main is exported?

view this post on Zulip Yan Chen (Jul 30 2025 at 21:14):

hmm, the wit for world imports does have import res. Maybe I need to plug in some real code instead of wit-bingen --stubs.

Yes, at the WIT level, virt.wasm:import res--virt.wasm:export res--plug-to--main.wasm:import res--main.wasm:export main.

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:16):

ah ok, then for the result, that's not actually possible. What you're describing is a situation where runtime-wise what's happening is that the final component would import res to get a host thing but then the exported function would actually take an internal, virtualized res interface. That means that there's no actual way to describe what you want in WIT or components as it's not a valid component.

In situations like this wac does its best to produce something and the result can be confusing (e.g. changing the interface definitions as you're seeing). Ideally wac would produce a more first-class error of sorts.

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:17):

At the component model level the resource that main refers to must either be imported or exported, it can't be purely internal, so one example would be to have the final component do both export res and export main for example.

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:21):

For example:

package composed:component;

let imports = new virt:imports { ...  };
let main = new root:component {
  res: imports.res,
  ...
};
export imports.res;
export main.main;

produces:

package root:component;

world root {
  export component:main/res;
  export component:main/main;
}
package component:main {
  interface res {
    resource res;
  }
  interface main {
    use res.{res};

    test: func(x: res);
  }
}

in your example repo

view this post on Zulip Alex Crichton (Jul 30 2025 at 21:21):

(and yeah you'd have to add some sort of use of the host import res within virt:imports to prevent it from being DCE'd)

view this post on Zulip Yan Chen (Jul 30 2025 at 23:05):

Thanks! This fixed my example repo. In my real code, I get a more tricky case which is not very easy to reproduce. Roughly, it's this WIT file

package component:main;

interface async-io {
  resource handle;
}

interface http-body {
  use async-io.{handle as body-handle};
}

interface http-resp {
  use http-body.{body-handle};
}

interface main {
  use http-body.{body-handle};
  test: func(x: body-handle);
}

world root {
  import async-io;
  import http-body;
  import http-resp;
  export main;
}

world imports {
  import async-io;
  import http-body;
  import http-resp;
  export async-io;
  export http-body;
  export http-resp;
}

After wit compose with export imports...; export main...;, for http-resp, I get use async-io.{handle as body-handle} instead of the original use http-body.{body-handle}.

I'm not sure if this difference matters as they are just type alias in Rust. But I still get failed to convert function to given type error when instantiating the wasm module. If we try that WIT file directly, we actually get the correct alias name, so it's harder to reproduce it. Just posting here in case you have a hunch of what might go wrong.

view this post on Zulip Yan Chen (Jul 31 2025 at 00:18):

Okay, I have a repo for this. http-resp is kind of "internal", as it's not directly mentioned in the original exports. After wac compose, it does get exported.

Contribute to chenyan2002/wac-bug-repo development by creating an account on GitHub.

view this post on Zulip Alex Crichton (Jul 31 2025 at 18:16):

hm ok that might be a bug in wac perhaps? That I think should all work otherwise though

view this post on Zulip Yan Chen (Jul 31 2025 at 18:20):

I did some more debugging. That alias is actually fine. The problem is that main.test now takes the body-handle resource from export:async-io.handle, not the import:async-io.handle. So the host is expecting a host-side resource handle, but the wasm provides a guest-side handle with the same name.

view this post on Zulip Alex Crichton (Jul 31 2025 at 18:27):

I think that's more-or-less a fundamental requirement here unfortunately. The main.wasm component exports a function that refers to a resource in an import, and you're satisfying that import with virt.wasm. You'd have to make a second wrapper of sorts which was a test function, for example, which took a host handle, then created a guest handle from that host handle, then called the main.wasm's test export with the guest handle

view this post on Zulip Alex Crichton (Jul 31 2025 at 18:27):

so you'd have to interpose both on the imports and on the exports

view this post on Zulip Yan Chen (Jul 31 2025 at 20:24):

So I add an export module in the repo with this wac script

let imports = new virt:imports { ... };
let main = new root:component { ...imports };
let final = new virt:exports { main: main.main, ... };
export imports...;
export main...;

And I get this error:

error: the encoding of the graph failed validation

Caused by:
    type mismatch for import `component:main/main`
    type mismatch in instance export `body-handle`
    resource types are not the same (ResourceId { globally_unique_id: 3, contextually_unique_id: 8 } vs. ResourceId { globally_unique_id: 3, contextually_unique_id: 1 }) (at offset 0x4685c6)

It feels like the wac script is not expressive enough to indicate which import are host vs guest side, and the solver is doing a best effort job?

view this post on Zulip Alex Crichton (Jul 31 2025 at 22:46):

perhaps? It depends on virt:exports and final there. While it's definitely a possibility that wac has bugs, it's also a possibility that this is model-able in the component model exactly as-is and needs some massaging (e.g. injecting the wrapper for both imports and exports)

view this post on Zulip Yan Chen (Jul 31 2025 at 23:17):

Looking at wac resolve, I think wac compose is actually doing the right thing: the resource gets virtualized, so the previously host side resource expectedly becomes a guest side resource, which eventually calls the host resource. The type mismatch is better resolved on the wasmtime-wit-bindgen side then.

compose.gif


Last updated: Dec 06 2025 at 06:05 UTC