Rinf: Rust in Flutter

repository·main·Indexed 25 days ago

https://github.com/cunarist/rinf

A framework for creating performant cross-platform applications using Rust for native business logic and Flutter for the UI layer. Rinf provides a type-safe, event-driven communication bridge between Dart and Rust via native FFI, supporting Linux, Android, Windows, macOS, iOS, Web, and experimental eLinux. It includes a CLI for scaffolding templates, generating Dart classes from Rust signals, and managing project configuration.

Tokens
17.9K
Snippets
54
Records
107
Agent score
81%

What's inside Rinf

  1. Overview of Rinf: Rust in Flutter

    main
    Rinf is a framework designed to combine Rust's high-performance native business logic with Flutter's flexible and beautiful GUI. It allows developers to write Rust code for heavy computations or complex logic and interact with it from a Flutter application using a type-safe, event-driven communication layer. All communication occurs through native FFI, ensuring high efficiency without the overhead of webviews or web servers.
  2. Understand the Rinf architecture and design principles

    main

    Rinf is designed for building cross-platform applications by combining Rust for business logic and Flutter for the UI layer.

    Key architectural principles:

    • Separation of Concerns: All business logic should be written in Rust. If your business logic is primarily in Dart, Rinf may not be the appropriate solution.
    • Communication Mechanism: Instead of direct function calls, Rinf uses a two-way, stream-based message-passing mechanism. This decouples the Rust business logic from the Flutter UI.
    • State Management Pattern:
      • Rust side: It is recommended to use the actor model. Actors should hold the application state.
      • Flutter side: Use tree-based state management (such as InheritedWidget or provider) to propagate view data. Flutter widgets should only receive the specific view data required to update the UI.
  3. Build and upload precompiled binaries via GitHub Actions

    main

    You can automate the precompilation and uploading of binaries using a GitHub Action. The action requires two secrets: RELEASE_PRIVATE_KEY (for signing) and RELEASE_GITHUB_TOKEN (for uploading to releases).

    Use the dart run build_tool precompile-binaries command.

    Key Arguments:

    • --manifest-dir: Path to the Rust manifest directory.
    • --repository: The repository identifier.
    • --target <rust-triple>: (Optional) Override to build for a specific target.
    • --android-sdk-location: Required for Android builds.
    • --android-ndk-version: Required for Android builds.
    • --android-min-sdk-version: Required for Android builds.
    on:
      push:
        branches: [main]
    
    name: Precompile Binaries
    
    jobs:
      Precompile:
        runs-on: ${{ matrix.os }}
        strategy:
          fail-fast: false
          matrix:
            os:
              - ubuntu-latest
              - macOS-latest
              - windows-latest
        steps:
          - uses: actions/checkout@v2
          - uses: dart-lang/setup-dart@v1
          - name: Install GTK
            if: (matrix.os == 'ubuntu-latest')
            run: sudo apt-get update && sudo apt-get install libgtk-3-dev
          - name: Precompile
            if: (matrix.os == 'macOS-latest') || (matrix.os == 'windows-latest')
            run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions
            working-directory: super_native_extensions/cargokit/build_tool
            env:
              GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
              PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }}
          - name: Precompile (with Android)
            if: (matrix.os == 'ubuntu-latest')
            run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions --android-sdk-location=/usr/local/lib/android/sdk --android-ndk-version=24.0.8215888 --android-min-sdk-version=23
            working-directory: super_native_extensions/cargokit/build_tool
            env:
              GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
              PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }}
  4. Avoid panicking in Rust code

    main

    To ensure predictable application behavior and prevent hangs on the web platform (wasm32-unknown-unknown), do not use panicking code like .unwrap() or .expect(). Instead, use the idiomatic Result<T, E> type to handle errors gracefully. Rinf expects business logic to be handled in Rust, and panics cannot be caught on the web, which may cause Flutter callers to wait indefinitely.

    fn good() -> Result<(), SomeError> {
      let option = get_option();
      let value_a = option.ok_or(SomeError)?;
      let result = get_result();
      let value_b = result?;
      Ok(())
    }
  5. Migrate from Rinf 6 to 7

    main

    Upgrading from version 6 to 7 involves several API and workflow changes:

    1. Rust Main Function: Explicitly bind the main function with an async runtime (e.g., tokio) and await the rinf::dart_shutdown() future.
    2. Protobuf Annotations: Remove the RINF: prefix from Protobuf message annotations. Change [RINF:DART-SIGNAL] to [DART-SIGNAL].
    3. Module Imports:
      • In Dart, import messages from the root generated module instead of inner modules.
      • In Rust, import messages from the root generated module.
    4. Web Server: Use the rinf server CLI command to run the Flutter web server with the required arguments.
    // Rust main function requirement
    [tokio::main]
    async fn main() {
      // ...
      rinf::dart_shutdown().await;
    }
    // Protobuf annotation change
    // [DART-SIGNAL]
    message SomeMessage {}
    // Dart import change
    import 'generated.dart';
    # Run Flutter web server
    rinf server
  6. Run and build for the web

    main

    Web support requires a manual WebAssembly (Wasm) build step from Rust before running or building the Flutter app. Rinf uses wasm-bindgen and wasm-pack with the web target internally.

    Development (Serving the app)

    To serve the web application, use rinf wasm to generate the necessary Wasm modules, then run Flutter with specific cross-origin headers. Rinf provides a helper command rinf server that prints the full command required for development.

    Production (Building the app)

    To build an optimized release version, use rinf wasm --release followed by the Flutter build command.

    Deployment Requirements

    When deploying to a web server, you MUST configure the server to include the following HTTP headers to enable SharedArrayBuffer support:

    • cross-origin-opener-policy: same-origin
    • cross-origin-embedder-policy: require-corp

    Additionally, ensure your server is configured to serve .wasm files with the application/wasm MIME type.

    # 1. Build Wasm and run for development
    rinf wasm
    flutter run --web-header=cross-origin-opener-policy=same-origin --web-header=cross-origin-embedder-policy=require-corp
    
    # 2. Build optimized Wasm and build for production
    rinf wasm --release
    flutter build web
  7. Send signals from Rust to Dart

    main

    To stream data or notify Dart from Rust, follow these steps:

    1. Define the Signal in Rust: Create a struct in your Rust crate and annotate it with #[derive(Serialize, RustSignal)].
    2. Generate Dart Code: Run the Rinf CLI to generate the corresponding Dart classes.
      rinf gen
    3. Send the Signal in Rust: Use the generated .send_signal_to_dart() method on your signal instance.
    4. Receive the Signal in Dart: Use the generated .rustSignalStream property within a StreamBuilder widget to react to incoming signals.
    // 1. Define in Rust
    #[derive(Serialize, RustSignal)]
    pub struct MyAmazingNumber {
      pub current_number: i32,
    }
    
    // 3. Send in Rust
    MyAmazingNumber { current_number }.send_signal_to_dart();
    // 4. Receive in Dart
    StreamBuilder(
      stream: MyAmazingNumber.rustSignalStream,
      builder: (context, snapshot) {
        final signalPack = snapshot.data;
        if (signalPack == null) return Text('Nothing received yet');
        final myAmazingNumber = signalPack.message;
        return Text(myAmazingNumber.currentNumber.toString());
      },
    )
  8. Configure Rust-analyzer for WebAssembly (Wasm) linting

    main

    By default, Rust-analyzer runs in native mode. To enable type checking and linting for the web target (wasm32-unknown-unknown), create a .cargo/config.toml file and set the target. You must restart the Rust language server for changes to take effect.

    # .cargo/config.toml
    [build]
    # Uncomment the line below to switch Rust-analyzer to perform
    # type checking and linting in webassembly mode, for the web target.
    # You might have to restart Rust-analyzer for this change to take effect.
    target = "wasm32-unknown-unknown"
  9. Test pure Rust business logic

    main

    To test Rust code that does not involve Dart communication, use standard Rust testing patterns. You can use #[tokio::test] for asynchronous functions and run the tests using the Cargo CLI.

    #[tokio::test]
    async fn my_async_test() {
      let result = async_function().await;
      assert_eq!(result, 42);
    }
    
    async fn async_function() -> i32 {
      42
    }
    cargo test