Appearance
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 模块必须导出以下接口:
| 导出 | 签名 | 说明 |
|---|---|---|
memory | Memory | WASM 线性内存 |
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) int64host_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,避免重复调用 __functions | Reload 时清除 |
| 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会关闭所有实例并清除缓存,调用期间该插件不可用