diff --git a/documentation/docs/applying-template.md b/documentation/docs/applying-template.md index 15184b01..d11819cd 100644 --- a/documentation/docs/applying-template.md +++ b/documentation/docs/applying-template.md @@ -57,7 +57,9 @@ Various comments are written in the actual code to help you understand the whole If you already have a Rust crate that you want to use here, just put it inside `./native` and set it as a dependency of the `hub` crate. -Now by heading over to `./native/hub/src/lib.rs`, you can start writing Rust! +Now, by heading over to `./native/hub/src/lib.rs`, you can start writing Rust! + +Example code for guidance can be found [here](https://github.com/cunarist/rinf/tree/main/flutter_package/example). !!! info diff --git a/documentation/docs/frequently-asked-questions.md b/documentation/docs/frequently-asked-questions.md index 86e5a362..53244186 100644 --- a/documentation/docs/frequently-asked-questions.md +++ b/documentation/docs/frequently-asked-questions.md @@ -249,6 +249,7 @@ pub async fn respond() { ``` ```rust title="native/hub/src/lib.rs" +#[tokio::main] async fn main() { tokio::spawn(sample_functions::respond()); } diff --git a/documentation/docs/state-management.md b/documentation/docs/state-management.md index 2bc2422e..fce75887 100644 --- a/documentation/docs/state-management.md +++ b/documentation/docs/state-management.md @@ -8,45 +8,47 @@ Rinf performs best when the application logic is written entirely in Rust, with The actor model is highly recommended for managing asynchronous state in Rust. By encapsulating state and behavior within actor structs, which maintain ownership and handle their own async tasks, the actor model provides a scalable and modular way to manage complex state interactions. -1. **Encapsulation**: Actors encapsulate state and behavior, allowing for modular and maintainable code. -2. **Concurrency**: Each actor operates independently, making it easier to handle concurrent tasks without manual synchronization. -3. **Scalability**: Actors are well-suited for scalable systems where tasks and state management need to be handled in parallel. - -Several crates on `crates.io` provide building blocks for implementing the actor model in Rust. Although Rinf uses `tokio` by default, you can choose any async Rust runtime that fits your needs. Consider exploring these crates to find one that aligns with your requirements. - -Here’s a basic example using the [`actix`](https://github.com/actix/actix) crate, a popular choice for the actor model: +Here’s a basic example using the [`messages`](https://crates.io/crates/messages) crate, which is a flexible and runtime-agnostic actor library that works nicely with Rinf. ```rust title="native/hub/src/lib.rs" -use actix::prelude::*; +use messages::prelude::*; -rinf::write_interface!() +rinf::write_interface!(); -// this is our Message -// we have to define the response type (rtype) -#[derive(Message)] -#[rtype(usize)] +// Represents a message to calculate the sum of two numbers. struct Sum(usize, usize); -// Actor definition +// Actor definition that will hold state in real apps. struct Calculator; -impl Actor for Calculator { - type Context = Context; -} +// Implement `Actor` trait for `Calculator`. +impl Actor for Calculator {} -// now we need to implement `Handler` on `Calculator` for the `Sum` message. +// Implement `Handler` for `Calculator` to handle `Sum` messages. +#[async_trait] impl Handler for Calculator { - type Result = usize; // <- Message response type - - fn handle(&mut self, msg: Sum, _ctx: &mut Context) -> Self::Result { + type Result = usize; + async fn handle(&mut self, msg: Sum, _: &Context) -> Self::Result { msg.0 + msg.1 } } -#[actix::main] // <- starts the system and block until future resolves +// Implement the start method for `Calculator`. +impl Calculator { + pub fn start() -> Address { + let context = Context::new(); + let actor = Self {}; + let addr = context.address(); + tokio::spawn(context.run(actor)); + addr + } +} + +// Main function to start the business logic. +#[tokio::main] async fn main() { - let addr = Calculator.start(); - let res = addr.send(Sum(10, 5)).await; // <- send message and get future for result + let mut addr = Calculator::start(); + let res = addr.send(Sum(10, 5)).await; match res { Ok(result) => println!("SUM: {}", result), @@ -55,6 +57,10 @@ async fn main() { } ``` +Several crates on `crates.io` provide building blocks for implementing the actor model in Rust. Consider exploring these crates to find one that aligns with your requirements. + +Please refer to the [example code](https://github.com/cunarist/rinf/tree/main/flutter_package/example) for detailed usage. + ## 🧱 Static Variables Generally, it's advisable to avoid static variables due to their characteristics, which can lead to issues such as difficulties in testing and managing lifetimes. If you must use static variables, you can declare them as shown below, ensuring they span the entire duration of the app. diff --git a/documentation/docs/tutorial.md b/documentation/docs/tutorial.md index 646e751d..b9384706 100644 --- a/documentation/docs/tutorial.md +++ b/documentation/docs/tutorial.md @@ -83,6 +83,7 @@ pub async fn calculate_precious_data() { ```rust title="native/hub/src/lib.rs" mod tutorial_functions; +#[tokio::main] async fn main() { tokio::spawn(tutorial_functions::calculate_precious_data()); } @@ -138,6 +139,7 @@ pub async fn stream_amazing_number() { ```rust title="native/hub/src/lib.rs" mod tutorial_functions; +#[tokio::main] async fn main() { tokio::spawn(tutorial_functions::stream_amazing_number()); } @@ -226,6 +228,7 @@ pub async fn tell_treasure() { ```rust title="native/hub/src/lib.rs" mod tutorial_functions; +#[tokio::main] async fn main() { tokio::spawn(tutorial_functions::tell_treasure()); } diff --git a/flutter_package/example/native/hub/Cargo.toml b/flutter_package/example/native/hub/Cargo.toml index c9cf3a5a..d3f9b7a1 100644 --- a/flutter_package/example/native/hub/Cargo.toml +++ b/flutter_package/example/native/hub/Cargo.toml @@ -22,4 +22,6 @@ tokio_with_wasm = { version = "0.7.1", features = [ "macros", ] } wasm-bindgen = "0.2.93" +messages = "0.3.1" +anyhow = "1.0.89" sample_crate = { path = "../sample_crate" } diff --git a/flutter_package/example/native/hub/src/actors.rs b/flutter_package/example/native/hub/src/actors.rs new file mode 100644 index 00000000..f6e23cbd --- /dev/null +++ b/flutter_package/example/native/hub/src/actors.rs @@ -0,0 +1,77 @@ +//! The actor model is highly recommended for state management, +//! as it provides modularity and scalability. +//! This module demonstrates how to use actors +//! within the async system in Rust. +//! To build a solid app, do not communicate by sharing memory; +//! instead, share memory by communicating. + +use crate::common::*; +use crate::messages::*; +use messages::prelude::*; +use rinf::debug_print; + +// The letter type for communicating with an actor. +pub struct ClickedLetter; + +// The actor that holds the counter state and handles messages. +pub struct CountingActor { + // The counter number. + count: i32, +} + +// Implementing the `Actor` trait for `CountingActor`. +// This defines `CountingActor` as an actor in the async system. +impl Actor for CountingActor {} + +impl CountingActor { + pub fn new(counting_addr: Address) -> Self { + spawn(Self::listen_to_button_click(counting_addr)); + CountingActor { count: 0 } + } + + async fn listen_to_button_click(mut counting_addr: Address) { + // Spawn an asynchronous task to listen for + // button click signals from Dart. + let receiver = SampleNumberInput::get_dart_signal_receiver(); + // Continuously listen for signals. + while let Some(dart_signal) = receiver.recv().await { + let letter = dart_signal.message.letter; + debug_print!("{letter}"); + // Send a letter to the counting actor. + let _ = counting_addr.send(ClickedLetter).await; + } + } +} + +#[async_trait] +impl Handler for CountingActor { + type Result = (); + // Handles messages received by the actor. + async fn handle(&mut self, _msg: ClickedLetter, _context: &Context) { + // Increase the counter number. + let new_number = self.count + 7; + self.count = new_number; + + // The send method is generated from a marked Protobuf message. + SampleNumberOutput { + current_number: new_number, + dummy_one: 11, + dummy_two: None, + dummy_three: vec![22, 33, 44, 55], + } + .send_signal_to_dart(); + } +} + +// Creates and spawns the actors in the async system. +pub async fn create_actors() -> Result<()> { + // Create actor contexts. + let counting_context = Context::new(); + let counting_addr = counting_context.address(); + + // Spawn actors. + let actor = CountingActor::new(counting_addr); + spawn(counting_context.run(actor)); + + Ok(()) +} diff --git a/flutter_package/example/native/hub/src/common.rs b/flutter_package/example/native/hub/src/common.rs index fbe289e2..b312b827 100644 --- a/flutter_package/example/native/hub/src/common.rs +++ b/flutter_package/example/native/hub/src/common.rs @@ -1,10 +1,13 @@ -use std::error::Error; +// `tokio_with_wasm` enables `tokio` code +// to run directly on the web. +pub use tokio_with_wasm::alias as tokio; -/// This `Result` type alias allows handling any error type -/// that implements the `Error` trait. -/// In practice, it is recommended to use custom solutions -/// or crates like `anyhow` dedicated to error handling. -/// Building an app differs from writing a library, as apps -/// may encounter numerous error situations, which is why -/// a single, flexible error type is needed. -pub type Result = std::result::Result>; +/// This `Result` type alias unifies the error type. +/// Building an app differs from writing a library, +/// as app may encounter numerous error situations. +/// Therefore, a single, flexible error type is recommended. +pub type Result = anyhow::Result; + +/// Because spawn functions are used very often, +/// we make them accessible from everywhere. +pub use tokio::task::{spawn, spawn_blocking}; diff --git a/flutter_package/example/native/hub/src/lib.rs b/flutter_package/example/native/hub/src/lib.rs index 9e06fc31..0588ec64 100644 --- a/flutter_package/example/native/hub/src/lib.rs +++ b/flutter_package/example/native/hub/src/lib.rs @@ -1,24 +1,26 @@ //! This `hub` crate is the //! entry point of the Rust logic. +mod actors; mod common; mod messages; mod sample_functions; +use common::*; use tokio_with_wasm::alias as tokio; rinf::write_interface!(); -// You can go with any async runtime, not just tokio's. +// You can go with any async library, not just `tokio`. #[tokio::main(flavor = "current_thread")] async fn main() { // Spawn concurrent tasks. // Always use non-blocking async functions like `tokio::fs::File::open`. // If you must use blocking code, use `tokio::task::spawn_blocking` // or the equivalent provided by your async library. - tokio::spawn(sample_functions::tell_numbers()); - tokio::spawn(sample_functions::stream_fractal()); - tokio::spawn(sample_functions::run_debug_tests()); + spawn(sample_functions::stream_fractal()); + spawn(sample_functions::run_debug_tests()); + spawn(actors::create_actors()); // Keep the main function running until Dart shutdown. rinf::dart_shutdown().await; diff --git a/flutter_package/example/native/hub/src/sample_functions.rs b/flutter_package/example/native/hub/src/sample_functions.rs index 0b96af27..731fd57a 100644 --- a/flutter_package/example/native/hub/src/sample_functions.rs +++ b/flutter_package/example/native/hub/src/sample_functions.rs @@ -2,7 +2,6 @@ use crate::common::*; use crate::messages::*; -use crate::tokio; use rinf::debug_print; use std::time::Duration; @@ -12,34 +11,6 @@ const IS_DEBUG_MODE: bool = true; #[cfg(not(debug_assertions))] const IS_DEBUG_MODE: bool = false; -// Business logic for the counter widget. -pub async fn tell_numbers() { - let mut vector = Vec::new(); - - // Stream getter is generated from a marked Protobuf message. - let 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. - let number_input = dart_signal.message; - let letter = number_input.letter; - debug_print!("{letter}"); - - // Perform a simple calculation. - vector.push(true); - let current_number = (vector.len() as i32) * 7; - - // The send method is generated from a marked Protobuf message. - SampleNumberOutput { - current_number, - dummy_one: number_input.dummy_one, - dummy_two: number_input.dummy_two, - dummy_three: number_input.dummy_three, - } - .send_signal_to_dart(); - } -} - // Business logic for the fractal image. pub async fn stream_fractal() { let mut current_scale: f64 = 1.0; @@ -47,7 +18,7 @@ pub async fn stream_fractal() { let (sender, mut receiver) = tokio::sync::mpsc::channel(5); // Send frame join handles in order. - tokio::spawn(async move { + spawn(async move { loop { // Wait for 40 milliseconds on each frame tokio::time::sleep(Duration::from_millis(40)).await; @@ -62,7 +33,7 @@ pub async fn stream_fractal() { // Calculate the fractal image // parallelly in a separate thread pool. - let join_handle = tokio::task::spawn_blocking(move || { + let join_handle = spawn_blocking(move || { sample_crate::draw_fractal_image(current_scale) }); let _ = sender.send(join_handle).await; @@ -70,7 +41,7 @@ pub async fn stream_fractal() { }); // Receive frame join handles in order. - tokio::spawn(async move { + spawn(async move { loop { let join_handle = match receiver.recv().await { Some(inner) => inner, @@ -95,18 +66,6 @@ pub async fn stream_fractal() { }); } -// A dummy function that uses sample messages to eliminate warnings. -#[allow(dead_code)] -async fn use_messages() { - let _ = SampleInput::get_dart_signal_receiver(); - SampleOutput { - kind: 3, - oneof_input: Some(sample_output::OneofInput::Age(25)), - } - .send_signal_to_dart(); - let _ = DeeperDummy {}; -} - // Business logic for testing various crates. pub async fn run_debug_tests() -> Result<()> { if !IS_DEBUG_MODE { @@ -170,7 +129,7 @@ pub async fn run_debug_tests() -> Result<()> { let mut join_handles = Vec::new(); let chunk_size = 10_i32.pow(6); for level in 0..10 { - let join_handle = tokio::task::spawn_blocking(move || { + let join_handle = spawn_blocking(move || { let mut prime_count = 0; let count_from = level * chunk_size + 1; let count_to = (level + 1) * chunk_size; @@ -208,7 +167,7 @@ pub async fn run_debug_tests() -> Result<()> { debug_print!("Debug tests completed!"); - tokio::spawn(async { + spawn(async { // Panic in a separate task // to avoid memory leak on the web. // On the web (`wasm32-unknown-unknown`), @@ -219,3 +178,15 @@ pub async fn run_debug_tests() -> Result<()> { Ok(()) } + +// A dummy function that uses sample messages to eliminate warnings. +#[allow(dead_code)] +async fn use_messages() { + let _ = SampleInput::get_dart_signal_receiver(); + SampleOutput { + kind: 3, + oneof_input: Some(sample_output::OneofInput::Age(25)), + } + .send_signal_to_dart(); + let _ = DeeperDummy {}; +} diff --git a/flutter_package/example/native/sample_crate/src/error.rs b/flutter_package/example/native/sample_crate/src/error.rs index 94fd4ff8..6eb7dae6 100644 --- a/flutter_package/example/native/sample_crate/src/error.rs +++ b/flutter_package/example/native/sample_crate/src/error.rs @@ -2,17 +2,26 @@ use std::error::Error; use std::fmt; #[derive(Debug)] -pub struct ExampleError(pub Box); +pub enum ExampleError { + Fractal, + HardwareId, + WebApi, +} 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}") + match self { + Self::Fractal => { + write!(f, "Failed to generate fractal") + } + Self::HardwareId => { + write!(f, "Unable to retrieve hardware ID") + } + Self::WebApi => { + write!(f, "Web API call failed") + } + } } } -impl Error for ExampleError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(self.0.as_ref()) - } -} +impl Error for ExampleError {} diff --git a/flutter_package/example/native/sample_crate/src/fractal.rs b/flutter_package/example/native/sample_crate/src/fractal.rs index 12ad1423..0b52b8c2 100644 --- a/flutter_package/example/native/sample_crate/src/fractal.rs +++ b/flutter_package/example/native/sample_crate/src/fractal.rs @@ -29,7 +29,7 @@ pub fn draw_fractal_image(scale: f64) -> Result, ExampleError> { match result { Ok(_) => Ok(image_data), - Err(error) => Err(ExampleError(error.into())), + Err(_) => Err(ExampleError::Fractal), } } diff --git a/flutter_package/example/native/sample_crate/src/lib.rs b/flutter_package/example/native/sample_crate/src/lib.rs index dd1af987..ef5b3fd9 100644 --- a/flutter_package/example/native/sample_crate/src/lib.rs +++ b/flutter_package/example/native/sample_crate/src/lib.rs @@ -17,7 +17,7 @@ pub fn get_hardward_id() -> Result { .add_component(machineid_rs::HWIDComponent::CPUCores); let hwid = builder .build("mykey") - .map_err(|error| ExampleError(error.into()))?; + .map_err(|_| ExampleError::HardwareId)?; Ok(hwid) } #[cfg(not(any( @@ -39,9 +39,9 @@ pub fn get_current_time() -> DateTime { pub async fn fetch_from_web_api(url: &str) -> Result { let fetched = reqwest::get(url) .await - .map_err(|error| ExampleError(error.into()))? + .map_err(|_| ExampleError::WebApi)? .text() .await - .map_err(|error| ExampleError(error.into()))?; + .map_err(|_| ExampleError::WebApi)?; Ok(fetched) } diff --git a/flutter_package/template/native/hub/src/lib.rs b/flutter_package/template/native/hub/src/lib.rs index 8b222e34..28a75bdf 100644 --- a/flutter_package/template/native/hub/src/lib.rs +++ b/flutter_package/template/native/hub/src/lib.rs @@ -4,11 +4,12 @@ mod messages; mod sample_functions; -// use tokio_with_wasm::alias as tokio; // Uncomment this line to target the web. +// Uncomment below to target the web. +// use tokio_with_wasm::alias as tokio; rinf::write_interface!(); -// You can go with any async runtime, not just tokio's. +// You can go with any async library, not just `tokio`. #[tokio::main(flavor = "current_thread")] async fn main() { // Spawn concurrent tasks. diff --git a/flutter_package/template/native/hub/src/sample_functions.rs b/flutter_package/template/native/hub/src/sample_functions.rs index 3ec6b5fb..758a91e7 100644 --- a/flutter_package/template/native/hub/src/sample_functions.rs +++ b/flutter_package/template/native/hub/src/sample_functions.rs @@ -13,3 +13,9 @@ pub async fn communicate() { rinf::debug_print!("{message:?}"); } } + +// Though async tasks work, using the actor model +// is highly recommended for state management +// to achieve modularity and scalability in your app. +// To understand how to use the actor model, +// refer to the Rinf documentation. diff --git a/rust_crate/src/error.rs b/rust_crate/src/error.rs index 489c8f86..14754371 100644 --- a/rust_crate/src/error.rs +++ b/rust_crate/src/error.rs @@ -11,14 +11,14 @@ pub enum RinfError { impl fmt::Display for RinfError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - RinfError::NoDartIsolate => { - write!(f, "Dart isolate for Rust signals was not created.") + Self::NoDartIsolate => { + write!(f, "Dart isolate for Rust signals was not created") } - RinfError::CannotDecodeMessage => { - write!(f, "Could not decode the message.") + Self::CannotDecodeMessage => { + write!(f, "Could not decode the message") } - RinfError::NoSignalHandler => { - write!(f, "Could not find the handler for Dart signal.") + Self::NoSignalHandler => { + write!(f, "Could not find the handler for Dart signal") } } }