Skip to content

WASM 插件 (plugin) ​

SSS-RUN 内置 WASM 插件系统,基于 wazero 实现。插件以 .wasm 二进制形式部署,通过 require("plugin/xxx") 懒加载调用,支持 Go、Rust、AssemblyScript 等任意可编译为 WASM 的语言编写。

特性 ​

  • 懒加载:插件在首次 require 时才编译加载,未使用的插件零开销
  • 实例池复用:通过实例池复用 WASM 模块实例,高并发下性能稳定
  • 宿主函数:插件可调用宿主提供的日志、配置读取、HTTP 请求能力
  • 热重载:支持运行时重新加载插件,无需重启服务
  • 多语言支持:Go (tinygo)、Rust、AssemblyScript、C 等均可编写插件

目录结构 ​

可执行文件目录/
└── plugins/
    └── hello/
        └── hello.wasm     # 插件 WASM 文件

插件文件路径规则:plugins/{插件名}/{插件名}.wasm

使用方式 ​

在 JS 脚本中调用插件 ​

javascript
// 加载插件(懒加载,首次 require 时编译)
var hello = require("plugin/hello")

// 调用插件函数
var msg = hello.hello("World")   // → "Hello, World!"
var sum = hello.add(1, 2)        // → 3
var upper = hello.upper("abc")   // → "ABC"

在接口函数中使用 ​

javascript
function main() {
    // 加载加密插件
    var crypto = require("plugin/crypto")

    // 调用插件函数
    var hash = crypto.md5(request.Input("password"))

    return { hash: hash }
}

错误处理 ​

插件调用失败会抛出异常,使用 try/catch 捕获:

javascript
try {
    var result = require("plugin/hello").add(1, 2)
} catch (e) {
    log.Error("插件调用失败: " + e.message)
}

WASM 插件契约 ​

每个插件 WASM 模块必须导出以下接口:

导出签名说明
memoryMemoryWASM 线性内存
alloc(size: i32) → i32分配内存,返回指针(供宿主写入数据)
dealloc(ptr: i32)释放内存(可选,但建议实现)
__dispatch(fn_ptr, fn_len, args_ptr, args_len) → i64核心调度函数,返回 ptr<<32 | len
__functions() → i64返回函数名列表 JSON,格式 ptr<<32 | len

返回值约定 ​

__dispatch 和 __functions 的返回值为 i64,高 32 位为结果指针,低 32 位为结果长度:

返回值 = (结果指针 << 32) | 结果长度

参数与返回值格式 ​

调用 xxx.hello("world", 123) 时:

json
// 传入 __dispatch 的参数 JSON
args = ["world", 123]

// __dispatch 返回的结果 JSON
{"ok": "Hello, world"}       // 成功
{"err": "function not found"} // 失败

__functions 返回函数名列表:

json
["hello", "add", "upper"]

宿主函数 ​

WASM 插件可调用宿主导出的函数(通过 env 模块):

host_log — 日志输出 ​

go
// 向宿主日志写入字符串
host_log(ptr, len)

host_get_config — 读取配置 ​

go
// 读取宿主配置(通过 viper 读取任意键)
// 返回 i64: ptr<<32 | len,内容为 {"key": "...", "value": ...}
host_get_config(key_ptr, key_len) int64

host_http — HTTP 请求 ​

go
// 通过宿主发起 HTTP 请求
// 请求 JSON: {"method": "GET", "url": "...", "headers": {...}, "body": "..."}
// 返回 i64: ptr<<32 | len,内容为 {"status": 200, "body": "...", "headers": {...}}
host_http(req_ptr, req_len) int64

插件示例(Go + tinygo) ​

以下是一个完整的插件示例,提供 hello、add、upper 三个函数:

go
// plugins/hello/hello.go
// 编译: tinygo build -o hello.wasm -target wasm -no-debug hello.go
package main

import (
    "encoding/json"
    "fmt"
    "unsafe"
)

// allocRegistry 跟踪 alloc 分配的内存,供 dealloc 释放
var (
    allocRegistry = make(map[uintptr][]byte)
    resultBuf     []byte
)

func main() {}

