제가 볼려고 간단하게 정리한 글입니다.
자세한 내용은 아래 공식 메뉴얼을 확인해주세요!


참고자료




프론트엔드에서 Rust 호출

src-tauri/src/lib.rs 파일에서 명령어를 정의할 수 있습니다.

#[tauri::command]
fn my_custom_command() {
  println!("I was invoked from JavaScript!");
}
  • 이때 #[tauri::command] 애노테이션을 사용해야 합니다.


#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![my_custom_command]) // <- 추가
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
  • src-tauri/src/lib.rs 파일을 열어서 코드를 수정해야 합니다.
  • invoke_handler 메소드를 사용해줍니다.


	.invoke_handler(tauri::generate_handler![cmd_a, cmd_b])
  • 명령어 여러 개 넣을 수도 있습니다.


// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/core';

// When using the Tauri global script (if not using the npm package)
// Be sure to set `app.withGlobalTauri` in `tauri.conf.json` to true
const invoke = window.__TAURI__.core.invoke;

// Invoke the command
invoke('my_custom_command');
  • 마지막으로 자바 스크립트에서 호출해서 사용하면 됩니다.


분리된 모듈에서 정의

lib.rs 파일이 아닌 분리된 모듈에서 정의하려면 다른 방법을 써야합니다.

#[tauri::command]
pub fn my_custom_command() {
  println!("I was invoked from JavaScript!");
}
  • src-tauri/src/commands.rs 이런 파일에 분리시켜서 사용할 때 pub 키워드를 앞에 붙입니다.


mod commands;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![commands::my_custom_command]) // <- 추가
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
  • 그 다음에 invoke_handler 메소드를 통해 핸들러를 달아줍니다.
  • 이때 crate::command 이런 식으로 적어줘야 합니다.


인자 전달

#[tauri::command]
fn my_custom_command(invoke_message: String) {
  println!("I was invoked from JavaScript, with this message: {}", invoke_message);
}


invoke('my_custom_command', { invokeMessage: 'Hello!' });
  • 인자는 camelCase 형태로 적어야 합니다.


데이터 반환

#[tauri::command]
fn my_custom_command() -> String {
  "Hello from Rust!".into()
}


invoke('my_custom_command').then((message) => console.log(message));
  • invoke 함수는 promise를 리턴합니다. 그래서 위처럼 풀어주는 과정이 필요합니다.


use tauri::ipc::Response;
#[tauri::command]
fn read_file() -> Response {
  let data = std::fs::read("/path/to/file").unwrap();
  tauri::ipc::Response::new(data)
}
  • array buffers 데이터를 반환하는 방법입니다.
  • 이 예제에서 tauri::ipc::Response를 사용하는데 최적화된 방법이라고 합니다.


오류 처리

#[tauri::command]
fn login(user: String, password: String) -> Result<String, String> {
  if user == "tauri" && password == "tauri" {
    // resolve
    Ok("logged_in".to_string())
  } else {
    // reject
    Err("invalid credentials".to_string())
  }
}
  • Result 형태로 반환되게 만듭니다.
invoke('login', { user: 'tauri', password: '0j4rijw8=' })
  .then((message) => console.log(message))
  .catch((error) => console.error(error));
  • 자바스크립트에서 catch를 이용해서 오류를 감지하게끔 만듭니다.


오류 처리 (심화)

위에서는 그냥 String을 받아서 출력시켰습니다. 그런데 이렇게하면 에러 종류를 구분하기가 어려워집니다. 이를 위해서 error 인터페이스를 정의하는게 좋습니다.

