Skip to content
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

Merged
merged 12 commits into from
Mar 11, 2021

Conversation

alexcrichton
Copy link
Member

@alexcrichton alexcrichton commented Mar 10, 2021

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 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<&TypedFunc<P, R>>;
}

impl TypedFunc<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 TypedFunc<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 TypedFunc value. Otherwise an error is returned.

Once you have a TypedFunc 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).

@github-actions github-actions bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label Mar 10, 2021
@github-actions
Copy link

Subscribe to Label Action

cc @peterhuene

This issue or pull request has been labeled: "wasmtime:api"

Thus the following users have been cc'd because of the following labels:

  • peterhuene: wasmtime:api

To subscribe or unsubscribe from this label, edit the .github/subscribe-to-label.json configuration file.

Learn more.

@alexcrichton
Copy link
Member Author

I was also struct with inspiration after implementing this and I've gone ahead and implemented Func::typed for multi-value returns, which is something we didn't previously support via Func::get*

@github-actions github-actions bot added the wasmtime:docs Issues related to Wasmtime's documentation label Mar 10, 2021
Copy link
Member

@peterhuene peterhuene left a 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.

crates/wasmtime/src/func.rs Outdated Show resolved Hide resolved
crates/wasmtime/src/func.rs Outdated Show resolved Hide resolved
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
@alexcrichton
Copy link
Member Author

Ok I'm really liking how Instance::get_typed_func worked out!

I've updated and rebased on recent changes now too

@alexcrichton alexcrichton requested a review from peterhuene March 11, 2021 17:36
Copy link
Member

@peterhuene peterhuene left a 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.

@alexcrichton alexcrichton merged commit 2697a18 into bytecodealliance:main Mar 11, 2021
@alexcrichton alexcrichton deleted the typed-func branch March 11, 2021 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wasmtime:api Related to the API of the `wasmtime` crate itself wasmtime:docs Issues related to Wasmtime's documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants