#extism #wasm-plugin #host #kit #pdk #interface #export

extism-pdk

Extism插件开发套件(PDK)for Rust

19个版本 (5个稳定版)

1.2.1 2024年8月13日
1.2.0 2024年5月21日
1.1.0 2024年2月22日
1.0.0-rc12023年11月15日
0.1.0 2022年11月30日

#70 in Rust模式

Download history 842/week @ 2024-05-04 796/week @ 2024-05-11 1036/week @ 2024-05-18 1564/week @ 2024-05-25 1462/week @ 2024-06-01 1178/week @ 2024-06-08 929/week @ 2024-06-15 1035/week @ 2024-06-22 523/week @ 2024-06-29 3062/week @ 2024-07-06 2894/week @ 2024-07-13 2469/week @ 2024-07-20 2315/week @ 2024-07-27 1184/week @ 2024-08-03 1017/week @ 2024-08-10 2307/week @ 2024-08-17

7,357 每月下载量
10 个crate中使用 (9个直接使用)

BSD-3-Clause

1MB
476 代码行

Extism Rust PDK

crates.io

此库可用于使用Rust编写Extism插件

安装

使用Cargo生成lib项目

cargo new --lib my-plugin

crates.io添加库。

cargo add extism-pdk

修改你的Cargo.toml以设置crate类型为cdylib(这指示编译器生成动态库,对于我们的目标将是Wasm二进制文件)

[lib]
crate_type = ["cdylib"]

Rustup和wasm32-unknown-unknown安装

下面的示例将使用wasm32-unknown-unknown目标。如果尚未安装,您需要在构建此示例之前进行安装。最简单的方法是使用rustup

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装rustup后,添加wasm32-unknown-unknown目标

rustup target add wasm32-unknown-unknown

入门指南

编写Extism插件的目标是将您的Rust代码编译成Wasm模块,该模块导出供宿主应用调用的函数。您首先应该了解的是创建导出。让我们编写一个简单的程序,该程序导出一个名为greet的函数,该函数接受一个字符串作为名字并返回一个问候字符串。为此,我们在导出函数上使用#[plugin_fn]

use extism_pdk::*;

#[plugin_fn]
pub fn greet(name: String) -> FnResult<String> {
    Ok(format!("Hello, {}!", name))
}

由于我们不需要任何系统访问,我们可以将此编译为轻量级的wasm32-unknown-unknown目标,而不是使用wasm32-wasi目标

cargo build --target wasm32-unknown-unknown

注意:您也可以在.cargo/config.toml中设置默认目标

[build]
target = "wasm32-unknown-unknown"

这将把您的编译好的wasm放在target/wasm32-unknown-unknown/debug。现在我们可以使用Extism CLI的run命令来测试它

extism call target/wasm32-unknown-unknown/debug/my_plugin.wasm greet --input "Benjamin"
# => Hello, Benjamin!

注意:我们还有一个名为Extism Playground的基于Web的插件测试器

关于导出的更多信息

plugin_fn宏添加到您的函数中会做几件事情。它将您的函数作为导出暴露出来,并处理一些底层的ABI细节,使得您可以将Wasm函数声明为普通Rust函数。以下是一些可以定义的导出示例。

原始类型

您可能希望经常做的事情是通过一些原始Rust数据在客户端和宿主之间传递。plugin_fn宏可以为您映射这些类型

注意plugin_fn宏使用convert crate来自动转换并传递跨越客户端/宿主边界的类型。

// f32 and f64
#[plugin_fn]
pub fn add_pi(input: f32) -> FnResult<f64> {
    Ok(input as f64 + 3.14f64)
}

// i32, i64, u32, u64
#[plugin_fn]
pub fn sum_42(input: i32) -> FnResult<i64> {
    Ok(input as i64 + 42i64)
}

// u8 vec
#[plugin_fn]
pub fn process_bytes(input: Vec<u8>) -> FnResult<Vec<u8>> {
    // process bytes here
    Ok(input)
}

// Strings
#[plugin_fn]
pub fn process_string(input: String) -> FnResult<String> {
    // process string here
    Ok(input)
}

JSON

我们提供了一个Json类型,允许您传递实现serde::Deserialize的struct作为参数,以及serde::Serialize作为返回值

#[derive(serde::Deserialize)]
struct Add {
    a: u32,
    b: u32,
}
#[derive(serde::Serialize)]
struct Sum {
    sum: u32,
}

#[plugin_fn]
pub fn add(Json(add): Json<Add>) -> FnResult<Json<Sum>> {
    let sum = Sum { sum: add.a + add.b };
    Ok(Json(sum))
}

同样的事情可以使用extism-convert derive宏完成

#[derive(serde::Deserialize, FromBytes)]
#[encoding(Json)]
struct Add {
    a: u32,
    b: u32,
}

#[derive(serde::Serialize, ToBytes)]
#[encoding(Json)]
struct Sum {
    sum: u32,
}

#[plugin_fn]
pub fn add(add: Add) -> FnResult<Sum> {
    let sum = Sum { sum: add.a + add.b };
    Ok(sum)
}

原始导出接口

plugin_fn是一个很好的宏抽象,但有时您可能需要更多的控制。您可以直接编码到导出函数的原始ABI接口。