#[derive(Debug, thiserror::Error)]
enum Error {
  #[error(transparent)]
  Io(#[from] std::io::Error),
  #[error("failed to parse as string: {0}")]
  Utf8(#[from] std::str::Utf8Error),
}

#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
enum ErrorKind {
  Io(String),
  Utf8(String),
}

impl serde::Serialize for Error {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: serde::ser::Serializer,
  {
    let error_message = self.to_string();
    let error_kind = match self {
      Self::Io(_) => ErrorKind::Io(error_message),
      Self::Utf8(_) => ErrorKind::Utf8(error_message),
    };
    error_kind.serialize(serializer)
  }
}

#[tauri::command]
fn read() -> Result<Vec<u8>, Error> {
  let data = std::fs::read("/path/to/file")?;
  Ok(data)
}
  • 왜 이렇게 코드가 길어지냐... 하면
    • 일단 Rust에서 프론트엔드로 전달될 때 JSON 형태로 직렬화가 됩니다.
    • 이때 serde::Serialize 규칙이 적용됩니다.
type ErrorKind = {
  kind: 'io' | 'utf8';
  message: string;
};

invoke('read').catch((e: ErrorKind) => {});
  • kind를 통해서 파일 접근 오류인지, 인코딩 에러인지 알 수 있습니다.


비동기 처리

UI의 멈춤이나 느려짐을 막기위해 무거운 작업을 비동기로 돌리는 경우가 많습니다.

// Declare the async function using String instead of &str, as &str is borrowed and thus unsupported
#[tauri::command]
async fn my_custom_command(value: String) -> String {
  // Call another async function and wait for it to finish
  some_async_function().await;
  value
}
  • async 키워드를 이용하면 됩니다.
  • 여기서 조심해야할 점은 &str이 아닌 String을 쓰는 것입니다.
    • 정확히는 borrowed 타입은 비동기에서 지원하지 않기에 owned 타입으로 바꾸는 과정이 필요합니다.
    • 여기에서는 &str -> String 처럼 빌린 타입을 소유 타입으로 바꿉니다.
// Return a Result<String, ()> to bypass the borrowing issue
#[tauri::command]
async fn my_custom_command(value: &str) -> Result<String, ()> {
  // Call another async function and wait for it to finish
  some_async_function().await;
  // Note that the return value must be wrapped in `Ok()` now.
  Ok(format!(value))
}
  • 아니면 Result로 감싸는 방법이 있습니다.
  • 이 방법보단 그냥 owned 타입으로 바꾸는 방법을 많이 쓰는 것 같습니다.
invoke('my_custom_command', { value: 'Hello, Async!' }).then(() =>
  console.log('Completed!')
);
  • 자바스크립트에서는 promise 형태로 반환되므로 다른 명령과 동일하게 작동됩니다.


채널

use tokio::io::AsyncReadExt;

#[tauri::command]
async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) {
  // for simplicity this example does not include error handling
  let mut file = tokio::fs::File::open(path).await.unwrap();

  let mut chunk = vec![0; 4096];

  loop {
    let len = file.read(&mut chunk).await.unwrap();
    if len == 0 {
      // Length of zero means end of file.
      break;
    }
    reader.send(&chunk).unwrap();
  }
}
  • 데이터를 스트리밍할 때 채널을 이용합니다.
  • 이 예제에서는 4096 바이트 단위로 데이터를 전달합니다.


WebviewWindow 접근

#[tauri::command]
async fn my_custom_command(webview_window: tauri::WebviewWindow) {
  println!("WebviewWindow: {}", webview_window.label());
}


AppHandle 접근

#[tauri::command]
async fn my_custom_command(app_handle: tauri::AppHandle) {
  let app_dir = app_handle.path().app_dir();
  use tauri::GlobalShortcutManager;
  app_handle.global_shortcut_manager().register("CTRL + U", move || {});
}


이벤트 시스템

Tauri는 Rust와 프론트엔드 간의 양방향 통신에 사용할 수 있는 이벤트 시스템을 제공합니다. (이 파트의 설명은 간단하게만 적어놔서 공식 메뉴얼을 보는 걸 추천드립니다.)


커맨드 (invoke)이벤트 (emit, listen)
방향vue -> Rust양방향
타입 안정성OX (JSON payload만)
반환값promiseX
비유함수 호출broadcast


Vue -> Rust

  1. global 시스템 (등록된 모든 리스너에게 전달)
    • import { emit } from '@tauri-apps/api/event';
      import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
      // emit(eventName, payload)
      emit('file-selected', '/path/to/file');
      const appWebview = getCurrentWebviewWindow();
      appWebview.emit('route-changed', { url: window.location.href });
      
