# Native 机制
秋之盒桌面端(Tauri)提供 NativeService,允许插件加载和调用原生共享库(.dylib / .dll / .so)。这是插件突破 Web 沙箱、访问本地系统能力的桥梁。
# 概览
插件代码
↓ NativeService.loadLibrary()
Tauri IPC (invoke)
↓
Rust 后端 (dlopen/dlsym)
↓
原生共享库 (.dylib/.dll/.so)
| Web 端 | 桌面端(Tauri) | |
|---|---|---|
supported | false | true |
| 可用功能 | 无 | 加载 .dylib / .dll / .so |
| 检测方式 | nativeService.supported | 自动检测 |
# 基本用法
import { useService } from '@autumnbox/sdk/hooks';
import { NativeService } from '@autumnbox/sdk/services';
const MyApp: React.FC = () => {
const native = useService(NativeService);
const loadAndCall = async () => {
// 检查平台是否支持
if (!native.supported.value) {
alert('Native 功能仅在桌面端可用');
return;
}
// 加载原生库
const lib = await native.loadLibrary('/path/to/mylib.dylib');
// 调用函数(参数自动 JSON 序列化,返回值也是 JSON)
const result = await lib.call('my_function', {
input: 'hello',
count: 42,
});
console.log('原生函数返回:', result);
// 释放库(可选,组件卸载时会自动清理)
await lib.dispose();
};
return <button onClick={loadAndCall}>调用原生库</button>;
};
# 平台检测
const native = useService(NativeService);
// 当前平台是否支持 Native
native.supported.value; // boolean
// 平台信息
native.platform.value; // { os: 'darwin' | 'win32' | 'linux' | 'web', arch: 'x86_64' | 'aarch64' }
根据平台信息加载对应的二进制:
const { os, arch } = native.platform.value;
const libPath = {
darwin: arch === 'aarch64' ? 'libs/arm64/libmy.dylib' : 'libs/x64/libmy.dylib',
win32: 'libs/x64/my.dll',
linux: 'libs/x64/libmy.so',
}[os];
if (libPath) {
const lib = await native.loadLibrary(libPath);
}
# INativeLibrary 接口
interface INativeLibrary {
/** 调用库中的导出函数 */
call(functionName: string, args?: unknown): Promise<unknown>;
/** 释放库,释放后不能再调用 */
dispose(): Promise<void>;
}
call(fn, args)— 调用库中通过#[no_mangle] pub extern "C"导出的函数。args会自动 JSON 序列化传入,返回值也会 JSON 反序列化dispose()— 释放库句柄。建议在清理函数中调用
# 打包原生库到插件
将平台特定的二进制文件放入 resources/ 目录:
my-plugin/
├── resources/
│ ├── libs/
│ │ ├── arm64/
│ │ │ └── libhelper.dylib
│ │ ├── x64/
│ │ │ ├── libhelper.dylib
│ │ │ ├── helper.dll
│ │ │ └── libhelper.so
│ │ └── ...
│ └── lang/
└── src/
运行时通过 getResource 加载到临时文件:
import { useService } from '@autumnbox/sdk/hooks';
import { NativeService } from '@autumnbox/sdk/services';
import { usePluginResource } from '@autumnbox/sdk/hooks';
const useNativeLib = () => {
const native = useService(NativeService);
const { data: libBlob } = usePluginResource('libs/x64/libhelper.dylib');
const load = async () => {
if (!libBlob || !native.supported.value) return null;
// loadLibrary 接受 Blob,会自动写入临时文件后 dlopen
const lib = await native.loadLibrary(libBlob);
return lib;
};
return { load };
};
loadLibrary 可以接受文件路径字符串或 Blob。传 Blob 时,Tauri 后端会写入临时文件再加载。
# C ABI 编写指南
原生库必须通过 C ABI 导出函数。Tauri 后端通过 dlopen / dlsym 加载共享库,按函数名定位符号,然后以 JSON 字符串传递参数和返回值。
# 通信协议
插件 JS
↓ lib.call('fn_name', { key: value })
↓ 自动 JSON.stringify → UTF-8 bytes
Tauri IPC
↓ dlsym("fn_name")
↓ fn_name(json_ptr, json_len) → result_ptr
↓ 读取 result 为 UTF-8 bytes → JSON.parse
↓ 返回给 JS
函数签名约定:
// C ABI 签名
// 参数:JSON UTF-8 字节的指针和长度
// 返回:JSON UTF-8 字节的指针(调用方负责释放)
void* fn_name(const uint8_t* input_json, size_t input_len);
# Rust 实现
Rust 是编写原生库的首选语言——内存安全、与 Tauri 技术栈一致。
// Cargo.toml
// [lib]
// crate-type = ["cdylib"] ← 生成 .dylib / .dll / .so
//
// [dependencies]
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use serde::{Deserialize, Serialize};
// ── 输入/输出结构体 ──
#[derive(Deserialize)]
struct Input {
message: String,
count: i32,
}
#[derive(Serialize)]
struct Output {
result: String,
total: i32,
}
// ── 导出函数 ──
/// # Safety
/// input_json 必须指向有效的 UTF-8 JSON 字节,长度为 input_len。
#[no_mangle]
pub unsafe extern "C" fn my_function(input_json: *const u8, input_len: usize) -> *mut u8 {
// 1. 解析 JSON 输入
let input_bytes = std::slice::from_raw_parts(input_json, input_len);
let input: Input = match serde_json::from_slice(input_bytes) {
Ok(v) => v,
Err(e) => return error_response(&format!("parse error: {e}")),
};
// 2. 执行业务逻辑
let output = Output {
result: input.message.to_uppercase(),
total: input.count * 2,
};
// 3. 返回 JSON
json_response(&output)
}
// ── 辅助函数 ──
fn json_response<T: Serialize>(value: &T) -> *mut u8 {
let json = serde_json::to_vec(value).unwrap_or_default();
leak_bytes(json)
}
fn error_response(msg: &str) -> *mut u8 {
let json = format!(r#"{{"error":"{}"}}"#, msg.replace('"', r#"\""#));
leak_bytes(json.into_bytes())
}
/// 将 Vec<u8> 泄漏为裸指针,由调用方(Tauri)负责释放。
fn leak_bytes(mut bytes: Vec<u8>) -> *mut u8 {
let ptr = bytes.as_mut_ptr();
std::mem::forget(bytes);
ptr
}
关键点:
#[no_mangle]— 阻止 Rust 名称修饰,保持函数名不变extern "C"— 使用 C 调用约定crate-type = ["cdylib"]— 生成动态链接库(.dylib/.dll/.so)- 内存:返回的指针通过
std::mem::forget泄漏,由 Tauri 侧释放
# C/C++ 实现
// helper.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// 简单的 JSON 处理(生产环境建议用 cJSON 等库)
char* my_function(const unsigned char* input_json, size_t input_len) {
// 解析输入(简化示例)
char* input = malloc(input_len + 1);
memcpy(input, input_json, input_len);
input[input_len] = '\0';
// 构建输出 JSON
char* output = malloc(256);
snprintf(output, 256, "{\"result\":\"processed\",\"input_length\":%zu}", input_len);
free(input);
return output; // 调用方负责释放
}
编译:
# macOS
clang -shared -o libhelper.dylib helper.c
# Linux
gcc -shared -fPIC -o libhelper.so helper.c
# Windows (MSVC)
cl /LD helper.c /Fe:helper.dll
# Go 实现
package main
import "C"
import (
"encoding/json"
"unsafe"
)
type Input struct {
Message string `json:"message"`
Count int `json:"count"`
}
type Output struct {
Result string `json:"result"`
Total int `json:"total"`
}
//export my_function
func my_function(inputJSON *C.char, inputLen C.size_t) *C.char {
bytes := C.GoBytes(unsafe.Pointer(inputJSON), C.int(inputLen))
var input Input
json.Unmarshal(bytes, &input)
output := Output{
Result: input.Message,
Total: input.Count * 2,
}
result, _ := json.Marshal(output)
return C.CString(string(result))
}
func main() {}
编译:go build -buildmode=c-shared -o libhelper.so
# 注意事项
| 要点 | 说明 |
|---|---|
| 名称修饰 | C++ 必须用 extern "C" {} 包裹导出函数,否则 dlsym 找不到符号 |
| 内存所有权 | 函数返回的指针由 Tauri 侧释放。不要返回栈上的局部变量 |
| 线程安全 | 函数可能被主线程调用,耗时操作应自行开线程 |
| 错误处理 | 不要 panic / abort。错误应通过返回 JSON {"error": "..."} 传达 |
| 跨平台 | 同一功能需要为每个目标平台编译对应的二进制(x64/arm64, macOS/Windows/Linux) |
# 完整示例
import React, { useState } from 'react';
import { Button, Card, Typography, Alert } from 'antd';
import { useService } from '@autumnbox/sdk/hooks';
import { NativeService } from '@autumnbox/sdk/services';
import { usePluginResource } from '@autumnbox/sdk/hooks';
import type { AutumnApp } from '@autumnbox/sdk';
import type { INativeLibrary } from '@autumnbox/sdk/services';
const { Title, Text } = Typography;
const NativeDemoView: React.FC = () => {
const native = useService(NativeService);
const { data: libBlob, loading } = usePluginResource('libs/x64/libhelper.dylib');
const [result, setResult] = useState<string>('');
if (!native.supported.value) {
return <Alert type="warning" message="Native 功能仅在桌面端可用" />;
}
const handleCall = async () => {
if (!libBlob) return;
let lib: INativeLibrary | null = null;
try {
lib = await native.loadLibrary(libBlob);
const output = await lib.call('process_data', {
input: 'hello from plugin',
iterations: 10,
});
setResult(JSON.stringify(output, null, 2));
} catch (err) {
setResult(`Error: ${err}`);
} finally {
await lib?.dispose();
}
};
return (
<div style={{ padding: 24 }}>
<Title level={3}>Native Demo</Title>
<Card>
<p>平台: <Text code>{native.platform.value.os}/{native.platform.value.arch}</Text></p>
<Button onClick={handleCall} loading={loading}>调用原生函数</Button>
{result && <pre style={{ marginTop: 16 }}>{result}</pre>}
</Card>
</div>
);
};
export const NativeDemoApp: AutumnApp = {
id: 'native-demo',
name: 'app.name.native_demo',
icon: 'data:image/svg+xml,...',
component: NativeDemoView,
};
# 限制与注意事项
- 仅桌面端:Web 端
supported为false,调用loadLibrary会抛出错误 - ABI 兼容:原生库必须使用 C ABI 导出函数(Rust 用
extern "C",C/C++ 用extern "C") - JSON 序列化:参数和返回值通过 JSON 传递,不支持二进制数据直传
- 线程安全:原生函数在主线程调用,耗时操作应自行管理线程
- 安全审查:通过插件商店发布包含原生库的插件时,会经过额外的安全审查
# 未来规划
.NET 程序集加载(loadDotNetAssembly)— 计划中- 原生对象实例管理(
createInstance/getObject)— 计划中