デスクトップのプラグインの作成方法も包含していますが,メインは iOS・Android としています
本記事に誤りを見つけた場合は,コメントにて優しく教えていただけると助かります
概要
本記事では Tauri 2.0 においてプラグインを作成する方法を紹介します
大筋は↓に沿って実装しています
https://v2.tauri.app/ja/develop/plugins/
はじめに
Tauri 2.0 ではモバイルアプリを作成できるようになりました ![]()
一方で,Tauri が提供するAPIのみでは不十分なこともあり,Swift や Kotlin (以降,ネイティブコード)を直接触りたいこともあると思います.
しかし,通常の Tauri アプリではネイティブコードを使用することはできません.
そこで,自作プラグインを作成することでネイティブコードを触れるようにします!!
環境
MacOS 14.6.1 (Sonoma)
M3 Pro
前提
Node.js や Rust,Android Studio,Xcode などがインストール済みであることを前提としています.
詳細については↓を参照してください
Tauri ではいろんなツールを使用できますが,今回は pnpm を使用しています.
他のツールを使用したい場合は↓を参考に,適宜置き換えてください
0. Tauri プロジェクトのセットアップ
既存の Tauri プロジェクトに追加する場合や,プラグインの開発だけの場合であれば,このセクションは不要です.
一応,私の実装検証環境が再現できるように記述しています
Tauri プロジェクトの作成
以下のコマンドで pnpm を使用してプロジェクトを作成できます
pnpm create tauri-app # pnpm の場合
参考: https://v2.tauri.app/ja/start/create-project/
コマンド実行時の記入・選択例
$ pnpm create tauri-app
Project name:test
Identifier (com.satooru.test) ›dev.satooru.tauri-test-01
Choose which language to use for your frontend:TypeScript / JavaScript
Choose your package manager:pnpm
Choose your UI template:React
Choose your UI flavor:TypeScript
これでプロジェクトが作成できました ![]()
パッケージのインストール
忘れずにインストールしておきましょう
pnpm install
Tauri アプリの動作確認
デスクトップ
pnpm tauri dev
参考: https://v2.tauri.app/ja/start/create-project/
動作確認(iOS)
Tauri アプリ側で iOS アプリを初期化します.
基本的に一度だけで良いです
pnpm tauri ios init
Xcodeで署名をします.
Xcodeで src-tauri/gen/apple/<app-name>.xcodeproj を開きます
TARGETS > test_iOS > Signing & Capabilities を開いて署名します
署名の手順は省略します
署名の手順は Preparing your app for distribution - Apple Developer などを参考にしてください
エミュレータで動作確認をします
pnpm tauri ios dev
pnpm tauri ios dev "iPhone 16" # シミュレータ端末を指定する場合
動作確認(Android)
Tauri アプリ側で Android アプリを初期化します.
基本的に一度だけで良いです
pnpm tauri android init
シミュレータで動作確認をします
pnpm tauri android dev
このような画面になればOKです
1. Tauri プラグインのセットアップ
セットアップ
以下のコマンドで新しくプラグインを作成できます
実行位置はどこでも問題ありませんが,使用したいプロジェクト直下で作成すると後のプラグインの使用が楽になります
npx @tauri-apps/cli plugin new <plugin-name>
Tauri プラグイン名には tauri-plugin- という接頭辞が推奨されています.
プラグイン名には自動で tauri-plugin- が着くようになっているため <plugin-name> に含める必要はありません
コマンド実行例
$ npx @tauri-apps/cli plugin new test
以降の tauti-plugin-test は各々のプラグイン名に置き換えて考えてください
ディレクトリの説明
プラグイン作成後は以下のようなディレクトリが作成されます
$ tree -L 2 ./tauri-plugin-test
./tauri-plugin-test
├── Cargo.toml
├── README.md
├── build.rs
├── examples
│ └── tauri-app
├── guest-js
│ └── index.ts
├── package.json
├── permissions
│ └── default.toml
├── rollup.config.js
├── src
│ ├── commands.rs
│ ├── desktop.rs
│ ├── error.rs
│ ├── lib.rs
│ ├── mobile.rs
│ └── models.rs
└── tsconfig.json
examples
プラグインの使用例を実装します
自分用のプラグインであれば不要なので削除して問題ありません
guest-js
Tauri アプリ内の WebView 側から呼び出しやすくするための JavaScript(TypeScript) を実装します
例えば WebView からプラグインの Rust を呼び出すとき,invoke('plugin:<plugin-name>|<command-name>') と実行します.
これには型がついておらず使い勝手が悪い状態です.
これを Tauri アプリで書いても良いですが,プラグイン内の guest-js で書いてあげると責任が明確化して WebView での使い回しが良くなります
permissions
プラグインで実装されたコマンドの権限設定を記述します.
初期設定では default.toml があります.
Tauri アプリ側の src-tauri/capabilities/default.json から <plugin-name>:default を許可すると,default.toml に記述された権限が設定されます
後々コマンドを追加した場合 permissions/autogenerated/commands/*.toml が自動生成されます.
これにより,default.toml に権限 (許可: allow-<command-name>,拒否: deny-<command-name>) を設定できるようになります
デフォルトでは拒否されているので,許可する必要があります(後述)
src
Rust を記述します
プラグインの初期設定・コマンドの実装・ネイティブコードの呼び出し等々を実装します
2. WebView から Rust コマンドを呼び出す
プラグイン側
文字列を受け取り,文字数を返すシンプルなコマンドを Rust で実装して WebView から呼び出してみます
コマンドの実装
tauri-plugin-test/src/commands.rs に新しくコマンドを作成します
#[command]
pub(crate) async fn count_chars<R: Runtime>(_app: AppHandle<R>, payload: CountCharsRequest) -> Result<i32>
{
let count = payload.text_value.chars().count() as i32;
Ok(count)
}
tauri-plugin-test/src/models.rs に WebView から受け取る値の型として CountCharsRequest を定義します
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CountCharsRequest {
pub text_value: String,
}
#[serde(rename_all = "camelCase")] のお陰で Rust 側では snale_case,WebView(TS)側では camelCase に自動で変換してくれます ヤッター
(text_value という絶妙なフィールド名なのは,camelCase に変換されることを確認するためです)
※ commands.rs では use crate::models::*; で全てインポートされているので CountCharsRequest を改めてインポートする必要はありません
コマンドの解放
プラグインからコマンドを呼び出せるようにします
tauri-plugin-test/src/lib.rs で count_chars コマンドを登録します
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("test")
.invoke_handler(tauri::generate_handler![
commands::ping,
+ commands::count_chars
])
.setup(|app, api| {
#[cfg(mobile)]
let test = mobile::init(app, api)?;
#[cfg(desktop)]
let test = desktop::init(app, api)?;
app.manage(test);
Ok(())
})
.build()
}
参考: https://v2.tauri.app/ja/develop/plugins/#コマンドの追加
コマンドの権限設定
権限を自動生成する設定を追加します.
tauri-plugin-test/build.rs に↓のようにコマンドを追加します
const COMMANDS: &[&str] = &["ping", "count_chars"];
参考: https://v2.tauri.app/ja/develop/plugins/#コマンドのアクセス権
権限設定ファイルを生成します.
tauri-plugin-<plugin-name> ディレクトリで↓を実行します
cargo build
tauri-plugin-test/permissions/autogenerated/commands/count_chars.toml が生成されたのを確認できるはずです
Tauri アプリにプラグインを追加した場合,Tauri アプリをビルドするとプラグインもビルドされるため,上記の cargo build を実行する必要はありません.
Tauri アプリを再ビルドせずに権限設定ファイルを生成したい場合などには使うと良いかもしれません
デフォルトの権限リストに追加します.
tauri-plugin-test/permissions/default.toml に↓のように権限を追加します
[default]
description = "Default permissions for the plugin"
permissions = ["allow-ping", "allow-count-chars"]
guest-js の実装
tauri-plugin-test/guest-js/index.ts にて↓の関数を追加します
export interface CountCharsResponse {
count: number;
}
export async function countChars(text: string): Promise<CountCharsResponse> {
return await invoke<CountCharsResponse>("plugin:test|count_chars", {
payload: {
textValue: text,
},
});
}
Rust側の CountCharsRequest では text_value と snake_case だったのに対し,ここでは textValue と camelCase になっているのが分かります
プラグイン側でのインストールも忘れずにしてください
cd tauri-plugin-test
pnpm install
guest-js を変更時は忘れずに↓を実行して guest-js のビルドをしてください
cd tauri-plugin-test
pnpm build
cargo build は Rust側のビルドのみをしています.
guest-js を変更時は毎回必ず pnpm build をする必要があります.
これでプラグイン側での設定は完了です ![]()
Tauri アプリ側
Rust側の依存関係にプラグインを追加
Cargo.toml の dependencies に↓のように追加します
[dependencies]
+ tauri-plugin-test = { path = "../tauri-plugin-test" }
WebView側の依存関係にプラグインを追加
package.json の dependencies に↓のように追加します
"dependencies": {
+ "tauri-plugin-test-api": "file:./tauri-plugin-test"
},
パッケージ名は tauri-plugin-<plugin-name>-api が推奨されています
また,可能であれば npmスコープを付けることを推奨されていますが,本記事では省略します
参考: https://v2.tauri.app/ja/develop/plugins/#命名規則
Tauri アプリ内でもインストールするのを忘れないようにしましょう
pnpm install
Tauri アプリ内でのプラグインの初期化
src-tauri/src/lib.rs で tauri_plugin_test を呼び出して初期化します
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
+ .plugin(tauri_plugin_test::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
権限の付与
src-tauri/capabilities/default.json でプラグインのコマンドを実行できるように権限を追加します
一度 Tarui アプリを立ち上げて実行しておきます.
これにより,test プラグインが初期化され schema などに権限名が追加されます.
schema に権限名がない(補完が効かない)場合や,実行時に権限名がないとエラーが出た場合は大体これのせいです
pnpm tauri dev
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
+ "test:default"
]
}
test:defaut ではなく test:allow-count-chars のようにコマンドごとに権限を与えることもできます
WebView から呼び出す
今回は React を選択したので React の例を挙げます
import { useState } from "react";
import { countChars } from "tauri-plugin-test-api";
import "./App.css";
function App() {
const [text, setText] = useState("");
const [count, setCount] = useState(0);
async function onChange(e: React.ChangeEvent<HTMLInputElement>) {
try {
const value = e.currentTarget.value;
setText(value);
const charCountRes = await countChars(value);
setCount(charCountRes.count);
} catch (error) {
console.error("Error counting characters:", error);
}
}
return (
<main className="container">
<input type="text" onChange={onChange} value={text} />
<p>Character count: {count}</p>
</main>
);
}
export default App;
動作確認
pnpm tauri ios dev
pnpm tauri ios dev "iPhone 16" # シミュレータ端末を指定する場合
うまくいかない場合は,node_modulesをインストールしなおしたり,権限を確認したりすると良いです
コンソールにエラーが出ているはずなので要チェックです
3. WebView からネイティブコードを呼び出す
Rustで実装した文字数を数えるコマンドを Swift や Kotlin で実装してみます.
ただし,WebView からネイティブコードを直接呼び出すことはできず,一度 Rust 側を経由する必要があります
Swift(iOS)
iOS機能の追加
↓のコマンドを実行して iOS 機能を追加します.
もし既に tauri-plugin-test/ios フォルダがある場合は作成されません.
作り直したい場合は tauri-plugin-test/ios を抹消する必要があります.
プラグイン作成後にいつの間にか作られていることがありますが,(多分)不要なファイルが沢山あるので作り直すとよいです
cd tauri-plugin-test
npx @tauri-apps/cli plugin ios init
参考: https://v2.tauri.app/ja/develop/plugins/develop-mobile/#プラグイン・プロジェクトの初期化
このコマンドを実行するとファイルにコードを追加しろと出てきますが,プラグイン作成時に既に書かれているものもあります.
また,指示されたファイルと作成時に書かれているファイルが違うものもあります.重複しないように気をつけてください
本記事に沿えば問題ないはずです
これにより tauri-plugin-test/ios フォルダが作成されます.
この中でも主に tauri-plugin-test/ios/Sources/ExamplePlugin.swift を触ります
ExamplePlugin という名前は可愛くないので自分のプラグイン名に変えるとよいですが,ここでは省略します
コマンドの実装
ExamplePlugin.swift に↓のようにコマンドを実装します
+class CountCharsRequest: Decodable {
+ let text_value: String
+}
class ExamplePlugin: Plugin {
@objc public func ping(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(PingArgs.self)
invoke.resolve(["value": args.value ?? ""])
}
+ @objc public func countChars(_ invoke: Invoke) throws {
+ let args = try invoke.parseArgs(CountCharsRequest.self)
+ let count = args.textValue.count
+ invoke.resolve(["count": count])
+ }
+}
返り値がない場合でもメソッドの最後に invoke.resolve() を実行する必要があります.
さもなければ WebView 側で永遠に Promise が解決されない状態となります
iOS ディレクトリの指定(基本的に不要)
Tauri プラグインに iOS ディレクトリの位置を教えてあげます.
ただし,基本的にはプラグイン作成時に追加されているはずなので不要です
const COMMANDS: &[&str] = &["ping", "count_chars"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
.android_path("android")
+ .ios_path("ios")
.build();
}
iOS 機能の初期化(基本的に不要)
iOS 専用のプラグイン初期化のコードを追加します.
ただし,基本的にはプラグイン作成時に追加されているはずなので不要です
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_test);
iOS 機能の登録(基本的に不要)
iOS 専用のプラグイン登録のコードを追加します.
ただし,基本的にはプラグイン作成時に追加されているはずなので不要です
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Test<R>> {
#[cfg(target_os = "android")]
let handle = api.register_android_plugin("", "ExamplePlugin")?;
+ #[cfg(target_os = "ios")]
+ let handle = api.register_ios_plugin(init_plugin_test)?;
+ Ok(Test(handle))
}
Rust側でネイティブコードのコマンドの紐付け
Rust側でネイティブコードのコマンドを紐づけます
mobile.rs でネイティブコードのコマンドを呼び出すメソッドを追加します
impl<R: Runtime> Test<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
self
.0
.run_mobile_plugin("ping", payload)
.map_err(Into::into)
}
+ pub fn count_chars(&self, payload: CountCharsRequest) -> crate::Result<i32> {
+ self
+ .0
+ .run_mobile_plugin("countChars", payload)
+ .map_err(Into::into)
+ }
}
このとき,run_mobile_plugin の第一引数は,ネイティブコードのメソッド名と完全一致させる必要があります
Rust側のコマンドの変更
この変更をすると,デスクトップアプリ側では count_chars が見つからずエラーになります.
デスクトップ側では tauri-plugin-test/src/desktop.rs に count_chars メソッドを実装することで対応できますが,ここでは省略します
commands.rs を↓のように書き換えます.
#[command]
pub(crate) async fn count_chars<R: Runtime>(
app: AppHandle<R>,
payload: CountCharsRequest,
) -> Result<i32> {
app.test().count_chars(payload)
}
動作確認(iOS)
pnpm tauri ios dev
Android(Kotlin)
Android機能の追加
↓のコマンドを実行して Android 機能を追加します.
もし既に tauri-plugin-test/android フォルダがある場合は作成されません.
作り直したい場合は tauri-plugin-test/ios を抹消する必要があります.
プラグイン作成後にいつの間にか作られていることがありますが,(多分)不要なファイルが沢山あるので作り直すとよいです
cd tauri-plugin-test
npx @tauri-apps/cli plugin android init
コマンド実行時の入力例
$ npx @tauri-apps/cli plugin android init
? What should be the Android Package ID for your plugin?:dev.satooru.testplugin01
ここで入力したパッケージIDは後ほど使うので覚えておいてください
(忘れた場合でも tauri-plugin-test/android/build.gradle.kts に書かれています)
参考: https://v2.tauri.app/ja/develop/plugins/develop-mobile/#プラグイン・プロジェクトの初期化
このコマンドを実行するとファイルにコードを追加しろと出てきますが,プラグイン作成時に既に書かれているものもあります.
また,指示されたファイルと作成時に書かれているファイルが違うものもあります.重複しないように気をつけてください
本記事に沿えば問題ないはずです
これにより tauri-plugin-test/android フォルダが作成されます.
この中でも主に tauri-plugin-test/android/src/main/java/ExamplePlugin.kt を触ります
ExamplePlugin という名前は可愛くないので自分のプラグイン名に変えるとよいですが,ここでは省略します
コマンドの実装
ExamplePlugin.kt に↓のようにコマンドを実装します
+@InvokeArg
+class CountCharsRequest {
+ var textValue: String? = null
+}
@TauriPlugin
class ExamplePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = Example()
@Command
fun ping(invoke: Invoke) {
val args = invoke.parseArgs(PingArgs::class.java)
val ret = JSObject()
ret.put("value", implementation.pong(args.value ?: "default value :("))
invoke.resolve(ret)
}
+ @Command
+ fun countChars(invoke: Invoke) {
+ val args = invoke.parseArgs(CountCharsRequest::class.java)
+ val count = args.textValue?.length ?: 0
+
+ val ret = JSObject()
+ ret.put("count", count)
+ invoke.resolve(ret)
+ }
}
※本来はロジックを Example() 側に実装して ExamplePlugin() は橋渡しに専念させるべき(らしい by ChatGPT)です
返り値がない場合でもメソッドの最後に invoke.resolve() を実行する必要があります.
さもなければ WebView 側で永遠に Promise が解決されない状態となります
Android ディレクトリの指定(基本的に不要)
Tauri プラグインに Android ディレクトリの位置を教えてあげます.
ただし,基本的にはプラグイン作成時に追加されているはずなので不要です
const COMMANDS: &[&str] = &["ping", "count_chars"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)
+ .android_path("android")
.ios_path("ios")
.build();
}
Android 機能の登録
iOS 専用のプラグイン登録のコードを追加します.
基本的にはプラグイン作成時に追加されていますが,register_android_plugin() の第一引数を書き換える必要があります
<package-id> には #Android機能の追加 時に入力したパッケージIDを指定してください
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Test<R>> {
+ #[cfg(target_os = "android")]
+ let handle = api.register_android_plugin("<package-id>", "ExamplePlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_test)?;
Ok(Test(handle))
}
Rust側でネイティブコードのコマンドの紐付け
Swift(iOS) の方で対応済みの場合はスキップしてください
iOS をスキップした方向けに再掲しています
Rust側でネイティブコードのコマンドを紐づけます
mobile.rs でネイティブコードのコマンドを呼び出すメソッドを追加します
impl<R: Runtime> Test<R> {
pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
self
.0
.run_mobile_plugin("ping", payload)
.map_err(Into::into)
}
+ pub fn count_chars(&self, payload: CountCharsRequest) -> crate::Result<i32> {
+ self
+ .0
+ .run_mobile_plugin("countChars", payload)
+ .map_err(Into::into)
+ }
}
このとき,run_mobile_plugin の第一引数は,ネイティブコードのメソッド名と完全一致させる必要があります
Rust側のコマンドの変更
Swift(iOS) の方で対応済みの場合はスキップしてください
iOS をスキップした方向けに再掲しています
この変更をすると,デスクトップアプリ側では count_chars が見つからずエラーになります.
デスクトップ側では tauri-plugin-test/src/desktop.rs に count_chars メソッドを実装することで対応できますが,ここでは省略します
commands.rs を↓のように書き換えます.
#[command]
pub(crate) async fn count_chars<R: Runtime>(
app: AppHandle<R>,
payload: CountCharsRequest,
) -> Result<i32> {
app.test().count_chars(payload)
}
pnpm tauri android dev
無事に文字数が出ればOKです!! ヤッター
参考






