fzyzcjy / flutter_rust_bridge

Flutter/Dart <-> Rust binding generator, feature-rich, but seamless and simple.
https://fzyzcjy.github.io/flutter_rust_bridge/
MIT License
4.1k stars 280 forks source link

How to manage local state in Rust? #252

Closed AlienKevin closed 2 years ago

AlienKevin commented 2 years ago

Discussed in https://github.com/fzyzcjy/flutter_rust_bridge/discussions/251

Originally posted by **AlienKevin** December 28, 2021 ## Question I have a large and complex local state that is only needed in Rust and is costly to send back and forth. What's the best way to manage the local state in Rust? ## Attempt My current attempt is as follows: In `api.rs`, I have a struct called `Api` that contains most of my local state: ``` struct Api { pub dict: RichDict, pub release_time: DateTime, } ``` I wrapped the two states in a struct so I can use Serde to serialize and deserialize. Then, I implemented some associated functions to initialize and update the `Api` struct, along with some methods to retrieve information from the struct: ``` impl Api { pub fn new(app_dir: String) -> Self { // Load/create the Cantonese dictionary and set the release_time } pub fn pr_search(&self, capacity: u32, query: &str) -> Vec { // search for a pronunciation in dictionary definitions } fn get_new_dict>(api_path: &P) -> Api { // Download the latest dictionary from server } } ``` Next, I create a thread_local `RefCell` to store the `Api` struct for use throughout the app session in Rust: ``` thread_local!(static API: RefCell> = RefCell::new(None)); ``` I then create a `init_api` function that can be called from Dart to initialize the `Api` struct in Rust: ``` pub fn init_api(input_app_dir: String) -> Result<()> { API.with(|api_cell| { *api_cell.borrow_mut() = Some(Api::new(input_app_dir)); }); Ok(()) } ``` Lastly, I implement a public function called `pr_search` to query the local `Api` struct: ``` pub fn pr_search(capacity: u32, query: String) -> Result> { API.with(|api_cell| Ok(api_cell.borrow().as_ref().unwrap().pr_search(capacity, &query)) ) } ``` ## Issues encountered 1. Receive ` FfiException(PANIC_ERROR, Once instance has previously been poisoned, null)` after **hot restart**. Presumably the `RefCell` is declared/used twice?? 2. `println!()` does not work inside associated functions and methods of the `Api` struct, so it's hard for me to debug them
AlienKevin commented 2 years ago

Solutions

Thanks to @fzyzcjy for kindly pointing out the solutions to the two issues I encountered.

Solution to Once instance poisoning

Replace thread_local RefCell with Mutex prevents Once poisoning. You can use either the Rust standard library's std::sync::Mutex or Mutex provided by the parking_lot crate. Parking_lot claims that their mutexes have no poisoning and uses less memory. I used the parking_lot's mutex in conjunction with flutter_rust_bridge and everything is fine so far. If you are not familiar with Mutex, check out the Shared State Concurrency section of the Rust Book.

To manage local Rust states with Mutex, you need to use it in conjunction with the lazy_static library.

  1. Declare Api state
    lazy_static! {
    static ref API: Mutex<Option<Api>> = Mutex::new(None);
    }
  2. Create a init_api function that can be called from Dart to initialize the Api struct in Rust:
    pub fn init_api(input_app_dir: String) -> Result<()> {
    *API.lock() = Some(Api::new(input_app_dir));
    Ok(())
    }
  3. Implement a public function called pr_search to query the local Api struct:
    pub fn pr_search(capacity: u32, query: String) -> Result<Vec<PrSearchResult>> {
    Ok((*API.lock()).as_ref().unwrap().pr_search(capacity, &query))
    }

Solution to println!() not working

This solution targets iOS logging. Check out the android_logger crate if you want to enable logging on Android.

You need external rust bindings to Apple's universal logging system for iOS logging to work. The only functioning library that I can find is oslog, which implements the logger interface set out by the log crate. Follow the steps below to set up iOS logging:

  1. Import logging libraries
    use oslog::{OsLogger};
    use log::{LevelFilter, info};
  2. Declare a boolean flag to keep track of whether the logger is initialized. This prevents the log crate from throwing Logger already set error when you Hot Restart the app and the init_api function got called again in Dart.
    static ref IS_LOG_INITIALIZED: Mutex<bool> = Mutex::new(false);
  3. Initialize logging in Api::new()
    if !*IS_LOG_INITIALIZED.lock() {
      OsLogger::new("com.example")
           .level_filter(LevelFilter::Debug)
           .category_level_filter("Settings", LevelFilter::Trace)
           .init()
           .unwrap();
      *IS_LOG_INITIALIZED.lock() = true;
    }
  4. Call info!() to print debug messages
    info!("Your debug message goes here");
fzyzcjy commented 2 years ago

🎉 🎉 🎉

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new issue.