diff --git a/.github/workflows/example_app.yaml b/.github/workflows/example_app.yaml index 482471feb..f6083d0ee 100644 --- a/.github/workflows/example_app.yaml +++ b/.github/workflows/example_app.yaml @@ -69,7 +69,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: "zulu" - java-version: "11" + java-version: "17" - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/quality_control.yaml b/.github/workflows/quality_control.yaml index cc861e651..9051d3da4 100644 --- a/.github/workflows/quality_control.yaml +++ b/.github/workflows/quality_control.yaml @@ -23,6 +23,8 @@ jobs: code: name: dart-and-rust-code runs-on: ubuntu-latest + env: + RUSTFLAGS: -D warnings steps: - name: Checkout repository @@ -52,14 +54,21 @@ jobs: working-directory: flutter_ffi_plugin/example/ run: rinf message - - name: Check for any errors + # Targets are basically combinations of + # web/native and debug/release. + - name: Check for errors in various targets run: | rustup target add wasm32-unknown-unknown - RUSTFLAGS="-D warnings" - cargo check - cargo check --release - cargo check --target wasm32-unknown-unknown - cargo check --target wasm32-unknown-unknown --release + cargo clippy + cargo clippy --release + cargo clippy --target wasm32-unknown-unknown + cargo clippy --target wasm32-unknown-unknown --release + + # The `--all-features` flag doesn't work for the entire workspace. + # That's why we are checking only the library crate. + - name: Check for errors with all features enabled + working-directory: rust_crate/ + run: cargo clippy --all-features - name: Analyze code run: | diff --git a/.github/workflows/test_app.yaml b/.github/workflows/test_app.yaml index 97d5668fb..d1f8a519c 100644 --- a/.github/workflows/test_app.yaml +++ b/.github/workflows/test_app.yaml @@ -70,7 +70,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: "zulu" - java-version: "11" + java-version: "17" - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/user_app.yaml b/.github/workflows/user_app.yaml index ba023c7da..d21926565 100644 --- a/.github/workflows/user_app.yaml +++ b/.github/workflows/user_app.yaml @@ -53,7 +53,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: "zulu" - java-version: "11" + java-version: "17" - name: Setup Python uses: actions/setup-python@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 02453321e..b59dc68e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 6.12.1 + +- Fixed linefeed problem in published files. + +## 6.12.0 + +- Generated message channels are now more efficient. +- Minimum Android SDK version increased from 16 to 21. Thanks @debanjanbasu! +- The `finalizeRust()` function in Dart has been removed. The `tokio` async runtime, which holds Rust logic, still drops automatically when the app ends. +- Bumped `prost` version to 0.12.6. Thanks @yeoupooh! +- Internal code has been organized. + ## 6.11.1 - Fixed a bug with Dart's extension methods in the generated message code. diff --git a/README.md b/README.md index cd954b972..f409ef9c8 100644 --- a/README.md +++ b/README.md @@ -32,32 +32,30 @@ All platforms available with Flutter are [tested](https://github.com/cunarist/ri Below is Dart code with widgets, and following that is Rust code with business logic. ```dart -MySchemaInput( - fieldOne: 1, - fieldTwo: 2, - fieldThree: 3, -).sendSignalToRust() - -final stream = MySchemaOutput.rustSignalStream; -await for (final rustSignal in stream) { - // Custom Dart logic -} +StreamBuilder( + stream: MyMessage.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + if (rustSignal == null) { + return Text("Nothing received yet"); + } + final myMessage = rustSignal.message; + return Text(myMessage.currentNumber.toString()); + }, +), ``` ```rust -MySchemaOutput { - field_four: 4, - field_five: 5, - field_six: 6, -}.send_signal_to_dart() - -let mut receiver = MySchemaInput::get_dart_signal_receiver(); -while let Some(dart_signal) = receiver.recv().await { - // Custom Rust logic +MyMessage { + current_number: 7, + other_bool: true, } +.send_signal_to_dart(); ``` -All the message classes and structs are generated by Rinf. You can simply define the message schema with Protobuf. Sending messages between Dart and Rust is very convenient. +Of course, the opposite way from Dart to Rust is also possible in a similar manner. + +All message classes and structs are generated by Rinf. You can define the message schema simply with Protobuf, making message passing between Dart and Rust very convenient. ## 🎁 Benefits diff --git a/automate/__main__.py b/automate/__main__.py index 20d4e3a06..d444894d9 100644 --- a/automate/__main__.py +++ b/automate/__main__.py @@ -55,18 +55,18 @@ def replace_text_in_file(filepath: str, change_from: str, change_to: str): ) replace_text_in_file( "native/hub/src/lib.rs", - "// use tokio_with_wasm", - "use tokio_with_wasm", + "// use tokio_with_wasm::alias as tokio;", + "use tokio_with_wasm::alias as tokio;", ) replace_text_in_file( "native/hub/Cargo.toml", - "# wasm-bindgen", - "wasm-bindgen", + "# tokio_with_wasm", + "tokio_with_wasm", ) replace_text_in_file( "native/hub/Cargo.toml", - "# tokio_with_wasm", - "tokio_with_wasm", + "# wasm-bindgen", + "wasm-bindgen", ) os.chdir("../") @@ -109,18 +109,18 @@ def replace_text_in_file(filepath: str, change_from: str, change_to: str): ) replace_text_in_file( "native/hub/src/lib.rs", - "// use tokio_with_wasm", - "use tokio_with_wasm", + "// use tokio_with_wasm::alias as tokio;", + "use tokio_with_wasm::alias as tokio;", ) replace_text_in_file( "native/hub/Cargo.toml", - "# wasm-bindgen", - "wasm-bindgen", + "# tokio_with_wasm", + "tokio_with_wasm", ) replace_text_in_file( "native/hub/Cargo.toml", - "# tokio_with_wasm", - "tokio_with_wasm", + "# wasm-bindgen", + "wasm-bindgen", ) os.chdir("../") @@ -130,6 +130,11 @@ def replace_text_in_file(filepath: str, change_from: str, change_to: str): "flutter_ffi_plugin/example/native/*", "user_app/native/*", ) + replace_text_in_file( + "Cargo.toml", + 'rinf = { path = "./rust_crate" }', + "", + ) elif sys.argv[1] == "prepare-example-app": os.chdir("./flutter_ffi_plugin/example") diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index 9ed7b4332..692c3cec3 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -1,6 +1,8 @@ # Configuration -You can customize some Rinf behaviors by configuring the `pubspec.yaml` file. Rinf will change its behaviors by reading the fields below. All fields are optional and it's not necessary to write them. +## 📋 YAML File + +You can customize the behavior of Rinf CLI commands by configuring the `pubspec.yaml` file. All fields are optional and it's not necessary to write them. ```yaml title="pubspec.yaml" rinf: @@ -16,3 +18,14 @@ You can check the current configuration status by running the command below in t ```bash title="CLI" rinf config ``` + +## 📦 Crate Features + +Customizing the behavior of the Rinf crate is possible through its crate features. + +```toml title="native/hub/Cargo.toml" +rinf = { version = "0.0.0", features = ["feature-name"] } +``` + +- `multi-worker`: Starts a worker thread for each CPU core available on the system within the `tokio` runtime by enabling its `rt-multi-thread` feature. By default, the `tokio` runtime uses only one thread. Enabling this feature allows the `tokio` runtime to utilize all the cores on your computer. This feature does not affect applications on the web platform. +- `show-backtrace`: Prints the full backtrace in the CLI when a panic occurs in debug mode. In general, backtrace is not very helpful when debugging async apps, so consider using [`tracing`](https://crates.io/crates/tracing) for logging purposes. Note that this feature does not affect debugging on the web platform. diff --git a/documentation/docs/frequently-asked-questions.md b/documentation/docs/frequently-asked-questions.md index d64be28e8..b404a74ba 100644 --- a/documentation/docs/frequently-asked-questions.md +++ b/documentation/docs/frequently-asked-questions.md @@ -44,7 +44,7 @@ android { ### How does concurrency work under the hood? -On native platforms, Dart runs in a single thread as usual, while Rust utilizes the async `tokio` runtime to take advantage of all cores on the computer, allowing async tasks to run efficiently within that runtime. +On native platforms, Dart runs in the main thread, while Rust utilizes the async `tokio` runtime, allowing async tasks to run efficiently within a separate thread. On the web, Dart and Rust both run inside JavaScript's async event loop in the main thread, with Rust `Future`s being converted into JavaScript `Promise`s internally. This is a necessary constraint because [webassembly component proposal](https://github.com/WebAssembly/proposals) is not stabilized as of February 2024. @@ -131,9 +131,9 @@ There might be various Rust codes with these attribute above: ```rust title="Rust" #[cfg(target_family = "wasm")] -... +{} #[cfg(not(target_family = "wasm"))] -... +{} ``` Since the environments of the web and native platforms are so different, there are times when you need to use these attributes to include and exclude parts of the code depending on whether they are targeting web or not. @@ -148,6 +148,8 @@ By default, Rust-analyzer runs in native mode. To make it run in webassembly mod target = "wasm32-unknown-unknown" ``` +You need to restart Rust language server for this to take effect. + ### CMake cache is broken after I moved the app folder ```title="Output" @@ -171,12 +173,9 @@ If you are using older Android versions, you may encounter errors due to issues To address this, you can modify `AndroidManifest.xml` files under `./android/app/src/` as follows. ```xml title="android/app/src/**/AndroidManifest.xml" -... -... ``` ### How can I await a response? @@ -188,7 +187,7 @@ However, if you really need to store some state in a Flutter widget, you can ach ```proto title="messages/tutorial_resource.proto" syntax = "proto3"; package tutorial_resource; -... + // [RINF:DART-SIGNAL] message MyUniqueInput { int32 interaction_id = 1; @@ -203,7 +202,6 @@ message MyUniqueOutput { ``` ```dart title="lib/main.dart" -... import 'dart:async'; import 'package:example_app/messages/tutorial_resource.pb.dart'; @@ -211,21 +209,17 @@ var currentInteractionId = 0; final myUniqueOutputs = Map>(); void main() async { - ... MyUniqueOutput.rustSignalStream.listen((rustSignal) { final myUniqueInput = rustSignal.message; myUniqueOutputs[myUniqueInput.interactionId]!.complete(myUniqueInput); }); - ... } -... ``` ```dart title="lib/main.dart" -... import 'dart:async'; import 'package:example_app/messages/tutorial_resource.pb.dart'; -... + onPressed: () async { final completer = Completer(); myUniqueOutputs[currentInteractionId] = completer; @@ -236,14 +230,13 @@ onPressed: () async { currentInteractionId += 1; final myUniqueOutput = await completer.future; }, -... ``` ```rust title="native/hub/src/sample_functions.rs" -pub async fn respond() { +pub async fn respond() -> Result<()> { use messages::tutorial_resource::*; - let mut receiver = MyUniqueInput::get_dart_signal_receiver(); + let mut receiver = MyUniqueInput::get_dart_signal_receiver()?; while let Some(dart_signal) = receiver.recv().await { let my_unique_input = dart_signal.message; MyUniqueOutput { @@ -252,12 +245,13 @@ pub async fn respond() { } .send_signal_to_dart(); } + + Ok(()) } ``` ```rust title="native/hub/src/lib.rs" async fn main() { - ... tokio::spawn(sample_functions::respond()); } ``` @@ -270,7 +264,7 @@ Here are the current constraints of the `wasm32-unknown-unknown` target: - Numerous functionalities within `std::fs` remain unimplemented. - Various features of `std::net` are not available. Consider using `reqwest` crate instead. `reqwest` supports `wasm32-unknown-unknown` and relies on JavaScript to perform network communications. -- `std::thread::spawn` doesn't work. Consider using `tokio_with_wasm::tokio::task::spawn_blocking` instead. +- `std::thread::spawn` doesn't work. Consider using `tokio_with_wasm::task::spawn_blocking` instead. - Several features of `std::time::Instant` are unimplemented. Consider using `chrono` as an alternative. `chrono` supports `wasm32-unknown-unknown` and relies on JavaScript to obtain system time. - In case of a panic in an asynchronous Rust task, it aborts and throws a JavaScript `RuntimeError` [which Rust cannot catch](https://stackoverflow.com/questions/59426545/rust-paniccatch-unwind-no-use-in-webassembly). A recommended practice is to replace `.unwrap` with `.expect` or handle errors with `Err` instances. @@ -324,9 +318,7 @@ import './messages/generated.dart'; async void main() { await initializeRust(compiledLibPath: "/path/to/library/libhub.so"); - ... } -... ``` This provided path will be used for finding dynamic library files on native platforms with Dart's `DynamicLibrary.open([compiledLibPath])`, and for loading the JavaScript module on the web with `import init, * as wasmBindings from "[compiledLibPath]"`. diff --git a/documentation/docs/graceful-shutdown.md b/documentation/docs/graceful-shutdown.md index 950adb541..42bd8447a 100644 --- a/documentation/docs/graceful-shutdown.md +++ b/documentation/docs/graceful-shutdown.md @@ -1,12 +1,15 @@ # Graceful Shutdown -When the Flutter app is closed, the entire `tokio` runtime on the Rust side will be terminated automatically. However, you might need to run some finalization code in Rust before the app closes. This might involve saving files or disposing of resources. To achieve this, you can call `finalizeRust()` in Dart to terminate all Rust tasks before closing the Flutter app. +When the Flutter app is closed, the entire `tokio` async runtime on the Rust side will be terminated automatically. Even if the app is force-closed, the `tokio` async runtime will be properly dropped. + +When using Rinf, the lifetime of the `tokio` runtime follows that of the Dart runtime. This behavior is different from typical `tokio` executables where its async runtime lives throughout the async `main()` function of Rust. + +In some cases, you might need to run some finalization code in Rust before the app closes. This might involve saving files or disposing of resources. To achieve this, you can use Flutter's `AppLifecycleListener` to run something or to get user confirmation before closing the Flutter app. ```dart title="lib/main.dart" import 'dart:ui'; import 'package:flutter/material.dart'; -import './messages/generated.dart'; -... + class MyApp extends StatefulWidget { const MyApp({super.key}); @@ -17,8 +20,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { final _appLifecycleListener = AppLifecycleListener( onExitRequested: () async { - // Terminate Rust tasks before closing the Flutter app. - await finalizeRust(); + // Do something here before the app is exited. return AppExitResponse.exit; }, ); @@ -37,5 +39,8 @@ class _MyAppState extends State { ); } } -... ``` + +It's worth noting that `AppLifecycleListener` or `dispose` cannot always be relied upon for app closings. Below is a text snippet quoted from the official [Flutter docs](https://api.flutter.dev/flutter/widgets/State/dispose.html): + +> There is no way to predict when application shutdown will happen. For example, a user's battery could catch fire, or the user could drop the device into a swimming pool, or the operating system could unilaterally terminate the application process due to memory pressure. Applications are responsible for ensuring they behave well even in the face of rapid, unscheduled termination. diff --git a/documentation/docs/messaging.md b/documentation/docs/messaging.md index 83cb90b6b..67fd07058 100644 --- a/documentation/docs/messaging.md +++ b/documentation/docs/messaging.md @@ -1,93 +1,99 @@ # Messaging -!!! warning - - If you are using Rinf version 5 or earlier, please refer to the [historical documentation](https://github.com/cunarist/rinf/blob/v5.4.0/documentation/docs/writing-code.md). With the introduction of Rinf version 6, a simpler way for communication between Dart and Rust has been implemented, and the system has undergone significant changes. - -Marking Protobuf messages with special comments empower them with the capability to transmit signals across Dart and Rust. This is achieved by allowing Rinf's code generator to create the necessary channels for communication between Dart and Rust. - There are 2 types of special comments that you can mark messages with. -## 📭 Dart Signal +## 📢 Rust Signal -`[RINF:DART-SIGNAL]` generates a channel from Dart to Rust. +`[RINF:RUST-SIGNAL]` generates a channel from Rust to Dart. ```proto title="Protobuf" -// [RINF:DART-SIGNAL] -message MyDataInput { ... } +// [RINF:RUST-SIGNAL] +message MyDataOutput { ... } ``` ```dart title="Dart" -MyDataInput( ... ).sendSignalToRust(); +StreamBuilder( + stream: MyDataOutput.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + if (rustSignal == null) { + // Return an empty widget. + } + MyDataOutput message = rustSignal.message; + // Return a filled widget. + }, +) ``` ```rust title="Rust" -let mut receiver = MyDataInput::get_dart_signal_receiver(); -while let Some(dart_signal) = receiver.recv().await { - let message: MyDataInput = dart_signal.message; - // Custom Rust logic here -} +MyDataOutput { ... }.send_signal_to_dart(); ``` -Use `[RINF:DART-SIGNAL-BINARY]` to include binary data without the overhead of serialization. +Use `[RINF:RUST-SIGNAL-BINARY]` to include binary data without the overhead of serialization. ```proto title="Protobuf" -// [RINF:DART-SIGNAL-BINARY] -message MyDataInput { ... } +// [RINF:RUST-SIGNAL-BINARY] +message MyDataOutput { ... } ``` ```dart title="Dart" -final binary = Uint8List(64); -MyDataInput( ... ).sendSignalToRust(binary); +StreamBuilder( + stream: MyDataOutput.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + if (rustSignal == null) { + // Return an empty widget. + } + MyDataOutput message = rustSignal.message; + Uint8List binary = rustSignal.binary; + // Return a filled widget. + }, +) ``` ```rust title="Rust" -let mut receiver = MyDataInput::get_dart_signal_receiver(); -while let Some(dart_signal) = receiver.recv().await { - let message: MyDataInput = dart_signal.message; - let binary: Vec = dart_signal.binary; - // Custom Rust logic here -} +let binary: Vec = vec![0; 64]; +MyDataOutput { ... }.send_signal_to_dart(binary); ``` -## 📢 Rust Signal +## 📭 Dart Signal -`[RINF:RUST-SIGNAL]` generates a channel from Rust to Dart. +`[RINF:DART-SIGNAL]` generates a channel from Dart to Rust. ```proto title="Protobuf" -// [RINF:RUST-SIGNAL] -message MyDataOutput { ... } +// [RINF:DART-SIGNAL] +message MyDataInput { ... } ``` ```dart title="Dart" -final stream = MyDataOutput.rustSignalStream; -await for (final rustSignal in stream) { - MyDataOutput message = rustSignal.message; - // Custom Dart logic here -} +MyDataInput( ... ).sendSignalToRust(); ``` ```rust title="Rust" -MyDataOutput { ... }.send_signal_to_dart(); +let mut receiver = MyDataInput::get_dart_signal_receiver()?; +while let Some(dart_signal) = receiver.recv().await { + let message: MyDataInput = dart_signal.message; + // Custom Rust logic here +} ``` -Use `[RINF:RUST-SIGNAL-BINARY]` to include binary data without the overhead of serialization. +Use `[RINF:DART-SIGNAL-BINARY]` to include binary data without the overhead of serialization. ```proto title="Protobuf" -// [RINF:RUST-SIGNAL-BINARY] -message MyDataOutput { ... } +// [RINF:DART-SIGNAL-BINARY] +message MyDataInput { ... } ``` ```dart title="Dart" -final stream = MyDataOutput.rustSignalStream; -await for (final rustSignal in stream) { - MyDataOutput message = rustSignal.message; - Uint8List binary = rustSignal.binary; - // Custom Dart logic here -} +final binary = Uint8List(64); +MyDataInput( ... ).sendSignalToRust(binary); ``` ```rust title="Rust" -let binary: Vec = vec![0; 64]; -MyDataOutput { ... }.send_signal_to_dart(binary); +let mut receiver = MyDataInput::get_dart_signal_receiver()?; +while let Some(dart_signal) = receiver.recv().await { + let message: MyDataInput = dart_signal.message; + let binary: Vec = dart_signal.binary; + // Custom Rust logic here +} ``` diff --git a/documentation/docs/running-and-building.md b/documentation/docs/running-and-building.md index 2539a0105..261652970 100644 --- a/documentation/docs/running-and-building.md +++ b/documentation/docs/running-and-building.md @@ -33,7 +33,9 @@ rinf wasm --release flutter run --release # Choose a browser ``` -To build the optimized release version of the web application: +To build the optimized release version of the web application[^4]: + +[^4]: Rinf supports hosting a Flutter app at a [non-root location](https://docs.flutter.dev/ui/navigation/url-strategies#hosting-a-flutter-app-at-a-non-root-location). For example, you can place your Flutter app in `https://mywebsite.com/subpath/deeperpath/`. ```bash title="CLI" rinf wasm --release diff --git a/documentation/docs/tutorial.md b/documentation/docs/tutorial.md index 8669599e9..3ccd0618f 100644 --- a/documentation/docs/tutorial.md +++ b/documentation/docs/tutorial.md @@ -5,12 +5,10 @@ To grasp the basic concepts, it's beneficial to follow a step-by-step tutorial. Before we start, make sure that there's a `Column` somewhere in your widget tree. This will contain our tutorial widgets. ```dart title="lib/main.dart" -... child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [], ) -... ``` ## 🚨 From Dart to Rust @@ -19,9 +17,9 @@ Let's say that you want to create a new button in Dart that sends an array of nu Write a new `.proto` file in the `./messages` directory with a new message. Note that the message should have the comment `[RINF:DART-SIGNAL]` above it. -```proto title="messages/tutorial_resource.proto" +```proto title="messages/tutorial_messages.proto" syntax = "proto3"; -package tutorial_resource; +package tutorial_messages; // [RINF:DART-SIGNAL] message MyPreciousData { @@ -39,9 +37,8 @@ rinf message Create a button widget in Dart that accepts the user input. ```dart title="lib/main.dart" -... -import 'package:example_app/messages/tutorial_resource.pb.dart'; -... +import 'package:test_app/messages/tutorial_messages.pb.dart'; + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -54,20 +51,21 @@ child: Column( }, child: Text("Send a Signal from Dart to Rust"), ), -... + ] +) ``` Let's listen to this message in Rust. This simple function will add one to each element in the array and capitalize all letters in the string. -```rust title="native/hub/src/sample_functions.rs" -... +```rust title="native/hub/src/tutorial_functions.rs" +use crate::common::*; use crate::messages; use rinf::debug_print; -... -pub async fn calculate_precious_data() { - use messages::tutorial_resource::*; - let mut receiver = MyPreciousData::get_dart_signal_receiver(); // GENERATED +pub async fn calculate_precious_data() -> Result<()> { + use messages::tutorial_messages::*; + + let mut receiver = MyPreciousData::get_dart_signal_receiver()?; // GENERATED while let Some(dart_signal) = receiver.recv().await { let my_precious_data = dart_signal.message; @@ -81,21 +79,20 @@ pub async fn calculate_precious_data() { debug_print!("{new_numbers:?}"); debug_print!("{new_string}"); } + + Ok(()) } -... ``` ```rust title="native/hub/src/lib.rs" -... -mod sample_functions; -... +mod tutorial_functions; + async fn main() { -... - tokio::spawn(sample_functions::calculate_precious_data()); + tokio::spawn(tutorial_functions::calculate_precious_data()); } ``` -Now we can see the printed output in the command-line when clicking the button! +Now run the app with `flutter run`. We can see the printed output in the command-line when clicking the button! ```title="Output" flutter: [4, 5, 6] @@ -108,10 +105,10 @@ Let's say that you want to send increasing numbers every second from Rust to Dar Define the message. Note that the message should have the comment `[RINF:RUST-SIGNAL]` above it. -```proto title="messages/tutorial_resource.proto" +```proto title="messages/tutorial_messages.proto" syntax = "proto3"; -package tutorial_resource; -... +package tutorial_messages; + // [RINF:RUST-SIGNAL] message MyAmazingNumber { int32 current_number = 1; } ``` @@ -124,39 +121,38 @@ rinf message Define an async Rust function that runs forever, sending numbers to Dart every second. -```rust title="native/hub/src/sample_functions.rs" -... +```toml title="native/hub/Cargo.toml" +tokio = { version = "1", features = ["sync", "rt", "time"] } +``` + +```rust title="native/hub/src/tutorial_functions.rs" use crate::messages; -... +use std::time::Duration; + pub async fn stream_amazing_number() { - use messages::tutorial_resource::*; + use messages::tutorial_messages::*; let mut current_number: i32 = 1; loop { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_secs(1)).await; MyAmazingNumber { current_number }.send_signal_to_dart(); // GENERATED current_number += 1; } } -... ``` ```rust title="native/hub/src/lib.rs" -... -mod sample_functions; -... +mod tutorial_functions; + async fn main() { -... - tokio::spawn(sample_functions::stream_amazing_number()); + tokio::spawn(tutorial_functions::stream_amazing_number()); } ``` Finally, receive the signals in Dart with `StreamBuilder` and rebuild the widget accordingly. ```dart title="lib/main.dart" -... -import 'package:example_app/messages/tutorial_resource.pb.dart'; -... +import 'package:test_app/messages/tutorial_messages.pb.dart'; children: [ StreamBuilder( stream: MyAmazingNumber.rustSignalStream, // GENERATED @@ -170,17 +166,17 @@ children: [ return Text(currentNumber.toString()); }, ), -... +] ``` ## 🤝 Back and Forth You can easily show the updated state on the screen by combining those two ways of message passing. -```proto title="messages/tutorial_resource.proto" +```proto title="messages/tutorial_messages.proto" syntax = "proto3"; -package tutorial_resource; -... +package tutorial_messages; + // [RINF:DART-SIGNAL] message MyTreasureInput {} @@ -193,9 +189,8 @@ rinf message ``` ```dart title="lib/main.dart" -... -import 'package:example_app/messages/tutorial_resource.pb.dart'; -... +import 'package:test_app/messages/tutorial_messages.pb.dart'; + children: [ StreamBuilder( stream: MyTreasureOutput.rustSignalStream, // GENERATED @@ -215,31 +210,32 @@ children: [ }, child: Text('Send the input'), ), -... +] ``` -```rust title="native/hub/src/sample_functions.rs" -... +```rust title="native/hub/src/tutorial_functions.rs" +use crate::common::*; use crate::messages; -... -pub async fn tell_treasure() { - use messages::tutorial_resource::*; + +pub async fn tell_treasure() -> Result<()> { + use messages::tutorial_messages::*; let mut current_value: i32 = 1; - let mut receiver = MyTreasureInput::get_dart_signal_receiver(); // GENERATED + + let mut receiver = MyTreasureInput::get_dart_signal_receiver()?; // GENERATED while let Some(_) = receiver.recv().await { MyTreasureOutput { current_value }.send_signal_to_dart(); // GENERATED current_value += 1; } + + Ok(()) } ``` ```rust title="native/hub/src/lib.rs" -... -mod sample_functions; -... +mod tutorial_functions; + async fn main() { -... - tokio::spawn(sample_functions::tell_treasure()); + tokio::spawn(tutorial_functions::tell_treasure()); } ``` diff --git a/documentation/overrides/home.html b/documentation/overrides/home.html index 4fb486a0a..d597b887f 100644 --- a/documentation/overrides/home.html +++ b/documentation/overrides/home.html @@ -207,53 +207,6 @@

