Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation III #125

Merged
merged 10 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,34 @@ If you're ready to start, then start with [a step-by-step tutorial to make a Rus
If you're new to uniFFI, then [**the UniFFI user guide**](https://mozilla.github.io/uniffi-rs/latest/)
or [**the UniFFI examples**](https://github.com/mozilla/uniffi-rs/tree/main/examples#example-uniffi-components) are interesting places to start.

## Why `uniffi-bindgen-react-native`?

- Spend more time writing Typescript and Rust
- Full compatibility with `uniffi-rs`
- Your Rust SDK is portable across multiple languages.

### Why not, say WASM, via `wasm-bindgen`?

WASM is an amazing virtual machine however:

- your Rust crate must make alternative arrangements if it needs things that the virtual machine does not offer:
- threads and
- file access.
- you need to maintain a separate FFI (this is a temporary issue, solvable by something like uniFFI).

## Who is using `uniffi-bindgen-react-native`?

- [@unomed/react-native-matrix-sdk](https://www.npmjs.com/package/@unomed/react-native-matrix-sdk)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


## Prior art and related projects

- [cawfree/react-native-webassembly](https://github.com/cawfree/react-native-webassembly)

## Contributing

If this tool sounds interesting to you, please help us develop it! You can:

* View the [contributor guidelines](./docs/contributing.md).
* View the [contributor guidelines](https://jhugman.github.io/uniffi-bindgen-react-native/).
* File or work on [issues](https://github.com/jhugman/uniffi-bindgen-react-native/issues) here in GitHub.
<!--
* Join discussions in the [#uniffi:mozilla.org](https://matrix.to/#/#uniffi:mozilla.org) room on Matrix.
Expand Down
19 changes: 13 additions & 6 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

# Getting started

- [Before you start](getting-started/pre-installation.md)
- [Step by step: Make your first library project](getting-started/guide.md)
- [Before you start](guides/pre-installation.md)
- [Step by step: Make your first library project](guides/getting-started.md)
- [Publishing your library project](guides/publishing.md)

# Mapping Rust on to Typescript

Expand All @@ -15,15 +16,16 @@
- [Records: Objects without methods](idioms/records.md)
- [Enums and Tagged Unions](idioms/enums.md)
- [Errors](idioms/errors.md)
- [Callback interfaces]()
- [Callback interfaces](idioms/callback-interfaces.md)
- [Promises/Futures](idioms/promises.md)
- [Async Callback interfaces]()
- [Async Callback interfaces](idioms/async-callbacks.md)

# Contributing

- [Local development](contributing/local-development.md)
- [Adding or changing generated turbo-module templates](contributing/changing-turbo-module-templates.md)
- [Changing generated Typescript or C++ templates](contributing/changing-bindings-templates.md)
- [Turbo-module Templates: Adding or changing](contributing/changing-turbo-module-templates.md)
- [Documentation: Contributing or reviewing](contributing/documentation.md)
- [Typescript or C++ templates: Changing](contributing/changing-bindings-templates.md)
- [Cutting a Release](./contributing/cutting-a-release.md)

# Reference
Expand All @@ -34,3 +36,8 @@
- [Generating a Turbo Module](reference/turbo-module-files.md)
- [Reserved words](reference/reserved-words.md)
- [Potential collisions](reference/potential-collisions.md)

# Internals

- [Lifting and lowering](./internals/lifting-and-lowering.md)
- [NativeModule.ts and Codegen](./internals/rn-codegen.md)
58 changes: 58 additions & 0 deletions docs/src/contributing/documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Contributing or reviewing documentation

A project is only as good as its docs!

The documentation is in [markdown](https://rust-lang.github.io/mdBook/format/markdown.html), and lives in the `docs/src` directory.

You can edit the files directly with a text editor.

## Before you start

The following assumes you have checked out the `uniffi-bindgen-react-native` project and that Rust is installed.

## Install `mdbook`

The docs are produced by [`mdbook`, a static-site generator](https://rust-lang.github.io/mdBook/index.html) written for documenting Rust projects.

`uniffi-bindgen-react-native` uses this with a few plugins. You can install it by opening the terminal and using `cd` to navigate to the project directory, then running the following command:

```sh
./scripts/run-bootstrap-docs.sh
```

## Run `mdbook serve`

`mdbook` can now be run from the `docs` directory.

From within the project directory, run the following:

```sh
cd docs
mdbook serve
```

This will produce output like:

```sh
2024-10-14 12:59:35 [INFO] (mdbook::book): Book building has started
2024-10-14 12:59:35 [INFO] (mdbook::book): Running the html backend
2024-10-14 12:59:35 [INFO] (mdbook::book): Running the linkcheck backend
2024-10-14 12:59:35 [INFO] (mdbook::renderer): Invoking the "linkcheck" renderer
2024-10-14 12:59:36 [INFO] (mdbook::cmd::serve): Serving on: http://localhost:3000
2024-10-14 12:59:36 [INFO] (warp::server): Server::run; addr=[::1]:3000
2024-10-14 12:59:36 [INFO] (warp::server): listening on http://[::1]:3000
```

## Make some changes

You can edit pages with your text editor.

New pages should be added to the `SUMMARY.md` file so that a) `mdbook` knows about them and b) they ends up in the table of contents.

You can now navigate your browser to [localhost:3000](http://localhost:3000/) to see the changes you've made.

## Pushing these changes back into the project

A normal Pull Request flow is used to push these changes back into the project.

---
2 changes: 1 addition & 1 deletion docs/src/contributing/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Pre-installation

This guide is in addition to the [Pre-installation](../getting-started/pre-installation.md) guide.
This guide is in addition to the [Pre-installation](../guides/pre-installation.md) guide.

```sh
git clone https://github.com/jhugman/uniffi-bindgen-react-native
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ Opening `package.json` add the following:
+ "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",
Expand Down Expand Up @@ -136,7 +135,6 @@ Until then, you need to add the dependency to the app's Podfile, in this case `e
+ # We need to specify this here in the app because we can't add a local dependency within
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is publishing to cocoapods still an option?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Publishing uniffi-bindgen-react-native? yes definitely.

+ # 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
Expand Down Expand Up @@ -166,6 +164,14 @@ yarn ubrn:checkout

This will checkout the `uniffi-starter` repo into the `rust_modules` directory within your project.

You may want to add to `.gitignore` at this point:

```diff
+# From uniffi-bindgen-react-native
+rust_modules/
+*.a
```

## Step 4: Build the Rust

Building for iOS will:
Expand Down Expand Up @@ -199,6 +205,10 @@ You can change the targets that get built by adding a comma separated list to th
yarn ubrn:android --targets aarch64-linux-android,armv7-linux-androideabi
```

```admonish warning title="Troubleshooting"
This won't happen with the `uniffi-starter` library, however a common error is to not enable a `staticlib` crate type in the project's `Cargo.toml`. Instructions on how to do this are given [here](../reference/commandline.md#admonition-note).
```

## Step 5: Write an example app exercising the Rust API

Here, we're editing the app file at `example/src/App.tsx`.
Expand Down
49 changes: 49 additions & 0 deletions docs/src/guides/publishing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Publishing your library project

```admonish warning title="Help wanted"
I haven't had any experience of publishing libraries for React Native.

I would love some [help with this document](../contributing/documentation.md).
```

## Binary builds

In order to distribute pre-built packages you will need to add to `.gitignore` the built `.a`:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would reword this slightly. The main reason you'll want to add rust_modules/ and *.a to .gitignore is so that you don't track the large binaries in git (and the getting started page already gives that tip). The issue though is that when publishing, npm will also use .gitignore to decide which files to include in the package.

This can be worked around in two ways:

  1. By using .npmignore. This will require you to duplicate those ignore patterns that git and npm have in common (which will be most of them). I personally think this isn't a good option because it's quite a footgun. You'll always have to remember to duplicate new entries as you add them in future.
  2. By applying some clever logic by overriding parts of .gitignore with the files array in package.json.


```diff
# From uniffi-bindgen-react-native
rust_modules/
+*.a
```

but add them back in to the `files` section of `package.json`[^issue121].

[^issue121]: [This advice](https://github.com/jhugman/uniffi-bindgen-react-native/issues/121) is from @Johennes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it should go here or into #121 but it's probably a good idea run npm pack --dry-run and inspect the package contents before doing any actual publishing. I guess that's probably good practice for any npm release but it might be worth pointing out explicitly here due to the ignore trickery.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right: we should land this and the improve this page with #121. Rather than me re-wording (without knowledge) your comment, would you mind raising a PR?


## Source packages

If asking your users to compile Rust source is acceptable, then adding a `postinstall` script to `package.json` may be enough.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might also be helpful to advise remind consumers to build the Rust crate in release mode when shipping their projects? Maybe that's obvious. I just thought of it because the size difference can be enormous.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another excellent suggestion.


If you've kept the scripts from the [Getting Started guide](./getting-started.md#step-2-add-uniffi-bindgen-react-native-to-the-project), then adding:

```diff
scripts: {
"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",
+ "postinstall": "yarn ubrn:checkout && yarn ubrn:android && yarn ubrn:ios",
```

## Add `uniffi-bindgen-react-native` to your README.md

If you publish your source code anywhere, it would be lovely if you could add something to your README.md. For example:

```diff
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
+ and [uniffi-bindgen-react-native](https://github.com/jhugman/uniffi-bindgen-react-native)
```

## Add your project to the `uniffi-bindgen-react-native` README.md

Once your project is published and would like some cross-promotion, perhaps you'd like to raise a PR to add it to the [`uniffi-bindgen-react-native` README](https://github.com/jhugman/uniffi-bindgen-react-native/blob/main/README.md#who-is-using-uniffi-bindgen-react-native).
48 changes: 48 additions & 0 deletions docs/src/idioms/async-callbacks.md
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
# Async Callback interfaces

[Callback interfaces and foreign traits](./callback-interfaces.md) can expose methods which are asynchronous. A toy example here:

```rust
#[uniffi::export(with_foreign)]
#[async_trait::async_trait]
trait MyFetcher {
async get(url: String) -> String;
}

fetch_with_fetcher(url: String, fetcher: Arc<dyn MyFetcher>) -> String {
fetcher.fetch(url).await
}
```

Used from Typescript:

```typescript
class TsFetcher implements MyFetcher {
async get(url: string): Promise<string> {
return await fetch(url).text()
}
}

fetchWithFetcher("https://example.com", new TsFetcher());
```

You can see this in action in the [`futures` fixture](https://github.com/jhugman/uniffi-bindgen-react-native/tree/main/fixtures/futures).

## Task cancellation

When the Rust Future is completed, it is dropped, and Typescript is informed. If the Future is dropped before it has completed, it has been cancelled. `uniffi-bindgen-react-native` can use this information to call the async callback to cancel, using [the standard `AbortController` and `AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) machinery.

`uniffi-bindgen-react-native` generates an optional argument for each async callback method, which is an options bag containing an `AbortSignal`.

It is up to the implementer of each method whether they want to use it or not.

Using exactly the same `MyFetcher` trait from above, this example passes the signal straight to [the `fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#canceling_a_request).

```typescript
class TsFetcher implements MyFetcher {
async get(url: string, asyncOptions?: { signal: AbortSignal }): Promise<string> {
return await fetch(url, asyncOptions).text()
}
}

fetchWithFetcher("https://example.com", new TsFetcher());
```
108 changes: 108 additions & 0 deletions docs/src/idioms/callback-interfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Callback interfaces

Callbacks and function literals are not directly supported by `uniffi-rs`.

However, [callback __interfaces__](https://mozilla.github.io/uniffi-rs/latest/proc_macro/index.html#the-uniffiexportcallback_interface-attribute) are, that is: instances of Typescript classes can be passed to Rust. The Typescript methods of those objects may then be called from Rust.

```rust
#[uniffi::export(callback_interface)]
pub trait MyLogger {
fn is_enabled() -> bool;
fn error(message: string);
fn log(message: string);
}

#[uniffi::export]
fn greet_with_logger(who: String, logger: Box<dyn MyLogger>) {
if logger.is_enabled() {
logger.log(format!("Hello, {who}!"));
}
}
```

In Typescript, this can be used:

```typescript
class ConsoleLogger implements MyLogger {
isEnabled(): boolean {
return true;
}
error(message: string) {
console.error(messgae);
}
log(message: string) {
console.log(messgae);
}
}

greetWithLogger(new ConsoleLogger(), "World");
```

So-called [Foreign Traits](https://mozilla.github.io/uniffi-rs/latest/foreign_traits.html) can also be used. These are traits that can be implemented by either Rust or a foreign language: from the Typescript point of view, these are exactly the same as callback interfaces. They differ on the Rust side, using `Rc<>` instead of `Box<>`.


```rust
#[uniffi::export(with_foreign)]
pub trait MyLogger {
fn error(message: string);
fn log(message: string);
}

#[uniffi::export]
fn greet_with_logger(who: String, logger: Arc<dyn MyLogger>) {
logger.log(format!("Hello, {who}!"));
}
```

These trait objects can be implemented by Rust or Typescript, and can be passed back and forth between the two sides of the FFI.

## Errors

Errors are propagated from Typescript to Rust:

```rust
#[derive(uniffi::Error)]
enum MyError {
LoggingDisabled,
}

#[uniffi::export(callback_interface)]
pub trait MyLogger {
fn is_enabled() -> bool;
fn log(message: string) -> Result<(), MyError>;
}

#[uniffi::export]
fn greet_with_logger(who: String, logger: Box<dyn MyLogger>) -> Result<(), MyError> {
logger.log(format!("Hello, {who}!"));
}
```

If an error is thrown in Typescript, it ends up in Rust:

```typescript
class ConsoleLogger implements MyLogger {
isEnabled(): boolean {
return false;
}
log(message: string) {
if (!this.isEnabled()) {
throw new MyError.LoggingDisabled();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if I throw an error that doesn't conform to the MyError declared in the rust code?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha. I'd missed this. Thanks for spotting that.

I've added some more docs.

}
console.log(message);
}
}

try {
greetWithLogger(new ConsoleLogger(), "World");
} catch (e: any) {
if (MyError.instanceOf(e)) {
switch (e.tag) {
case MyError_Tags.LoggingDisabled: {
// handle the logging disabled error.
break;
}
}
}
}
```
1 change: 0 additions & 1 deletion docs/src/idioms/callbacks.md

This file was deleted.

Loading