-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redo the statically typed Func
API
#2719
Conversation
Subscribe to Label Actioncc @peterhuene
This issue or pull request has been labeled: "wasmtime:api"
Thus the following users have been cc'd because of the following labels:
To subscribe or unsubscribe from this label, edit the |
I was also struct with inspiration after implementing this and I've gone ahead and implemented |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just the one nit and a question about perhaps adding Instance::get_typed_func
.
These changes are fantastic and I think the new interface is much more elegant and understandable.
This commit reimplements the `Func` API with respect to statically typed dispatch. Previously `Func` had a `getN` and `getN_async` family of methods which were implemented for 0 to 16 parameters. The return value of these functions was an `impl Fn(..)` closure with the appropriate parameters and return values. There are a number of downsides with this approach that have become apparent over time: * The addition of `*_async` doubled the API surface area (which is quite large here due to one-method-per-number-of-parameters). * The [documentation of `Func`][old-docs] are quite verbose and feel "polluted" with all these getters, making it harder to understand the other methods that can be used to interact with a `Func`. * These methods unconditionally pay the cost of returning an owned `impl Fn` with a `'static` lifetime. While cheap, this is still paying the cost for cloning the `Store` effectively and moving data into the closed-over environment. * Storage of the return value into a struct, for example, always requires `Box`-ing the returned closure since it otherwise cannot be named. * Recently I had the desire to implement an "unchecked" path for invoking wasm where you unsafely assert the type signature of a wasm function. Doing this with today's scheme would require doubling (again) the API surface area for both async and synchronous calls, further polluting the documentation. The main benefit of the previous scheme is that by returning a `impl Fn` it was quite easy and ergonomic to actually invoke the function. In practice, though, examples would often have something akin to `.get0::<()>()?()?` which is a lot of things to interpret all at once. Note that `get0` means "0 parameters" yet a type parameter is passed. There's also a double function invocation which looks like a lot of characters all lined up in a row. Overall, I think that the previous design is starting to show too many cracks and deserves a rewrite. This commit is that rewrite. The new design in this commit is to delete the `getN{,_async}` family of functions and instead have a new API: impl Func { fn typed<P, R>(&self) -> Result<&Typed<P, R>>; } impl Typed<P, R> { fn call(&self, params: P) -> Result<R, Trap>; async fn call_async(&self, params: P) -> Result<R, Trap>; } This should entirely replace the current scheme, albeit by slightly losing ergonomics use cases. The idea behind the API is that the existence of `Typed<P, R>` is a "proof" that the underlying function takes `P` and returns `R`. The `Func::typed` method peforms a runtime type-check to ensure that types all match up, and if successful you get a `Typed` value. Otherwise an error is returned. Once you have a `Typed` then, like `Func`, you can either `call` or `call_async`. The difference with a `Typed`, however, is that the params/results are statically known and hence these calls can be much more efficient. This is a much smaller API surface area from before and should greatly simplify the `Func` documentation. There's still a problem where `Func::wrapN_async` produces a lot of functions to document, but that's now the sole offender. It's a nice benefit that the statically-typed-async verisons are now expressed with an `async` function rather than a function-returning-a-future which makes it both more efficient and easier to understand. The type `P` and `R` are intended to either be bare types (e.g. `i32`) or tuples of any length (including 0). At this time `R` is only allowed to be `()` or a bare `i32`-style type because multi-value is not supported with a native ABI (yet). The `P`, however, can be any size of tuples of parameters. This is also where some ergonomics are lost because instead of `f(1, 2)` you now have to write `f.call((1, 2))` (note the double-parens). Similarly `f()` becomes `f.call(())`. Overall I feel that this is a better tradeoff than before. While not universally better due to the loss in ergonomics I feel that this design is much more flexible in terms of what you can do with the return value and also understanding the API surface area (just less to take in). [old-docs]: https://docs.rs/wasmtime/0.24.0/wasmtime/struct.Func.html#method.get0
813aa50
to
0f604f4
Compare
Ok I'm really liking how I've updated and rebased on recent changes now too |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reviewed the Instance::get_typed_func
changes 👍
Looks like the tests aren't compiling though.
This commit reimplements the
Func
API with respect to statically typeddispatch. Previously
Func
had agetN
andgetN_async
family ofmethods which were implemented for 0 to 16 parameters. The return value
of these functions was an
impl Fn(..)
closure with the appropriateparameters and return values.
There are a number of downsides with this approach that have become
apparent over time:
*_async
doubled the API surface area (which is quitelarge here due to one-method-per-number-of-parameters).
Func
are quite verbose and feel"polluted" with all these getters, making it harder to understand the
other methods that can be used to interact with a
Func
.impl Fn
with a'static
lifetime. While cheap, this is still paying thecost for cloning the
Store
effectively and moving data into theclosed-over environment.
requires
Box
-ing the returned closure since it otherwise cannot benamed.
invoking wasm where you unsafely assert the type signature of a wasm
function. Doing this with today's scheme would require doubling
(again) the API surface area for both async and synchronous calls,
further polluting the documentation.
The main benefit of the previous scheme is that by returning a
impl Fn
it was quite easy and ergonomic to actually invoke the function. In
practice, though, examples would often have something akin to
.get0::<()>()?()?
which is a lot of things to interpret all at once.Note that
get0
means "0 parameters" yet a type parameter is passed.There's also a double function invocation which looks like a lot of
characters all lined up in a row.
Overall, I think that the previous design is starting to show too many
cracks and deserves a rewrite. This commit is that rewrite.
The new design in this commit is to delete the
getN{,_async}
family offunctions and instead have a new API:
This should entirely replace the current scheme, albeit by slightly
losing ergonomics use cases. The idea behind the API is that the
existence of
TypedFunc<P, R>
is a "proof" that the underlying functiontakes
P
and returnsR
. TheFunc::typed
method peforms a runtimetype-check to ensure that types all match up, and if successful you get
a
TypedFunc
value. Otherwise an error is returned.Once you have a
TypedFunc
then, likeFunc
, you can eithercall
orcall_async
. The difference with aTyped
, however, is that theparams/results are statically known and hence these calls can be much
more efficient.
This is a much smaller API surface area from before and should greatly
simplify the
Func
documentation. There's still a problem whereFunc::wrapN_async
produces a lot of functions to document, but that'snow the sole offender. It's a nice benefit that the
statically-typed-async verisons are now expressed with an
async
function rather than a function-returning-a-future which makes it both
more efficient and easier to understand.
The type
P
andR
are intended to either be bare types (e.g.i32
)or tuples of any length (including 0). At this time
R
is only allowedto be
()
or a barei32
-style type because multi-value is notsupported with a native ABI (yet). The
P
, however, can be any size oftuples of parameters. This is also where some ergonomics are lost
because instead of
f(1, 2)
you now have to writef.call((1, 2))
(note the double-parens). Similarly
f()
becomesf.call(())
.Overall I feel that this is a better tradeoff than before. While not
universally better due to the loss in ergonomics I feel that this design
is much more flexible in terms of what you can do with the return value
and also understanding the API surface area (just less to take in).