From fefaa3d8cafc0a6722f8f99ad04b9b158afdb99e Mon Sep 17 00:00:00 2001 From: James Hugman Date: Sat, 17 Aug 2024 12:11:33 +0100 Subject: [PATCH 1/4] Docs: Docs for command line, config file and getting started --- docs/src/SUMMARY.md | 10 + docs/src/api/README.md | 0 docs/src/api/commandline.md | 357 +++++++++++++++++ docs/src/api/config-yaml.md | 148 +++++++ docs/src/api/turbo-module-files.md | 23 ++ docs/src/api/uniffi-toml.md | 68 ++++ docs/src/getting-started/README.md | 1 + docs/src/getting-started/guide.md | 401 +++++++++++++++++++ docs/src/getting-started/pre-installation.md | 71 ++++ 9 files changed, 1079 insertions(+) create mode 100644 docs/src/api/README.md create mode 100644 docs/src/api/commandline.md create mode 100644 docs/src/api/config-yaml.md create mode 100644 docs/src/api/turbo-module-files.md create mode 100644 docs/src/api/uniffi-toml.md create mode 100644 docs/src/getting-started/README.md create mode 100644 docs/src/getting-started/guide.md create mode 100644 docs/src/getting-started/pre-installation.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 43a28d04..c10a166e 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,3 +1,13 @@ # Summary [Introduction](README.md) + +* [Getting Started](getting-started/README.md) + - [Before you start](getting-started/pre-installation.md) + - [Step by step: Make your first library project](getting-started/guide.md) + +* [Reference](api/README.md) + - [Command Line](api/commandline.md) + - [Configuring your project](api/config-yaml.md) + - [Tweaking code generation](api/uniffi-toml.md) + - [Generating a Turbo Module](api/turbo-module-files.md) diff --git a/docs/src/api/README.md b/docs/src/api/README.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/api/commandline.md b/docs/src/api/commandline.md new file mode 100644 index 00000000..f231f1e8 --- /dev/null +++ b/docs/src/api/commandline.md @@ -0,0 +1,357 @@ +`uniffi-bindgen-react-native` the command line utility that ties together much of the building of Rust, and the generating the bindings and turbo-modules. + +Most commands take a `--config FILE` option. This is a YAML file which collects commonly used options together, and is [documented here](config-yaml.md). + +You can make the command available to `package.json` by adding a script: + +```json +{ + "scripts": { + "ubrn": "npx uniffi-bindgen-react-native" + } +} +``` + +This makes `ubrn` available to other scripts in `package.json`. + +If you find yourself running commands from the command line, you can alias the command + +```bash +alias ubrn=$(npx uniffi-bindgen-react-native --path) +``` + +allows you to run the command from the shell as `ubrn`, which is simpler to type. From hereon, commands will be given as `ubrn` commands. + + +# The `ubrn` command + +Running `ubrn --help` gives the following output: + +```sh +Usage: uniffi-bindgen-react-native + +Commands: + checkout Checkout a given Github repo into `rust_modules` + build Build (and optionally generate code) for Android or iOS + generate Generate bindings or the turbo-module glue code from the Rust + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` + +## `checkout` +Checkout a given Git repo into `rust_modules`. + +```sh +Usage: uniffi-bindgen-react-native checkout [OPTIONS] + +Arguments: + The repository where to get the crate + +Options: + --config + --branch The branch or tag which to checkout [default: main] + -h, --help Print help +``` +The checkout command can be operated in two ways, either: +1. with a `REPO` argument and optional `--branch` argument. OR +2. with a [config file][config] which may specify a repo and branch, or just a `directory`. + +If the config file is set to a repo, then the repo is cloned in to `./rust_modules/${NAME}`. + +# `build` + +[config]: config-yaml.md + +This takes care of the work of compiling the Rust, ready for generating bindings. Each variant takes a: + +- `--config` [config file][config]. +- `--and-generate` this runs the `generate all` command immediately after building. +- `--targets` a comma separated list of targets, specific to each platform. This overrides the values in the config file. +- `--release` builds a release version of the library. + +## `build android` + +Build the crate for use on an Android device or emulator, using `cargo ndk`, which in turn uses Android Native Development Kit. + +``` +Usage: uniffi-bindgen-react-native build android [OPTIONS] --config + +Options: + --config + The configuration file for this build + + -t, --targets ... + Comma separated list of targets, that override the values in the `config.yaml` file. + + Android: aarch64-linux-android, armv7-linux-androideabi, x86_64-linux-android i686-linux-android, + + Synonyms for: arm64-v8a, armeabi-v7a, x86_64, x86 + + -r, --release + Build a release build + + --no-cargo + If the Rust library has been built for at least one target, then don't re-run cargo build. + + This may be useful if you are using a pre-built library or are managing the build process yourself. + + -g, --and-generate + Optionally generate the bindings and turbo-module code for the crate + + --no-jniLibs + Suppress the copying of the Rust library into the JNI library directories + + -h, --help + Print help (see a summary with '-h') +``` + +`--release` sets the release profile for `cargo`. + +`--and-generate` is a convenience option to pass the built library file to `generate bindings` and `generate turbo-module`. + +Once the library files (one for each target) are created, they are copied into the `jniLibs` specified by the YAML configuration. + +```admonish note +React Native requires that the Rust library be built as a static library. The CMake based build will combine the C++ with the static library into a shared object. + +To configure Rust to build a static library, you should ensure `staticlib` is in the `crate-type` list in the `[lib]` section of the `Cargo.toml` file. Minimally, this should be in the `Cargo.toml` manifest file: + +
+[lib]
+crate-type = ["staticlib"]
+
+
+``` + +We also need to make sure that we were linking to the correct NDK. + +This changes from RN version to version, but in our usage we had to set an `ANDROID_NDK_HOME` variable in our script for this to pick up the appropriate version. For example: + +```bash +export ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/26.1.10909125/ +``` + +You can find the version you need in your react-native `android/build.gradle` file in the `ndkVersion` variable. + +## `build ios` + +Build the crate for use on an iOS device or simulator. +``` +Build the crate for use on an iOS device or simulator + +Usage: uniffi-bindgen-react-native build ios [OPTIONS] --config + +Options: + --config + The configuration file for this build + + --sim-only + Only build for the simulator + + --no-sim + Exclude builds for the simulator + + --no-xcodebuild + Does not perform the xcodebuild step to generate the xcframework + + The xcframework will need to be generated externally from this tool. This is useful when adding extra bindings (e.g. Swift) to the project. + + -t, --targets ... + Comma separated list of targets, that override the values in the `config.yaml` file. + + iOS: aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios + + -r, --release + Build a release build + + --no-cargo + If the Rust library has been built for at least one target, then don't re-run cargo build. + + This may be useful if you are using a pre-built library or are managing the build process yourself. + + -g, --and-generate + Optionally generate the bindings and turbo-module code for the crate + + -h, --help + Print help (see a summary with '-h') +``` + +The configuration file refers to [the YAML configuration][config]. + +`--sim-only` and `--no-sim` restricts the targets to targets with/without `sim` in the target triple. + +`--and-generate` is a convenience option to pass the built library file to `generate bindings` and `generate turbo-module`. + +Once the target libraries are compiled, and a config file is specified, they are passed to `xcodebuild -create-xcframework` to generate an `xcframework`. + +```admonish note +React Native requires that the Rust library be built as a static library. The `xcodebuild` based build will combine the C++ with the static library `.xcframework` file. + +To configure Rust to build a static library, you should ensure `staticlib` is in the `crate-type` list in the `[lib]` section of the `Cargo.toml` file. Minimally, this should be in the `Cargo.toml` manifest file: + +
+[lib]
+crate-type = ["staticlib"]
+
+
+``` + +# `generate` + +This command is to generate code for: + +1. turbo-modules: installing the Rust crate into a running React Native app +2. bindings: the code needed to actually bridge between Javascript and the Rust library. + +All subcommands require a [configuration file][config]. + +If you're already using `--and-generate`, then you don't need to know how to invoke this command. + +```sh +Generate bindings or the turbo-module glue code from the Rust. + +These steps are already performed when building with `--and-generate`. + +Usage: uniffi-bindgen-react-native generate + +Commands: + bindings Generate just the Typescript and C++ bindings + turbo-module Generate the TurboModule code to plug the bindings into the app + all Generate the Bindings and TurboModule code from a library file and a YAML config file + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help + Print help (see a summary with '-h') +``` + +## `generate bindings` +Generate just the bindings. In most cases, this command should not be called directly, but with the build, with `--and-generate`. + +```admonish info +This command follows the command line format of other `uniffi-bindgen` commands. Most arguments are passed straight to [`uniffi-bindgen::library_mode::generate_bindings`](https://docs.rs/uniffi_bindgen/0.28/uniffi_bindgen/library_mode/fn.generate_bindings.html). + +For more/better documentation, please see the linked docs. +``` + +```admonish warning +Because this mirrors other `uniffi-bindgen`s, the `--config` option here is asking for a [`uniffi.toml`](uniffi-toml) file. +``` + +This command will generate two typescript files and two C++ files per Uniffi namespace. These are: `namespace.ts`, `namespace-ffi.ts`, `namespace.h`, `namespace.cpp`, substituting `namespace` for names derived from the Rust crate. + +The [namespace is defined as](https://docs.rs/uniffi_bindgen/latest/uniffi_bindgen/interface/struct.ComponentInterface.html#method.namespace): + +> The string namespace within which this API should be presented to the caller. +> +> This string would typically be used to prefix function names in the FFI, to build a package or module name for the foreign language, etc. + +It may also be thought of as a crate or sub-crate which exports uniffi API. + +The C++ files will be put into the `--cpp-dir` and the typescript files into the `--ts-dir`. + +The C++ files can register themselves with the Hermes runtime. + +``` +Usage: uniffi-bindgen-react-native generate bindings [OPTIONS] --ts-dir --cpp-dir + +Arguments: + + A UDL file or library file + +Options: + --lib-file + The path to a dynamic library to attempt to extract the definitions from and extend the component interface with + + --crate + Override the default crate name that is guessed from UDL file path. + + In library mode, this + + --config + The location of the uniffi.toml file + + --library + Treat the input file as a library, extracting any Uniffi definitions from that + + --no-format + By default, bindgen will attempt to format the code with prettier and clang-format + + --ts-dir + The directory in which to put the generated Typescript + + --cpp-dir + The directory in which to put the generated C++ + + -h, --help + Print help (see a summary with '-h') +``` +## `generate turbo-module` +Generate the TurboModule code to plug the bindings into the app. + +More details about the files generated is shown [here](turbo-module-files.md). + +``` +Usage: uniffi-bindgen-react-native generate turbo-module --config [NAMESPACES]... + +Arguments: + [NAMESPACES]... The namespaces that are generated by `generate bindings` + +Options: + --config The configuration file for this build + -h, --help Print help +``` + +The namespaces in the commmand line are derived from the crate that has had its bindings created. + +```admonish info +The locations of the files are derived from [the configuration file][config] and the project's package.json` file. + +The relationships between files are preserved-- e.g. where one file points to another via a relative path, the relative path is calculated from these locations. +``` + +## `generate all` + +This command performs the generation of both `bindings` and `turbo-module`, using a `lib.a` file. + +This is a convenience method for users who do not or cannot use the `ubrn build` commands. + +```sh +Generate the Bindings and TurboModule code from a library file and a YAML config file. + +This is the second step of the `--and-generate` option of the build command. + +Usage: uniffi-bindgen-react-native generate all --config + +Arguments: + + A path to staticlib file + +Options: + --config + The configuration file for this project + + -h, --help + Print help (see a summary with '-h') +``` + +# `help` + +Prints the help message. + +``` +Usage: uniffi-bindgen-react-native + +Commands: + checkout Checkout a given Github repo into `rust_modules` + build Build (and optionally generate code) for Android or iOS + generate Generate bindings or the turbo-module glue code from the Rust + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` + +You can add `--help` to any command to get more information about that command. diff --git a/docs/src/api/config-yaml.md b/docs/src/api/config-yaml.md new file mode 100644 index 00000000..e1c1be0d --- /dev/null +++ b/docs/src/api/config-yaml.md @@ -0,0 +1,148 @@ +# Configuration for `uniffi-bindgen-react-native` + +The configuration yaml file is a collection of configuration options used in one or more [commands](commandline.md). + +The file is designed to be easy to start. A **minimal** configuation would be: + +```yaml +rust: + directory: ./rust + manifest-file: Cargo.toml +``` + +Getting started from here would require a command to start the Rust: + +```sh +cargo init --lib ./rust +cd ./rust +cargo add uniffi +``` + +# YAML entries + +## `rust` + +```yaml +rust: + repo: https://github.com/example/my-rust-sdk + branch: main + manifest-path: crates/my-api/Cargo.toml +``` +In this case, the `ubrn checkout` command will clone the given repo with the branch/ref into the `rust_modules` directory of the project. + +If run a second time, no overwriting will occur. + +The `manifest-path` is the path relative to the root of the Rust workspace directory. In this case, the manifest is expected to be, relative to your React Native library project: `./rust_modules/my-rust-sdk/crates/my-api/Cargo.tml`. + +```yaml +crate: + directory: ./rust + manifest-path: crates/my-api/Cargo.toml +``` +In this case, the `./rust` directory tells `ubrn` where the Rust workspace is, relative to your React Native library project. The `manifest-path` is the relative path from the workspace file to the crate which will be used to build bindings. + +## `bindings` + +This section governs the generation of the bindings— the nitty-gritty of the Rust API translated into Typescript. This is mostly the location on disk of where these files will end up, but also has a second configuration file. + +```yaml +bindings: + cpp: cpp/bindings + ts: ts/bindings + uniffiToml: ./uniffi.toml +``` +The [`uniffi.toml` file](uniffi-toml.md) configures custom types, to further customize the conversion into Typescript data-types. + +If missing, the defaults will be used: +``` +bindings: + cpp: cpp/generated + ts: ts/generated +``` +## `android` + +This is to configure the build steps for the Rust, the bindings, and the turbo-module code for Android. + +This section can be omitted entirely, as sensible defaults are provided. If you do want to edit the defaults, these are the members of the `android` section with their defaults: + +```yaml +android: + directory: ./android + cargoExtras: [] + targets: + - aarch64-linux-android + - armv7-linux-androideabi + - i686-linux-android + - x86_64-linux-android + apiLevel: 21 + jniLibs: src/main/jniLibs + packageName: +``` + +The `directory` is the location of the Android project, relative to the root of the React Native library project. + +`targets` is a list of targets to build for. The Rust source code is built once per target. + +`cargoExtras` is a list of extra arguments passed directly to the `cargo build` command. + +`apiLevel` is the minimum API level to target: this is passed to the `cargo ndk` command as a `--platform` argument. + +```admonish tip +Reducing the number of targets to build for will speed up the edit-compile-run cycle. +``` + +`packageName` is the name of the Android package that Codegen used to generate the TurboModule. This is derived from the `package.json` file, and can almost always be left. + +To customize the `packageName`, you should edit or add the entry at the path `codegenConfig`/`android`/`javaPackageName` in `package.json`. + +## `ios` + +This is to configure the build steps for the Rust, the bindings, and the turbo-module code for iOS. + +This section can be omitted entirely, as sensible defaults are provided. If you do want to edit the defaults, these are the members of the `ios` section with their defaults: + +```yaml +ios: + directory: ios + cargoExtras:: [] + targets: + - aarch64-apple-ios + - aarch64-apple-ios-sim + xcodebuildExtras: [] + frameworkName: build/MyFramework +``` + + +The `directory` is the location of the iOS project, relative to the root of the React Native library project. + +`targets` is a list of targets to build for. The Rust source code is built once per target. + +`cargoExtras` is a list of extra arguments passed directly to the `cargo build` command. + +`xcodebuildExtras` is a list of extra arguments passed directly to the `xcodebuild` command. + +## `turboModule` + +This section configures the location of the Typescript and C++ files generated by the `generate turbo-module` command. + +If absent, the defaults will be used: + +```yaml +turboModule: + ts: src + cpp: cpp +``` + +The Typescript files are the `index.ts` file, and the `Codegen` installer file. + +```admonish info +By default, the `index.ts` file is intended to be the entry point for your library. + +In this case, changing the location of the `ts` directory will require changing the `main` or `react-native` entry in the `package.json` file. +``` + +## `noOverwrite` + +This list of [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)) of file that should not be generated or overwritten by the `--and-generate` flag, and the `generate turbo-module` command. + +This is useful if you have customized one or more of the generated files, and do not want lose those changes. diff --git a/docs/src/api/turbo-module-files.md b/docs/src/api/turbo-module-files.md new file mode 100644 index 00000000..7cd2373c --- /dev/null +++ b/docs/src/api/turbo-module-files.md @@ -0,0 +1,23 @@ +# Generating Turbo Module files to install the bindings + +The bindings of the Rust library consist of several C++ files and several typescript files. + +There is a host of smaller files that need to be configured with these namespaces, and with configuration from the [config YAML](config-yaml.md) file. + +These include: + +- For Javascript: + - An `index.ts` file, to call into the installation process, initialize the bindings for each namespace, and re-export the generated bindings for client code. + - A Codegen file, to generates install methods from Javascript to Java and Objective C. +- For Android: + - A `Package.java` and `Module.java` file, which receives the codegen'd install method calls, to get the Hermes `JavascriptRuntime` and `CallInvokerHolder` to pass it via JNI to + - A `cpp-adapter.cpp` to receive the JNI calls, and converts those into `jsi::Runtime` and `react::CallInvoker` then calls into generic C++ install code. +- Generic C++ install code: + - A turbo-module installation `.h` and `.cpp` which catches the calls from Android and iOS and registers the bindings C++ with the Hermes `jsi::Runtime`. +- For iOS: + - a `Module.h` and `Module.mm` file which receives the codegen'd install method calls, and digs around to find the `jsi::Runtime` and `react::CallInvoker`. It then calls into the generic C++ install code. +- To build for iOS: + - A podspec file to tell Xcode about the generated files, and the framework name/location of the compiled Rust library. +- To build for Android + - A `CMakeLists.txt` file to configure the Android specific tool chain for all the generated C++ files. + - The `build.gradle` file which tells keeps the codegen package name in-sync and configures `cmake`. (note to self, this could be done from within the `CMakeLists.txt` file). diff --git a/docs/src/api/uniffi-toml.md b/docs/src/api/uniffi-toml.md new file mode 100644 index 00000000..c4126d74 --- /dev/null +++ b/docs/src/api/uniffi-toml.md @@ -0,0 +1,68 @@ +The `uniffi.toml` file is a toml file used to customize [the generation of C++ and Typescript](https://mozilla.github.io/uniffi-rs/0.27/bindings.html). + +As of time of writing, only `typescript` bindings generation exposes any options for customization, and only for `customTypes`. + +### Typescript custom types + +From [the uniffi-rs manual](https://mozilla.github.io/uniffi-rs/latest/udl/custom_types.html): + +> Custom types allow you to extend the UniFFI type system to support types from your Rust crate or 3rd party libraries. This works by converting to and from some other UniFFI type to move data across the FFI. + +This table customizes how a type called `MillisSinceEpoch` comes out of Rust. + +We happen to know that it crosses the FFI as a Rust `i64`, which +converts to a JS `bigint`, but we can do better. + +```toml +[bindings.typescript.customTypes.MillisSinceEpoch] +# Name of the type in the Typescript code. +typeName = "Date" +# Expression to lift from `bigint` to the higher-level representation `Date`. +lift = 'new Date(Number({}))' +# Expression to lower from `Date` to the low-level representation, `bigint`. +lower = "BigInt({}.getTime())" +``` + +This table customizes how a type called `Url` comes out of Rust. +We happen to know that it crosses the FFI as a `string`. + +```toml +[bindings.typescript.customTypes.Url] +# We want to use our own Url class; because it's also called +# Url, we don't need to specify a typeName. +# Import the Url class from ../src/converters +imports = [ [ "Url", "../src/converters" ] ] +# Expressions to convert between strings and URLs. +# The `{}` is substituted for the value. +lift = "new Url({})" +lower = "{}.toString()" +``` +We can provide zero or more imports which are slotted into a JS import statement. This allows us to import `type` and from modules in `node_modules`. + +The next example is a bit contrived, but allows us to see how to customize a generated type that came from Rust. + +The `EnumWrapper` is defined in Rust as: + +```rust +pub struct EnumWrapper(MyEnum); +uniffi::custom_newtype!(EnumWrapper, MyEnum); +``` + +In the `uniffi.toml` file, we want to convert the wrapped `MyEnum` into a `string`. In this case, the `string` is the custom type, and we need to provide code to convert to and from the custom type. +```toml +[bindings.typescript.customTypes.EnumWrapper] +typeName = "string" +# An expression to get from the custom (a string), to the underlying enum. +lower = "{}.indexOf('A') >= 0 ? new MyEnum.A({}) : new MyEnum.B({})" +# An expression to get from the underlying enum to the custom string. +# It has to be an expression, so we use an immediately executing anonymous function. +lift = """((v: MyEnum) => { + switch (v.tag) { + case MyEnum_Tags.A: + return v.inner[0]; + case MyEnum_Tags.B: + return v.inner[0]; + } +})({}) +""" +``` diff --git a/docs/src/getting-started/README.md b/docs/src/getting-started/README.md new file mode 100644 index 00000000..bad55622 --- /dev/null +++ b/docs/src/getting-started/README.md @@ -0,0 +1 @@ +# Getting Started diff --git a/docs/src/getting-started/guide.md b/docs/src/getting-started/guide.md new file mode 100644 index 00000000..e472916f --- /dev/null +++ b/docs/src/getting-started/guide.md @@ -0,0 +1,401 @@ +# Step-by-step tutorial + +This tutorial will get you started, by taking an existing Rust crate, and building a React Native library from it. + +By the end of this tutorial you will: + +1. have a working turbo-module library, +1. an example app, running in both Android and iOS, +1. seen how to set up `uniffi-bindgen-react-native` for your library. + +## Step 1: Start with builder-bob + +We first use `create-react-native-library` to generate our basic turbo-module library. + +```admonish warning title="Known issue with builder-bob" +`create-react-native-library` has changed a few things around, and so the following does not work yet with the latest version of `create-react-native-library`. + +We can still work with a previous version of builder-bob for this tutorial. +``` + +```sh +npx create-react-native-library@0.35.1 my-rust-lib +``` + +The important bits are: +``` +✔ What type of library do you want to develop? › Turbo module +✔ Which languages do you want to use? › C++ for Android & iOS +``` + +For following along, here are the rest of my answers. + +``` +✔ What is the name of the npm package? … react-native-my-rust-lib +✔ What is the description for the package? … My first React Native library in Rust +✔ What is the name of package author? … James Hugman +✔ What is the email address for the package author? … james@nospam.fm +✔ What is the URL for the package author? … https://github.com/jhugman +✔ What is the URL for the repository? … https://github.com/jhugman/react-native-my-rust-lib +✔ What type of library do you want to develop? › Turbo module +✔ Which languages do you want to use? › C++ for Android & iOS +✔ Project created successfully at my-rust-lib! +``` + +Most of the rest of the command line guide will be done within the directory this has just created. + +```sh +cd my-rust-lib +``` + +Verify everything works before adding Rust: + +For iOS: + +```sh +yarn +(cd example/ios && pod install) +yarn example start +``` + +Then `i` for iOS. + +After that has launched, then you can hit the `a` key to launch Android. + +We should, if all has gone to plan, see `Result = 21` on screen. + +## Step 2: Add `uniffi-bindgen-react-native` to the project + +Using `yarn` add the `uniffi-bindgen-react-native` package to your project. + +```sh +yarn add uniffi-bindgen-react-native +``` + +```admonish warning title="Pre-release" +While this is before the first release, we're installing straight from github. + +`yarn add uniffi-bindgen-react-native@https://github.com/jhugman/uniffi-bindgen-react-native` +``` + +Opening `package.json` add the following: + +```diff + "scripts": { ++ "ubrn:ios": "ubrn build ios --config ubrn.config.yaml --and-generate && (cd example/ios && pod install)", ++ "ubrn:android": "ubrn build android --config ubrn.config.yaml --and-generate", ++ "ubrn:checkout": "ubrn checkout --config ubrn.config.yaml", ++ "ubrn:clean": "rm -Rf cpp/ android/src/main/java ios/ src/Native* src/generated/ src/index.ts*", ++ "postinstall": "yarn ubrn:checkout && yarn ubrn:android && yarn ubrn:ios", + "example": "yarn workspace react-native-my-rust-lib-example", + "test": "jest", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "prepare": "bob build", + "release": "release-it" + }, +``` + +You can call the config file whatever you want, I have called it `ubrn.config.yaml` in this example. + +For now, let's just clean the files out we don't need: + +```sh +yarn ubrn:clean +``` + +```admonish hint +If you're going to be using the `uniffi-bindgen-react-native` command direct from the command line, it may be worth setting up an alias. In bash you can do this: +``` + +```sh +alias ubrn=$(yarn ubrn --path) +``` + +There is a guide to the `ubrn` command [here][cli]. + +[cli]: ../api/commandline.md + + +```admonish warning title="Pre-release" +While this is before the first release, we're installing straight from local `node_modules`. + +After release, the C++ runtime will be published to Cocoa Pods. + +Until thenm you need to add the dependency to the `example/ios/Podfile`: +``` + +```diff + use_react_native!( + :path => config[:reactNativePath], + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + ++ # We need to specify this here in the app because we can't add a local dependency within ++ # the react-native-matrix-rust-sdk ++ pod 'uniffi-bindgen-react-native', :path => '../../node_modules/uniffi-bindgen-react-native' + +``` + +## Step 3: Create the `ubrn.config.yaml` file + +Full documentation on how to configure your library can be found in [the YAML configuration file page][config] of this book. + +[config]: ../api/config-yaml.md + +For now, we just want to get started; let's start with an existing Rust crate that has uniffi bindings. + +```yaml +--- +name: MyRustLib +rust: + # forked from https://github.com/ianthetechie/uniffi-starter + # and bumped to 0.28.1 + repo: https://github.com/jhugman/uniffi-starter + branch: jhugman/bump-uniffi-rs-to-0.28.1 + manifestPath: rust/foobar/Cargo.toml +``` + +## Step 4: Checkout the Rust code + +Now, you should be able to checkout the Rust into the library. + +```sh +yarn ubrn:checkout +``` + +This will checkout the `uniffi-starter` repo into the `rust_modules` directory within your project. + +## Step 4: Build the Rust + +Building for iOS will: + +1. Build the Rust crate for iOS, including the uniffi scaffolding in Rust. +1. Build an `xcframework` +1. Generate the typescript and C++ bindings between Hermes and the Rust. +1. Generate the files to make a turbo-module from the C++. + +```sh +yarn ubrn:ios +``` + +Building for Android will: + +1. Build the Rust crate for Android, including the uniffi scaffolding in Rust. +1. Build an `xcframework` +1. Generate the typescript and C++ bindings between Hermes and the Rust. +1. Generate the files to make a turbo-module from the C++. + +```sh +yarn ubrn:android +``` + +## Step 5: Write an example app exercising the Rust API + +Here, we're editing the app file at `example/src/App.tsx`. + +First we delete the starter code given to us by `create-react-native-library`: + +```diff +import { StyleSheet, View, Text } from 'react-native'; +-import { multiply } from 'react-native-my-rust-lib'; +- +-const result = multiply(3, 7); + +export default function App() { +``` + +Next, add the following lines in place of the ones we just deleted: + +```ts +import { Calculator, type BinaryOperator, SafeAddition, ComputationResult } from '../../src'; + +// A Rust object +const calculator = new Calculator(); +// A Rust object implementing BinaryOperator +const addOp = new SafeAddition(); + +// A Typescript class, implementing BinaryOperator +class SafeMultiply implements BinaryOperator { + perform(lhs: bigint, rhs: bigint): bigint { + return lhs * rhs; + } +} +const multOp = new SafeMultiply(); + +// bigints +const three = 3n; +const seven = 7n; + +// Perform the calculation, and to get an object +// representing the computation result. +const computation: ComputationResult = calculator + .calculate(addOp, three, three) + .calculateMore(multOp, seven) + .lastResult()!; + +// Unpack the bigint value into a string. +const result = computation.value.toString(); +``` + +## Step 6: Run the example app + +Now you can run the apps on Android and iOS: + +```sh +yarn example start +``` + +As with the starter app from `create-react-native-library`, there is very little to look at. + +We should, if all has gone to plan, see `Result = 42` on screen. + +## Step 7: Make changes in the Rust + +We can edit the Rust, in this case in `rust_modules/uniffi-starter/rust/foobar/src/lib.rs`. + +If you're already familiar with Rust, you will notice that there is very little unusual about this file, apart from a few `uniffi` proc macros scattered here or there. + +If you're not familiar with Rust, you might add a function to the Rust: + +```rust +#[uniffi::export] +pub fn greet(who: String) -> String { + format!("Hello, {who}!") +} +``` + +Then run either `yarn ubrn:ios` or `yarn ubrn:android`. + +Once either of those are run, you should be able to import the `greet` function into `App.tsx`. + +# Appendix: the Rust + +The Rust library is presented here for comparison with the `App.tsx` above. + +All credit should go to the author, [ianthetechie][ianthetechie]. + +[ianthetechie]: https://github.com/ianthetechie/ + +```rust +use std::sync::Arc; +use std::time::{Duration, Instant}; +// You must call this once +uniffi::setup_scaffolding!(); + +// What follows is an intentionally ridiculous whirlwind tour of how you'd expose a complex API to UniFFI. + +#[derive(Debug, PartialEq, uniffi::Enum)] +pub enum ComputationState { + /// Initial state with no value computed + Init, + Computed { + result: ComputationResult + }, +} + +#[derive(Copy, Clone, Debug, PartialEq, uniffi::Record)] +pub struct ComputationResult { + pub value: i64, + pub computation_time: Duration, +} + +#[derive(Debug, PartialEq, thiserror::Error, uniffi::Error)] +pub enum ComputationError { + #[error("Division by zero is not allowed.")] + DivisionByZero, + #[error("Result overflowed the numeric type bounds.")] + Overflow, + #[error("There is no existing computation state, so you cannot perform this operation.")] + IllegalComputationWithInitState, +} + +/// A binary operator that performs some mathematical operation with two numbers. +#[uniffi::export(with_foreign)] +pub trait BinaryOperator: Send + Sync { + fn perform(&self, lhs: i64, rhs: i64) -> Result; +} + +/// A somewhat silly demonstration of functional core/imperative shell in the form of a calculator with arbitrary operators. +/// +/// Operations return a new calculator with updated internal state reflecting the computation. +#[derive(PartialEq, Debug, uniffi::Object)] +pub struct Calculator { + state: ComputationState, +} + +#[uniffi::export] +impl Calculator { + #[uniffi::constructor] + pub fn new() -> Self { + Self { + state: ComputationState::Init + } + } + + pub fn last_result(&self) -> Option { + match self.state { + ComputationState::Init => None, + ComputationState::Computed { result } => Some(result) + } + } + + /// Performs a calculation using the supplied binary operator and operands. + pub fn calculate(&self, op: Arc, lhs: i64, rhs: i64) -> Result { + let start = Instant::now(); + let value = op.perform(lhs, rhs)?; + + Ok(Calculator { + state: ComputationState::Computed { + result: ComputationResult { + value, + computation_time: start.elapsed() + } + } + }) + } + + /// Performs a calculation using the supplied binary operator, the last computation result, and the supplied operand. + /// + /// The supplied operand will be the right-hand side in the mathematical operation. + pub fn calculate_more(&self, op: Arc, rhs: i64) -> Result { + let ComputationState::Computed { result } = &self.state else { + return Err(ComputationError::IllegalComputationWithInitState); + }; + + let start = Instant::now(); + let value = op.perform(result.value, rhs)?; + + Ok(Calculator { + state: ComputationState::Computed { + result: ComputationResult { + value, + computation_time: start.elapsed() + } + } + }) + } +} + +#[derive(uniffi::Object)] +struct SafeAddition {} + +// Makes it easy to construct from foreign code +#[uniffi::export] +impl SafeAddition { + #[uniffi::constructor] + fn new() -> Self { + SafeAddition {} + } +} + +#[uniffi::export] +impl BinaryOperator for SafeAddition { + fn perform(&self, lhs: i64, rhs: i64) -> Result { + lhs.checked_add(rhs).ok_or(ComputationError::Overflow) + } +} + +``` diff --git a/docs/src/getting-started/pre-installation.md b/docs/src/getting-started/pre-installation.md new file mode 100644 index 00000000..58d78a1c --- /dev/null +++ b/docs/src/getting-started/pre-installation.md @@ -0,0 +1,71 @@ +# Before you start + +Better resources are available than this site for installing these dependencies. + +Below are a list of the dependencies, and a non-comprehensive instructions on how to get them onto your system. + +## Install Rust +If Rust isn't already installed on your system, you should install it as per the [rust-lang.org install instructions](https://www.rust-lang.org/tools/install). + +This will add `cargo` and `rustup` to your path, which are the main entry points into Rust. + +## Install C++ tooling + +These commands will add the tooling needed to compile and run the generated C++ code. + +Optionally, `clang-format` can be installed to format the generated C++ code. + +For MacOS, using homebrew: +```sh +brew install cmake ninja clang-format +``` + +For Debian flavoured Linux: +```sh +apt-get install cmake ninja clang-format +``` + +For generared Typescript, the existing `prettier` installation is detected and your configuration is used. + +## Android + +### Add the Android specific targets + +This command adds the backends for the Rust compiler to emit machine code for different Android architectures. + +```sh +rustup target add \ + aarch64-linux-android \ + armv7-linux-androideabi \ + i686-linux-android \ + x86_64-linux-android +``` + +### Install `cargo-ndk` + +> This cargo extension handles all the environment configuration needed for successfully building libraries for Android from a Rust codebase, with support for generating the correct jniLibs directory structure. + +```sh +cargo install cargo-ndk +``` + +## iOS + +### Ensure `xcodebuild` is avaiable + +This command checks if Xcode command line tools are available, and if not, will start the installation process. + +```sh +xcode-select --install +``` + +### Add the iOS specific targets + +This command adds the backends for the Rust compiler to emit machine code for different iOS architectures. + +```sh +rustup target add \ + aarch64-apple-ios \ + aarch64-apple-ios-sim \ + x86_64-apple-ios +``` From 3d8fc52c61a091a12772106abcfc553207862a3a Mon Sep 17 00:00:00 2001 From: James Hugman Date: Thu, 15 Aug 2024 14:48:41 +0100 Subject: [PATCH 2/4] Fixup several small bugs/inconsistencies found while documenting --- crates/ubrn_bindgen/src/bindings/mod.rs | 2 -- .../ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs | 2 ++ crates/ubrn_cli/src/android.rs | 3 +-- crates/ubrn_cli/src/config/mod.rs | 2 +- crates/ubrn_cli/src/generate.rs | 5 ++--- crates/ubrn_cli/src/ios.rs | 1 + crates/ubrn_cli/src/rust.rs | 8 +++++++- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/ubrn_bindgen/src/bindings/mod.rs b/crates/ubrn_bindgen/src/bindings/mod.rs index 61c9efea..e18e9405 100644 --- a/crates/ubrn_bindgen/src/bindings/mod.rs +++ b/crates/ubrn_bindgen/src/bindings/mod.rs @@ -75,8 +75,6 @@ pub struct SourceArgs { lib_file: Option, /// Override the default crate name that is guessed from UDL file path. - /// - /// In library mode, this #[clap(long = "crate")] crate_name: Option, diff --git a/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs b/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs index a90b69a1..0983596b 100644 --- a/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs +++ b/crates/ubrn_bindgen/src/bindings/react_native/uniffi_toml.rs @@ -61,7 +61,9 @@ pub(crate) struct CustomTypeConfig { #[serde(default)] pub(crate) imports: Vec<(String, String)>, pub(crate) type_name: Option, + #[serde(alias = "lift")] pub(crate) into_custom: TemplateExpression, + #[serde(alias = "lower")] pub(crate) from_custom: TemplateExpression, } diff --git a/crates/ubrn_cli/src/android.rs b/crates/ubrn_cli/src/android.rs index e585c62a..4ec838bc 100644 --- a/crates/ubrn_cli/src/android.rs +++ b/crates/ubrn_cli/src/android.rs @@ -37,7 +37,6 @@ pub(crate) struct AndroidConfig { #[serde(default = "AndroidConfig::default_platform", alias = "platform")] pub(crate) api_level: usize, - #[allow(dead_code)] #[serde(default = "AndroidConfig::default_package_name")] pub(crate) package_name: String, } @@ -190,8 +189,8 @@ impl AndroidArgs { cargo_extras: &ExtraArgs, api_level: usize, ) -> Result> { - let rust_dir = crate_.directory()?; let manifest_path = crate_.manifest_path()?; + let rust_dir = crate_.crate_dir()?; let metadata = crate_.metadata()?; let mut target_files = HashMap::new(); let profile = self.common_args.profile(); diff --git a/crates/ubrn_cli/src/config/mod.rs b/crates/ubrn_cli/src/config/mod.rs index b8f6dea7..6dbe0ec9 100644 --- a/crates/ubrn_cli/src/config/mod.rs +++ b/crates/ubrn_cli/src/config/mod.rs @@ -22,7 +22,7 @@ pub(crate) struct ProjectConfig { #[serde(default = "ProjectConfig::default_repository")] pub(crate) repository: String, - #[serde(rename = "crate")] + #[serde(rename = "rust", alias = "crate")] pub(crate) crate_: CrateConfig, #[serde(default)] diff --git a/crates/ubrn_cli/src/generate.rs b/crates/ubrn_cli/src/generate.rs index e1344a0f..00a4aa08 100644 --- a/crates/ubrn_cli/src/generate.rs +++ b/crates/ubrn_cli/src/generate.rs @@ -24,7 +24,7 @@ impl GenerateArgs { #[derive(Debug, Subcommand)] pub(crate) enum GenerateCmd { - /// Generate the just the bindings + /// Generate just the Typescript and C++ bindings Bindings(BindingsArgs), /// Generate the TurboModule code to plug the bindings into the app TurboModule(TurboModuleArgs), @@ -75,8 +75,7 @@ impl GenerateAllArgs { let pwd = ubrn_common::pwd()?; let lib_file = pwd.join(&self.lib_file); let modules = { - let dir = project.crate_.directory()?; - ubrn_common::cd(&dir)?; + ubrn_common::cd(&project.crate_.crate_dir()?)?; let ts_dir = project.bindings.ts_path(root); let cpp_dir = project.bindings.cpp_path(root); let config = project.bindings.uniffi_toml_path(root); diff --git a/crates/ubrn_cli/src/ios.rs b/crates/ubrn_cli/src/ios.rs index abb7c05e..db5be7e1 100644 --- a/crates/ubrn_cli/src/ios.rs +++ b/crates/ubrn_cli/src/ios.rs @@ -250,6 +250,7 @@ impl IOsArgs { let ios = &config.ios; let project_root = config.project_root(); let ios_dir = ios.directory(project_root); + ubrn_common::mk_dir(&ios_dir)?; let mut library_args = Vec::new(); for library in target_files { // :eyes: single dash arg. diff --git a/crates/ubrn_cli/src/rust.rs b/crates/ubrn_cli/src/rust.rs index 40a7b053..d3858025 100644 --- a/crates/ubrn_cli/src/rust.rs +++ b/crates/ubrn_cli/src/rust.rs @@ -43,6 +43,12 @@ impl CrateConfig { Ok(self.directory()?.join(&self.manifest_path)) } + pub(crate) fn crate_dir(&self) -> Result { + let manifest = self.manifest_path()?; + let dir = manifest.parent().unwrap(); + Ok(dir.into()) + } + pub(crate) fn metadata(&self) -> Result { self.manifest_path()?.try_into() } @@ -57,7 +63,7 @@ pub(crate) enum RustSource { #[derive(Debug, Deserialize)] pub(crate) struct OnDiskArgs { - #[serde(alias = "rust")] + #[serde(alias = "rust", alias = "directory")] pub(crate) src: String, } From b91a05e00cf3673589cfda6edfe164536935e36c Mon Sep 17 00:00:00 2001 From: James Hugman Date: Wed, 2 Oct 2024 00:45:17 +0100 Subject: [PATCH 3/4] Fixup turbo-module templates to work with getting started guides --- .../ubrn_cli/src/codegen/templates/ModuleTemplate.h | 2 +- .../src/codegen/templates/ModuleTemplate.java | 2 +- crates/ubrn_cli/src/config/mod.rs | 10 ++++++++++ crates/ubrn_cli/src/config/npm.rs | 11 +++++++++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.h b/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.h index a5cf6732..cb2cdd4a 100644 --- a/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.h +++ b/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.h @@ -6,7 +6,7 @@ #ifdef RCT_NEW_ARCH_ENABLED #import "{{ self.config.project.tm.name() }}.h" -@interface {{ self.config.project.name_upper_camel() }} : NSObject <{{ self.config.project.tm.spec_name() }}> +@interface {{ self.config.project.name_upper_camel() }} : NSObject <{{ self.config.project.codegen_filename() }}Spec> #else #import diff --git a/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.java b/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.java index 02d2d901..fffc7c89 100644 --- a/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.java +++ b/crates/ubrn_cli/src/codegen/templates/ModuleTemplate.java @@ -11,7 +11,7 @@ import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder; @ReactModule(name = {{ module_class_name }}.NAME) -public class {{ module_class_name }} extends {{ self.config.project.tm.spec_name() }} { +public class {{ module_class_name }} extends {{ self.config.project.codegen_filename() }}Spec { public static final String NAME = "{{ name }}"; public {{ module_class_name }}(ReactApplicationContext reactContext) { diff --git a/crates/ubrn_cli/src/config/mod.rs b/crates/ubrn_cli/src/config/mod.rs index 6dbe0ec9..a1dc090c 100644 --- a/crates/ubrn_cli/src/config/mod.rs +++ b/crates/ubrn_cli/src/config/mod.rs @@ -59,6 +59,16 @@ fn trim_react_native(name: &str) -> String { name.trim_matches('-').trim_matches('_').to_string() } +fn trim_react_native_2(name: &str) -> String { + name.strip_prefix("RN") + .unwrap_or(name) + .replace("ReactNative", "") + .replace("react-native", "") + .trim_matches('-') + .trim_matches('_') + .to_string() +} + impl ProjectConfig { pub(crate) fn project_root(&self) -> &Utf8Path { &self.crate_.project_root diff --git a/crates/ubrn_cli/src/config/npm.rs b/crates/ubrn_cli/src/config/npm.rs index b42462d0..641af69a 100644 --- a/crates/ubrn_cli/src/config/npm.rs +++ b/crates/ubrn_cli/src/config/npm.rs @@ -7,7 +7,7 @@ use heck::ToUpperCamelCase; use serde::Deserialize; -use super::trim_react_native; +use super::{trim_react_native, trim_react_native_2}; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -34,7 +34,14 @@ impl PackageJson { .android .java_package_name .clone() - .unwrap_or_else(|| format!("com.{}", self.name().to_upper_camel_case().to_lowercase())) + .unwrap_or_else(|| { + format!( + "com.{}", + trim_react_native_2(&self.name) + .to_upper_camel_case() + .to_lowercase() + ) + }) } pub(crate) fn repo(&self) -> &PackageJsonRepo { From 327e15f29fc0998559b62950b48b94738e7279dd Mon Sep 17 00:00:00 2001 From: James Hugman Date: Wed, 2 Oct 2024 00:46:43 +0100 Subject: [PATCH 4/4] Increment version to match uniffi-rs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52347be9..c56d1d06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniffi-bindgen-react-native", - "version": "0.0.1", + "version": "0.28.0", "description": "Uniffi bindings generator for calling Rust from React Native", "homepage": "https://github.com/jhugman/uniffi-bindgen-react-native", "repository": {