Platform Support

-
-

Communication

-
-
-

Dart with Widgets

-
-
-MySchemaInput(
-  fieldOne: 1,
-  fieldTwo: 2,
-  fieldThree: 3,
-).sendSignalToRust()
-
-final stream = MySchemaOutput.rustSignalStream;
-await for (final rustSignal in stream) {
-  // Custom Dart logic
-}
-          
-
-
-
-

Rust with Business Logic

-
-
-MySchemaOutput {
-    field_four: 4,
-    field_five: 5,
-    field_six: 6,
-}.send_signal_to_dart()
-
-let mut receiver = MySchemaInput::get_dart_signal_receiver();
-while let Some(dart_signal) = receiver.recv().await {
-    // Custom Rust logic
-}
-          
-
-
-
-
-

- All the message classes and structs are generated by Rinf. You can - simply define the message schema with Protobuf. Sending messages between - Dart and Rust is very convenient. -

-
-
-

Benefits

diff --git a/flutter_ffi_plugin/CHANGELOG.md b/flutter_ffi_plugin/CHANGELOG.md index d56b916d4..400fe3dc1 100644 --- a/flutter_ffi_plugin/CHANGELOG.md +++ b/flutter_ffi_plugin/CHANGELOG.md @@ -1,3 +1,15 @@ +## 6.12.1 + +- Fixed linefeed problem in published files. + +## 6.12.0 + +- Generated message channels are now more efficient. +- Minimum Android SDK version increased from 16 to 21. Thanks @debanjanbasu! +- The `finalizeRust()` function in Dart has been removed. The `tokio` async runtime, which holds Rust logic, still drops automatically when the app ends. +- Bumped `prost` version to 0.12.6. Thanks @yeoupooh! +- Internal code has been organized. + ## 6.11.1 - Fixed a bug with Dart's extension methods in the generated message code. diff --git a/flutter_ffi_plugin/README.md b/flutter_ffi_plugin/README.md index cd954b972..f409ef9c8 100644 --- a/flutter_ffi_plugin/README.md +++ b/flutter_ffi_plugin/README.md @@ -32,32 +32,30 @@ All platforms available with Flutter are [tested](https://github.com/cunarist/ri Below is Dart code with widgets, and following that is Rust code with business logic. ```dart -MySchemaInput( - fieldOne: 1, - fieldTwo: 2, - fieldThree: 3, -).sendSignalToRust() - -final stream = MySchemaOutput.rustSignalStream; -await for (final rustSignal in stream) { - // Custom Dart logic -} +StreamBuilder( + stream: MyMessage.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + if (rustSignal == null) { + return Text("Nothing received yet"); + } + final myMessage = rustSignal.message; + return Text(myMessage.currentNumber.toString()); + }, +), ``` ```rust -MySchemaOutput { - field_four: 4, - field_five: 5, - field_six: 6, -}.send_signal_to_dart() - -let mut receiver = MySchemaInput::get_dart_signal_receiver(); -while let Some(dart_signal) = receiver.recv().await { - // Custom Rust logic +MyMessage { + current_number: 7, + other_bool: true, } +.send_signal_to_dart(); ``` -All the message classes and structs are generated by Rinf. You can simply define the message schema with Protobuf. Sending messages between Dart and Rust is very convenient. +Of course, the opposite way from Dart to Rust is also possible in a similar manner. + +All message classes and structs are generated by Rinf. You can define the message schema simply with Protobuf, making message passing between Dart and Rust very convenient. ## 🎁 Benefits diff --git a/flutter_ffi_plugin/android/build.gradle b/flutter_ffi_plugin/android/build.gradle index 32230e317..123deb834 100755 --- a/flutter_ffi_plugin/android/build.gradle +++ b/flutter_ffi_plugin/android/build.gradle @@ -1,64 +1,69 @@ -// The Android Gradle Plugin builds the native code with the Android NDK. - -group 'com.cunarist.rinf' -version '1.0' +group = "com.cunarist.rinf" +version = "1.0-SNAPSHOT" buildscript { + ext.kotlin_version = "1.7.10" repositories { google() mavenCentral() } dependencies { - // The Android Gradle Plugin knows how to build native code with the NDK. - classpath 'com.android.tools.build:gradle:7.3.0' + classpath("com.android.tools.build:gradle:7.3.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") } } -rootProject.allprojects { +allprojects { repositories { google() mavenCentral() } } -apply plugin: 'com.android.library' +apply plugin: "com.android.library" +apply plugin: "kotlin-android" android { if (project.android.hasProperty("namespace")) { - namespace 'com.cunarist.rinf' + namespace = "com.cunarist.rinf" } - // Bumping the plugin compileSdkVersion requires all clients of this plugin - // to bump the version in their app. - compileSdkVersion 31 - - // Simply use the `android.ndkVersion` - // declared in the `./android/app/build.gradle` file of the Flutter project. - ndkVersion android.ndkVersion + compileSdk = 34 - // Invoke the shared CMake build with the Android Gradle Plugin. - externalNativeBuild { - cmake { - path "../src/CMakeLists.txt" + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } - // The default CMake version for the Android Gradle Plugin is 3.10.2. - // https://developer.android.com/studio/projects/install-ndk#vanilla_cmake - // - // The Flutter tooling requires that developers have CMake 3.10 or later - // installed. You should not increase this version, as doing so will cause - // the plugin to fail to compile for some customers of the plugin. - // version "3.10.2" - } + kotlinOptions { + jvmTarget = "1.8" } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" } defaultConfig { - minSdkVersion 16 + minSdk = 21 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/flutter_ffi_plugin/bin/rinf.dart b/flutter_ffi_plugin/bin/rinf.dart index 458321b77..8db92fa57 100755 --- a/flutter_ffi_plugin/bin/rinf.dart +++ b/flutter_ffi_plugin/bin/rinf.dart @@ -1,6 +1,7 @@ import 'src/config.dart'; import 'src/helpers.dart'; import 'src/message.dart'; +import 'src/internet.dart'; Future main(List args) async { if (args.isEmpty) { @@ -10,6 +11,7 @@ Future main(List args) async { } final rinfConfig = await loadVerifiedRinfConfig("pubspec.yaml"); + await checkConnectivity(); switch (args[0]) { case "config": diff --git a/flutter_ffi_plugin/bin/src/helpers.dart b/flutter_ffi_plugin/bin/src/helpers.dart index ece9703e8..a52d67ece 100644 --- a/flutter_ffi_plugin/bin/src/helpers.dart +++ b/flutter_ffi_plugin/bin/src/helpers.dart @@ -4,6 +4,7 @@ import 'package:yaml/yaml.dart'; import 'config.dart'; import 'message.dart'; import 'common.dart'; +import 'internet.dart'; /// Creates new folders and files to an existing Flutter project folder. Future applyRustTemplate({ @@ -197,31 +198,52 @@ Future copyDirectory(Uri source, Uri destination) async { } Future buildWebassembly({bool isReleaseMode = false}) async { - // Verify Rust toolchain. - print("Verifying Rust toolchain for the web." + - "\nThis might take a while if there are new updates."); - await Process.run("rustup", ["toolchain", "install", "nightly"]); - await Process.run("rustup", [ - "+nightly", - "component", - "add", - "rust-src", - ]); - await Process.run("rustup", [ - "+nightly", - "target", - "add", - "wasm32-unknown-unknown", - ]); // For actual compilation - await Process.run("rustup", [ - "target", - "add", - "wasm32-unknown-unknown", - ]); // For Rust-analyzer - await Process.run("cargo", ["install", "wasm-pack"]); - await Process.run("cargo", ["install", "wasm-bindgen-cli"]); + // Ensure Rust toolchain. + if (isInternetConnected) { + print("Ensuring Rust toolchain for the web." + + "\nThis is done by installing it globally on the system."); + final processResults = []; + processResults.add(await Process.run("rustup", [ + "toolchain", + "install", + "nightly", + ])); + processResults.add(await Process.run("rustup", [ + "+nightly", + "component", + "add", + "rust-src", + ])); + processResults.add(await Process.run("rustup", [ + "+nightly", + "target", + "add", + "wasm32-unknown-unknown", + ])); // For actual compilation + processResults.add(await Process.run("rustup", [ + "target", + "add", + "wasm32-unknown-unknown", + ])); // For Rust-analyzer + processResults.add(await Process.run("cargo", [ + "install", + "wasm-pack", + ])); + processResults.add(await Process.run("cargo", [ + "install", + "wasm-bindgen-cli", + ])); + processResults.forEach((processResult) { + if (processResult.exitCode != 0) { + print(processResult.stderr.toString().trim()); + throw Exception('Cannot globally install Rust toolchain for the web.'); + } + }); + } else { + print("Skipping ensurement of Rust toolchain for the web."); + } - // Verify Flutter SDK web server's response headers. + // Patch Flutter SDK web server's response headers. try { await patchServerHeaders(); print("Patched Flutter SDK's web server with CORS HTTP headers."); diff --git a/flutter_ffi_plugin/bin/src/internet.dart b/flutter_ffi_plugin/bin/src/internet.dart new file mode 100644 index 000000000..02d8eef6c --- /dev/null +++ b/flutter_ffi_plugin/bin/src/internet.dart @@ -0,0 +1,7 @@ +import 'package:internet_connection_checker/internet_connection_checker.dart'; + +var isInternetConnected = false; + +Future checkConnectivity() async { + isInternetConnected = await InternetConnectionChecker().hasConnection; +} diff --git a/flutter_ffi_plugin/bin/src/message.dart b/flutter_ffi_plugin/bin/src/message.dart index 691a3f955..7854f517e 100644 --- a/flutter_ffi_plugin/bin/src/message.dart +++ b/flutter_ffi_plugin/bin/src/message.dart @@ -3,6 +3,7 @@ import 'package:path/path.dart'; import 'package:watcher/watcher.dart'; import 'config.dart'; import 'common.dart'; +import 'internet.dart'; enum MarkType { dartSignal, @@ -46,7 +47,7 @@ Future generateMessageCode({ resourcesInFolders, ); - // Verify `package` statement in `.proto` files. + // Include `package` statement in `.proto` files. // Package name should be the same as the filename // because Rust filenames are written with package name // and Dart filenames are written with the `.proto` filename. @@ -76,18 +77,24 @@ Future generateMessageCode({ } // Generate Rust message files. - if (!silent) { - print("Verifying `protoc-gen-prost` for Rust." + - "\nThis might take a while if there are new updates."); - } - final cargoInstallCommand = await Process.run('cargo', [ - 'install', - 'protoc-gen-prost', - ...(messageConfig.rustSerde ? ['protoc-gen-prost-serde'] : []) - ]); - if (cargoInstallCommand.exitCode != 0) { - print(cargoInstallCommand.stderr.toString().trim()); - throw Exception('Cannot globally install `protoc-gen-prost` Rust crate'); + if (isInternetConnected) { + if (!silent) { + print("Ensuring `protoc-gen-prost` for Rust." + + "\nThis is done by installing it globally on the system."); + } + final cargoInstallCommand = await Process.run('cargo', [ + 'install', + 'protoc-gen-prost', + ...(messageConfig.rustSerde ? ['protoc-gen-prost-serde'] : []) + ]); + if (cargoInstallCommand.exitCode != 0) { + print(cargoInstallCommand.stderr.toString().trim()); + throw Exception('Cannot globally install `protoc-gen-prost` Rust crate'); + } + } else { + if (!silent) { + print("Skipping ensurement of `protoc-gen-prost` for Rust."); + } } for (final entry in resourcesInFolders.entries) { final subPath = entry.key; @@ -164,19 +171,25 @@ Future generateMessageCode({ } // Generate Dart message files. - if (!silent) { - print("Verifying `protoc_plugin` for Dart." + - "\nThis might take a while if there are new updates."); - } - final pubGlobalActivateCommand = await Process.run('dart', [ - 'pub', - 'global', - 'activate', - 'protoc_plugin', - ]); - if (pubGlobalActivateCommand.exitCode != 0) { - print(pubGlobalActivateCommand.stderr.toString().trim()); - throw Exception('Cannot globally install `protoc_plugin` Dart package'); + if (isInternetConnected) { + if (!silent) { + print("Ensuring `protoc_plugin` for Dart." + + "\nThis is done by installing it globally on the system."); + } + final pubGlobalActivateCommand = await Process.run('dart', [ + 'pub', + 'global', + 'activate', + 'protoc_plugin', + ]); + if (pubGlobalActivateCommand.exitCode != 0) { + print(pubGlobalActivateCommand.stderr.toString().trim()); + throw Exception('Cannot globally install `protoc_plugin` Dart package'); + } + } else { + if (!silent) { + print("Skipping ensurement of `protoc_plugin` for Dart."); + } } for (final entry in resourcesInFolders.entries) { final subPath = entry.key; @@ -248,15 +261,10 @@ import 'package:rinf/rinf.dart'; use crate::tokio; use prost::Message; -use rinf::send_rust_signal; -use rinf::DartSignal; -use rinf::SharedCell; -use std::cell::RefCell; +use rinf::{debug_print, send_rust_signal, DartSignal, RinfError}; use std::sync::Mutex; -use std::sync::OnceLock; -use tokio::sync::mpsc::unbounded_channel; -use tokio::sync::mpsc::UnboundedReceiver; -use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + ''', atFront: true, ); @@ -272,39 +280,45 @@ use tokio::sync::mpsc::UnboundedSender; await insertTextToFile( rustPath, ''' -type ${messageName}Cell = SharedCell<( - Option>>, +type ${messageName}Cell = Mutex>, Option>>, -)>; +)>>; pub static ${snakeName.toUpperCase()}_CHANNEL: ${messageName}Cell = - OnceLock::new(); + Mutex::new(None); impl ${normalizePascal(messageName)} { - pub fn get_dart_signal_receiver() -> UnboundedReceiver> { - let cell = ${snakeName.toUpperCase()}_CHANNEL - .get_or_init(|| { - let (sender, receiver) = unbounded_channel(); - Mutex::new(RefCell::new(Some((Some(sender), Some(receiver))))) - }) + pub fn get_dart_signal_receiver() + -> Result>, RinfError> + { + let mut guard = ${snakeName.toUpperCase()}_CHANNEL .lock() - .unwrap(); + .map_err(|_| RinfError::LockMessageChannel)?; + if guard.is_none() { + let (sender, receiver) = unbounded_channel(); + guard.replace((sender, Some(receiver))); + } #[cfg(debug_assertions)] { // After Dart's hot restart, // a sender from the previous run already exists // which is now closed. - let borrowed = cell.borrow(); - let pair = borrowed.as_ref().unwrap(); - let is_closed = pair.0.as_ref().unwrap().is_closed(); - drop(borrowed); - if is_closed { + let pair = guard + .as_ref() + .ok_or(RinfError::NoMessageChannel)?; + if pair.0.is_closed() { let (sender, receiver) = unbounded_channel(); - cell.replace(Some((Some(sender), Some(receiver)))); + guard.replace((sender, Some(receiver))); } } - let pair = cell.take().unwrap(); - cell.replace(Some((pair.0, None))); - pair.1.expect("A receiver can be taken only once") + let pair = guard + .take() + .ok_or(RinfError::NoMessageChannel)?; + guard.replace((pair.0, None)); + let receiver = pair + .1 + .ok_or(RinfError::MessageReceiverTaken)?; + Ok(receiver) } } ''', @@ -365,11 +379,14 @@ final ${camelName}Controller = StreamController>(); ''' impl ${normalizePascal(messageName)} { pub fn send_signal_to_dart(&self) { - send_rust_signal( + let result = send_rust_signal( ${markedMessage.id}, self.encode_to_vec(), Vec::new(), ); + if let Err(error) = result { + debug_print!("{error}\\n{self:?}"); + } } } ''', @@ -381,11 +398,14 @@ impl ${normalizePascal(messageName)} { ''' impl ${normalizePascal(messageName)} { pub fn send_signal_to_dart(&self, binary: Vec) { - send_rust_signal( + let result = send_rust_signal( ${markedMessage.id}, self.encode_to_vec(), binary, ); + if let Err(error) = result { + debug_print!("{error}\\n{self:?}"); + } } } ''', @@ -403,28 +423,23 @@ impl ${normalizePascal(messageName)} { use crate::tokio; use prost::Message; -use rinf::debug_print; -use rinf::DartSignal; -use std::cell::RefCell; +use rinf::{debug_print, DartSignal, RinfError}; use std::collections::HashMap; -use std::sync::Mutex; +use std::error::Error; use std::sync::OnceLock; use tokio::sync::mpsc::unbounded_channel; -type SignalHandlers = - OnceLock, Vec) + Send>>>>; -static SIGNAL_HANDLERS: SignalHandlers = OnceLock::new(); +type Handler = dyn Fn(&[u8], &[u8]) -> Result<(), RinfError> + Send + Sync; +type SignalHandlers = HashMap>; +static SIGNAL_HANDLERS: OnceLock = OnceLock::new(); pub fn handle_dart_signal( message_id: i32, - message_bytes: Vec, - binary: Vec -) { - let mutex = SIGNAL_HANDLERS.get_or_init(|| { - let mut hash_map = - HashMap - ::, Vec) + Send + 'static>> - ::new(); + message_bytes: &[u8], + binary: &[u8] +) -> Result<(), RinfError> { + let hash_map = SIGNAL_HANDLERS.get_or_init(|| { + let mut new_hash_map: SignalHandlers = HashMap::new(); '''; for (final entry in markedMessagesAll.entries) { final subpath = entry.key; @@ -441,42 +456,43 @@ pub fn handle_dart_signal( var modulePath = subpath.replaceAll("/", "::"); modulePath = modulePath == "::" ? "" : modulePath; rustReceiveScript += ''' -hash_map.insert( +new_hash_map.insert( ${markedMessage.id}, - Box::new(|message_bytes: Vec, binary: Vec| { + Box::new(|message_bytes: &[u8], binary: &[u8]| { use super::$modulePath$filename::*; - let message = ${normalizePascal(messageName)}::decode( - message_bytes.as_slice() - ).unwrap(); + let message = + ${normalizePascal(messageName)}::decode(message_bytes) + .map_err(|_| RinfError::DecodeMessage)?; let dart_signal = DartSignal { message, - binary, + binary: binary.to_vec(), }; - let cell = ${snakeName.toUpperCase()}_CHANNEL - .get_or_init(|| { - let (sender, receiver) = unbounded_channel(); - Mutex::new(RefCell::new(Some((Some(sender), Some(receiver))))) - }) + let mut guard = ${snakeName.toUpperCase()}_CHANNEL .lock() - .unwrap(); + .map_err(|_| RinfError::LockMessageChannel)?; + if guard.is_none() { + let (sender, receiver) = unbounded_channel(); + guard.replace((sender, Some(receiver))); + } #[cfg(debug_assertions)] { // After Dart's hot restart, // a sender from the previous run already exists // which is now closed. - let borrowed = cell.borrow(); - let pair = borrowed.as_ref().unwrap(); - let is_closed = pair.0.as_ref().unwrap().is_closed(); - drop(borrowed); - if is_closed { + let pair = guard + .as_ref() + .ok_or(RinfError::NoMessageChannel)?; + if pair.0.is_closed() { let (sender, receiver) = unbounded_channel(); - cell.replace(Some((Some(sender), Some(receiver)))); + guard.replace((sender, Some(receiver))); } } - let borrowed = cell.borrow(); - let pair = borrowed.as_ref().unwrap(); - let sender = pair.0.as_ref().unwrap(); + let pair = guard + .as_ref() + .ok_or(RinfError::NoMessageChannel)?; + let sender = &pair.0; let _ = sender.send(dart_signal); + Ok(()) }), ); '''; @@ -485,12 +501,14 @@ hash_map.insert( } } rustReceiveScript += ''' - Mutex::new(hash_map) + new_hash_map }); - let guard = mutex.lock().unwrap(); - let signal_handler = guard.get(&message_id).unwrap(); - signal_handler(message_bytes, binary); + let signal_handler = match hash_map.get(&message_id) { + Some(inner) => inner, + None => return Err(RinfError::NoSignalHandler), + }; + signal_handler(message_bytes, binary) } '''; await File.fromUri(rustOutputPath.join('generated.rs')) @@ -510,11 +528,6 @@ Future initializeRust({String? compiledLibPath}) async { startRustLogic(); } -Future finalizeRust() async { - stopRustLogic(); - await Future.delayed(const Duration(milliseconds: 10)); -} - final signalHandlers = { '''; for (final entry in markedMessagesAll.entries) { diff --git a/flutter_ffi_plugin/example/ios/Runner.xcodeproj/project.pbxproj b/flutter_ffi_plugin/example/ios/Runner.xcodeproj/project.pbxproj index c4f6c96cd..573d57482 100755 --- a/flutter_ffi_plugin/example/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter_ffi_plugin/example/ios/Runner.xcodeproj/project.pbxproj @@ -215,7 +215,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/flutter_ffi_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter_ffi_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09b..8e3ca5dfe 100755 --- a/flutter_ffi_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_ffi_plugin/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ createState() => _MyAppState(); -} - -class _MyAppState extends State { - final _appLifecycleListener = AppLifecycleListener( - onExitRequested: () async { - // Terminate Rust tasks before closing the Flutter app. - await finalizeRust(); - return AppExitResponse.exit; - }, - ); - - @override - void dispose() { - _appLifecycleListener.dispose(); - super.dispose(); - } - +class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( diff --git a/flutter_ffi_plugin/example/macos/Runner.xcodeproj/project.pbxproj b/flutter_ffi_plugin/example/macos/Runner.xcodeproj/project.pbxproj index 464c19cd6..9f48d207f 100755 --- a/flutter_ffi_plugin/example/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter_ffi_plugin/example/macos/Runner.xcodeproj/project.pbxproj @@ -259,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/flutter_ffi_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter_ffi_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e93dc9245..1242ce6d6 100755 --- a/flutter_ffi_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_ffi_plugin/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ = std::result::Result>; diff --git a/flutter_ffi_plugin/example/native/hub/src/lib.rs b/flutter_ffi_plugin/example/native/hub/src/lib.rs index 26bc5a855..017c2991f 100755 --- a/flutter_ffi_plugin/example/native/hub/src/lib.rs +++ b/flutter_ffi_plugin/example/native/hub/src/lib.rs @@ -1,11 +1,12 @@ //! This `hub` crate is the //! entry point of the Rust logic. +mod common; mod messages; mod sample_functions; // use tokio; -use tokio_with_wasm::tokio; // Uncomment this line to target the web +use tokio_with_wasm::alias as tokio; rinf::write_interface!(); diff --git a/flutter_ffi_plugin/example/native/hub/src/sample_functions.rs b/flutter_ffi_plugin/example/native/hub/src/sample_functions.rs index 63075d1bc..44e3713c5 100755 --- a/flutter_ffi_plugin/example/native/hub/src/sample_functions.rs +++ b/flutter_ffi_plugin/example/native/hub/src/sample_functions.rs @@ -1,8 +1,10 @@ //! This crate is written for Rinf demonstrations. +use crate::common::*; use crate::messages; use crate::tokio; use rinf::debug_print; +use std::time::Duration; use tokio::sync::Mutex; // Using the `cfg` macro enables conditional statement. @@ -11,16 +13,16 @@ const IS_DEBUG_MODE: bool = true; #[cfg(not(debug_assertions))] const IS_DEBUG_MODE: bool = false; -// This is one of the best ways to keep a global mutable state in Rust. -// You can also use `tokio::sync::RwLock` or `tokio::sync::OnceCell`. +// This is one of the ways to keep a global mutable state in Rust. +// You can also use `tokio::sync::RwLock` or `std::lazy::LazyLock`. static VECTOR: Mutex> = Mutex::const_new(Vec::new()); // Business logic for the counter widget. -pub async fn tell_numbers() { +pub async fn tell_numbers() -> Result<()> { use messages::counter_number::*; // Stream getter is generated from a marked Protobuf message. - let mut receiver = SampleNumberInput::get_dart_signal_receiver(); + let mut receiver = SampleNumberInput::get_dart_signal_receiver()?; while let Some(dart_signal) = receiver.recv().await { // Extract values from the message received from Dart. // This message is a type that's declared in its Protobuf file. @@ -42,6 +44,8 @@ pub async fn tell_numbers() { } .send_signal_to_dart(); } + + Ok(()) } // Business logic for the fractal image. @@ -57,7 +61,7 @@ pub async fn stream_fractal() { tokio::spawn(async move { loop { // Wait for 40 milliseconds on each frame - tokio::time::sleep(std::time::Duration::from_millis(40)).await; + tokio::time::sleep(Duration::from_millis(40)).await; if sender.capacity() == 0 { continue; } @@ -79,9 +83,15 @@ pub async fn stream_fractal() { // Receive frame join handles in order. tokio::spawn(async move { loop { - let join_handle = receiver.recv().await.unwrap(); - let received_frame = join_handle.await.unwrap(); - if let Some(fractal_image) = received_frame { + let join_handle = match receiver.recv().await { + Some(inner) => inner, + None => continue, + }; + let received_frame = match join_handle.await { + Ok(inner) => inner, + Err(_) => continue, + }; + if let Ok(fractal_image) = received_frame { // Stream the image data to Dart. SampleFractal { current_scale, @@ -98,23 +108,24 @@ pub async fn stream_fractal() { // A dummy function that uses sample messages to eliminate warnings. #[allow(dead_code)] -async fn use_messages() { +async fn use_messages() -> Result<()> { use messages::sample_folder::enum_and_oneof::*; - _ = SampleInput::get_dart_signal_receiver(); + let _ = SampleInput::get_dart_signal_receiver()?; SampleOutput { kind: 3, oneof_input: Some(sample_output::OneofInput::Age(25)), } - .send_signal_to_dart() + .send_signal_to_dart(); + Ok(()) } // Business logic for testing various crates. -pub async fn run_debug_tests() { +pub async fn run_debug_tests() -> Result<()> { if !IS_DEBUG_MODE { - return; + return Ok(()); } - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_secs(1)).await; debug_print!("Starting debug tests."); // Get the current time. @@ -123,28 +134,24 @@ pub async fn run_debug_tests() { // Fetch data from a web API. let url = "http://jsonplaceholder.typicode.com/todos/1"; - let web_response = sample_crate::fetch_from_web_api(url).await; - debug_print!("Response from a web API: {web_response}"); + let web_response = sample_crate::fetch_from_web_api(url).await?; + debug_print!("Response from a web API: {web_response:?}"); // Use a crate that accesses operating system APIs. - let option = sample_crate::get_hardward_id(); - if let Some(hwid) = option { - debug_print!("Hardware ID: {hwid}"); - } else { - debug_print!("Hardware ID is not available on this platform."); - } + let hwid = sample_crate::get_hardward_id()?; + debug_print!("Hardware ID: {hwid:?}"); // Test `tokio::join!` for futures. let join_first = async { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_secs(1)).await; debug_print!("First future finished."); }; let join_second = async { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; + tokio::time::sleep(Duration::from_secs(2)).await; debug_print!("Second future finished."); }; let join_third = async { - tokio::time::sleep(std::time::Duration::from_secs(3)).await; + tokio::time::sleep(Duration::from_secs(3)).await; debug_print!("Third future finished."); }; tokio::join!(join_first, join_second, join_third); @@ -201,10 +208,21 @@ pub async fn run_debug_tests() { join_handles.push(join_handle); } for join_handle in join_handles { - let text = join_handle.await.unwrap(); - debug_print!("{text}"); + if let Ok(text) = join_handle.await { + debug_print!("{text}"); + } } debug_print!("Debug tests completed!"); - panic!("INTENTIONAL DEBUG PANIC"); + + tokio::spawn(async { + // Panic in a separate task + // to avoid memory leak on the web. + // On the web (`wasm32-unknown-unknown`), + // catching and unwinding panics is not possible. + // It is better to avoid panicking code at all costs on the web. + panic!("INTENTIONAL DEBUG PANIC"); + }); + + Ok(()) } diff --git a/flutter_ffi_plugin/example/native/sample_crate/src/error.rs b/flutter_ffi_plugin/example/native/sample_crate/src/error.rs new file mode 100644 index 000000000..94fd4ff8b --- /dev/null +++ b/flutter_ffi_plugin/example/native/sample_crate/src/error.rs @@ -0,0 +1,18 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct ExampleError(pub Box); + +impl fmt::Display for ExampleError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let source = self.0.as_ref(); + write!(f, "An error occured inside the example code.\n{source}") + } +} + +impl Error for ExampleError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self.0.as_ref()) + } +} diff --git a/flutter_ffi_plugin/example/native/sample_crate/src/fractal.rs b/flutter_ffi_plugin/example/native/sample_crate/src/fractal.rs index f23053c9a..af555c975 100644 --- a/flutter_ffi_plugin/example/native/sample_crate/src/fractal.rs +++ b/flutter_ffi_plugin/example/native/sample_crate/src/fractal.rs @@ -2,6 +2,7 @@ //! Copied and modified from //! https://github.com/abour/fractal repository. +use crate::error::ExampleError; use image::ImageEncoder; const WIDTH: u32 = 384; @@ -10,7 +11,7 @@ const BUF_SIZE: u32 = WIDTH * HEIGHT * 3; const SIZE: f64 = 0.000000001; const MAX_ITER: u32 = 1000; -pub fn draw_fractal_image(scale: f64) -> Option> { +pub fn draw_fractal_image(scale: f64) -> Result, ExampleError> { let point_x: f64 = -0.5557506; let point_y: f64 = -0.55560; let mut buffer: Vec = vec![0; BUF_SIZE as usize]; @@ -27,8 +28,8 @@ pub fn draw_fractal_image(scale: f64) -> Option> { ); match result { - Ok(_) => Some(image_data), - Err(_) => None, + Ok(_) => Ok(image_data), + Err(error) => Err(ExampleError(error.into())), } } diff --git a/flutter_ffi_plugin/example/native/sample_crate/src/lib.rs b/flutter_ffi_plugin/example/native/sample_crate/src/lib.rs index 0e7d26483..431353466 100755 --- a/flutter_ffi_plugin/example/native/sample_crate/src/lib.rs +++ b/flutter_ffi_plugin/example/native/sample_crate/src/lib.rs @@ -1,39 +1,42 @@ //! This crate is written for Rinf demonstrations. -pub use fractal::draw_fractal_image; - +mod error; mod fractal; -// `machineid_rs` only supports desktop platforms. +use error::ExampleError; + +pub use fractal::draw_fractal_image; +// `machineid_rs` only supports desktop platforms. #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub fn get_hardward_id() -> Option { +pub fn get_hardward_id() -> Result { let mut builder = machineid_rs::IdBuilder::new(machineid_rs::Encryption::MD5); builder .add_component(machineid_rs::HWIDComponent::SystemID) .add_component(machineid_rs::HWIDComponent::CPUCores); - let hwid = builder.build("mykey").unwrap(); - Some(hwid) + let hwid = builder + .build("mykey") + .map_err(|error| ExampleError(error.into()))?; + Ok(hwid) } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -pub fn get_hardward_id() -> Option { - None +pub fn get_hardward_id() -> Result { + Ok(String::from("UNSUPPORTED")) } // `chrono` supports all platforms, including web. - use chrono::{offset, DateTime}; pub fn get_current_time() -> DateTime { offset::Local::now() } // `reqwest` supports all platforms, including web. - -pub async fn fetch_from_web_api(url: &str) -> String { - reqwest::get(url) +pub async fn fetch_from_web_api(url: &str) -> Result { + let fetched = reqwest::get(url) .await - .expect("Could not get the response from the example web API.") + .map_err(|error| ExampleError(error.into()))? .text() .await - .expect("Could not read body from the web response.") + .map_err(|error| ExampleError(error.into()))?; + Ok(fetched) } diff --git a/flutter_ffi_plugin/example/web/index.html b/flutter_ffi_plugin/example/web/index.html index be820e83e..1aa025dd6 100755 --- a/flutter_ffi_plugin/example/web/index.html +++ b/flutter_ffi_plugin/example/web/index.html @@ -31,29 +31,8 @@ example - - - - - + diff --git a/flutter_ffi_plugin/lib/rinf.dart b/flutter_ffi_plugin/lib/rinf.dart index debb3802b..41ae39841 100755 --- a/flutter_ffi_plugin/lib/rinf.dart +++ b/flutter_ffi_plugin/lib/rinf.dart @@ -13,28 +13,18 @@ export 'src/interface.dart' show RustSignal; /// This function might not be necessary for major platforms /// but can be useful when the app runs on embedded devices. void setCompiledLibPath(String? path) { - setCompiledLibPathExtern(path); + setCompiledLibPathReal(path); } /// Prepares the native interface /// needed to communicate with Rust. Future prepareInterface(HandleRustSignal handleRustSignal) async { - await prepareInterfaceExtern(handleRustSignal); + await prepareInterfaceReal(handleRustSignal); } /// Starts the `main` function in Rust. void startRustLogic() async { - startRustLogicExtern(); -} - -/// Terminates all Rust tasks. -/// Calling this function before closing the Flutter app -/// can prevent potential resource leaks that may occur -/// if the Rust side is abruptly terminated. -/// Please note that on the web, this function does not have any effect, -/// as tasks are managed by the JavaScript runtime, not Rust. -void stopRustLogic() async { - stopRustLogicExtern(); + startRustLogicReal(); } /// Sends a signal to Rust. @@ -43,7 +33,7 @@ void sendDartSignal( Uint8List messageBytes, Uint8List binary, ) async { - sendDartSignalExtern( + sendDartSignalReal( messageId, messageBytes, binary, diff --git a/flutter_ffi_plugin/lib/src/interface.dart b/flutter_ffi_plugin/lib/src/interface.dart index f7676b79f..a3bc1c8ab 100644 --- a/flutter_ffi_plugin/lib/src/interface.dart +++ b/flutter_ffi_plugin/lib/src/interface.dart @@ -10,7 +10,13 @@ typedef HandleRustSignal = void Function(int, Uint8List, Uint8List); /// This type is generic, and the message /// can be of any type declared in Protobuf. class RustSignal { - T message; - Uint8List binary; + /// The message instance of a class generated by Protobuf. + final T message; + + /// Binary data included in the signal. + /// This field is useful for sending custom bytes + /// without the overhead of serialization/deserialization. + final Uint8List binary; + RustSignal(this.message, this.binary); } diff --git a/flutter_ffi_plugin/lib/src/interface_os.dart b/flutter_ffi_plugin/lib/src/interface_os.dart index 7407ed6b7..81610d161 100644 --- a/flutter_ffi_plugin/lib/src/interface_os.dart +++ b/flutter_ffi_plugin/lib/src/interface_os.dart @@ -7,11 +7,11 @@ import 'dart:isolate'; import 'interface.dart'; import 'dart:convert'; -void setCompiledLibPathExtern(String? path) { +void setCompiledLibPathReal(String? path) { setDynamicLibPath(path); } -Future prepareInterfaceExtern( +Future prepareInterfaceReal( HandleRustSignal handleRustSignal, ) async { /// This should be called once at startup @@ -53,10 +53,10 @@ Future prepareInterfaceExtern( }); // Make Rust prepare its isolate to send data to Dart. - prepareIsolateExtern(rustSignalPort.sendPort.nativePort); + prepareIsolateReal(rustSignalPort.sendPort.nativePort); } -void startRustLogicExtern() { +void startRustLogicReal() { final rustFunction = rustLibrary.lookupFunction( 'start_rust_logic_extern', @@ -64,16 +64,8 @@ void startRustLogicExtern() { rustFunction(); } -void stopRustLogicExtern() { - final rustFunction = - rustLibrary.lookupFunction( - 'stop_rust_logic_extern', - ); - rustFunction(); -} - /// Sends bytes to Rust. -Future sendDartSignalExtern( +Future sendDartSignalReal( int messageId, Uint8List messageBytes, Uint8List binary, @@ -85,20 +77,10 @@ Future sendDartSignalExtern( binaryMemory.asTypedList(binary.length).setAll(0, binary); final rustFunction = rustLibrary.lookupFunction< - Void Function( - IntPtr, - Pointer, - IntPtr, - Pointer, - IntPtr, - ), - void Function( - int, - Pointer, - int, - Pointer, - int, - )>('send_dart_signal_extern'); + Void Function(Int32, Pointer, UintPtr, Pointer, UintPtr), + void Function(int, Pointer, int, Pointer, int)>( + 'send_dart_signal_extern', + ); rustFunction( messageId, @@ -112,13 +94,10 @@ Future sendDartSignalExtern( malloc.free(binaryMemory); } -void prepareIsolateExtern(int port) { - final rustFunction = rustLibrary.lookupFunction< - Void Function( - IntPtr, - ), - void Function( - int, - )>('prepare_isolate_extern'); +void prepareIsolateReal(int port) { + final rustFunction = + rustLibrary.lookupFunction( + 'prepare_isolate_extern', + ); rustFunction(port); } diff --git a/flutter_ffi_plugin/lib/src/interface_web.dart b/flutter_ffi_plugin/lib/src/interface_web.dart index 97642a4a8..4e1fb016e 100644 --- a/flutter_ffi_plugin/lib/src/interface_web.dart +++ b/flutter_ffi_plugin/lib/src/interface_web.dart @@ -7,11 +7,11 @@ import 'interface.dart'; import 'dart:async'; import 'dart:convert'; -void setCompiledLibPathExtern(String? path) { +void setCompiledLibPathReal(String? path) { setJsLibPath(path); } -Future prepareInterfaceExtern( +Future prepareInterfaceReal( HandleRustSignal handleRustSignal, ) async { await loadJsFile(); @@ -33,7 +33,7 @@ Future prepareInterfaceExtern( }; } -void startRustLogicExtern() { +void startRustLogicReal() { if (wasAlreadyLoaded) { return; } @@ -41,11 +41,7 @@ void startRustLogicExtern() { jsObject.callMethod('start_rust_logic_extern', []); } -void stopRustLogicExtern() { - // Dummy function to match that of the OS module. -} - -void sendDartSignalExtern( +void sendDartSignalReal( int messageId, Uint8List messageBytes, Uint8List binary, diff --git a/flutter_ffi_plugin/lib/src/load_web.dart b/flutter_ffi_plugin/lib/src/load_web.dart index 759492a26..a4861d5ba 100644 --- a/flutter_ffi_plugin/lib/src/load_web.dart +++ b/flutter_ffi_plugin/lib/src/load_web.dart @@ -23,13 +23,19 @@ Future loadJsFile() async { final loadCompleter = Completer(); js.context['completeRinfLoad'] = loadCompleter.complete; + // Flutter app doesn't always have the top-level path of the domain. + // Sometimes, the flutter app might be placed in a lower path. + // This variable includes domain and the base path. + final baseHref = Uri.base; + // Use the default JavaScript path unless provided. - final path = jsLibPath ?? "/pkg/hub.js"; + final path = jsLibPath ?? "pkg/hub.js"; + final fullUrl = baseHref.resolve(path); final scriptElement = ScriptElement(); scriptElement.type = "module"; scriptElement.innerHtml = ''' -import init, * as wasmBindings from "$path"; +import init, * as wasmBindings from "$fullUrl"; await init(); window.rinf = { ...wasmBindings }; completeRinfLoad(); diff --git a/flutter_ffi_plugin/pubspec.yaml b/flutter_ffi_plugin/pubspec.yaml index 2a04a432b..94b9c5ba3 100644 --- a/flutter_ffi_plugin/pubspec.yaml +++ b/flutter_ffi_plugin/pubspec.yaml @@ -1,6 +1,6 @@ name: rinf description: Rust for native business logic, Flutter for flexible and beautiful GUI -version: 6.11.1 +version: 6.12.1 repository: https://github.com/cunarist/rinf environment: @@ -15,6 +15,7 @@ dependencies: watcher: ^1.1.0 ffi: ^2.1.0 yaml: ^3.1.2 + internet_connection_checker: ^1.0.0 dev_dependencies: lints: ">=3.0.0 <5.0.0" diff --git a/flutter_ffi_plugin/template/native/hub/Cargo.toml b/flutter_ffi_plugin/template/native/hub/Cargo.toml index ab480ee8c..a739bfc75 100644 --- a/flutter_ffi_plugin/template/native/hub/Cargo.toml +++ b/flutter_ffi_plugin/template/native/hub/Cargo.toml @@ -12,8 +12,10 @@ edition = "2021" crate-type = ["lib", "cdylib", "staticlib"] [dependencies] -rinf = "6.11.1" -prost = "0.12.3" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } -# wasm-bindgen = "0.2.92" # Uncomment this line to target the web -# tokio_with_wasm = "0.4.4" # Uncomment this line to target the web +rinf = "6.12.1" +prost = "0.12.6" +tokio = { version = "1", features = ["sync", "rt"] } + +# Uncomment below to target the web. +# tokio_with_wasm = { version = "0.6.0", features = ["sync", "rt"] } +# wasm-bindgen = "0.2.92" diff --git a/flutter_ffi_plugin/template/native/hub/src/common.rs b/flutter_ffi_plugin/template/native/hub/src/common.rs new file mode 100644 index 000000000..44f453a42 --- /dev/null +++ b/flutter_ffi_plugin/template/native/hub/src/common.rs @@ -0,0 +1,7 @@ +use std::error::Error; + +/// Using this `Result` type alias allows +/// handling any error type that implements the `Error` trait. +/// This approach eliminates the need +/// to depend on external crates for error handling. +pub type Result = std::result::Result>; diff --git a/flutter_ffi_plugin/template/native/hub/src/lib.rs b/flutter_ffi_plugin/template/native/hub/src/lib.rs index 393735909..60f4aa223 100644 --- a/flutter_ffi_plugin/template/native/hub/src/lib.rs +++ b/flutter_ffi_plugin/template/native/hub/src/lib.rs @@ -1,10 +1,12 @@ //! This `hub` crate is the //! entry point of the Rust logic. +mod common; mod messages; -use tokio; -// use tokio_with_wasm::tokio; // Uncomment this line to target the web +use crate::common::*; +use tokio; // Comment this line to target the web. +// use tokio_with_wasm::alias as tokio; // Uncomment this line to target the web. rinf::write_interface!(); @@ -14,9 +16,18 @@ rinf::write_interface!(); // If you really need to use blocking code, // use `tokio::task::spawn_blocking`. async fn main() { + tokio::spawn(communicate()); +} + +async fn communicate() -> Result<()> { use messages::basic::*; // Send signals to Dart like below. SmallNumber { number: 7 }.send_signal_to_dart(); // Get receivers that listen to Dart signals like below. - let _ = SmallText::get_dart_signal_receiver(); + let mut receiver = SmallText::get_dart_signal_receiver()?; + while let Some(dart_signal) = receiver.recv().await { + let message: SmallText = dart_signal.message; + rinf::debug_print!("{message:?}"); + } + Ok(()) } diff --git a/rust_crate/Cargo.toml b/rust_crate/Cargo.toml index f2534bd61..7e3d4e9ba 100644 --- a/rust_crate/Cargo.toml +++ b/rust_crate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinf" -version = "6.11.1" +version = "6.12.1" edition = "2021" license = "MIT" description = "Rust for native business logic, Flutter for flexible and beautiful GUI" @@ -8,19 +8,23 @@ repository = "https://github.com/cunarist/rinf" documentation = "https://rinf.cunarist.com" rust-version = "1.70" +[features] +multi-worker = ["tokio/rt-multi-thread"] +show-backtrace = ["backtrace"] +bevy = ["dep:bevy_ecs"] + [target.'cfg(not(target_family = "wasm"))'.dependencies] os-thread-local = "0.1.3" -backtrace = "0.3.69" +backtrace = { version = "0.3.69", optional = true } protoc-prebuilt = "0.3.0" home = "0.5.9" which = "6.0.0" -allo-isolate = "0.1.24" +allo-isolate = "0.1.25" +tokio = { version = "1", features = ["rt"] } bevy_ecs = { version = "0.13", optional = true } [target.'cfg(target_family = "wasm")'.dependencies] js-sys = "0.3.69" wasm-bindgen = "0.2.92" -bevy_ecs = { version = "0.13", optional = true } - -[features] -bevy = ["dep:bevy_ecs"] +wasm-bindgen-futures = "0.4.42" +bevy_ecs = { version = "0.13", optional = true } \ No newline at end of file diff --git a/rust_crate/README.md b/rust_crate/README.md index cd954b972..f409ef9c8 100644 --- a/rust_crate/README.md +++ b/rust_crate/README.md @@ -32,32 +32,30 @@ All platforms available with Flutter are [tested](https://github.com/cunarist/ri Below is Dart code with widgets, and following that is Rust code with business logic. ```dart -MySchemaInput( - fieldOne: 1, - fieldTwo: 2, - fieldThree: 3, -).sendSignalToRust() - -final stream = MySchemaOutput.rustSignalStream; -await for (final rustSignal in stream) { - // Custom Dart logic -} +StreamBuilder( + stream: MyMessage.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + if (rustSignal == null) { + return Text("Nothing received yet"); + } + final myMessage = rustSignal.message; + return Text(myMessage.currentNumber.toString()); + }, +), ``` ```rust -MySchemaOutput { - field_four: 4, - field_five: 5, - field_six: 6, -}.send_signal_to_dart() - -let mut receiver = MySchemaInput::get_dart_signal_receiver(); -while let Some(dart_signal) = receiver.recv().await { - // Custom Rust logic +MyMessage { + current_number: 7, + other_bool: true, } +.send_signal_to_dart(); ``` -All the message classes and structs are generated by Rinf. You can simply define the message schema with Protobuf. Sending messages between Dart and Rust is very convenient. +Of course, the opposite way from Dart to Rust is also possible in a similar manner. + +All message classes and structs are generated by Rinf. You can define the message schema simply with Protobuf, making message passing between Dart and Rust very convenient. ## 🎁 Benefits diff --git a/rust_crate/src/error.rs b/rust_crate/src/error.rs new file mode 100644 index 000000000..c1d99107f --- /dev/null +++ b/rust_crate/src/error.rs @@ -0,0 +1,47 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub enum RinfError { + LockDartIsolate, + NoDartIsolate, + BuildRuntime, + LockMessageChannel, + NoMessageChannel, + MessageReceiverTaken, + DecodeMessage, + NoSignalHandler, +} + +impl fmt::Display for RinfError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RinfError::LockDartIsolate => { + write!(f, "Could not acquire the Dart isolate lock.") + } + RinfError::NoDartIsolate => { + write!(f, "Dart isolate for Rust signals was not created.") + } + RinfError::BuildRuntime => { + write!(f, "Could not build the tokio runtime.") + } + RinfError::LockMessageChannel => { + write!(f, "Could not acquire the message channel lock.") + } + RinfError::NoMessageChannel => { + write!(f, "Message channel was not created.",) + } + RinfError::MessageReceiverTaken => { + write!(f, "Each Dart signal receiver can be taken only once.") + } + RinfError::DecodeMessage => { + write!(f, "Could not decode the message.") + } + RinfError::NoSignalHandler => { + write!(f, "Could not find the handler for Dart signal.") + } + } + } +} + +impl Error for RinfError {} diff --git a/rust_crate/src/externs.rs b/rust_crate/src/externs.rs deleted file mode 100644 index 9f39fa081..000000000 --- a/rust_crate/src/externs.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(not(target_family = "wasm"))] -pub use backtrace; -#[cfg(not(target_family = "wasm"))] -pub use os_thread_local; diff --git a/rust_crate/src/interface.rs b/rust_crate/src/interface.rs index 9136cfe89..9fdfb8f05 100644 --- a/rust_crate/src/interface.rs +++ b/rust_crate/src/interface.rs @@ -1,6 +1,5 @@ -use std::cell::RefCell; -use std::sync::Mutex; -use std::sync::OnceLock; +use crate::error::RinfError; +use std::future::Future; #[cfg(not(target_family = "wasm"))] use super::interface_os::*; @@ -9,10 +8,6 @@ use super::interface_web::*; #[cfg(feature = "bevy")] use bevy_ecs::event::Event; - -/// This is a mutable cell type that can be shared across threads. -pub type SharedCell = OnceLock>>>; - /// This contains a message from Dart. /// Optionally, a custom binary called `binary` can also be included. /// This type is generic, and the message @@ -20,11 +15,44 @@ pub type SharedCell = OnceLock>>>; /// If the bevy feature is used, every message can be received as an event in Bevy. #[cfg_attr(feature = "bevy", derive(Event))] pub struct DartSignal { + /// The message instance of a struct generated by Protobuf. pub message: T, + /// Binary data included in the signal. + /// This field is useful for sending custom bytes + /// without the overhead of serialization/deserialization. pub binary: Vec, } +/// Runs the async main function in Rust. +/// On native platforms, futures usually implement the `Send` trait +/// to be safely sent between threads. +/// Even in a single-threaded (current-thread) runtime, +/// the `Runtime` object itself might be moved between threads, +/// along with all the tasks it manages. +#[cfg(not(target_family = "wasm"))] +pub fn start_rust_logic(main_future: F) -> Result<(), RinfError> +where + F: Future + Send + 'static, +{ + start_rust_logic_real(main_future) +} + +/// Runs the async main function in Rust. +/// On the web, futures usually don't implement the `Send` trait +/// because JavaScript environment is fundamentally single-threaded. +#[cfg(target_family = "wasm")] +pub fn start_rust_logic(main_future: F) -> Result<(), RinfError> +where + F: Future + 'static, +{ + start_rust_logic_real(main_future) +} + /// Send a signal to Dart. -pub fn send_rust_signal(message_id: i32, message_bytes: Vec, binary: Vec) { - send_rust_signal_extern(message_id, message_bytes, binary); +pub fn send_rust_signal( + message_id: i32, + message_bytes: Vec, + binary: Vec, +) -> Result<(), RinfError> { + send_rust_signal_real(message_id, message_bytes, binary) } diff --git a/rust_crate/src/interface_os.rs b/rust_crate/src/interface_os.rs index 65002ece6..dfb078993 100644 --- a/rust_crate/src/interface_os.rs +++ b/rust_crate/src/interface_os.rs @@ -1,29 +1,131 @@ -use super::SharedCell; -use allo_isolate::IntoDart; -use allo_isolate::Isolate; -use allo_isolate::ZeroCopyBuffer; +use crate::error::RinfError; +use allo_isolate::{IntoDart, Isolate, ZeroCopyBuffer}; +use os_thread_local::ThreadLocal; use std::cell::RefCell; -use std::panic::catch_unwind; -use std::sync::Mutex; -use std::sync::OnceLock; +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::task::{Context, Poll, Waker}; +use std::thread; +use tokio::runtime::Builder; -static DART_ISOLATE: SharedCell = OnceLock::new(); +static DART_ISOLATE: Mutex> = Mutex::new(None); #[no_mangle] pub extern "C" fn prepare_isolate_extern(port: i64) { - let _ = catch_unwind(|| { - let dart_isolate = Isolate::new(port); - let cell = DART_ISOLATE - .get_or_init(|| Mutex::new(RefCell::new(None))) - .lock() - .unwrap(); - cell.replace(Some(dart_isolate)); - }); + let dart_isolate = Isolate::new(port); + let mut guard = match DART_ISOLATE.lock() { + Ok(inner) => inner, + Err(_) => { + let error = RinfError::LockDartIsolate; + println!("{error}"); + return; + } + }; + guard.replace(dart_isolate); } -pub fn send_rust_signal_extern(message_id: i32, message_bytes: Vec, binary: Vec) { - let cell = DART_ISOLATE.get().unwrap().lock().unwrap(); - let dart_isolate = cell.borrow().unwrap(); +// We use `os_thread_local` so that when the program fails +// and the main thread exits unexpectedly, +// the whole async tokio runtime can shut down as well +// by receiving a signal via the shutdown channel. +// Without this solution, zombie threads inside the tokio runtime +// might outlive the app. +type ShutdownSenderLock = OnceLock>>>; +static SHUTDOWN_SENDER: ShutdownSenderLock = OnceLock::new(); + +pub fn start_rust_logic_real(main_future: F) -> Result<(), RinfError> +where + F: Future + Send + 'static, +{ + // Enable backtrace output for panics. + #[cfg(debug_assertions)] + { + #[cfg(not(feature = "backtrace"))] + { + std::panic::set_hook(Box::new(|panic_info| { + crate::debug_print!("A panic occurred in Rust.\n{panic_info}"); + })); + } + #[cfg(feature = "backtrace")] + { + std::panic::set_hook(Box::new(|panic_info| { + let backtrace = backtrace::Backtrace::new(); + crate::debug_print!("A panic occurred in Rust.\n{panic_info}\n{backtrace:?}"); + })); + } + } + + // Prepare the channel that will notify tokio runtime to shutdown + // after the main Dart thread has gone. + let (shutdown_sender, shutdown_receiver) = shutdown_channel(); + let shutdown_sender_lock = + SHUTDOWN_SENDER.get_or_init(move || ThreadLocal::new(|| RefCell::new(None))); + shutdown_sender_lock.with(|cell| cell.replace(Some(shutdown_sender))); + + // Build the tokio runtime. + #[cfg(not(feature = "multi-worker"))] + { + let tokio_runtime = Builder::new_current_thread() + .enable_all() + .build() + .map_err(|_| RinfError::BuildRuntime)?; + thread::spawn(move || { + tokio_runtime.spawn(main_future); + tokio_runtime.block_on(shutdown_receiver); + // Dropping the tokio runtime makes it shut down. + drop(tokio_runtime); + }); + } + #[cfg(feature = "multi-worker")] + { + static TOKIO_RUNTIME: Mutex> = Mutex::new(None); + let tokio_runtime = Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|_| RinfError::BuildRuntime)?; + tokio_runtime.spawn(async { + main_future.await; + }); + tokio_runtime.spawn(async { + shutdown_receiver.await; + thread::spawn(|| { + if let Ok(mut guard) = TOKIO_RUNTIME.lock() { + let runtime_option = guard.take(); + if let Some(runtime) = runtime_option { + // Dropping the tokio runtime makes it shut down. + drop(runtime); + } + } + }) + }); + if let Ok(mut guard) = TOKIO_RUNTIME.lock() { + // If there was already a tokio runtime previously, + // most likely due to Dart's hot restart, + // its tasks as well as itself will be terminated, + // being replaced with the new one. + let runtime_option = guard.replace(tokio_runtime); + if let Some(previous_runtime) = runtime_option { + drop(previous_runtime); + } + } + } + + Ok(()) +} + +pub fn send_rust_signal_real( + message_id: i32, + message_bytes: Vec, + binary: Vec, +) -> Result<(), RinfError> { + // When `DART_ISOLATE` is not initialized, just return the error. + // This can happen when running test code in Rust. + let guard = DART_ISOLATE + .lock() + .map_err(|_| RinfError::LockDartIsolate)?; + let dart_isolate = guard.as_ref().ok_or(RinfError::NoDartIsolate)?; // If a `Vec` is empty, we can't just simply send it to Dart // because panic can occur from null pointers. @@ -47,4 +149,54 @@ pub fn send_rust_signal_extern(message_id: i32, message_bytes: Vec, binary: ] .into_dart(), ); + + Ok(()) +} + +struct ShutdownSender { + is_sent: Arc, + waker: Arc>>, +} + +impl Drop for ShutdownSender { + fn drop(&mut self) { + self.is_sent.store(true, Ordering::SeqCst); + if let Ok(mut guard) = self.waker.lock() { + if let Some(waker) = guard.take() { + waker.wake(); + } + } + } +} + +struct ShutdownReceiver { + is_sent: Arc, + waker: Arc>>, +} + +impl Future for ShutdownReceiver { + type Output = (); + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.is_sent.load(Ordering::SeqCst) { + Poll::Ready(()) + } else { + if let Ok(mut guard) = self.waker.lock() { + guard.replace(cx.waker().clone()); + } + Poll::Pending + } + } +} + +fn shutdown_channel() -> (ShutdownSender, ShutdownReceiver) { + let is_sent = Arc::new(AtomicBool::new(false)); + let waker = Arc::new(Mutex::new(None)); + + let sender = ShutdownSender { + is_sent: Arc::clone(&is_sent), + waker: Arc::clone(&waker), + }; + let receiver = ShutdownReceiver { is_sent, waker }; + + (sender, receiver) } diff --git a/rust_crate/src/interface_web.rs b/rust_crate/src/interface_web.rs index f3179ff89..a32b0c4b2 100644 --- a/rust_crate/src/interface_web.rs +++ b/rust_crate/src/interface_web.rs @@ -1,20 +1,42 @@ +use crate::error::RinfError; use js_sys::Uint8Array; +use std::future::Future; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; + +pub fn start_rust_logic_real(main_future: F) -> Result<(), RinfError> +where + F: Future + 'static, +{ + // Add kind description for panics. + #[cfg(debug_assertions)] + { + std::panic::set_hook(Box::new(|panic_info| { + crate::debug_print!("A panic occurred in Rust.\n{panic_info}"); + })); + } + + // Run the main function. + spawn_local(main_future); + + Ok(()) +} #[wasm_bindgen] extern "C" { - #[wasm_bindgen(js_namespace = rinf, js_name = send_rust_signal_extern)] - pub fn send_rust_signal_extern_raw( - resource: i32, - message_bytes: Uint8Array, - binary: Uint8Array, - ); + #[wasm_bindgen(js_namespace = rinf)] + pub fn send_rust_signal_extern(resource: i32, message_bytes: Uint8Array, binary: Uint8Array); } -pub fn send_rust_signal_extern(message_id: i32, message_bytes: Vec, binary: Vec) { - send_rust_signal_extern_raw( +pub fn send_rust_signal_real( + message_id: i32, + message_bytes: Vec, + binary: Vec, +) -> Result<(), RinfError> { + send_rust_signal_extern( message_id, js_sys::Uint8Array::from(message_bytes.as_slice()), js_sys::Uint8Array::from(binary.as_slice()), ); + Ok(()) } diff --git a/rust_crate/src/lib.rs b/rust_crate/src/lib.rs index 07b839796..fae10f70c 100644 --- a/rust_crate/src/lib.rs +++ b/rust_crate/src/lib.rs @@ -1,6 +1,4 @@ -pub use interface::*; - -pub mod externs; +mod error; mod macros; mod interface; @@ -8,3 +6,6 @@ mod interface; mod interface_os; #[cfg(target_family = "wasm")] mod interface_web; + +pub use error::*; +pub use interface::*; diff --git a/rust_crate/src/macros.rs b/rust_crate/src/macros.rs index d06b13cc8..d6042b236 100644 --- a/rust_crate/src/macros.rs +++ b/rust_crate/src/macros.rs @@ -1,5 +1,3 @@ -#![allow(clippy::crate_in_macro_def)] - #[macro_export] /// Writes the interface code /// needed to communicate with Dart. @@ -8,119 +6,49 @@ macro_rules! write_interface { () => { #[cfg(not(target_family = "wasm"))] - mod interface_os { - use crate::tokio::runtime::Builder; - use crate::tokio::runtime::Runtime; - use rinf::externs::os_thread_local::ThreadLocal; - use std::cell::RefCell; - use std::panic::catch_unwind; - use std::sync::OnceLock; - - // We use `os_thread_local` so that when the program fails - // and the main thread exits unexpectedly, - // the whole async tokio runtime can disappear as well. - type TokioRuntime = OnceLock>>>; - static TOKIO_RUNTIME: TokioRuntime = OnceLock::new(); - - #[no_mangle] - pub extern "C" fn start_rust_logic_extern() { - let _ = catch_unwind(|| { - // Enable backtrace output for panics. - #[cfg(debug_assertions)] - { - use rinf::debug_print; - use rinf::externs::backtrace::Backtrace; - std::panic::set_hook(Box::new(|panic_info| { - let backtrace = Backtrace::new(); - debug_print!("A panic occurred in Rust.\n{panic_info}\n{backtrace:?}"); - })); - } - - // Run the main function. - let tokio_runtime = Builder::new_multi_thread().enable_all().build().unwrap(); - tokio_runtime.spawn(crate::main()); - let os_cell = - TOKIO_RUNTIME.get_or_init(|| ThreadLocal::new(|| RefCell::new(None))); - os_cell.with(move |cell| { - // If there was already a tokio runtime previously, - // most likely due to Dart's hot restart, - // its tasks as well as itself will be terminated, - // being replaced with the new one. - cell.replace(Some(tokio_runtime)); - }); - }); + #[no_mangle] + pub extern "C" fn start_rust_logic_extern() { + let result = $crate::start_rust_logic(main()); + if let Err(error) = result { + rinf::debug_print!("{error}"); } + } - #[no_mangle] - pub extern "C" fn stop_rust_logic_extern() { - let _ = catch_unwind(|| { - let os_cell = - TOKIO_RUNTIME.get_or_init(|| ThreadLocal::new(|| RefCell::new(None))); - os_cell.with(move |cell| { - // Dropping the tokio runtime causes it to shut down. - cell.take(); - }); - }); + #[cfg(target_family = "wasm")] + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn start_rust_logic_extern() { + let result = $crate::start_rust_logic(main()); + if let Err(error) = result { + rinf::debug_print!("{error}"); } + } - #[no_mangle] - pub extern "C" fn send_dart_signal_extern( - message_id: i64, - message_pointer: *const u8, - message_size: usize, - binary_pointer: *const u8, - binary_size: usize, - ) { - let message_bytes = unsafe { - std::slice::from_raw_parts(message_pointer as *mut u8, message_size).to_vec() - }; - let binary = unsafe { - std::slice::from_raw_parts(binary_pointer as *mut u8, binary_size).to_vec() - }; - let _ = catch_unwind(|| { - crate::messages::generated::handle_dart_signal( - message_id as i32, - message_bytes, - binary, - ); - }); + #[cfg(not(target_family = "wasm"))] + #[no_mangle] + pub unsafe extern "C" fn send_dart_signal_extern( + message_id: i32, + message_pointer: *const u8, + message_size: usize, + binary_pointer: *const u8, + binary_size: usize, + ) { + use std::slice::from_raw_parts; + let message_bytes = unsafe { from_raw_parts(message_pointer, message_size) }; + let binary = unsafe { from_raw_parts(binary_pointer, binary_size) }; + let result = messages::generated::handle_dart_signal(message_id, message_bytes, binary); + if let Err(error) = result { + rinf::debug_print!("{error}"); } } #[cfg(target_family = "wasm")] - mod interface_web { - use crate::tokio; - use std::panic::catch_unwind; - use wasm_bindgen::prelude::wasm_bindgen; - - #[wasm_bindgen] - pub fn start_rust_logic_extern() { - let _ = catch_unwind(|| { - // Add kind description for panics. - #[cfg(debug_assertions)] - { - use rinf::debug_print; - std::panic::set_hook(Box::new(|panic_info| { - debug_print!("A panic occurred in Rust.\n{panic_info}"); - })); - } - - // Run the main function. - tokio::spawn(crate::main()); - }); - } - - #[wasm_bindgen] - pub fn send_dart_signal_extern(message_id: i32, message_bytes: &[u8], binary: &[u8]) { - let message_bytes = message_bytes.to_vec(); - let binary = binary.to_vec(); - let _ = catch_unwind(|| { - crate::messages::generated::handle_dart_signal( - message_id, - message_bytes, - binary, - ); - }); + #[wasm_bindgen::prelude::wasm_bindgen] + pub fn send_dart_signal_extern(message_id: i32, message_bytes: &[u8], binary: &[u8]) { + let message_bytes = message_bytes; + let binary = binary; + let result = messages::generated::handle_dart_signal(message_id, message_bytes, binary); + if let Err(error) = result { + rinf::debug_print!("{error}"); } } }; @@ -137,11 +65,16 @@ macro_rules! debug_print { ( $( $t:tt )* ) => { let rust_report = format!( $( $t )* ); #[cfg(debug_assertions)] - rinf::send_rust_signal( - -1, // This is a special message ID for Rust reports - Vec::new(), - rust_report.into_bytes(), - ); + { + let result = $crate::send_rust_signal( + -1, // This is a special message ID for Rust reports + Vec::new(), + rust_report.clone().into_bytes(), + ); + if let Err(error) = result { + println!("{error}\n{rust_report}"); + } + } #[cfg(not(debug_assertions))] let _ = rust_report; } diff --git a/rust_crate/src/main.rs b/rust_crate/src/main.rs index ee8aa8a8a..108cef1ef 100644 --- a/rust_crate/src/main.rs +++ b/rust_crate/src/main.rs @@ -1,39 +1,52 @@ #[cfg(not(target_family = "wasm"))] -fn main() { +fn main() -> Result<(), Box> { use std::env; use std::fs; use std::path; use std::process; - // Verify Protobuf compiler. - let protoc_path; - if let Ok(installed) = which::which("protoc") { + // Ensure Protobuf compiler. + let protoc_path = if let Ok(installed) = which::which("protoc") { // Get the path of Protobuf compiler that's already installed. println!("Detected `protoc`, skipping auto installation."); - protoc_path = installed.parent().unwrap().to_path_buf(); + installed + .parent() + .ok_or("Could not get the parent of `protoc` path.")? + .to_path_buf() } else { // Install Protobuf compiler and get the path. - let home_path = home::home_dir().unwrap(); + let home_path = + home::home_dir().ok_or("Could not get home directory for `protoc` installation.")?; let out_path = home_path.join(".local").join("bin"); - fs::create_dir_all(&out_path).unwrap(); - env::set_var("OUT_DIR", out_path.to_str().unwrap()); + fs::create_dir_all(&out_path)?; + env::set_var( + "OUT_DIR", + out_path + .to_str() + .ok_or("Could not set the path for `protoc` installation.")?, + ); let install_result = protoc_prebuilt::init("25.2"); - if install_result.is_err() { - println!("Automatic installation of `protoc` failed."); - println!("Try installing `protoc` manually and adding it to PATH."); - } - let (protoc_binary_path, _) = install_result.unwrap(); - protoc_path = protoc_binary_path.parent().unwrap().to_path_buf(); - } + let (protoc_binary_path, _) = install_result.map_err(|_| { + format!( + "{}\n{}", + "Automatic installation of `protoc` failed.", + "Try installing `protoc` manually and adding it to PATH." + ) + })?; + protoc_binary_path + .parent() + .ok_or("Could not get the parent of installed `protoc` path.")? + .to_path_buf() + }; // Find the path where Dart executables are located. #[cfg(target_family = "windows")] - let pub_cache_bin_path = path::PathBuf::from(env::var("LOCALAPPDATA").unwrap()) + let pub_cache_bin_path = path::PathBuf::from(env::var("LOCALAPPDATA")?) .join("Pub") .join("Cache") .join("bin"); #[cfg(target_family = "unix")] - let pub_cache_bin_path = path::PathBuf::from(env::var("HOME").unwrap()) + let pub_cache_bin_path = path::PathBuf::from(env::var("HOME")?) .join(".pub-cache") .join("bin"); @@ -44,22 +57,23 @@ fn main() { }; path_var.push(protoc_path); path_var.push(pub_cache_bin_path); - env::set_var("PATH", env::join_paths(path_var).unwrap()); + env::set_var("PATH", env::join_paths(path_var)?); // Get command-line arguments excluding the program name. let dart_command_args: Vec = env::args().skip(1).collect(); - // Build the command to run the Dart script. - let dart_path = which::which("dart").expect("Couldn't find Dart executable"); + // Run the Dart script. + let dart_path = which::which("dart")?; let mut command = process::Command::new(dart_path); command.args(["run", "rinf"]); command.args(&dart_command_args); + command.status()?; - // Execute the command - let _ = command.status(); + Ok(()) } #[cfg(target_family = "wasm")] -fn main() { +fn main() -> Result<(), Box> { // Dummy function to make the linter happy. + Ok(()) }