golang跨语言WebAssembly插件系统开发框架Extism Go SDK的使用

Golang跨语言WebAssembly插件系统开发框架Extism Go SDK的使用

安装

通过go get安装:

go get github.com/extism/go-sdk

快速入门

创建插件

Extism的核心概念是插件(Plug-in)。插件可以看作存储在.wasm文件中的代码模块。

以下是一个加载演示插件的示例:

package main

import (
	"context"
	"fmt"
	"github.com/extism/go-sdk"
	"os"
)

func main() {
	manifest := extism.Manifest{
		Wasm: []extism.Wasm{
			extism.WasmUrl{
				Url: "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm",
			},
		},
	}

	ctx := context.Background()
	config := extism.PluginConfig{}
	plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{})

	if err != nil {
		fmt.Printf("Failed to initialize plugin: %v\n", err)
		os.Exit(1)
	}
}

调用插件函数

这个插件是用Rust编写的,功能是计算字符串中的元音字母数量。它导出了一个函数count_vowels

func main() {
    // ...
	data := []byte("Hello, World!")
	exit, out, err := plugin.Call("count_vowels", data)
	if err != nil {
		fmt.Println(err)
		os.Exit(int(exit))
	}

	response := string(out)
	fmt.Println(response)
    // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
}

运行后会输出JSON格式的元音统计结果:

$ go run main.go
# => {"count":3,"total":3,"vowels":"aeiouAEIOU"}

插件状态

插件可以是状态保持的(stateful)或无状态的(stateless)。我们的元音计数插件会记住它曾经统计过的元音总数:

func main () {
    // ...
    exit, out, err := plugin.Call("count_vowels", []byte("Hello, World!"))
    if err != nil {
        fmt.Println(err)
        os.Exit(int(exit))
    }
    fmt.Println(string(out))
    // => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}

    exit, out, err = plugin.Call("count_vowels", []byte("Hello, World!"))
    if err != nil {
        fmt.Println(err)
        os.Exit(int(exit))
    }
    fmt.Println(string(out))
    // => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"}
}

配置

插件可以接受配置对象。我们的元音计数插件可以配置哪些字符被视为元音:

func main() {
    manifest := extism.Manifest{
        Wasm: []extism.Wasm{
            extism.WasmUrl{
                Url: "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm",
            },
        },
        Config: map[string]string{
            "vowels": "aeiouyAEIOUY",
        },
    }

    ctx := context.Background()
    config := extism.PluginConfig{}

    plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{})

    if err != nil {
        fmt.Printf("Failed to initialize plugin: %v\n", err)
        os.Exit(1)
    }

    exit, out, err := plugin.Call("count_vowels", []byte("Yellow, World!"))
    if err != nil {
        fmt.Println(err)
        os.Exit(int(exit))
    }

    fmt.Println(string(out))
    // => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"}
}

主机函数(Host Functions)

我们可以通过主机函数给插件提供新的能力。下面是一个KV存储的示例:

// 模拟KV存储
kvStore := make(map[string][]byte)

// 定义读取KV的函数
kvRead := extism.NewHostFunctionWithStack(
    "kv_read",
    func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
        key, err := p.ReadString(stack[0])
        if err != nil {
            panic(err)
        }

        value, success := kvStore[key]
        if !success {
            value = []byte{0, 0, 0, 0}
        }

        stack[0], err = p.WriteBytes(value)
    },
    []ValueType{ValueTypePTR},
    []ValueType{ValueTypePTR},
)

// 定义写入KV的函数
kvWrite := extism.NewHostFunctionWithStack(
    "kv_write",
    func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
        key, err := p.ReadString(stack[0])
        if err != nil {
            panic(err)
        }

        value, err := p.ReadBytes(stack[1])
        if err != nil {
            panic(err)
        }

        kvStore[key] = value
    },
    []ValueType{ValueTypePTR, ValueTypePTR},
    []ValueType{},
)

// 创建插件时传入主机函数
plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{kvRead, kvWrite});

启用编译缓存

可以启用编译缓存来加速后续的插件初始化:

ctx := context.Background()
cache := wazero.NewCompilationCache()
defer cache.Close(ctx)

config := PluginConfig{
    EnableWasi:    true,
    ModuleConfig:  wazero.NewModuleConfig(),
    RuntimeConfig: wazero.NewRuntimeConfig().WithCompilationCache(cache),
}

_, err := NewPlugin(ctx, manifest, config, []HostFunction{})

预编译插件

如果需要创建多个插件实例,可以先编译插件:

// 第一步:编译插件
compiledPlugin, err := extism.NewCompiledPlugin(ctx, manifest, config, []extism.HostFunction{})
if err != nil {
    panic(err)
}

// 第二步:从编译好的插件创建实例
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        
        plugin, err := compiledPlugin.Instance(ctx, extism.PluginInstanceConfig{})
        if err != nil {
            fmt.Printf("Failed to initialize plugin: %v\n", err)
            return
        }
        defer plugin.Close(ctx)
        
        data := []byte(fmt.Sprintf("Hello, World from goroutine %d!", id))
        _, out, err := plugin.Call("count_vowels", data)
        if err != nil {
            fmt.Println(err)
            return
        }
        
        fmt.Printf("Goroutine %d result: %s\n", id, string(out))
    }(i)
}
wg.Wait()

启用文件系统访问

WASM插件可以读写外部文件。我们需要在extism.Manifest中添加AllowedPaths映射:

package main

import (
	"context"
	"fmt"
	"os"

	extism "github.com/extism/go-sdk"
)