#[no_mangle]
pub unsafe extern "C" fn greet() -> i32 {
    let name = unwrap!(input::<String>());
    let result = format!("Hello, {}!", name);
    unwrap!(output(result));
    0i32
}

配置

配置是键值对,可以在创建插件时由宿主传递。这些可以在每个函数调用之间传递一些数据时,静态配置插件非常有用。以下是一个简单的示例

#[plugin_fn]
pub fn greet() -> FnResult<String> {
    let user = config::get("user").expect("'user' key set in config");
    Ok(format!("Hello, {}!", user))
}

要测试它,Extism CLI有一个--config选项,允许您传递key=value

extism call my_plugin.wasm greet --config user=Benjamin
# => Hello, Benjamin!

变量

变量是另一种键值机制,但它是一个可变的数据存储,将在函数调用之间持久存在。只要宿主已加载且未释放插件,这些变量就会持续存在。您可以使用var::getvar::set来操作它们。

#[plugin_fn]
pub fn count() -> FnResult<i64> {
    let mut c = var::get("count")?.unwrap_or(0);
    c = c + 1;
    var::set("count", c)?;
    Ok(c)
}

日志记录

由于Wasm模块默认情况下没有系统访问权限,打印到stdout不会工作(除非您使用WASI)。Extism提供了一些简单的日志宏,允许您使用宿主应用进行日志记录,而无需授予插件执行系统调用的权限。主要的是log!,但我们还有一些按日志级别命名的便利宏

#[plugin_fn]
pub fn log_stuff() -> FnResult<()> {
    log!(LogLevel::Info, "Some info!");
    log!(LogLevel::Warn, "A warning!");
    log!(LogLevel::Error, "An error!");

    // optionally you can use the leveled macros: 
    info!("Some info!");
    warn!("A warning!");
    error!("An error!");

    Ok(())
}

来自 Extism CLI

extism call my_plugin.wasm log_stuff --log-level=info
2023/09/30 11:52:17 Some info!
2023/09/30 11:52:17 A warning!
2023/09/30 11:52:17 An error!

注意:从CLI中,您需要传递一个级别并使用--log-level。如果您使用我们的SDK在自托管主机中运行插件,请确保您调用set_log_file"stdout"或某个文件位置。

HTTP

有时让插件进行HTTP调用很有用。

注意:有关请求和响应类型的更多信息,请参阅HttpRequest文档。

#[plugin_fn]
pub fn http_get(Json(req): Json<HttpRequest>) -> FnResult<Vec<u8>> {
    let res = http::request::<()>(&req, None)?;
    Ok(res.body())
}

导入(主机函数)

与任何其他代码模块一样,Wasm不仅允许您将函数导出给外部世界,还可以导入它们。主机函数允许插件导入在主机中定义的函数。例如,如果您的托管应用程序是用Python编写的,它可以将Python函数传递到您的Rust插件中,您可以在其中调用它。

这个话题可能会相当复杂,我们尚未完全抽象出您需要正确完成此操作所需的Wasm知识。因此,我们建议在开始之前阅读我们的主机函数概念文档

一个简单的示例

主机函数与导出有类似的接口。您只需在您的lib.rs顶部声明它们为extern即可。您只需声明接口,因为实现是主机的责任

#[host_fn]
extern "ExtismHost" {
    fn a_python_func(input: String) -> String; 
}

注意:在底层,此宏将此转换为传递指针作为参数和返回值的接口。如果您想传递原始的、取消引用的wasm值,请参阅下面的原始接口文档。

要在一个特定命名空间中声明主机函数,将模块名称传递给host_fn

#[host_fn("extism:host/user")]

注意:我们接受的数据类型与导出相同,因为接口也使用convert crate

要调用此函数,我们必须使用unsafe关键字。另外请注意,如果调用失败,它会自动将函数返回值用Result包装。

#[plugin_fn]
pub fn hello_from_python() -> FnResult<String> {
    let output = unsafe { a_python_func("An argument to send to Python".into())? };
    Ok(output)
}

测试它

由于某些必须提供实现,我们实际上无法从Extism CLI测试此功能。因此,让我们在这里编写Python端。查看主机SDK文档以实现您选择语言的宿主函数。

from extism import host_fn, Plugin

@host_fn()
def a_python_func(input: str) -> str:
    # just printing this out to prove we're in Python land
    print("Hello from Python!")

    # let's just add "!" to the input string
    # but you could imagine here we could add some
    # applicaiton code like query or manipulate the database
    # or our application APIs
    return input + "!"

现在,当加载插件时,我们传递主机函数

manifest = {"wasm": [{"path": "/path/to/plugin.wasm"}]}
plugin = Plugin(manifest, functions=[a_python_func], wasi=True)
result = plugin.call('hello_from_python', b'').decode('utf-8')
print(result)
python3 app.py
# => Hello from Python!
# => An argument to send to Python!

原始导入接口

与导出一样,我们使用一些魔法将参数和返回值转换为指针。在罕见的情况下,您可能希望将原始wasm值传递到主机(而不是指针)。如果这样做,您需要降级到原始接口。例如,想象一个将两个i64相加的接口

extern "C" {
    fn sum(a: i64, b: i64) -> i64;
}

伸出援手!

有问题或只是想进来打个招呼吗?加入Discord

依赖项

~2.6-4MB
~78K SLoC