  2. Webview 시스템 (특정 창에만 전달)
    • import { emitTo } from '@tauri-apps/api/event';
      import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
      // emitTo(webviewLabel, eventName, payload)
      emitTo('settings', 'settings-update-requested', {
          key: 'notification',
          value: 'all',
      });
      const appWebview = getCurrentWebviewWindow();
      appWebview.emitTo('editor', 'file-changed', {
          path: '/path/to/file',
          contents: 'file contents',
      });
      


Rust -> Vue

  1. Global Events
    • use tauri::{AppHandle, Emitter};
      #[tauri::command]
      fn download(app: AppHandle, url: String) {
          app.emit("download-started", &url).unwrap();
          for progress in [1, 15, 50, 80, 100] {
        	  app.emit("download-progress", progress).unwrap();
          }
          app.emit("download-finished", &url).unwrap();
      }
      
  2. Webview Events
    • use tauri::{AppHandle, Emitter};
      #[tauri::command]
      fn login(app: AppHandle, user: String, password: String) {
          let authenticated = user == "tauri-apps" && password == "tauri";
          let result = if authenticated { "loggedIn" } else { "invalidCredentials" };
          app.emit_to("login", "login-result", result).unwrap();
      }
      
      • Emitter#emit_to를 이용해서 특정 창에 전달 가능합니다.
    • use tauri::{AppHandle, Emitter, EventTarget};
      #[tauri::command]
      fn open_file(app: AppHandle, path: std::path::PathBuf) {
          app.emit_filter("open-file", path, |target| match target {
        	  EventTarget::WebviewWindow { label } => label == "main" || label == "file-viewer",
        	  _ => false,
          }).unwrap();
      }
      
      • 또한 Emitter#emit_filter를 이용해서 창 목록에 전달할 수 있습니다.


Listening to Events

  1. 프론트엔드에서
    • // Listening to global events
      import { listen } from '@tauri-apps/api/event';
      
      type DownloadStarted = {
          url: string;
          downloadId: number;
          contentLength: number;
      };
      listen<DownloadStarted>('download-started', (event) => {
          console.log(
        	  `downloading ${event.payload.contentLength} bytes from ${event.payload.url}`
          );
      });
      
    • // Listening to webview-specific events
      import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
      
      const appWebview = getCurrentWebviewWindow();
      appWebview.listen<string>('logged-in', (event) => {
          localStorage.setItem('session-token', event.payload);
      });
      
    • // 이벤트 수신 중지
      import { listen } from '@tauri-apps/api/event';
      
      const unlisten = await listen('download-started', (event) => {});
      unlisten();
      
  2. Rust에서
    • // Listening to global events
      use tauri::Listener;
      
      #[cfg_attr(mobile, tauri::mobile_entry_point)]
      pub fn run() {
          tauri::Builder::default()
        	  .setup(|app| {
        		  app.listen("download-started", |event| {
        			  if let Ok(payload) = serde_json::from_str::<DownloadStarted>(&event.payload()) {
        				  println!("downloading {}", payload.url);
        			  }
        		  });
        		  Ok(())
        	  })
        	  .run(tauri::generate_context!())
        	  .expect("error while running tauri application");
      }
      
    • // Listening to webview-specific events
      use tauri::{Listener, Manager};
      
      #[cfg_attr(mobile, tauri::mobile_entry_point)]
      pub fn run() {
          tauri::Builder::default()
        	  .setup(|app| {
        		  let webview = app.get_webview_window("main").unwrap();
        		  webview.listen("logged-in", |event| {
        			  let session_token = event.data;
        			  // save token..
        		  });
        		  Ok(())
        	  })
        	  .run(tauri::generate_context!())
        	  .expect("error while running tauri application");
      }
      
    • // 이벤트 수신 중지
      // unlisten outside of the event handler scope:
      let event_id = app.listen("download-started", |event| {});
      app.unlisten(event_id);
      // unlisten when some event criteria is matched
      let handle = app.handle().clone();
      app.listen("status-changed", |event| {
          if event.data == "ready" {
        	  handle.unlisten(event.id);
          }
      });