Hi there! I'm working on porting our bindings over to the latest wit-bindgen (cd9d253d115040a8474eba6350340d7491e3183f) and a recent-ish wasmtime (b5e9fb710ba73bde295eec5ab57f6ef4f6459e79, there's a bindgen output discrepancy that I wanted to figure out later), and am sketching out how to best handle code sharing between worlds.
I have a main.wit
that defines two worlds:
world client {
import types: pkg.types
import component: pkg.component
import entity: pkg.entity
import player: pkg.player
import event: pkg.event
export guest: pkg.guest
}
world server {
import types: pkg.types
import component: pkg.component
import entity: pkg.entity
import player: pkg.player
import physics: pkg.physics
import event: pkg.event
import asset: pkg.asset
export guest: pkg.guest
}
I'm defining two worlds as there's two versions of this API (one for clientside WASM logic, one for serverside WASM logic). However, the interfaces that are shared use the same logic on both the host and the guest - that is, the host implementation for the functions defined in pkg.entity
is identical, and the guest would access them the same way.
However, wit-bindgen on the guest and wasmtime's bindgen both require me to specify a world. That means I can't specify both worlds in the same bindgen call, so I'd have to use two separate bindgen calls which would create duplicate non-interoperable types.
What would the best way to share common types while still ensuring that the bits that aren't common aren't dragged in as well?
I was thinking of defining a shared
world and moving the commonalities in there, but would I be able to use the types from that world, and would they be compatible? My ideal scenario here is defining my host / guest logic once for the shared functionality, and then defining them separately for the specific per-world functionality.
If all fails, I'll just chuck it all in the same world and use convention with the interface names to delineate what belongs to each domain, but I'd like to avoid that if possible.
you might be interested in this feature perhaps, but otherwise the rough intention is that guest programs and hosts are generated with the same world as a world describes a concrete component. Can you say a bit more about why you've got two worlds and how they're going to be used?
That proposal looks great! Will keep an eye on it.
No problem on the clarification. I built out the WASM integration for Ambient, which is a game runtime that loads games powered by WASM on the server; clients can then join the server and receive assets. At present, we have server-side only WASM and I've implemented bindings for that. Our host is in Rust, and our guest is currently also Rust (with some higher-level wrappers around the raw WIT types).
In the near-future, though, we'd like to send WASM to the client and have them run it, too. The API for both the client and the server (will) share a lot of functionality, and I'd like to avoid having to reimplement the common functionality. On the other hand, they also have points of divergence; there's functionality only one of the two sides will have access to.
So one host executable (ambient) can provide export bindings for both the client and server API, depending on which mode it's running on. There can be two guest modules, compiled against the same API crate wrapping WIT types (but with different feature flags to expose the different portions of the API).
That is:
server.wasm
component, which was compiled with the Ambient API (server
feature), which wraps the server
WIT world seen above.client
feature and the client
WIT world.The host in both cases should provide the same entity.set-component
implementation, and ideally the guest would use the same path to access it (i.e. so that I don't have to do cfg(feature = "server") use server as wit;
etc). However, physics.apply-force
wouldn't be available when building guest WASM for client
.
Let me know if you need any further clarification, but I'm thinking I'll probably have to use the same world for these at the moment.
Where does the sharing become necessary? With two wasm binaries you'd generate one world in each but there's no need to share types since they're separate binaries. Are you thinking the embedder code, though, would conditionally use types from either world?
That's right. On the guest side, it would be nice to share types/functions for convenience (so that I can have fn spawn(...) { wit::spawn(...) }
without having to alias wit
depending on the world), but that can be worked around. The more pressing issue is for the host.
I'd like to avoid reimplementing the interface functions and the code that consumes the WIT types on the host, as that's a lot of duplication for all of the code that interfaces with WIT. e.g. if I have entity.spawn: func(data: list<tuple<u32, component-value>>) -> entity-id
for both the client and server worlds, the host would have to reimplement spawn
twice, conversion from server.component-value
and client.component-value
to the host representation, and conversion from the host's entity-id
to server.entity-id
and client.entity-id
(and vice versa) - and this would have to happen for everything that's shared between the worlds.
Hm ok, I think it's basically the case that this isn't well supported today. Not to say it can't be, it would require work on the code generators though
Yeah, fair enough - I'll see how I go with using a single world, thanks!
Last updated: Dec 23 2024 at 13:07 UTC