//export alloc
func alloc(size int32) unsafe.Pointer {
    if size <= 0 {
        return nil
    }
    buf := make([]byte, size)
    ptr := uintptr(unsafe.Pointer(&buf[0]))
    allocRegistry[ptr] = buf
    return unsafe.Pointer(ptr)
}

//export dealloc
func dealloc(ptr unsafe.Pointer) {
    if ptr == nil {
        return
    }
    delete(allocRegistry, uintptr(ptr))
}

//export __dispatch
func dispatch(fnPtr unsafe.Pointer, fnLen int32, argsPtr unsafe.Pointer, argsLen int32) int64 {
    funcName := ptrToString(fnPtr, fnLen)
    argsData := ptrToBytes(argsPtr, argsLen)

    var args []interface{}
    if err := json.Unmarshal(argsData, &args); err != nil {
        return packResult([]byte(`{"err":"参数解析失败"}`))
    }

    var result map[string]interface{}
    switch funcName {
    case "hello":
        name := "World"
        if len(args) > 0 {
            if s, ok := args[0].(string); ok {
                name = s
            }
        }
        result = map[string]interface{}{"ok": fmt.Sprintf("Hello, %s!", name)}
    case "add":
        if len(args) < 2 {
            result = map[string]interface{}{"err": "add 需要两个参数"}
        } else {
            a := toFloat(args[0])
            b := toFloat(args[1])
            result = map[string]interface{}{"ok": a + b}
        }
    default:
        result = map[string]interface{}{"err": fmt.Sprintf("未知函数: %s", funcName)}
    }

    resultJSON, _ := json.Marshal(result)
    return packResult(resultJSON)
}

//export __functions
func functions() int64 {
    funcs := []string{"hello", "add"}
    data, _ := json.Marshal(funcs)
    return packResult(data)
}

func packResult(data []byte) int64 {
    resultBuf = data
    if len(data) == 0 {
        return 0
    }
    ptr := uintptr(unsafe.Pointer(&resultBuf[0]))
    length := uint32(len(data))
    return (int64(ptr) << 32) | int64(length)
}

编译插件 ​

使用 tinygo 编译 Go 插件为 WASM:

bash
# 安装 tinygo
# 参考: https://tinygo.org/getting-started/install/

# 编译插件
cd plugins/hello
tinygo build -o hello.wasm -target wasm -no-debug hello.go

插件示例(Rust) ​

使用 Rust 编写插件示例:

rust
// plugins/hello/hello.rs
use serde_json::{Value, json};

#[no_mangle]
pub extern "C" fn alloc(size: i32) -> i32 {
    // 分配 WASM 内存
}

#[no_mangle]
pub extern "C" fn dealloc(ptr: i32) {
    // 释放 WASM 内存
}

#[no_mangle]
pub extern "C" fn __dispatch(fn_ptr: i32, fn_len: i32, args_ptr: i32, args_len: i32) -> i64 {
    let func_name = read_string_from_memory(fn_ptr, fn_len);
    let args: Vec<Value> = read_json_from_memory(args_ptr, args_len);

    let result = match func_name.as_str() {
        "hello" => json!({"ok": format!("Hello, {}!", args[0])}),
        "add" => {
            let sum = args[0].as_i64().unwrap() + args[1].as_i64().unwrap();
            json!({"ok": sum})
        }
        _ => json!({"err": format!("unknown function: {}", func_name)}),
    };

    write_result_to_memory(result)
}

#[no_mangle]
pub extern "C" fn __functions() -> i64 {
    let funcs = json!(["hello", "add"]);
    write_result_to_memory(funcs)
}

插件示例(JavaScript / AssemblyScript) ​

使用 AssemblyScript 编写插件,需安装 AssemblyScript:

typescript
// plugins/hello/assembly/index.ts
export function hello(name: string): string {
  return `Hello, ${name}!`;
}

export function add(a: i32, b: i32): i32 {
  return a + b;
}

export function upper(str: string): string {
  return str.toUpperCase();
}

AssemblyScript 会自动生成 alloc/dealloc/__dispatch/__functions 等标准 WASM 接口,无需手动实现。

编译命令:

bash
npx asc assembly/index.ts -o hello.wasm --exportRuntime

插件示例(Python) ​