func main() {
	manifest := extism.Manifest{
		AllowedPaths: map[string]string{
			// 将主机目录data映射到WASM运行时内的/mnt目录
			"data": "/mnt",
		},
		Wasm: []extism.Wasm{
			extism.WasmFile{
				Path: "fs_plugin.wasm",
			},
		},
	}

	ctx := context.Background()
	config := extism.PluginConfig{
		EnableWasi: true,
	}
	plugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{})

	if err != nil {
		fmt.Printf("Failed to initialize plugin: %v\n", err)
		os.Exit(1)
	}

	data := []byte("Hello world, this is written from within our wasm plugin.")
	exit, _, err := plugin.Call("write_file", data)
	if err != nil {
		fmt.Println(err)
		os.Exit(int(exit))
	}
}

构建示例插件

示例插件也是用Go编写的,我们使用TinyGo来编译它们:

cd plugins/config
tinygo build -target wasi -o ../wasm/config.wasm main.go

以上是Extism Go SDK的基本使用方法,更多高级功能可以参考官方文档。


更多关于golang跨语言WebAssembly插件系统开发框架Extism Go SDK的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang跨语言WebAssembly插件系统开发框架Extism Go SDK的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Extism Go SDK 使用指南

Extism 是一个跨语言的 WebAssembly 插件系统,允许你在应用程序中安全地运行用户提供的代码。Extism Go SDK 提供了与 Extism 交互的 Go 语言接口。

安装

首先安装 Extism Go SDK:

go get github.com/extism/go-sdk

基本使用

1. 加载并运行 WebAssembly 模块

package main

import (
	"fmt"
	"github.com/extism/go-sdk"
)

func main() {
	// 从文件加载 wasm 模块
	wasm, err := extism.NewModuleFromFile("example.wasm")
	if err != nil {
		panic(err)
	}

	// 创建插件配置
	config := extism.PluginConfig{
		ModuleConfig: extism.ModuleConfig{
			Wasm: []extism.Wasm{wasm},
		},
	}

	// 创建插件实例
	plugin, err := extism.NewPlugin(config, extism.HostFunctions{}, false)
	if err != nil {
		panic(err)
	}

	// 调用插件中的函数
	input := []byte("Hello, Extism!")
	output, err := plugin.Call("greet", input)
	if err != nil {
		panic(err)
	}

	fmt.Println("Output:", string(output))
}

2. 定义宿主函数

宿主函数允许 WebAssembly 模块调用 Go 代码:

package main

import (
	"fmt"
	"github.com/extism/go-sdk"
)

// 定义宿主函数
func helloWorld(plugin *extism.Plugin, params []extism.Val, returns []extism.Val) extism.Error {
	name, err := plugin.InputString(params[0])
	if err != nil {
		return extism.Error{Code: 1, Message: err.Error()}
	}
	
	greeting := fmt.Sprintf("Hello, %s from Go!", name)
	
	err = returns[0].SetString(greeting, plugin)
	if err != nil {
		return extism.Error{Code: 1, Message: err.Error()}
	}
	
	return extism.Error{}
}

func main() {
	// 创建宿主函数映射
	hostFunctions := extism.HostFunctions{
		"hello_world": helloWorld,
	}

	// 加载 wasm 模块
	wasm, err := extism.NewModuleFromFile("example.wasm")
	if err != nil {
		panic(err)
	}

	// 创建插件配置
	config := extism.PluginConfig{
		ModuleConfig: extism.ModuleConfig{
			Wasm: []extism.Wasm{wasm},
		},
	}

	// 创建插件实例
	plugin, err := extism.NewPlugin(config, hostFunctions, false)
	if err != nil {
		panic(err)
	}

	// 调用插件函数
	output, err := plugin.Call("call_host", []byte("Extism"))
	if err != nil {
		panic(err)
	}

	fmt.Println("Output:", string(output))
}

高级功能

1. 内存管理

// 分配内存
offset, err := plugin.Malloc(len(input))
if err != nil {
	panic(err)
}

// 写入内存
err = plugin.MemWrite(offset, input)
if err != nil {
	panic(err)
}

// 读取内存
output, err := plugin.MemRead(offset, length)
if err != nil {
	panic(err)
}

2. 使用 WASI

config := extism.PluginConfig{
	ModuleConfig: extism.ModuleConfig{
		Wasm: []extism.Wasm{wasm},
	},
	WasiConfig: &extism.WasiConfig{
		Args: []string{"arg1", "arg2"},
		Env: map[string]string{
			"KEY": "VALUE",
		},
		// 其他 WASI 配置...
	},
}

3. 多模块支持

config := extism.PluginConfig{
	ModuleConfig: extism.ModuleConfig{
		Wasm: []extism.Wasm{
			extism.NewModuleFromFile("module1.wasm"),
			extism.NewModuleFromFile("module2.wasm"),
		},
	},
}

性能优化建议

  1. 复用插件实例:创建插件实例有一定开销,尽可能复用
  2. 批量调用:减少 Go 和 WASM 之间的上下文切换
  3. 合理设置内存限制:避免不必要的内存分配
  4. 预编译 WASM:使用 AOT 编译优化性能

错误处理

Extism Go SDK 提供了详细的错误信息:

output, err := plugin.Call("function", input)
if err != nil {
	if extismErr, ok := err.(extism.Error); ok {
		fmt.Printf("Error code: %d, message: %s\n", extismErr.Code, extismErr.Message)
	} else {
		fmt.Println("Non-Extism error:", err)
	}
	return
}

实际应用场景

  1. 用户自定义逻辑:允许用户上传业务逻辑作为插件
  2. 沙盒环境:安全执行不受信任的代码
  3. 多语言集成:在 Go 应用中运行其他语言编写的逻辑
  4. 动态扩展:无需重新编译主程序即可添加新功能

Extism Go SDK 提供了一种安全、高效的方式来集成 WebAssembly 插件到 Go 应用程序中,特别适合需要灵活性和安全性的场景。

回到顶部