Skip to content

Commit

Permalink
Merge pull request #456 from cunarist/improve-state-management-docs
Browse files Browse the repository at this point in the history
Improve state management docs
  • Loading branch information
temeddix authored Sep 29, 2024
2 parents 6e799af + a9ff342 commit 972a8d7
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 104 deletions.
4 changes: 3 additions & 1 deletion documentation/docs/applying-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions documentation/docs/frequently-asked-questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
54 changes: 30 additions & 24 deletions documentation/docs/state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>;
}
// 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<Sum> for Calculator {
type Result = usize; // <- Message response type

fn handle(&mut self, msg: Sum, _ctx: &mut Context<Self>) -> Self::Result {
type Result = usize;
async fn handle(&mut self, msg: Sum, _: &Context<Self>) -> 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<Self> {
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),
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions documentation/docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}
Expand Down
2 changes: 2 additions & 0 deletions flutter_package/example/native/hub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
77 changes: 77 additions & 0 deletions flutter_package/example/native/hub/src/actors.rs
Original file line number Diff line number Diff line change
@@ -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>) -> Self {
spawn(Self::listen_to_button_click(counting_addr));
CountingActor { count: 0 }
}

async fn listen_to_button_click(mut counting_addr: Address<Self>) {
// 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<ClickedLetter> for CountingActor {
type Result = ();
// Handles messages received by the actor.
async fn handle(&mut self, _msg: ClickedLetter, _context: &Context<Self>) {
// 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(())
}
21 changes: 12 additions & 9 deletions flutter_package/example/native/hub/src/common.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
/// 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<T> = anyhow::Result<T>;

/// Because spawn functions are used very often,
/// we make them accessible from everywhere.
pub use tokio::task::{spawn, spawn_blocking};
10 changes: 6 additions & 4 deletions flutter_package/example/native/hub/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
63 changes: 17 additions & 46 deletions flutter_package/example/native/hub/src/sample_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use crate::common::*;
use crate::messages::*;
use crate::tokio;
use rinf::debug_print;
use std::time::Duration;

Expand All @@ -12,42 +11,14 @@ 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;

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;
Expand All @@ -62,15 +33,15 @@ 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;
}
});

// Receive frame join handles in order.
tokio::spawn(async move {
spawn(async move {
loop {
let join_handle = match receiver.recv().await {
Some(inner) => inner,
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`),
Expand All @@ -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 {};
}
Loading

0 comments on commit 972a8d7

Please sign in to comment.