使用 wasm-pack 或 wasmtime-py 等工具,Python 也可以通过编译为 WASM 来编写插件。以下示例使用 py2wasm 工具链:

python
# plugins/hello/hello.py
import json

# 导出的 __dispatch 函数
def __dispatch(fn_name: str, args_json: str) -> str:
    args = json.loads(args_json)
    
    if fn_name == "hello":
        name = args[0] if args else "World"
        return json.dumps({"ok": f"Hello, {name}!"})
    elif fn_name == "add":
        a, b = args[0], args[1]
        return json.dumps({"ok": a + b})
    elif fn_name == "upper":
        return json.dumps({"ok": args[0].upper()})
    else:
        return json.dumps({"err": f"unknown function: {fn_name}"})


def __functions() -> str:
    return json.dumps(["hello", "add", "upper"])

注意:Python → WASM 的工具链仍在发展中,生产环境建议优先使用 Go 或 Rust。

插件示例(Java) ​

使用 TeaVM 将 Java 编译为 WASM:

java
// plugins/hello/HelloPlugin.java
import org.teavm.interop.Export;
import org.teavm.interop.Memory;
import java.util.*;

public class HelloPlugin {

    @Export(name = "alloc")
    public static int alloc(int size) {
        // TeaVM 自动管理内存
        return 0;
    }

    @Export(name = "dealloc")
    public static void dealloc(int ptr) {
        // TeaVM 自动管理内存
    }

    @Export(name = "__dispatch")
    public static long dispatch(int fnPtr, int fnLen, int argsPtr, int argsLen) {
        String fnName = readString(fnPtr, fnLen);
        String argsJson = readString(argsPtr, argsLen);

        // 解析参数
        // ... 使用 JSON 解析库

        return packResult(("{\"ok\": \"Hello from Java!\"}").getBytes());
    }

    @Export(name = "__functions")
    public static long functions() {
        byte[] data = "[\"hello\",\"add\",\"upper\"]".getBytes();
        return packResult(data);
    }

    private static long packResult(byte[] data) {
        // 将结果写入 WASM 内存并返回 ptr<<32 | len
        return 0; // 实现略
    }
}

编译命令:

bash
# 使用 TeaVM WASM backend
mvn package -P wasm
# 输出文件: target/generated/wasm/HelloPlugin.wasm

插件示例(C# / .NET) ​

使用 .NET 编写 WASM 插件,需安装 wasm-tools 工作负载,通过 NativeAOT 编译为 WASM:

csharp
// plugins/hello/HelloPlugin.cs
using System;
using System.Text.Json;
using System.Runtime.InteropServices;

public class HelloPlugin
{
    private static byte[]? _resultBuffer;

    // 必须导出: alloc
    [UnmanagedCallersOnly(EntryPoint = "alloc")]
    public static IntPtr Alloc(int size)
    {
        var buf = new byte[size];
        var handle = GCHandle.Alloc(buf, GCHandleType.Pinned);
        return handle.AddrOfPinnedObject();
    }

    // 必须导出: dealloc
    [UnmanagedCallersOnly(EntryPoint = "dealloc")]
    public static void Dealloc(IntPtr ptr)
    {
        // .NET NativeAOT 自动管理内存
    }

    // 必须导出: __dispatch
    [UnmanagedCallersOnly(EntryPoint = "__dispatch")]
    public static long Dispatch(int fnPtr, int fnLen, int argsPtr, int argsLen)
    {
        var fnName = Marshal.PtrToStringUTF8((IntPtr)fnPtr, fnLen);
        var argsJson = Marshal.PtrToStringUTF8((IntPtr)argsPtr, argsLen);

        object result;
        switch (fnName)
        {
            case "hello":
                var name = "World";
                result = new { ok = $"Hello, {name}!" };
                break;
            case "add":
                var args = JsonSerializer.Deserialize<int[]>(argsJson!);
                result = new { ok = args![0] + args[1] };
                break;
            case "upper":
                var str = JsonSerializer.Deserialize<string[]>(argsJson!);
                result = new { ok = str![0].ToUpper() };
                break;
            default:
                result = new { err = $"unknown function: {fnName}" };
                break;
        }

        var json = JsonSerializer.Serialize(result);
        var bytes = System.Text.Encoding.UTF8.GetBytes(json);
        return PackResult(bytes);
    }

    // 必须导出: __functions
    [UnmanagedCallersOnly(EntryPoint = "__functions")]
    public static long Functions()
    {
        var funcs = JsonSerializer.Serialize(new[] { "hello", "add", "upper" });
        var bytes = System.Text.Encoding.UTF8.GetBytes(funcs);
        return PackResult(bytes);
    }

    private static long PackResult(byte[] data)
    {
        _resultBuffer = data;
        var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0);
        return ((long)ptr << 32) | (uint)data.Length;
    }
}

