-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
1,120 additions
and
0 deletions.
There are no files selected for viewing
83 changes: 83 additions & 0 deletions
83
importable/2024-06-24-setting-up-uniffi-for-ios-simulators-and-watchos.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
--- | ||
title: "Setting up UniFFI for iOS, Simulators, and watchOS" | ||
tags: rust, wasm, leptos, mobile | ||
date: 2024-06-24 | ||
--- | ||
|
||
This is a part of the post [Mobile: A different way?](/posts/2024-06-25-mobile-a-different-way.html). | ||
|
||
There are some great resources out there on UniFFI already such as [this post](https://forgen.tech/en/blog/post/building-an-ios-app-with-rust-using-uniffi), but it doesn’t cover watchOS, so let’s take a quick tour through what I’ve set up in the example repository [https://github.com/Tehnix/template-mobile-wasm](https://github.com/Tehnix/template-mobile-wasm)[.](https://github.com/Tehnix/playground-mobile-wasm?tab=readme-ov-file) | ||
|
||
We've set up four crates: | ||
|
||
- `appy`: Our Leptos App, Capacitor, and the XCode project | ||
- `capacitor-rs`: Bridging code between the Capacitor JS library and our Rust code | ||
- `shared`: Our shared code that we might use in `appy`, and also want to expose in Swift to use in our Widgets or watchOS App | ||
- `mobile`: Where we will generate the Swift bindings from via UniFFI, reexporting everything from `shared` that’s made available to UniFFI via the macros | ||
|
||
I won’t go over the details to get these to play nicely with Cargo and Workspaces, check out the repository for that. Let’s instead focus on a simplified version of what `mobile` does (the rest assumes you’re in the `mobile/` directory). | ||
|
||
<div></div><!--more--> | ||
|
||
If starting from scratch, create a new cargo project: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(1).sh | ||
|
||
Update your `Cargo.toml`: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=mobile%5CCargo.toml | ||
|
||
Update your `src/lib.rs`: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=mobile%5Csrc%5Clib.rs | ||
|
||
We’re using `setup_scaffolding` to avoid needing to manually construct headers, modulemaps, and the UDL files (check out the [docs here](https://mozilla.github.io/uniffi-rs/0.27/tutorial/Rust_scaffolding.html#setup-for-crates-using-only-proc-macros)). | ||
|
||
<div class="callout"> | ||
<div class="callout-bulb">💡</div> | ||
If you are rexporting another crate, like I am in the example repository, you can replace all of the contents of `src/lib.rs` with this line pointing to your other crate, e.g. “shared”, `shared::uniffi_reexport_scaffolding!();` ([docs here](https://mozilla.github.io/uniffi-rs/0.27/tutorial/Rust_scaffolding.html#libraries-that-depend-on-uniffi-components)). | ||
</div> | ||
|
||
And finally, create a new file `src/bin/uniffi-bindgen.rs`: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=mobile%5Csrc%5Cbin%5Cuniffi-bindgen.rs | ||
|
||
We’re now ready to build the binary for generating our bindings, and then use that to generate the actual bindings: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(5).sh | ||
|
||
We also need to rename the FFI file to `module.modulemap` so that XCFramework will find it: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(6).sh | ||
|
||
Now, let's add support for iOS, the Simulator and macOS via rustup: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(7).sh | ||
|
||
and then build the library for all of our targets: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(8).sh | ||
|
||
We'll combine `x86_64-apple-ios` and `aarch64-apple-ios-sim` into a single binary later on, but for now we keep them separate. | ||
|
||
If we want watchOS we need to handle things a bit differently, since these are Tier 3 targets (i.e. rustup won't have their stdlib): | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(9).sh | ||
|
||
That's a lot of targets, which represent all the various Watch models, as well as the simulators (we always need both ARM and x86). | ||
|
||
`xcodebuild` won't be happy if we just drop them in individually, so we need to create a fat binary: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(10).sh | ||
|
||
We can then create our XCFramework: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(11).sh | ||
|
||
And finally, we'll combine `x86_64-apple-ios` and `aarch64-apple-ios-sim` into a single binary. If we included both of these in the XCFramework, `xcodebuild` would complain that these are the same, and not generate our XCFramework file. Oddly enough, it will not be able to build the project without both, so we let `xcodebuild` generate the XCFramework first, and then replace the binary with the fat binary: | ||
|
||
https://gist.github.com/Tehnix/74dfea95fe6b38de0fdd10050b20adb3.js?file=terminal%20(12).sh | ||
|
||
Done! | ||
|
||
As the final step we drag-n-drop ./ios/Shared.xcframework and ./bindings/shared.swift into the XCode project whereever you want them. I personally like to create a new group (folder) called `Generated` for them (the `build-ios.sh` script assumes that's the case). |
95 changes: 95 additions & 0 deletions
95
importable/2024-06-24-using-capacitor-plugins-from-rust-wasm.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
--- | ||
title: "Using Capacitor Plugins from Rust/WASM" | ||
tags: rust, wasm, leptos, mobile | ||
date: 2024-06-24 | ||
--- | ||
|
||
This is a part of the post [Mobile: A different way?](/posts/2024-06-25-mobile-a-different-way.html). | ||
|
||
Capacitor is normally used in combination with JavaScript projects, so there is little help available if you want to use it in a Rust/WASM project. Worry not! That's exactly what we'll take a look at in this post. | ||
|
||
Since Capacitor is a JS framework there will be a little extra work involved in interacting with any plugins, but it’s honestly not a lot we need to do. Let’s take a quick tour through what I’ve set up in the example repository [https://github.com/Tehnix/template-mobile-wasm](https://github.com/Tehnix/template-mobile-wasm)[.](https://github.com/Tehnix/playground-mobile-wasm?tab=readme-ov-file) | ||
|
||
<div></div><!--more--> | ||
|
||
We've set up four crates: | ||
|
||
- `appy`: Our Leptos App, Capacitor, and the XCode project | ||
- `capacitor-rs`: Bridging code between the Capacitor JS library and our Rust code | ||
- `shared`: Our shared code that we might use in `appy`, and also want to expose in Swift to use in our Widgets or watchOS App | ||
- `mobile`: Where we will generate the Swift bindings from via UniFFI, reexporting everything from `shared` that’s made available to UniFFI via the macros | ||
|
||
In this post we'll focus on the `capacitor-rs` and `appy` crates, which contain our Capacitor bridge and our app where we'll use it respectively. | ||
|
||
First, let’s create a new library where we’ll keep the Capacitor bridge related code in: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=terminal%20(1).sh | ||
|
||
We’ll also initialize an empty TypeScript project using bun which we’ll use to install the Capacitor dependencies and bundle our TypeScript files: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=terminal%20(2).sh | ||
|
||
We’ll then create a few new folders to host our code in (assuming you’re in the `./capacitor-rs` directory): | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=terminal%20(3).sh | ||
|
||
Let’s set up our `capacitor-rs/src/lib.rs` to expose the plugins folder: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=src%5Clib.rs | ||
|
||
And finally, let’s update our dependencies of the Rust project in `./capacitor-rs/Cargo.toml`: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=capacitor-rs%5CCargo.toml | ||
|
||
For this example we’ll set up the [Haptics](https://capacitorjs.com/docs/apis/haptics) plugin, which will allow us to provide tactile feedback from our App in response to user interactions. | ||
|
||
Our approach will be: | ||
|
||
1. Install the capacitor plugin in both the `./appy` and `./capacitor` projects | ||
1. `./appy`: Capacitor needs access to the plugins when packaging our App | ||
2. `./capacitor-rs`: We’ll need access to the libraries to be able to bundle them with bun into a single file [since `wasm_bindgen` does not support imports](https://rustwasm.github.io/wasm-bindgen/reference/js-snippets.html?highlight=imports#caveats) | ||
2. Add a TypeScript file that imports the plugin and exports functions to interact with it | ||
3. Bundle the TypeScript file using bun to create a single file without any imports in it | ||
4. Add a Rust file that uses the `wasm_bindgen` proc macro to create a Rust interface for the TypeScript functions we’ve defined | ||
|
||
Let’s add the TypeScript side of things in `./js/haptics.ts` which quite simply just wraps and exposes the Capacitor functionality: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=capacitor-rs%5Cjs%5Chaptics.ts | ||
|
||
We then bundle this with bun: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=7.tsx | ||
|
||
Which gives us a corresponding bundled file in `js-dist/haptics.js` that we can use. | ||
|
||
As the final step, we’ll bridge this into our Rust code by setting up `src/plugins/haptics.rs`: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=capacitor-rs%5Csrc%5Cplugins%5Chaptics.rs | ||
|
||
Let’s break down what’s going on here: | ||
|
||
- We mirror some of the types from the Capacitor plugin library into Rust to `ImpactOptions`, `ImpactStyle` (unfortunately `wasm_bindgen` doesn’t support generating these for us, but it’s more or less a copy-paste of the TypeScript code) | ||
- We point to our generated JavaScript file `#[wasm_bindgen(module = "/js-dist/haptics.js")]` which will ensure it gets included automatically | ||
- We setup the type signatures matching each of the TypeScript functions we defined and that we want to include | ||
|
||
We also need to add a `src/plugins/mod.rs` file to expose our new plugin: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=capacitor-rs%5Csrc%5Cplugins%5Cmod.rs | ||
|
||
We’re now ready to use it in our App by adding the new `capacitor-rs` crate to our dependencies in our WASM app: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=capacitor-rs%5CCargo.toml%20(title%3A%20%22Using%20Capacitor%20Plugins%20from%20Rust%5CWASM%22%0Atags%3A%20rust%2C%20wasm%2C%20leptos%2C%20mobile%0Adate%3A%202024-06-24).toml | ||
|
||
And then using it like you like a normal Rust function: | ||
|
||
https://gist.github.com/Tehnix/3d5c588437210239743244db0b34aa0c.js?file=example-component.rs | ||
|
||
And that’s it! | ||
|
||
Since I can’t exactly screenshot a haptic vibration, here’s an example where we use the [Capacitor](https://capacitorjs.com/docs/basics/utilities) utility function to determine which platform we are on, set up in the same way: | ||
|
||
<div class="clear two-images"> | ||
<a href="/resources/images/using-capacitor-plugins-from-rust-wasm-iphone.png" target="_blank" rel="noopener noreferrer"><img src="/resources/images/using-capacitor-plugins-from-rust-wasm-iphone.thumbnail.png" loading="lazy" alt="Screenshot of Capacitor running on iPhone" title="Screenshot of Capacitor running on iPhone" style="margin-right: 1%; width: 49%;" /></a> | ||
<a href="/resources/images/using-capacitor-plugins-from-rust-wasm-web.png" target="_blank" rel="noopener noreferrer"><img src="/resources/images/using-capacitor-plugins-from-rust-wasm-web.thumbnail.png" loading="lazy" alt="Screenshot of Capacitor running in Web" title="Screenshot of Capacitor running in Web" style="margin-left: 1%; width: 49%;" /></a> | ||
</div> | ||
<div class="clear"></div> |
Oops, something went wrong.