wit-component
wit-component
is a crate for creating and interacting with WebAssembly components based on the component model proposal.
The wit-component
crate is available through the wasm-tools
CLI suite under two subcommands:
# Create a component from the input core wasm module $ wasm-tools component new core.wasm -o component.wasm # Extract a `*.wit` interface from a component $ wasm-tools component wit component.wasm
Creates WebAssembly component binaries from input core WebAssembly modules. Input modules communicate with the canonical ABI to imported and exported interfaces described with *.wit
files. The wit interface is required to be embedded directly in the core wasm binary.
Supports “adapters” which can be used to bridge legacy core WebAssembly imported functions into component model functions. Adapters are themselves core wasm binaries which will be embedded into the final component. An adapter's exports can be imported by the main core wasm binary and the adapter can then call component model imports.
A *.wit
interface can be extracted from an existing component to see the interface that it exports and intends to import.
Note that this crate is intended to be a low-level detail of tooling for components. Developers will not necessarily interact with this tooling day-to-day, instead using wrappers such as cargo-component
which will automatically execute wit-component
to produce component binaries.
First wit-component
supports the wasm-based encoding of a WIT package:
$ cat demo.wit package my:demo; interface host { hello: func(); } world demo { import host; } $ wasm-tools component wit demo.wit -o demo.wasm --wasm # The output `demo.wasm` is a valid component binary $ wasm-tools validate --features component-model demo.wasm $ wasm-tools print demo.wasm # The `*.wit` file can be recovered from the `demo.wasm` as well $ wasm-tools component wit demo.wasm
Toolchain authors can use wit-component
to embed this component types section into a core wasm binary. For a small demo here a raw *.wat
wasm text file will be used where the demo.wit
argument is specified manually, however.
$ cat demo.core.wat (module (import "my:demo/host" "hello" (func)) ) $ wasm-tools component embed demo.wit --world demo demo.core.wat -o demo.wasm # See that there's a new `component-type` custom section $ wasm-tools objdump demo.wasm # Convert the core wasm into a component now $ wasm-tools component new demo.wasm -o demo.component.wasm # Like before the output `demo.wasm` is a valid component binary $ wasm-tools validate --features component-model demo.component.wasm $ wasm-tools print demo.component.wasm # Additionally like before the `*.wit` interface can still be extracted $ wasm-tools component wit demo.component.wasm
Here the demo.component.wasm
can now be shipped to a component runtime or embedded into hosts.
Toolchains producing components typically have a workflow that looks something like this:
wit-bindgen
], is used to generate source code.Steps (1) and (4) both take WIT as input and the goal is to make it such that you don't have to specify the same WIT on both ends. Instead the WIT from (1) should be seamlessly passed through to (4) without developers having to explicitly opt-in to it otherwise. To do this, the wit-component
crate and componentization process uses a custom section.
Part of the generated bindings from (1) above is source code (or similar) to instruct the language to embed a custom section in the final binary. The custom section's name must start with component-type
but can have any number of characters following it (e.g. it must match the regex ^component-type
). A module is allowed to have any number of custom sections, including zero.
Each custom section describes a WIT world
. The unit of bindings generation is a world
, and each world
used during step (1), which may possibly be in multiple separate libraries, will be encoded into custom sections. Custom sections typically have a name that includes the bindings generator, the bindings generator's version, the world, and an optional use-provided string. The contents of the custom section is a wasm-encoded WIT world.
An example of this is that for this document:
package a:b; world foo { import host: func(); export guest: func(); }
this WIT world is encoded to wasm as:
(component (type (;0;) (component (type (;0;) (component (type (;0;) (func)) (import "host" (func (;0;) (type 0))) (export (;1;) "guest" (func (type 0))) ) ) (export (;0;) "a:b/foo" (component (type 0))) ) ) (export (;1;) "foo" (type 0)) (@custom "package-docs" "\00{}") (@producers (processed-by "wit-component" "0.218.0") ) )
More details about the wasm encoding of WIT can be found upstream.
Note here that the WIT world is just the single component
-type export needed to decode a single world, no other items are encoded into this wasm.
When determining the world
for a wasm component the componentization process will read the input module, remove all sections that match ^component-type
, extract the world
that the type represents, and then “merge” all of the worlds together. This merging process can fail if there are conflicts, but otherwise duplicate imports are unified together.
The final componentization process then understands the WIT world
that's being used and assumes that the component follows the naming scheme in BuildTargets.md
upstream. (note that legacy names are also accepted at this time)
Part of what wit-component
does when creating a component is that it will merge WIT world
s from a number of sources. For example each custom section above may have a world. For tools like wasm-component-ld
worlds may also be supplied on the command line. The final component, however, can only have one set of imports and exports so a single world
needs to be created from all these input worlds.
There are two workhorse methods for performing this merging process and both live within the wit-parser
crate:
The first is unconditionally used to merge all worlds together. Each custom section, input argument, etc, are all merged with the wit_parser::Resolve::merge_worlds
method. This method may fail in certain situations, primarily if there are duplicate exports specified in different worlds (even if they‘re the same name they can’t unify to one export as it's not clear which export to pick). There are some other niche situations as well in which the merging process can fail.
Once all worlds are merged together with wit_parser::Resolve::merge_worlds
there is then an optional, but enabled by default, step to “trim” the world down based on semver versions. This method is wit_parser::Resolve::merge_world_imports_based_on_semver
and is used to change a world such as this:
world { import wasi:cli/environment@0.2.0; import wasi:cli/environment@0.2.1; }
into this
world { import wasi:cli/environment@0.2.1; }
Notably imports are deduplicated based on their semver versions of interfaces. The maximum version of the interface is always selected in the end. Only interfaces which are semver compatible are deduplicated, for example this world cannot be deduplicated further:
world { import wasi:cli/environment@0.2.0; import wasi:cli/environment@0.3.0; }
The main purpose for this deduplication is to enable internally within a component various libraries and runtimes to evolve at a different pace with respect to interface versions. By definition in WIT it should always be possible to use a larger versions, so older imports are automatically “upgraded” to newer imports in these situations. This ensures that, for example, only one wasi:io/poll/pollable
resource will be imported into the final component. If two were imported there would be no way to actually functionally use the resulting component.
Note that this semver-deduplication process is enabled by default but can be disabled through various options and flags in tooling.