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

Initial support for components #388

Merged
merged 5 commits into from
Oct 24, 2024
Merged

Initial support for components #388

merged 5 commits into from
Oct 24, 2024

Conversation

jbourassa
Copy link
Collaborator

@jbourassa jbourassa commented Oct 17, 2024

Initial stab at component support. I'm sharing this work for early feedback and in case it's useful; it's not quite ready (edit: we'll iterate in main). Any feedback is welcomed!

Disclaimer: I am learning about the component model, there are most likely still gaps in my understanding.

Current state

What it supports:

  • Compiling / serializing / deserializing components
  • Calling component func exports
  • Converting between component types and Ruby values for: numbers, strings, records, tuples, lists, options

TODOs:

  • Ruby-defined host calls
    • Consider using bindgen to infer signature?
  • WASI
  • Resources
  • Converting flags, results, variant, enums
  • Component introspection
  • Better exceptions
  • Long tail of (hopefully) simpler APIs to implement

Design choices

  • The bindings wrap wasmtime::component::Val out of necessity & simplicity

  • Component model types are mapped to Ruby types as much as possible, taking into account what wasmtime::component::Val allows

    • record: Hash with String keys¹
    • option<T>: either nil or T
    • tuple<T, U>: Array ([T, U])
    • result<T, E>: T or to a Wasmtime::Component::ResultError exception wrapping E. Edit: given how result can be nested, raising an exception is not a good fit.
      Updated proposal: Wasmtime::Component::Result mimicking a Rust Result (to be implemented)
    • flags: Array<String>¹
    • enum: String¹ (to be implemented)
    • variant: Wasmtime::Component::Variant wrapping the kind (String¹) and the optional value (T or nil) (to be implemented)
    • No implicit type conversions: "3" does not get coerced to a component model u32, 1 does not get coerced to a component model bool, etc.

    ¹: The Val union often has owned Strings, converting those (e.g. into Symbols) is an additional performance hit. If we had WIT-based codegen, we could pre-build Ruby objects for types once in the linker. At runtime, any components from that linker could re-use these objects. Examples of such objects: Ruby Symbols for component enums and variants, Data class for component records.

Performance

