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
value
imports,value
exports, and thestart
function. 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
Linker
wouldn't actually grow the ability to store values within it, ratherLinker::instantiate_pre
would grow a new type parameter,U
representing the values that the component both imports and exports.
- For each value imported into a component
U
would be queried whether it supports a value of that name and of the required type. This would result in a unique index passed toU
later at runtime. Part ofInstantiatePre
would be building a table of these indices.- During instantiation
U
would have methods to "lower then
th argument into this location", either into the stack or into linear memory. The destination would be statically determined prior at component-compile-time and then
passed 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
InstancePre
would takeU
as an argument and produceU::Result
as a result (or something like that)- The value results of instantiation would be required to be used prior to the
Instance
result 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*Index
types 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 theU
index at index 0 and then tellU
to lower that at locationY
"- Value exports could ideally use a somewhat similar scheme of precomputed indexes going through
U
or 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
U
implements 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
U
I 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
fact
should be somewhat easy to do here. Ideally this could leverage most of whatfact
already 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 thatfact
implements 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-return
it's not clear when to invokepost-return
I 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
value
imports,value
exports, and thestart
function. 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
Linker
wouldn't actually grow the ability to store values within it, ratherLinker::instantiate_pre
would grow a new type parameter,U
representing the values that the component both imports and exports.
- For each value imported into a component
U
would be queried whether it supports a value of that name and of the required type. This would result in a unique index passed toU
later at runtime. Part ofInstantiatePre
would be building a table of these indices.- During instantiation
U
would have methods to "lower then
th argument into this location", either into the stack or into linear memory. The destination would be statically determined prior at component-compile-time and then
passed 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
InstancePre
would takeU
as an argument and produceU::Result
as a result (or something like that)- The value results of instantiation would be required to be used prior to the
Instance
result 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*Index
types 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 theU
index at index 0 and then tellU
to lower that at locationY
"- Value exports could ideally use a somewhat similar scheme of precomputed indexes going through
U
or 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
U
implements 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
U
I 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
fact
should be somewhat easy to do here. Ideally this could leverage most of whatfact
already 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 thatfact
implements 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-return
it's not clear when to invokepost-return
I 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 23 2024 at 12:05 UTC