-
Notifications
You must be signed in to change notification settings - Fork 8
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
Documentation III #125
Changes from all commits
a7c77d5
fc71116
edb9ed2
092f4eb
1f7fff4
ee831c0
dd967a3
55c9bf2
ef24de9
ab75046
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
||
--- |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is publishing to cocoapods still an option? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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: | ||
|
@@ -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`. | ||
|
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`: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 This can be worked around in two ways:
|
||
|
||
```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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). |
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()); | ||
``` |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} | ||
} | ||
``` |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️