19个版本 (5个稳定版)
1.2.1 | 2024年8月13日 |
---|---|
1.2.0 | 2024年5月21日 |
1.1.0 | 2024年2月22日 |
1.0.0-rc1 | 2023年11月15日 |
0.1.0 | 2022年11月30日 |
#70 in Rust模式
7,357 每月下载量
在 10 个crate中使用 (9个直接使用)
1MB
476 代码行
Extism Rust PDK
此库可用于使用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::get和var::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