We want the Ruby bindings to be very fast. This version leaves some performance on the table due to the usage of wasmtime::component::Val:

  1. Strings are copied out of Wasm linear memory into Val, then copied again into a Ruby string. This could be avoided (requiring changes to Wasmtime's API IIUC).
  2. Val uses owned-Strings for enums, variant, flags. Ditto, maybe avoided with a different API (but likely not easy).
  3. Record field names, enum values, etc. are all distinct Ruby strings, requiring allocations each time they're returned from Wasm. This could be improved with bindgen as described above.

Yet, the current perf might be acceptable for an initial release. Benchmarking a no-op function that round trips a point record ({ "x" => u32, "y" => u32 }), on my M2 MacBook Air:

$ bundle exec rake bench:component_id
[...]
identity point record
                          1.114M (± 1.0%) i/s  (897.57 ns/i) -      5.650M in   5.071895s

For comparison, core Wasm:

$ bundle exec rake bench:func_call
[...]
Instance#invoke      4.205M (± 2.4%) i/s  (237.81 ns/i) -     21.365M in   5.084046s

On those examples, the overhead for core Wasm is ~200ns vs 900ns for a component call that round-trips a 2-fields record.

@jbourassa jbourassa mentioned this pull request Oct 17, 2024
@sandstrom
Copy link
Contributor

@jbourassa Awesome! 🎉

Maybe that'll come later, but I think just 2-3 simple examples in Ruby code on how to use it (that would explain what the interface looks like) might help to provide some feedback.

I've read the yard docs, so I think I get the main points, but an example would still be helpful.

Thoughts

@jbourassa
Copy link
Collaborator Author

I think just 2-3 simple examples in Ruby code on how to use it (that would explain what the interface looks like)

Fair point. There are tests and a benchmark that show the gist of it, but they're not doing much. I'll add a better examples once I knock out some of the meatier features.

For enum, maybe Data might also be a good fit?

Enums are more like C enums, example from the WIT explainer:

enum color {
    red,
    green,
    blue,
    yellow,
    other,
}

Data wouldn't be a good fit here, but it'd work perfectly for records. The gotcha is that we need some kind of WIT-based bindgen to generate & attach the data class to a specific Wasm type. I haven't started looking into bindgen yet, but I suspect it's quite a bit of work.

@sandstrom
Copy link
Contributor

Thanks @jbourassa!

We have a great use-case for this coming up within a few months (I hope), so eager to try this out in a little while.

Copy link
Member

@saulecabrera saulecabrera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a first pass this all looks very reasonable to me. I left a couple of thoughts/questions, I wouldn't consider any of them as blocking, if you're good with it we can consider landing this change as-is and continue iterating in-tree.

@jbourassa
Copy link
Collaborator Author

continue iterating in-tree

Once it's in main, it'll be released in the next version. I'm not sure it's good enough to be released: missing features, potentially unstable API, etc.

The main con of iterating here is the review burden: we'll end up with one very large PR. I see a couple options:

  1. Accept large PRs
  2. Use documentation to mark components as experimental
  3. Invent a solution for excluding components from release (using a cargo feature + ENV var for Ruby)
  4. Introduce a components feature branch that we merge in

I think option 2 (experimental mention in the doc) along with some more polish on this PR is reasonable, but I'm okay with other options too.

@saulecabrera
Copy link
Member

The main con of iterating here is the review burden: we'll end up with one very large PR.

Right -- I think that if we're not comfortable with the state of this PR for it to be available by default, instead of having to mark the entirety of the components API as experimental I'm leaning toward option 3 (this should be trivial in Rust, but I'm unsure about Ruby). The reason that I'm leaning toward 3 is that once we feel the implementation is good enough, it'll be easier to mark it as such, rather than having to audit and update the entire components API surface.

@jbourassa jbourassa force-pushed the components-1 branch 2 times, most recently from 84a0fd2 to 391d2ad Compare October 23, 2024 17:26
@jbourassa
Copy link
Collaborator Author

jbourassa commented Oct 23, 2024

I'm leaning toward option 3 (this should be trivial in Rust, but I'm unsure about Ruby).

Dug into it today and unfortunately it's not trivial, at least if we want to do it well. Some of the problems we have to solve:

  • the tooling for cross-compiling gem does not have a way to inject cargo features nor ENV vars
  • tag all component-related tests and examples so they're skipped if not compiled with the feature
  • run CI for both component and non-component branch
  • teach YARD to conditionally skip documentating the Ruby-defined APIs for components (edit: that's trivial)

All in all, it doesn't seem worth it, no?

The reason that I'm leaning toward 3 is that once we feel the implementation is good enough, it'll be easier to mark it as such, rather than having to audit and update the entire components API surface.

How does a feature flag avoid the need to audit the API? The way I see it, removing the flag is akin to dropping the doc mention or merging the feature branch in main. None of the options guarantees the APIs is good, and none prevent thorough PR reviews.


I think I can get to a place where the API is good enough soon, once the remaining types and host-defined functions are implemented. I'm sure there will still be breaking changes, but nothing too difficult to handle.

@saulecabrera
Copy link
Member

saulecabrera commented Oct 23, 2024

How does a feature flag avoid the need to audit the API?

Assuming I understood point 2 correctly, I'd imagine that in order to move us out of "work-in-progress" state we'd need to un-mark all the methods in the component API documentation as unstable; with a flag on the other hand it's mostly a matter of enabling it by default? This is mostly to avoid toil on our end.

Edit (clicked too soon the comment button)

Dug into it today and unfortunately it's not trivial, at least if we want to do it well. Some of the problems we have to solve:

I assumed that this was going to be easier, at least from Rust's perspective, but I do agree that working on all the steps that you've outlined is probably not worth it and probably the easiest is option 2.

@jbourassa
Copy link
Collaborator Author

Assuming I understood point 2 correctly, I'd imagine that in order to move us out of "work-in-progress" state we'd need to un-mark all the methods in the component API documentation as unstable;

Oh, I see. I was thinking a single mention on the top level Wasmtime::Component::Component class itself, like "support for Wasm component is experimental, expect APIs to change".

I guess another pro of that approach is that it's easier for people to provide feedback.

@jbourassa
Copy link
Collaborator Author

I added the experimental mention:

experimental mention

Now that v26 is releasing, I'll merge this on green and continue iterating with smaller PRs.

@jbourassa jbourassa marked this pull request as ready for review October 24, 2024 17:13
@jbourassa jbourassa merged commit 72a18aa into main Oct 24, 2024
23 checks passed
@saulecabrera saulecabrera deleted the components-1 branch October 24, 2024 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants