uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

WASM support

Open benkuly opened this issue 2 years ago • 15 comments

For full Kotlin Multiplatform support within uniffi-kotlin-multiplatform-bindings I need some sort of WASM support.

My idea was to expose RustBuffer, RustCallStatus and the scaffolding code with wasm-bindgen, but there are some problems. For example wasm-bindgen does not support MaybeUninit<RustBuffer>. What do you think about this idea? I don't have enough experience with Rust to be able to assess this.

┆Issue is synchronized with this Jira Task ┆Issue Number: UNIFFI-218

benkuly avatar Dec 14 '22 10:12 benkuly

Have you made any progress on this? I'm also investigating WASM JS bindings, and I'm gonna write my proposal for WASM JS implementation in this thread later this week.

arg0d avatar Jan 18 '23 08:01 arg0d

No more progess since opening the issue.

benkuly avatar Jan 18 '23 08:01 benkuly

I have made some experimentation, and here are my findings. Before proceeding with this idea, some feedback would be really helpful @mhammond @rfk

Rationalization

First of all, I have made some rationalization for why this is a good idea.

  • uniffi-bindgen-wasm-js would be an optional addon just like other foreign bindings. No major changes are needed in uniffi-rs upstream repository.
  • uniffi-rs scaffolding remains the same, even when you opt to use uniffi-bindgen-wasm-js. This is different from the first point, because its possible to fork uniffi_bindgen, and generate entirely different scaffolding for WASM. Forking the scaffolding code does not sound nice, because more changes = more maintenance.

Proposal

  • wasm_bindgen should be part of the initial solution, because at the moment its not practical to re-invent and replace wasm_bindgen (unless of course there is someone bored enough to undertake such a task).
  • To expose functions to wasm_bindgen ABI, generate a secondary scaffolding file. The consumer would compile the secondary scaffolding file with include!, just like the primary scaffolding file. For each extern "C" function in the primary scaffolding file, generate a secondary #[wasm_bindgen] pub fn wasm_* function, which would be exposed to JS ABI. The implementation of the function would be to call the original function from the primary scaffolding file.
  • wasm_bindgen is gonna take care of generating "intermediate" JS bindings. For the normal Rust<->Wasm use case, these bindings are completely adequate, but for Uniffi these are not adequate bindings. We can piggyback on these bindings to generate Uniffi bindings.
  • Generate bindings for JS like for any other language. The only difference being that instead of invoking C ABI, invoke wasm_bindgen ABI through the "intermediate" bindings generated by wasm_bindgen.
  • In the secondary scaffolding, use Vec<u8> to represent RustBuffer. Use RustBuffer::from_vec and RustBuffer::destroy_into_vec to convert between Vec<u8> and RustBuffer. This works out very well, because while moving between JS and primary scaffolding, there is only 1 copy being made: the copy by wasm_bindgen when transitioning between WASM and JS. No additional copy is needed in the primary<->secondary scaffolding layer, because of RustBuffer::from_vec and RustBuffer::destroy_into_vec.
  • In the secondary scaffolding, use -> Result<(), WasmCallStatus> instead of &mut RustCallStatus. From my research wasm_bindgen does not provide built-in support for passing in arbitrary structs as &mut from JS. However, wasm_bindgen does provide good enough support for returning Result. On Rust side, the function simply returns a Result, where the error is a type convertable to JS ABI. On JS side, when an error is returned, intermediate JS bindings throw the error value. This intermediate error value can be caught, refined into a Uniffi error class, and rethrown.
  • In the secondary scaffolding, objects should work basically out of the box. Uniffi C ABI exposes objects as pointers, and pointers are easily converted into u64.
  • In the secondary scaffolding, expose callback_init functions using js_sys::Function. This will require some additional scaffolding to transition between js_sys::Function and function pointers required by primary scaffolding C ABI. All other callback functions use handles (u64), so no special tricks are needed for those functions.

arg0d avatar Jan 19 '23 13:01 arg0d

At a very high abstract level, this makes perfect sense to me and is roughly the approach @bendk took for the Desktop Firefox JS bindings (although there we generate .webidl bindings and implementation etc). The fact these bindings could be external and the of the implementation: "No major changes are needed in uniffi-rs upstream repository" - what's not to like? :)

mhammond avatar Jan 23 '23 06:01 mhammond