编译命令:

bash
# 安装 wasm-tools 工作负载
dotnet workload install wasm-tools

# 创建类库项目并添加 NuGet 配置
dotnet new classlib -n HelloPlugin
cd HelloPlugin

# 编译为 WASM (NativeAOT)
dotnet publish -c Release \
  -p:NativeLib=Wasm \
  -p:TargetFramework=net8.0 \
  -p:PlatformTarget=AnyCPU

# 输出文件: bin/Release/net8.0/publish/HelloPlugin.wasm

完整使用示例 ​

示例 1:字符串处理插件 ​

javascript
// 使用插件处理字符串
var str = require("plugin/str")

function main() {
    var input = request.Input("text")
    var result = str.upper(input)
    return { original: input, upper: result }
}

示例 2:加密插件 ​

javascript
// 使用加密插件
var crypto = require("plugin/crypto")

function main() {
    var password = request.Input("password")
    var hash = crypto.md5(password)
    var token = crypto.sha256(password + "salt")

    return {
        md5: hash,
        sha256: token
    }
}

示例 3:图片处理插件 ​

javascript
// 使用图片处理插件
var image = require("plugin/image")

function main() {
    var file = request.Input("file")
    // 缩放图片
    var thumb = image.resize(file, 100, 100)
    // 添加水印
    var watermarked = image.watermark(thumb, "SSS-RUN")

    return { thumb: watermarked }
}

示例 4:在定时任务中使用 ​

javascript
// 定时任务:每天凌晨调用外部 API 并处理数据
var http = require("plugin/http")

function main() {
    var response = http.get("https://api.example.com/data")
    var data = JSON.parse(response.body)

    // 处理数据并入库
    for (var i = 0; i < data.length; i++) {
        db.Table("records").Insert(data[i])
    }

    return { processed: data.length }
}

缓存机制 ​

插件系统采用四级缓存,全部懒加载:

缓存层说明清除时机
L0 Proxy 缓存pluginName → goja.Value,同一 Runtime 复用Reload 时清除
L1 编译缓存pluginName → CompiledModule,避免重复编译Reload 时清除
L2 元数据缓存pluginName → PluginMeta,避免重复调用 __functionsReload 时清除
L3 实例池pluginName → instancePool,复用 WASM 实例Reload 时关闭并清除

缓存生命周期 ​

首次 require("plugin/hello")
  → L0 未命中 → L1 未命中 → 编译 .wasm → 缓存 L1
  → 实例化 → __functions() → 缓存 L2
  → 创建 Proxy → 缓存 L0
  → 返回 Proxy

二次 require("plugin/hello")
  → L0 命中 → 直接返回 Proxy(零开销)

首次调用 hello.add(1,2)
  → 从 L3 实例池获取实例(池空则新建)
  → 执行 __dispatch → 归还实例到池

Reload("hello")
  → 清除 L0/L1/L2/L3 全部缓存
  → 下次 require 重新走完整流程

注意事项 ​

  • 插件路径:插件文件必须放在 plugins/{插件名}/{插件名}.wasm
  • 内存管理:插件必须实现 alloc 和 dealloc,避免内存泄漏
  • 线程安全:实例池中的实例同一时刻只被一个 goroutine 使用,无需加锁
  • 超时限制:单次 WASM 调用最大超时 10 秒,防止恶意插件卡死
  • 参数大小:参数 JSON 最大 1MB,返回结果最大 10MB
  • 热重载:Reload 会关闭所有实例并清除缓存,调用期间该插件不可用

版权所有.