tessi / wasmex

Execute WebAssembly from Elixir
MIT License
558 stars 32 forks source link

Features/wasm components #630

Open superchris opened 2 months ago

superchris commented 2 months ago

This is a draft PR to show how the progress is coming along for supporting web assembly components. Check out wasm_components_test.exs to see where things are at

superchris commented 2 months ago

So I've hit a bit of wall here where it looks I'll need to do something drastic to get component support working the way I think it should. A little conext: A wasm component has, as it's definition, a .wit file which specifies the high level, typed, interface to the component. The wasmtime rust crate provides a bindgen macro which generates the rust bindings for a component. This means that for a component to be used, their will need to be some project specific rust code which depends on the the wasmex rust code. Trying to have one rustler nif depend on another rustler nif did not seem to go very well, which makes me think what would need to happen is the rustler nif would need to be specific to the components being used. In order for this to be viable, there would need to be considerable change to the wasmex codebase methinks, probably involving allowing the use to specify the Native module via configuration so that they could sub in their own, and then maybe a use Wasmex.Native macro to bring in all the wasmex stuff. I don't know if I've described this well enough to make any sense at all, but I'm trying to understand if I should keep marching in this direction or if this is so radical that I should fork and do component specific hex package. Love to hear any thoughts you might have @tessi etc

tessi commented 2 months ago

Hey @superchris

first things first: it's awesome that you start tackling this. Having good component support is the next most pressing thing for wasmex to have. thank you πŸ’œ

I started looking into components but didn't had the time yet to start a real prototype - so I'm afraid my understanding of how components work isn't perfect yet. My (maybe naΓ―ve?) understanding of components was that they are more or less normal wasm modules with added .wit type definitions. Those type definitions could be "used" by wasmex to read/write function results/params. I wasn't sure how that "using" of the WIT types could be done - my hope was to maybe have an elixir macro that defines proper elixir-structs, which wasmex then knows how to (de-)serialise to what the bytes the wasm module needs. Basically, like building our own bindgen.

Coming from that understanding, I didn't get the part on why we need to link other NIFs or use the wasmtime bindgen. Can you elaborate there, please? I probably just don't know enough to fully understand the problem - so just dropping a link to the relevant docs for me to educate myself also works.

If you're up for it, we could also just have a call and discuss/brainstorm possible approaches synchronous. :)

superchris commented 2 months ago

Hey @tessi! Thanks for wasmex :) So the issue I am facing is that I will need to generate two kinds of bindings, one in rust, and another in Elixir. In my branch I the rust bindings I need are in native/wasmex/src/todo_list.rs and the elixir bindings are the todo_* functions in Native. These will need to exist for each component, at least the way I understand things right now. This means each project will have rust code of it's own that will depend on the core wasmex rust code, which is the wall I hit. I'd be delighted to do a brainstorm call (and maybe try to better explain things), feel free to suggest some times that are good for you. I'm UTC-4 (EDT).

superchris commented 2 months ago

@tessi I think after reading your comments again I understand what you are saying. Having our own Elixir bindgen would be amazing, I was trying to do a lazier approach because I was intimidated by making to do the lowering and lifting of elixir types into wasm memory. I probably am not facile enough with rust and the internals of wasm to be able to tackle the elixir bindgen idea just yet. Anyways, happy to talk if you'd like πŸ˜„

tessi commented 2 months ago

yay, i'm on UTC+2 so that isn't horribly far off. I'm currently on watch for the barely sleeping, feverish baby so today is not a good day to talk sync. But we'll find a time :)

Not being very into wrangling bytes in rust shouldn't stop us. Maybe we can somehow expose the raw memory to elixir and write/read it from there. Not sure what's best, but we have options. I'll do some reading into wasmtimes bindgen and see how heavy it is. From what you wrote here, writing our own bindgen/wrapper/wit-type-serialiser still sounds like a good approach, if we can make it work - much easier to use than having to build a separate NIF and somehow link it πŸ€”

tessi commented 2 months ago

brainstorm idea: if we can implement Lower and Lift for elixir types/structs (see: https://github.com/bytecodealliance/wasmtime/blob/e51a68e64ae2e955b69a6f9e37f5479f8186ae91/crates/wasmtime/src/runtime/component/func/typed.rs#L505), then we have the load+store operations done. πŸ€”

wasmtimes bindgen! macro basically utilizes the Lift/Lower traits, which we can implement ourselves for elixir-structs instead of deriving it automatically from known rust types.

superchris commented 1 month ago

Hey @tessi I got a little further in my implementation and at this point I've hacked things up to the point its a bit of a mess ;) I had to switch out the wasi library to new ones (wasmtime-wasi and wasmtime-wasi-http) that support WASI p2. As a result, I was able to call into a component in JS that did external http calls using JS native fetch, which was pretty exciting. However, I'm still not quite sure how to move forward with an approach that wouldn't involve per component rust code. I think I might spin up a component specific package, even if just temporarily, to see what using per component rust code would even look like. However, I would be happy to pair on your ideas for wasmex and pursue both approaches.

superchris commented 2 weeks ago

I did end up making some amount of progress after I decided to split off to another project at least temporarily. It's still got some hand coded plumbing I need to get rid of, but have several examples working with tests to at least prove out the approach. Code is here: https://github.com/launchscout/wasm_components_ex

However after looking at wasmtime-rb where the beginning of component support has landed, I think their approach is better and would allow us to dynamically invoke functions on components without a separate nif lib per component as my current approach requires. I may try another PR based on it. In short, they convert ruby types back and forth to wasmcomponent val types in rust. This seems like a much smaller lift (pun intended :) ).

I still think the lift/lower idea is a good one, just not quite sure how to start and how ambitious it is compared to the other approaches. Happy to pair some time @tessi if you'd like

tessi commented 2 weeks ago

wow, cool! will have a look this evening - both, your examples as well as wasmtime-rb

actually, i'm not too too set on a single idea as long as users don't have to learn rust/nifs (given they already have a .wasm component). I mean, if that's what's required we have to, but better not :)

re pairing: I'm sure by now you're the subject matter expert on components :) happy to pair though. Let's arrange something in a private channel. How can I reach you best? bluesky/mastodon would be my favourites, mail works too

superchris commented 2 weeks ago

Hey @tessi I just started trying to use BlueSky more I am @superchrisnelson.bsky.social

tessi commented 2 weeks ago

@superchris thanks again for the call yesterday! was fun and it fuels more excitement for component support πŸ”₯ I started a parallel PR in #663 to experiment. As I said yesterday, this is not to take away from your great work - I just need to play with things to build an understanding and share ideas with you πŸ’œ

superchris commented 1 week ago

That sounds awesome more experiments are the way to go. Eventually the best path forward will emerge :)

superchris commented 1 week ago

@tessi I actually made some progress and was able to dynamically invoke a component function. I think it may be just a matter of filling in all the type conversions from here (not to minimize that effort, which isn't trivial).

tessi commented 1 week ago

code is a little gross, but maps and lists are working

very artful commit message πŸ˜†

in any way: @superchris I love your work and approach you've taken on this. the dynamic type mapping makes sense and seems to be extensible to cover all cases. love it!

I think this MR needs a clean up, some polishing, and structured tests (in the sense of covering all type cases systematically), but you've proven your approach works. This is the way to go, thanks!

I'm not exactly sure how to cover typical component extensions like Wasi, Http etc. I would love them to be some kind of option. so you can optionally enable wasi but don't have to (same for the others). is that possible? what do you think?