Skip to content

Commit

Permalink
Add new posts
Browse files Browse the repository at this point in the history
  • Loading branch information
Tehnix committed Jun 25, 2024
1 parent 1497758 commit 8e380dc
Show file tree
Hide file tree
Showing 24 changed files with 1,120 additions and 0 deletions.
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 importable/2024-06-24-using-capacitor-plugins-from-rust-wasm.md
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>
Loading

0 comments on commit 8e380dc

Please sign in to comment.