It makes a lot of sense to me as well and also reminds me of the Firefox JS work. That ended up working out, so I think this effort could too.

So, would each call to Rust would go like this: JS -> secondary scaffolding -> primary scaffolding -> actual rust function?

I wonder if you could skip the primary scaffolding altogether. Most of the existing complexity is defining FfiConverter impls for the types. The actual scaffolding calls are fairly easy to put together. I'm not against going through the extra function, I'd just be interested to know what prevents this and if we could update FfiConverter to fix those issues.

In the secondary scaffolding, use -> Result<(), WasmCallStatus> instead of &mut RustCallStatus. From my research wasm_bindgen does not provide built-in support for passing in arbitrary structs as &mut from JS. However, wasm_bindgen does provide good enough support for returning Result. On Rust side, the function simply returns a Result, where the error is a type convertable to JS ABI. On JS side, when an error is returned, intermediate JS bindings throw the error value. This intermediate error value can be caught, refined into a Uniffi error class, and rethrown.

This is a much nicer API IMO, and it would be nice if the other bindings did something similar. From a quick read, it seems like wasm-bindgen supports Result<T, JsValue>. Is there a way to represent a RustBuffer as a JsValue?

One thing that will probably be tricky is callback interfaces. I think it should be doable, but a pain.

bendk avatar Jan 23 '23 15:01 bendk

@bendk you are right, after digging into Rust scaffolding, it looks like it would be better to simply provide an alternate scaffolding implementation for WASM. I will investigate this further.

Yes, RustBuffer can be converted to JsValue, but it needs to be annotated with #[wasm_bindgen]. I would really like to avoid this, because it would mean having to insert WASM code into uniffi-rs upstream. But I think this can be avoided by simply created an identical WasmBuffer struct, and WASM scaffolding could simply map between RustBuffer and WasmBuffer as needed.

Why do you think that callback interfaces are tricky? Rust can easily call JS by using js_sys::JsFunction.

It looks like my department is deprioritizing WASM JS bindings for this quarter, but I have hope to come back to this within the next few months.

arg0d avatar Jan 24 '23 08:01 arg0d

Makes sense to me, excited to see how this progresses.

I don't think there's anything fundamentally hard about callback interfaces, it's just felt like they're a lot of work to implement relative to the rest of the features.

bendk avatar Jan 24 '23 21:01 bendk

what about using wasmer? https://wasmer.io

matthiasdebernardini avatar Apr 12 '23 16:04 matthiasdebernardini

what about using wasmer? wasmer.io

What about it? wasmer is a Wasm runtime. It can run any wasm you give it. wasm support in UniFFI would mean you can build it in a way that other parts that can interact with wasm can interact with the UniFFI-powered parts.

badboy avatar Apr 13 '23 10:04 badboy

Hello @arg0d! Have you had a chance to come back to this? I'd be highly interested.

gyzerok avatar May 25 '23 05:05 gyzerok

Hey @gyzerok, wasm bindings were deprioritized completely by the library team :( Due to small size of their library, and due to wasm limitations put on Rust code, it will be easier for them to maintain a separate JS version of the library.

arg0d avatar May 25 '23 07:05 arg0d

I see thank you!

@bendk in earlier discussion it is mentioned that Mozilla seems to have some uniffi implementation for WASM used for Firefox desktop. Is there any chance you'd be willing to share those?

gyzerok avatar May 25 '23 07:05 gyzerok

I see thank you!

@bendk in earlier discussion it is mentioned that Mozilla seems to have some uniffi implementation for WASM used for Firefox desktop. Is there any chance you'd be willing to share those?

(not ben) we don't have any Wasm implementation. There's a custom JS integration in Gecko, but that's very Gecko-specific.

badboy avatar May 25 '23 08:05 badboy

@arg0d as an user I tested wasm-bindgen and I would like to have uniffi-bindgen module with javascript directly inlcuded in order to avoid to keep my single udl file as required code modification for bindings.

gogo2464 avatar Sep 07 '23 18:09 gogo2464

This would definitely be super useful. I'm trying to migrate an Android app to Kotlin Multiplatform, including Wasm support, but I have a Rust shared library. Using UniFFI is pretty great so far for both Android, iOS, and Desktop. It's just the WebAssembly integration that's missing.

skaldebane avatar Jun 10 '24 19:06 skaldebane