alexcrichton opened issue #4844:
The current implementation of the component model traked in https://github.com/bytecodealliance/wasmtime/issues/4185 is fairly complete with the current state of the spec but a major gap is the implementation of
valueimports,valueexports, and thestartfunction. Currently Wasmtime will panic if any of these constructs are encountered.@dicej, @jameysharp, @elliottt, and I just had a chat about how we might go about implementing these and we think we have a promising set of ideas which could serve as the basis for implementing all of these features.
Simple case first
One thing we concluded was that tackling the simplest case first is probably the best where there's only one start function in a component and it takes all the imports as parameters. The start function in this case is also the last initializer within the component itself.
To implement this, some rough ideas we had were:
- A
Linkerwouldn't actually grow the ability to store values within it, ratherLinker::instantiate_prewould grow a new type parameter,Urepresenting the values that the component both imports and exports.
- For each value imported into a component
Uwould be queried whether it supports a value of that name and of the required type. This would result in a unique index passed toUlater at runtime. Part ofInstantiatePrewould be building a table of these indices.- During instantiation
Uwould have methods to "lower thenth argument into this location", either into the stack or into linear memory. The destination would be statically determined prior at component-compile-time and thenpassed in would be the result of type-checking in the first step (this is making it so instantiation doesn't do string lookups)- Instantiation of
InstancePrewould takeUas an argument and produceU::Resultas a result (or something like that)- The value results of instantiation would be required to be used prior to the
Instanceresult of instantiation (more on this later in open questions)- A new initializer for the start function would be emitted that would know ahead-of-time the canonical ABI and where to lower everything into. This could use the
Runtime*Indextypes to have fast access to memories as well.
- This initializer would optionally statically record "call malloc with these parameters to get space for the argument lowerings"
- The initializer would record some precomputed ordering of indices along the lines of "I know ahead of time that
U's index for argument"foo"was placed at index 0, so lookup theUindex at index 0 and then tellUto lower that at locationY"- Value exports could ideally use a somewhat similar scheme of precomputed indexes going through
Uor something like that. I'm having a harder time planning this out though since instead of the component asking for results it's the constructor of the result asking for names which could be a bit trickier. Hopefully not that much trickier.Whatever trait
Uimplements is one that we could add a custom derive for. Something like:#[derive(ValueImports)] struct MyCustomName { #[component(import = "bar")] foo: String, #[component(import = "foo.bar")] // e.g. an instance import `foo` where the instance exports a value named `bar` baz: String, } #[derive(ValueExports)] struct MyCustomName2 { #[component(import = "bar")] foo: String, #[component(import = "foo.bar")] // e.g. an instance export `foo` where the instance exports a value named `bar` baz: String, }(ok as I write this out I realize that the one
UI mentioned above should probably be separate type parameters for both imports and exports)Inter-component start functions
The next level of cases to handle after the above is implemented would be inter-component start functions where values from one start function flow into values of a different start function within the component graph. This sort of value transfer needs to be entirely handled by the precompiled artifact since the embedder is not even aware of the types involved here.
For this the rough assumption is that the usage of
factshould be somewhat easy to do here. Ideally this could leverage most of whatfactalready implements but at least to me it's not immediately obvious how this would be done. @jameysharp's idea was that we could model the start functions as a combination of the primitives thatfactimplements already today, although I don't think we came to a conclusion on exactly what these primitives are or what the combinations look like.Open questions
- I opened this issue to clarify but I'm at least personally not sure when it's safe to consider a return value of a start function either valid or invalid. Especially with
post-returnit's not clear when to invokepost-returnI think right now or when it's possible to reenter the instance. If reentering an instance is possible then we need to buffer the return value somewhere, but I don't think we want to be required to do that design-wise so I think some spec work may be necessary to flesh out the cases about dealing with return values and if/when a component can be reentered. (as the issue title suggest this probably 100% boils down to "when is the post-return function called?")
alexcrichton labeled issue #4844:
The current implementation of the component model traked in https://github.com/bytecodealliance/wasmtime/issues/4185 is fairly complete with the current state of the spec but a major gap is the implementation of
valueimports,valueexports, and thestartfunction. Currently Wasmtime will panic if any of these constructs are encountered.@dicej, @jameysharp, @elliottt, and I just had a chat about how we might go about implementing these and we think we have a promising set of ideas which could serve as the basis for implementing all of these features.
Simple case first
One thing we concluded was that tackling the simplest case first is probably the best where there's only one start function in a component and it takes all the imports as parameters. The start function in this case is also the last initializer within the component itself.
To implement this, some rough ideas we had were:
- A
Linkerwouldn't actually grow the ability to store values within it, ratherLinker::instantiate_prewould grow a new type parameter,Urepresenting the values that the component both imports and exports.
- For each value imported into a component
Uwould be queried whether it supports a value of that name and of the required type. This would result in a unique index passed toUlater at runtime. Part ofInstantiatePrewould be building a table of these indices.- During instantiation
Uwould have methods to "lower thenth argument into this location", either into the stack or into linear memory. The destination would be statically determined prior at component-compile-time and thenpassed in would be the result of type-checking in the first step (this is making it so instantiation doesn't do string lookups)- Instantiation of
InstancePrewould takeUas an argument and produceU::Resultas a result (or something like that)- The value results of instantiation would be required to be used prior to the
Instanceresult of instantiation (more on this later in open questions)- A new initializer for the start function would be emitted that would know ahead-of-time the canonical ABI and where to lower everything into. This could use the
Runtime*Indextypes to have fast access to memories as well.
- This initializer would optionally statically record "call malloc with these parameters to get space for the argument lowerings"
- The initializer would record some precomputed ordering of indices along the lines of "I know ahead of time that
U's index for argument"foo"was placed at index 0, so lookup theUindex at index 0 and then tellUto lower that at locationY"- Value exports could ideally use a somewhat similar scheme of precomputed indexes going through
Uor something like that. I'm having a harder time planning this out though since instead of the component asking for results it's the constructor of the result asking for names which could be a bit trickier. Hopefully not that much trickier.Whatever trait
Uimplements is one that we could add a custom derive for. Something like:#[derive(ValueImports)] struct MyCustomName { #[component(import = "bar")] foo: String, #[component(import = "foo.bar")] // e.g. an instance import `foo` where the instance exports a value named `bar` baz: String, } #[derive(ValueExports)] struct MyCustomName2 { #[component(import = "bar")] foo: String, #[component(import = "foo.bar")] // e.g. an instance export `foo` where the instance exports a value named `bar` baz: String, }(ok as I write this out I realize that the one
UI mentioned above should probably be separate type parameters for both imports and exports)Inter-component start functions
The next level of cases to handle after the above is implemented would be inter-component start functions where values from one start function flow into values of a different start function within the component graph. This sort of value transfer needs to be entirely handled by the precompiled artifact since the embedder is not even aware of the types involved here.
For this the rough assumption is that the usage of
factshould be somewhat easy to do here. Ideally this could leverage most of whatfactalready implements but at least to me it's not immediately obvious how this would be done. @jameysharp's idea was that we could model the start functions as a combination of the primitives thatfactimplements already today, although I don't think we came to a conclusion on exactly what these primitives are or what the combinations look like.Open questions
- I opened this issue to clarify but I'm at least personally not sure when it's safe to consider a return value of a start function either valid or invalid. Especially with
post-returnit's not clear when to invokepost-returnI think right now or when it's possible to reenter the instance. If reentering an instance is possible then we need to buffer the return value somewhere, but I don't think we want to be required to do that design-wise so I think some spec work may be necessary to flesh out the cases about dealing with return values and if/when a component can be reentered. (as the issue title suggest this probably 100% boils down to "when is the post-return function called?")
Last updated: Dec 06 2025 at 06:05 UTC