TauRPC

repository·main·Indexed 43 days ago

https://github.com/matsdk/taurpc

A Tauri extension providing a fully-typed, bidirectional IPC layer between a Rust backend and a TypeScript frontend. It utilizes Specta for runtime type generation, enabling type-safe Rust procedure calls, events, and Tauri Channels in the frontend framework.

Tokens
8.3K
Snippets
27
Records
31
Agent score
56%

What's inside TauRPC

  1. Use Routing to organize commands and events

    main

    To avoid cluttering a single trait, use the taurpc::Router to create nested command and event structures.

    1. Define a root trait (without a path).
    2. Define nested traits using #[taurpc::procedures(path = "...")]. Use #[taurpc(event)] on methods to mark them as events.
    3. Create a taurpc::Router and use .merge(handler) to combine multiple handlers.
    4. Use router.into_handler() in your Tauri invoke_handler.

    On the frontend, nested paths are accessed via the proxy object (e.g., taurpc.path.method()). Events are accessed via .on() (e.g., taurpc.path.method.on(...)).

    // Root procedures
    #[taurpc::procedures]
    trait Api {
        async fn hello_world();
    }
    
    #[derive(Clone)]
    struct ApiImpl;
    
    #[taurpc::resolvers]
    impl Api for ApiImpl {
        async fn hello_world(self) {
            println!("Hello world");
        }
    }
    
    // Nested procedures
    #[taurpc::procedures(path = "events")]
    trait Events {
        #[taurpc(event)]
        async fn event();
    }
    
    #[derive(Clone)]
    struct EventsImpl;
    
    #[taurpc::resolvers]
    impl Events for EventsImpl {}
    
    #[tokio::main]
    async fn main() {
        let router = taurpc::Router::new()
            .merge(ApiImpl.into_handler())
            .merge(EventsImpl.into_handler());
    
        #[cfg(debug_assertions)]
        taurpc::Exporter::new()
            .export(&router, "../src/bindings.ts")
            .unwrap();
    
        tauri::Builder::default()
            .invoke_handler(router.into_handler())
            .run(tauri::generate_context())
            .expect("error while running tauri application");
    }
  2. Define sync and async methods

    main

    Methods in a #[taurpc::procedures] trait are async by default. However, you can declare methods without async to create sync methods.

    • Async methods: Spawn on Tauri's async runtime. Use these for I/O or long-running tasks.
    • Sync methods: Respond inline on the thread that receives the request. These are suitable for quick, CPU-only procedures.

    Note: On the TypeScript frontend, both sync and async methods return a Promise.

    #[taurpc::procedures]
    trait Api {
        fn sync_method() -> String;
        async fn async_method() -> String;
    }
    
    #[derive(Clone)]
    struct ApiImpl;
    
    #[taurpc::resolvers]
    impl Api for ApiImpl {
        fn sync_method(self) -> String {
            "sync response".to_string()
        }
    
        async fn async_method(self) -> String {
            "async response".to_string()
        }
    }
  3. Quickstart: Set up TauRPC in a Tauri project

    main

    TauRPC provides a fully-typed IPC layer for Tauri commands and events. To get started, follow these steps:

    1. Add dependencies to your src-tauri/Cargo.toml:

      • taurpc
      • specta (with derive feature)
      • tokio (with full feature)
    2. Define and implement your API in Rust using the #[taurpc::procedures] trait and #[taurpc::resolvers] implementation.

    3. Export TypeScript bindings using taurpc::Exporter. It is recommended to wrap this in #[cfg(debug_assertions)] so bindings are generated during development.

    4. Register the handler in your Tauri builder using taurpc::create_ipc_handler(api_handler).

    5. Install the frontend package via pnpm install taurpc.

    6. Use the proxy in your TypeScript code by importing createTauRPCProxy from your generated bindings file.

    // src-tauri/src/main.rs
    
    #[taurpc::procedures]
    trait Api {
        async fn hello_world();
    }
    
    #[derive(Clone)]
    struct ApiImpl;
    
    #[taurpc::resolvers]
    impl Api for ApiImpl {
        async fn hello_world(self) {
            println!("Hello world");
        }
    }
    
    #[tokio::main]
    async fn main() {
        let api_handler = ApiImpl.into_handler();
    
        #[cfg(debug_assertions)]
        taurpc::Exporter::new()
            .export(&api_handler, "../src/bindings.ts")
            .unwrap();
    
        tauri::Builder::default()
            .invoke_handler(taurpc::create_ipc_handler(api_handler))
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }
  4. Access managed state in TauRPC procedures

    main

    You can share state between procedures by adding fields to your API implementation struct. If the state needs to be mutable, use a thread-safe container like Arc<Mutex<T>>.

    TauRPC procedures can also accept Tauri-specific arguments like window, app_handle, and webview_window directly in the method signature, just like standard Tauri commands.

    use std::sync::Arc;
    use tokio::sync::Mutex;
    use tauri::{Manager, Runtime, State, Window};
    
    type MyState = Arc<Mutex<String>>;
    
    #[taurpc::procedures]
    trait Api {
        async fn with_state();
        async fn with_window<R: Runtime>(window: Window<R>);
    }
    
    #[derive(Clone)]
    struct ApiImpl {
        state: MyState
    };
    
    #[taurpc::resolvers]
    impl Api for ApiImpl {
        async fn with_state(self) {
            // Access state via self.state
        }
    
        async fn with_window<R: Runtime>(self, window: Window<R>) {
            // Use the window object
        }
    }
    
    #[tokio::main]
    async fn main() {
        tauri::Builder::default()
            .invoke_handler(taurpc::create_ipc_handler(
                ApiImpl {
                    state: Arc::new(Mutex::new("state".to_string())),
                }
                .into_handler(),
            ))
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }
  5. Recommended IDE Setup for Tauri development

    main

    To ensure the best development experience (including autocomplete, type checking, and Rust integration), use VS Code with the following extensions installed:

    • Svelte: For Svelte component support.
    • Tauri: For Tauri-specific tooling.
    • rust-analyzer: For Rust language support and intelligence.
  6. Use Tauri Channels for typed callbacks

    main

    TauRPC supports Tauri Channels, allowing you to pass typed callback functions from your TypeScript frontend to your Rust backend.

    1. Define the data structure for the channel using #[taurpc::ipc_type].
    2. In your procedure trait, accept a Channel<T> parameter.
    3. In the resolver, use on_event.send(data) to push updates to the frontend.
    4. On the frontend, pass an async callback function to the command.
    // Rust: Define the type and the procedure
    #[taurpc::ipc_type]
    struct Update {
        progress: u8,
    }
    
    #[taurpc::procedures]
    trait Api {
        async fn update(on_event: Channel<Update>);
    }
    
    #[taurpc::resolvers]
    impl Api for ApiImpl {
        async fn update(self, on_event: Channel<Update>) {
            for progress in [15, 20, 35, 50, 90] {
                on_event.send(Update { progress }).unwrap();
            }
        }
    }
    // TypeScript: Call the command with a callback
    let taurpc = createTauRPCProxy();
    await taurpc.update((update) => {
      console.log(update.progress);
    });
  7. Setup a Tauri + Svelte + TypeScript project

    main
    This project provides a template for developing applications using Tauri, Svelte, and TypeScript within a Vite environment. It is designed to streamline the integration of a Rust-based backend with a Svelte frontend using TypeScript for type safety.
  8. Trigger typed events on the TypeScript frontend

    main

    You can trigger events from your Rust backend to your TypeScript frontend with full type safety. The #[taurpc::procedures] macro automatically generates an event trigger struct.

    By default, the trigger struct is named TauRpc{TraitName}EventTrigger. You can customize this name using the event_trigger attribute on the trait.

    To define an event, add the #[taurpc(event)] attribute to the method in your trait. Methods marked with #[taurpc(event)] do not require a corresponding resolver implementation.

    On the frontend, use the .on() method provided by the generated proxy to listen for these events. The .on() method returns an unlisten function that should be called during component cleanup (e.g., useEffect cleanup in React or onDestroy in Svelte).

    // 1. Define the API and custom trigger name
    #[taurpc::procedures(event_trigger = ApiEventTrigger)]
    trait Api {
        #[taurpc(event)]
        async fn hello_world();
    }
    
    #[derive(Clone)]
    struct ApiImpl;
    
    #[taurpc::resolvers]
    impl Api for ApiImpl {}
    
    #[tokio::main]
    async fn main() {
        tauri::Builder::default()
            .invoke_handler(taurpc::create_ipc_handler(ApiImpl.into_handler()))
            .setup(|app| {
                // 2. Use the trigger in the setup block
                let trigger = ApiEventTrigger::new(app.handle());
                trigger.hello_world()?;
                Ok(())
            })
            .run(tauri::generate_context!())
            .expect("error while running tauri application");
    }
    // 3. Listen on the frontend
    const unlisten = await taurpc.hello_world.on(() => {
      console.log('Hello World!')
    })
    
    // Cleanup
    unlisten()
  9. Configure TypeScript export settings

    main

    You can customize the TypeScript output by passing a specta_typescript::Typescript configuration to the taurpc::Exporter. This allows you to set custom headers or other specta-typescript specific options. Ensure you have the latest specta_typescript installed.

    #[cfg(debug_assertions)]
    taurpc::Exporter::new()
        .ts_config(
            specta_typescript::Typescript::default()
                .header("// My custom header")
        )
        .export(&router, "../src/bindings.ts")
        .unwrap();
  10. Configure custom error handling

    main

    By default, TauRPC procedures returning Result<T, E> will cause the TypeScript Promise to reject on error. To enable type-safe error handling on the frontend, configure the Exporter with ErrorHandlingMode::Result.

    In this mode, procedures return a union type: Promise<{ status: 'ok'; data: T } | { status: 'error'; error: E }>

    You can also provide a custom typed_error_impl string to define how errors are processed at runtime.

    #[cfg(debug_assertions)]
    taurpc::Exporter::new()
        .error_handling(taurpc::ErrorHandlingMode::Result)
        .export(&router, "../src/bindings.ts")
        .unwrap();
  11. Define IPC methods using the `#[taurpc::procedures]` macro

    main

    To define a set of IPC methods for Tauri, use the #[taurpc::procedures] attribute on a Rust trait. This macro parses the trait and its methods to generate the necessary handler logic.

    Method Rules and Constraints

    • Named Arguments Only: All arguments in the trait methods must be named (e.g., fn my_method(arg: Type)). Unnamed or pattern-based arguments are not supported and will cause a compilation error.
    • Async vs Sync:
      • async fn methods resolve on Tauri's async runtime.
      • Standard fn methods respond inline (synchronous).
    • Reserved Method Names: You cannot name your trait methods after the following reserved names, as they conflict with generated functions:
      • into_handler
      • setup
      • send_to (used for sending scoped events)
    • Skipping Methods: You can use the #[taurpc(skip)] attribute on a method to prevent it from being exported as an IPC method.
    #[taurpc::procedures]
    trait MyIpcMethods {
        // An async method (resolves on Tauri async runtime)
        async fn get_data(id: u32) -> String;
    
        // A sync method (responds inline)
        fn ping() -> bool;
    
        // A method that will be skipped by the macro
        #[taurpc(skip)]
        fn internal_helper();
    }