Native 机制

# 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 端 supportedfalse,调用 loadLibrary 会抛出错误
  • ABI 兼容:原生库必须使用 C ABI 导出函数(Rust 用 extern "C",C/C++ 用 extern "C"
  • JSON 序列化:参数和返回值通过 JSON 传递,不支持二进制数据直传
  • 线程安全:原生函数在主线程调用,耗时操作应自行管理线程
  • 安全审查:通过插件商店发布包含原生库的插件时,会经过额外的安全审查

# 未来规划

  • .NET 程序集加载loadDotNetAssembly)— 计划中
  • 原生对象实例管理(createInstance / getObject)— 计划中