├── .cargo └── config.toml ├── .github ├── CODEOWNERS ├── actions │ └── libextism │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── count_vowels.wasm ├── derive ├── Cargo.toml ├── LICENSE └── src │ └── lib.rs ├── example-schema.yaml ├── examples ├── count_vowels.rs ├── host_function.rs ├── host_function_host.rs ├── http.rs ├── http_headers.rs ├── reflect.rs └── sum.rs ├── src ├── config.rs ├── extism.rs ├── http.rs ├── lib.rs ├── macros.rs ├── memory.rs ├── to_memory.rs └── var.rs └── test ├── code.wasm ├── host_function.wasm ├── http.wasm └── http_headers.wasm /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zshipko @chrisdickinson 2 | -------------------------------------------------------------------------------- /.github/actions/libextism/action.yml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | name: libextism 4 | 5 | inputs: 6 | gh-token: 7 | description: "A GitHub PAT" 8 | default: ${{ github.token }} 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | repository: extism/cli 16 | path: .extism-cli 17 | - uses: ./.extism-cli/.github/actions/extism-cli 18 | - name: Install 19 | shell: bash 20 | run: sudo extism lib install --version git --github-token ${{ inputs.gh-token }} 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [pull_request, workflow_dispatch] 3 | 4 | jobs: 5 | test-example: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | rust: 11 | - stable 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: ./.github/actions/libextism 15 | 16 | - name: Install Rust 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: ${{ matrix.rust }} 20 | override: true 21 | 22 | - name: Install wasm32 target 23 | run: rustup target add wasm32-unknown-unknown 24 | 25 | - name: Build plugins 26 | run: make -B plugins 27 | 28 | - name: Test call command 29 | run: | 30 | TEST=$(extism call test/code.wasm count_vowels --input "this is a test" --set-config='{"thing": "1", "a": "b"}') 31 | echo $TEST | grep '"count":4' 32 | 33 | TEST=$(extism call test/http.wasm http_get --allow-host '*' --input '{"url": "https://jsonplaceholder.typicode.com/todos/1"}') 34 | echo $TEST | grep '"userId": 1' 35 | 36 | TEST=$(extism call test/host_function.wasm count_vowels --link extism:host/user=test/host_function_host.wasm --input "this is a test" --set-config='{"thing": "1", "a": "b"}') 37 | echo $TEST | grep '"count":40' 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | workflow_dispatch: 5 | 6 | name: Release 7 | 8 | jobs: 9 | publish-derive: 10 | name: publish-derive 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup Rust env 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | profile: minimal 21 | override: true 22 | target: wasm32-unknown-unknown 23 | 24 | - name: Release Rust extism-pdk-derive 25 | continue-on-error: true 26 | env: 27 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }} 28 | run: cargo publish -p extism-pdk-derive 29 | publish-pdk: 30 | name: publish-pdk 31 | needs: publish-derive 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v3 36 | 37 | - name: Setup Rust env 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: stable 41 | profile: minimal 42 | override: true 43 | target: wasm32-unknown-unknown 44 | 45 | - name: Release Rust extism-pdk 46 | env: 47 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }} 48 | run: cargo publish -p extism-pdk 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "extism-pdk" 3 | version = "1.4.1" 4 | edition = "2021" 5 | authors = ["The Extism Authors", "oss@extism.org"] 6 | license = "BSD-3-Clause" 7 | homepage = "https://extism.org" 8 | repository = "https://github.com/extism/rust-pdk" 9 | description = "Extism Plug-in Development Kit (PDK) for Rust" 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | serde = { version = "1", features = ["derive"] } 14 | serde_json = "1" 15 | extism-pdk-derive = { path = "./derive", version = "1.4.1" } 16 | extism-manifest = { version = "1.10.0", optional = true } 17 | extism-convert = { version = "1.10.0", features = ["extism-pdk-path"] } 18 | base64 = "0.22.1" 19 | 20 | [features] 21 | default = ["http", "msgpack"] 22 | http = ["extism-manifest"] 23 | msgpack = ["extism-convert/msgpack"] 24 | protobuf = ["extism-convert/protobuf"] 25 | 26 | [workspace] 27 | members = [ 28 | ".", 29 | "derive" 30 | ] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | plugins: 2 | mkdir -p test 3 | cargo build --release --example count_vowels 4 | cargo build --release --example http 5 | cargo build --release --example http_headers 6 | cargo build --release --example host_function 7 | cargo build --release --example host_function_host 8 | cp target/wasm32-unknown-unknown/release/examples/count_vowels.wasm test/code.wasm 9 | cp target/wasm32-unknown-unknown/release/examples/http.wasm test/http.wasm 10 | cp target/wasm32-unknown-unknown/release/examples/http_headers.wasm test/http_headers.wasm 11 | cp target/wasm32-unknown-unknown/release/examples/host_function.wasm test/host_function.wasm 12 | cp target/wasm32-unknown-unknown/release/examples/host_function_host.wasm test/host_function_host.wasm 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism Rust PDK 2 | 3 | [![crates.io](https://img.shields.io/crates/v/extism_pdk.svg)](https://crates.io/crates/extism-pdk) 4 | 5 | This library can be used to write 6 | [Extism Plug-ins](https://extism.org/docs/concepts/plug-in) in Rust. 7 | 8 | ## Install 9 | 10 | Generate a `lib` project with Cargo: 11 | 12 | ```bash 13 | cargo new --lib my-plugin 14 | ``` 15 | 16 | Add the library from [crates.io](https://crates.io/crates/extism-pdk). 17 | 18 | ```bash 19 | cargo add extism-pdk 20 | ``` 21 | 22 | Change your `Cargo.toml` to set the crate-type to `cdylib` (this instructs the 23 | compiler to produce a dynamic library, which for our target will be a Wasm 24 | binary): 25 | 26 | ```toml 27 | [lib] 28 | crate-type = ["cdylib"] 29 | ``` 30 | 31 | ### Rustup and wasm32-unknown-unknown installation 32 | 33 | Our example below will use the `wasm32-unknown-unknown` target. If this is not 34 | installed you will need to do so before this example will build. The easiest way 35 | to do this is use [`rustup`](https://rustup.rs/). 36 | 37 | ```bash 38 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 39 | ``` 40 | 41 | Once `rustup` is installed, add the `wasm32-unknown-unknown` target: 42 | 43 | ```bash 44 | rustup target add wasm32-unknown-unknown 45 | ``` 46 | 47 | ## Getting Started 48 | 49 | The goal of writing an 50 | [Extism plug-in](https://extism.org/docs/concepts/plug-in) is to compile your 51 | Rust code to a Wasm module with exported functions that the host application can 52 | invoke. The first thing you should understand is creating an export. Let's write 53 | a simple program that exports a `greet` function which will take a name as a 54 | string and return a greeting string. For this, we use the `#[plugin_fn]` macro 55 | on our exported function: 56 | 57 | ```rust 58 | use extism_pdk::*; 59 | 60 | #[plugin_fn] 61 | pub fn greet(name: String) -> FnResult { 62 | Ok(format!("Hello, {}!", name)) 63 | } 64 | ``` 65 | 66 | Since we don't need any system access for this, we can compile this to the 67 | lightweight `wasm32-unknown-unknown` target instead of using the `wasm32-wasip1` 68 | target: 69 | 70 | ```bash 71 | cargo build --target wasm32-unknown-unknown 72 | ``` 73 | 74 | > **Note**: You can also put a default target in `.cargo/config.toml`: 75 | 76 | ```toml 77 | [build] 78 | target = "wasm32-unknown-unknown" 79 | ``` 80 | 81 | This will put your compiled wasm in `target/wasm32-unknown-unknown/debug`. We 82 | can now test it using the [Extism CLI](https://github.com/extism/cli)'s `run` 83 | command: 84 | 85 | ```bash 86 | extism call target/wasm32-unknown-unknown/debug/my_plugin.wasm greet --input "Benjamin" 87 | # => Hello, Benjamin! 88 | ``` 89 | 90 | > **Note**: We also have a web-based, plug-in tester called the 91 | > [Extism Playground](https://playground.extism.org/) 92 | 93 | ### More About Exports 94 | 95 | Adding the 96 | [plugin_fn](https://docs.rs/extism-pdk/latest/extism_pdk/attr.plugin_fn.html) 97 | macro to your function does a couple things. It exposes your function as an 98 | export and it handles some of the lower level ABI details that allow you to 99 | declare your Wasm function as if it were a normal Rust function. Here are a few 100 | examples of exports you can define. 101 | 102 | ### Primitive Types 103 | 104 | A common thing you may want to do is pass some primitive Rust data back and 105 | forth. The 106 | [plugin_fn](https://docs.rs/extism-pdk/latest/extism_pdk/attr.plugin_fn.html) 107 | macro can map these types for you: 108 | 109 | > **Note**: The 110 | > [plugin_fn](https://docs.rs/extism-pdk/latest/extism_pdk/attr.plugin_fn.html) 111 | > macro uses the 112 | > [convert crate](https://github.com/extism/extism/tree/main/convert) to 113 | > automatically convert and pass types across the guest / host boundary. 114 | 115 | ```rust 116 | // f32 and f64 117 | #[plugin_fn] 118 | pub fn add_pi(input: f32) -> FnResult { 119 | Ok(input as f64 + 3.14f64) 120 | } 121 | 122 | // i32, i64, u32, u64 123 | #[plugin_fn] 124 | pub fn sum_42(input: i32) -> FnResult { 125 | Ok(input as i64 + 42i64) 126 | } 127 | 128 | // u8 vec 129 | #[plugin_fn] 130 | pub fn process_bytes(input: Vec) -> FnResult> { 131 | // process bytes here 132 | Ok(input) 133 | } 134 | 135 | // Strings 136 | #[plugin_fn] 137 | pub fn process_string(input: String) -> FnResult { 138 | // process string here 139 | Ok(input) 140 | } 141 | ``` 142 | 143 | ### Json 144 | 145 | We provide a 146 | [Json](https://docs.rs/extism-pdk/latest/extism_pdk/struct.Json.html) type that 147 | allows you to pass structs that implement serde::Deserialize as parameters and 148 | serde::Serialize as returns: 149 | 150 | ```rust 151 | #[derive(serde::Deserialize)] 152 | struct Add { 153 | a: u32, 154 | b: u32, 155 | } 156 | #[derive(serde::Serialize)] 157 | struct Sum { 158 | sum: u32, 159 | } 160 | 161 | #[plugin_fn] 162 | pub fn add(Json(add): Json) -> FnResult> { 163 | let sum = Sum { sum: add.a + add.b }; 164 | Ok(Json(sum)) 165 | } 166 | ``` 167 | 168 | The same thing can be accomplished using the `extism-convert` derive macros: 169 | 170 | ```rust 171 | #[derive(serde::Deserialize, FromBytes)] 172 | #[encoding(Json)] 173 | struct Add { 174 | a: u32, 175 | b: u32, 176 | } 177 | 178 | #[derive(serde::Serialize, ToBytes)] 179 | #[encoding(Json)] 180 | struct Sum { 181 | sum: u32, 182 | } 183 | 184 | #[plugin_fn] 185 | pub fn add(add: Add) -> FnResult { 186 | let sum = Sum { sum: add.a + add.b }; 187 | Ok(sum) 188 | } 189 | ``` 190 | 191 | ### Raw Export Interface 192 | 193 | [plugin_fn](https://docs.rs/extism-pdk/latest/extism_pdk/attr.plugin_fn.html) is 194 | a nice macro abstraction but there may be times where you want more control. You 195 | can code directly to the raw ABI interface of export functions. 196 | 197 | ```rust 198 | #[no_mangle] 199 | pub unsafe extern "C" fn greet() -> i32 { 200 | let name = unwrap!(input::()); 201 | let result = format!("Hello, {}!", name); 202 | unwrap!(output(result)); 203 | 0i32 204 | } 205 | ``` 206 | 207 | ## Configs 208 | 209 | Configs are key-value pairs that can be passed in by the host when creating a 210 | plug-in. These can be useful to statically configure the plug-in with some data 211 | that exists across every function call. Here is a trivial example: 212 | 213 | ```rust 214 | #[plugin_fn] 215 | pub fn greet() -> FnResult { 216 | let user = config::get("user").expect("'user' key set in config"); 217 | Ok(format!("Hello, {}!", user)) 218 | } 219 | ``` 220 | 221 | To test it, the [Extism CLI](https://github.com/extism/cli) has a `--config` 222 | option that lets you pass in `key=value` pairs: 223 | 224 | ```bash 225 | extism call my_plugin.wasm greet --config user=Benjamin 226 | # => Hello, Benjamin! 227 | ``` 228 | 229 | ## Variables 230 | 231 | Variables are another key-value mechanism but it's a mutable data store that 232 | will persist across function calls. These variables will persist as long as the 233 | host has loaded and not freed the plug-in. You can use 234 | [var::get](https://docs.rs/extism-pdk/latest/extism_pdk/var/fn.get.html) and 235 | [var::set](https://docs.rs/extism-pdk/latest/extism_pdk/var/fn.set.html) to 236 | manipulate them. 237 | 238 | ```rust 239 | #[plugin_fn] 240 | pub fn count() -> FnResult { 241 | let mut c = var::get("count")?.unwrap_or(0); 242 | c = c + 1; 243 | var::set("count", c)?; 244 | Ok(c) 245 | } 246 | ``` 247 | 248 | ## Logging 249 | 250 | Because Wasm modules by default do not have access to the system, printing to 251 | stdout won't work (unless you use WASI). Extism provides some simple logging 252 | macros that allow you to use the host application to log without having to give 253 | the plug-in permission to make syscalls. The primary one is 254 | [log!](https://docs.rs/extism-pdk/latest/extism_pdk/macro.log.html) but we also 255 | have some convenience macros named by log level: 256 | 257 | ```rust 258 | #[plugin_fn] 259 | pub fn log_stuff() -> FnResult<()> { 260 | log!(LogLevel::Info, "Some info!"); 261 | log!(LogLevel::Warn, "A warning!"); 262 | log!(LogLevel::Error, "An error!"); 263 | 264 | // optionally you can use the leveled macros: 265 | info!("Some info!"); 266 | warn!("A warning!"); 267 | error!("An error!"); 268 | 269 | Ok(()) 270 | } 271 | ``` 272 | 273 | From [Extism CLI](https://github.com/extism/cli): 274 | 275 | ```bash 276 | extism call my_plugin.wasm log_stuff --log-level=info 277 | 2023/09/30 11:52:17 Some info! 278 | 2023/09/30 11:52:17 A warning! 279 | 2023/09/30 11:52:17 An error! 280 | ``` 281 | 282 | > _Note_: From the CLI you need to pass a level with `--log-level`. If you are 283 | > running the plug-in in your own host using one of our SDKs, you need to make 284 | > sure that you call `set_log_file` to `"stdout"` or some file location. 285 | 286 | ## HTTP 287 | 288 | Sometimes it is useful to let a plug-in make HTTP calls. 289 | 290 | > **Note**: See 291 | > [HttpRequest](https://docs.rs/extism-pdk/latest/extism_pdk/struct.HttpRequest.html) 292 | > docs for more info on the request and response types: 293 | 294 | ```rust 295 | #[plugin_fn] 296 | pub fn http_get(Json(req): Json) -> FnResult> { 297 | let res = http::request::<()>(&req, None)?; 298 | Ok(res.body()) 299 | } 300 | ``` 301 | 302 | ## Imports (Host Functions) 303 | 304 | Like any other code module, Wasm not only let's you export functions to the 305 | outside world, you can import them too. Host Functions allow a plug-in to import 306 | functions defined in the host. For example, if you host application is written 307 | in Python, it can pass a Python function down to your Rust plug-in where you can 308 | invoke it. 309 | 310 | This topic can get fairly complicated and we have not yet fully abstracted the 311 | Wasm knowledge you need to do this correctly. So we recommend reading out 312 | [concept doc on Host Functions](https://extism.org/docs/concepts/host-functions) 313 | before you get started. 314 | 315 | ### A Simple Example 316 | 317 | Host functions have a similar interface as exports. You just need to declare 318 | them as `extern` on the top of your `lib.rs`. You only declare the interface as 319 | it is the host's responsibility to provide the implementation: 320 | 321 | ```rust 322 | #[host_fn] 323 | extern "ExtismHost" { 324 | fn a_python_func(input: String) -> String; 325 | } 326 | ``` 327 | 328 | > **Note**: Under the hood this macro turns this into an interface that passes a 329 | > pointer as an argument and a pointer as a return. If you want to pass raw, 330 | > dereferenced wasm values see the raw interface documentation below. 331 | 332 | To declare a host function in a specific namespace, pass the module name to the 333 | `host_fn` macro: 334 | 335 | ```rust 336 | #[host_fn("extism:host/user")] 337 | ``` 338 | 339 | > **Note**: The types we accept here are the same as the exports as the 340 | > interface also uses the 341 | > [convert crate](https://docs.rs/extism-convert/latest/extism_convert/). 342 | 343 | To call this function, we must use the `unsafe` keyword. Also note that it 344 | automatically wraps the function return with a Result in case the call fails. 345 | 346 | ```rust 347 | #[plugin_fn] 348 | pub fn hello_from_python() -> FnResult { 349 | let output = unsafe { a_python_func("An argument to send to Python".into())? }; 350 | Ok(output) 351 | } 352 | ``` 353 | 354 | ### Testing it out 355 | 356 | We can't really test this from the Extism CLI as something must provide the 357 | implementation. So let's write out the Python side here. Check out the 358 | [docs for Host SDKs](https://extism.org/docs/concepts/host-sdk) to implement a 359 | host function in a language of your choice. 360 | 361 | ```python 362 | from extism import host_fn, Plugin 363 | 364 | @host_fn() 365 | def a_python_func(input: str) -> str: 366 | # just printing this out to prove we're in Python land 367 | print("Hello from Python!") 368 | 369 | # let's just add "!" to the input string 370 | # but you could imagine here we could add some 371 | # applicaiton code like query or manipulate the database 372 | # or our application APIs 373 | return input + "!" 374 | ``` 375 | 376 | Now when we load the plug-in we pass the host function: 377 | 378 | ```python 379 | manifest = {"wasm": [{"path": "/path/to/plugin.wasm"}]} 380 | plugin = Plugin(manifest, functions=[a_python_func], wasi=True) 381 | result = plugin.call('hello_from_python', b'').decode('utf-8') 382 | print(result) 383 | ``` 384 | 385 | ```bash 386 | python3 app.py 387 | # => Hello from Python! 388 | # => An argument to send to Python! 389 | ``` 390 | 391 | ## Raw Import Interface 392 | 393 | Like exports, with imports we do some magic to turn the parameters and returns 394 | into pointers for you. In some rare situations, you might wish to pass raw wasm 395 | values to the host (not pointers). If you do, you need to drop down into a raw 396 | interface. E.g, imagine an interface that sums two i64s 397 | 398 | ```rust 399 | #[link(wasm_import_module = "extism:host/user")] 400 | extern "C" { 401 | fn sum(a: i64, b: i64) -> i64; 402 | } 403 | ``` 404 | 405 | ## Generating Bindings 406 | 407 | It's often very useful to define a schema to describe the function signatures 408 | and types you want to use between Extism SDK and PDK languages. 409 | 410 | [XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source 411 | framework to generate PDK bindings for Extism plug-ins. It's used by the 412 | [XTP Platform](https://www.getxtp.com/), but can be used outside of the platform 413 | to define any Extism compatible plug-in system. 414 | 415 | ### 1. Install the `xtp` CLI. 416 | 417 | See installation instructions 418 | [here](https://docs.xtp.dylibso.com/docs/cli#installation). 419 | 420 | ### 2. Create a schema using our OpenAPI-inspired IDL: 421 | 422 | ```yaml 423 | version: v1-draft 424 | exports: 425 | CountVowels: 426 | input: 427 | type: string 428 | contentType: text/plain; charset=utf-8 429 | output: 430 | $ref: "#/components/schemas/VowelReport" 431 | contentType: application/json 432 | # components.schemas defined in example-schema.yaml... 433 | ``` 434 | 435 | > See an example in [example-schema.yaml](./example-schema.yaml), or a full 436 | > "kitchen sink" example on 437 | > [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/). 438 | 439 | ### 3. Generate bindings to use from your plugins: 440 | 441 | ``` 442 | xtp plugin init --schema-file ./example-schema.yaml 443 | 1. TypeScript 444 | 2. Go 445 | > 3. Rust 446 | 4. Python 447 | 5. C# 448 | 6. Zig 449 | 7. C++ 450 | 8. GitHub Template 451 | 9. Local Template 452 | ``` 453 | 454 | This will create an entire boilerplate plugin project for you to get started 455 | with: 456 | 457 | ```rust 458 | // returns VowelReport (The result of counting vowels on the Vowels input.) 459 | pub(crate) fn count_vowels(input: String ) -> Result { 460 | todo!("Implement count_vowels") 461 | } 462 | ``` 463 | 464 | Implement the empty function(s), and run `xtp plugin build` to compile your 465 | plugin. 466 | 467 | > For more information about XTP Bindgen, see the 468 | > [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and 469 | > the official 470 | > [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema). 471 | 472 | ## Reach Out! 473 | 474 | Have a question or just want to drop in and say hi? 475 | [Hop on the Discord](https://extism.org/discord)! 476 | -------------------------------------------------------------------------------- /count_vowels.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/rust-pdk/ad480f8d7d29a99466197fd08626d5e6945e082b/count_vowels.wasm -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "extism-pdk-derive" 3 | version = "1.4.1" 4 | edition = "2021" 5 | authors = ["The Extism Authors", "oss@extism.org"] 6 | license = "BSD-3-Clause" 7 | homepage = "https://extism.org" 8 | description = "Helper package for the Extism Plug-in Development Kit (PDK) for Rust" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0" 15 | quote = "1.0" 16 | syn = { version = "2.0", features = ["full", "extra-traits"] } 17 | 18 | -------------------------------------------------------------------------------- /derive/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Dylibso, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span}; 2 | use quote::quote; 3 | use syn::{parse_macro_input, FnArg, GenericArgument, ItemFn, ItemForeignMod, PathArguments}; 4 | 5 | /// `plugin_fn` is used to define an Extism callable function to export 6 | /// 7 | /// It should be added to a function you would like to export, the function should 8 | /// accept a parameter that implements `extism_pdk::FromBytes` and return a 9 | /// `extism_pdk::FnResult` that contains a value that implements 10 | /// `extism_pdk::ToBytes`. This maps input and output parameters to Extism input 11 | /// and output instead of using function arguments directly. 12 | /// 13 | /// ## Example 14 | /// 15 | /// ```rust 16 | /// use extism_pdk::{FnResult, plugin_fn}; 17 | /// #[plugin_fn] 18 | /// pub fn greet(name: String) -> FnResult { 19 | /// let s = format!("Hello, {name}"); 20 | /// Ok(s) 21 | /// } 22 | /// ``` 23 | #[proc_macro_attribute] 24 | pub fn plugin_fn( 25 | _attr: proc_macro::TokenStream, 26 | item: proc_macro::TokenStream, 27 | ) -> proc_macro::TokenStream { 28 | let mut function = parse_macro_input!(item as ItemFn); 29 | 30 | if !matches!(function.vis, syn::Visibility::Public(..)) { 31 | panic!("extism_pdk::plugin_fn expects a public function"); 32 | } 33 | 34 | let name = &function.sig.ident; 35 | let constness = &function.sig.constness; 36 | let unsafety = &function.sig.unsafety; 37 | let generics = &function.sig.generics; 38 | let inputs = &mut function.sig.inputs; 39 | let output = &mut function.sig.output; 40 | let block = &function.block; 41 | 42 | let no_args = inputs.is_empty(); 43 | 44 | if name == "main" { 45 | panic!( 46 | "extism_pdk::plugin_fn must not be applied to a `main` function. To fix, rename this to something other than `main`." 47 | ) 48 | } 49 | 50 | match output { 51 | syn::ReturnType::Default => panic!( 52 | "extism_pdk::plugin_fn expects a return value, `()` may be used if no output is needed" 53 | ), 54 | syn::ReturnType::Type(_, t) => { 55 | if let syn::Type::Path(p) = t.as_ref() { 56 | if let Some(t) = p.path.segments.last() { 57 | if t.ident != "FnResult" { 58 | panic!("extism_pdk::plugin_fn expects a function that returns extism_pdk::FnResult"); 59 | } 60 | } else { 61 | panic!("extism_pdk::plugin_fn expects a function that returns extism_pdk::FnResult"); 62 | } 63 | } 64 | } 65 | } 66 | 67 | if no_args { 68 | quote! { 69 | #[no_mangle] 70 | pub #constness #unsafety extern "C" fn #name() -> i32 { 71 | #constness #unsafety fn inner #generics() #output { 72 | #block 73 | } 74 | 75 | let output = match inner() { 76 | core::result::Result::Ok(x) => x, 77 | core::result::Result::Err(rc) => { 78 | let err = format!("{:?}", rc.0); 79 | let mut mem = extism_pdk::Memory::from_bytes(&err).unwrap(); 80 | unsafe { 81 | extism_pdk::extism::error_set(mem.offset()); 82 | } 83 | return rc.1; 84 | } 85 | }; 86 | extism_pdk::unwrap!(extism_pdk::output(&output)); 87 | 0 88 | } 89 | } 90 | .into() 91 | } else { 92 | quote! { 93 | #[no_mangle] 94 | pub #constness #unsafety extern "C" fn #name() -> i32 { 95 | #constness #unsafety fn inner #generics(#inputs) #output { 96 | #block 97 | } 98 | 99 | let input = extism_pdk::unwrap!(extism_pdk::input()); 100 | let output = match inner(input) { 101 | core::result::Result::Ok(x) => x, 102 | core::result::Result::Err(rc) => { 103 | let err = format!("{:?}", rc.0); 104 | let mut mem = extism_pdk::Memory::from_bytes(&err).unwrap(); 105 | unsafe { 106 | extism_pdk::extism::error_set(mem.offset()); 107 | } 108 | return rc.1; 109 | } 110 | }; 111 | extism_pdk::unwrap!(extism_pdk::output(&output)); 112 | 0 113 | } 114 | } 115 | .into() 116 | } 117 | } 118 | 119 | /// `shared_fn` is used to define a function that will be exported by a plugin but is not directly 120 | /// callable by an Extism runtime. These functions can be used for runtime linking and mocking host 121 | /// functions for tests. If direct access to Wasm native parameters is needed, then a bare 122 | /// `extern "C" fn` should be used instead. 123 | /// 124 | /// All arguments should implement `extism_pdk::ToBytes` and the return value should implement 125 | /// `extism_pdk::FromBytes`, if `()` or `SharedFnResult<()>` then no value will be returned. 126 | /// ## Example 127 | /// 128 | /// ```rust 129 | /// use extism_pdk::{SharedFnResult, shared_fn}; 130 | /// #[shared_fn] 131 | /// pub fn greet2(greeting: String, name: String) -> SharedFnResult { 132 | /// let s = format!("{greeting}, {name}"); 133 | /// Ok(name) 134 | /// } 135 | /// ``` 136 | #[proc_macro_attribute] 137 | pub fn shared_fn( 138 | _attr: proc_macro::TokenStream, 139 | item: proc_macro::TokenStream, 140 | ) -> proc_macro::TokenStream { 141 | let mut function = parse_macro_input!(item as ItemFn); 142 | 143 | if !matches!(function.vis, syn::Visibility::Public(..)) { 144 | panic!("extism_pdk::shared_fn expects a public function"); 145 | } 146 | 147 | let name = &function.sig.ident; 148 | let constness = &function.sig.constness; 149 | let unsafety = &function.sig.unsafety; 150 | let generics = &function.sig.generics; 151 | let inputs = &mut function.sig.inputs; 152 | let output = &mut function.sig.output; 153 | let block = &function.block; 154 | 155 | let (raw_inputs, raw_args): (Vec<_>, Vec<_>) = inputs 156 | .iter() 157 | .enumerate() 158 | .map(|(i, x)| { 159 | let t = match x { 160 | FnArg::Receiver(_) => { 161 | panic!("Receiver argument (self) cannot be used in extism_pdk::shared_fn") 162 | } 163 | FnArg::Typed(t) => &t.ty, 164 | }; 165 | let arg = Ident::new(&format!("arg{i}"), Span::call_site()); 166 | ( 167 | quote! { #arg: extism_pdk::MemoryPointer<#t> }, 168 | quote! { #arg.get()? }, 169 | ) 170 | }) 171 | .unzip(); 172 | 173 | if name == "main" { 174 | panic!( 175 | "export_pdk::shared_fn must not be applied to a `main` function. To fix, rename this to something other than `main`." 176 | ) 177 | } 178 | 179 | let (no_result, raw_output) = match output { 180 | syn::ReturnType::Default => (true, quote! {}), 181 | syn::ReturnType::Type(_, t) => { 182 | let mut is_unit = false; 183 | if let syn::Type::Path(p) = t.as_ref() { 184 | if let Some(t) = p.path.segments.last() { 185 | if t.ident != "SharedFnResult" { 186 | panic!("extism_pdk::shared_fn expects a function that returns extism_pdk::SharedFnResult"); 187 | } 188 | match &t.arguments { 189 | PathArguments::AngleBracketed(args) => { 190 | if args.args.len() == 1 { 191 | match &args.args[0] { 192 | GenericArgument::Type(syn::Type::Tuple(t)) => { 193 | if t.elems.is_empty() { 194 | is_unit = true; 195 | } 196 | } 197 | _ => (), 198 | } 199 | } 200 | } 201 | _ => (), 202 | } 203 | } else { 204 | panic!("extism_pdk::shared_fn expects a function that returns extism_pdk::SharedFnResult"); 205 | } 206 | }; 207 | if is_unit { 208 | (true, quote! {}) 209 | } else { 210 | (false, quote! {-> u64 }) 211 | } 212 | } 213 | }; 214 | 215 | if no_result { 216 | quote! { 217 | #[no_mangle] 218 | pub #constness #unsafety extern "C" fn #name(#(#raw_inputs,)*) { 219 | #constness #unsafety fn inner #generics(#inputs) -> extism_pdk::SharedFnResult<()> { 220 | #block 221 | } 222 | 223 | 224 | let r = || inner(#(#raw_args,)*); 225 | if let Err(rc) = r() { 226 | panic!("{}", rc.to_string()); 227 | } 228 | } 229 | } 230 | .into() 231 | } else { 232 | quote! { 233 | #[no_mangle] 234 | pub #constness #unsafety extern "C" fn #name(#(#raw_inputs,)*) #raw_output { 235 | #constness #unsafety fn inner #generics(#inputs) #output { 236 | #block 237 | } 238 | 239 | let r = || inner(#(#raw_args,)*); 240 | match r().and_then(|x| extism_pdk::Memory::new(&x)) { 241 | core::result::Result::Ok(mem) => { 242 | mem.offset() 243 | }, 244 | core::result::Result::Err(rc) => { 245 | panic!("{}", rc.to_string()); 246 | } 247 | } 248 | } 249 | } 250 | .into() 251 | } 252 | } 253 | 254 | /// `host_fn` is used to import a host function from an `extern` block 255 | #[proc_macro_attribute] 256 | pub fn host_fn( 257 | attr: proc_macro::TokenStream, 258 | item: proc_macro::TokenStream, 259 | ) -> proc_macro::TokenStream { 260 | let namespace = if let Ok(ns) = syn::parse::(attr) { 261 | ns.value() 262 | } else { 263 | "extism:host/user".to_string() 264 | }; 265 | 266 | let item = parse_macro_input!(item as ItemForeignMod); 267 | if item.abi.name.is_none() || item.abi.name.unwrap().value() != "ExtismHost" { 268 | panic!("Expected `extern \"ExtismHost\"` block"); 269 | } 270 | let functions = item.items; 271 | 272 | let mut gen = quote!(); 273 | 274 | for function in functions { 275 | if let syn::ForeignItem::Fn(function) = function { 276 | let name = &function.sig.ident; 277 | let original_inputs = function.sig.inputs.clone(); 278 | let output = &function.sig.output; 279 | 280 | let vis = &function.vis; 281 | let generics = &function.sig.generics; 282 | let mut into_inputs = vec![]; 283 | let mut converted_inputs = vec![]; 284 | 285 | let (output_is_ptr, converted_output) = match output { 286 | syn::ReturnType::Default => (false, quote!(())), 287 | syn::ReturnType::Type(_, _) => (true, quote!(u64)), 288 | }; 289 | 290 | for input in &original_inputs { 291 | match input { 292 | syn::FnArg::Typed(t) => { 293 | let mut input = t.clone(); 294 | input.ty = Box::new(syn::Type::Verbatim(quote!(u64))); 295 | converted_inputs.push(syn::FnArg::Typed(input)); 296 | match &*t.pat { 297 | syn::Pat::Ident(i) => { 298 | into_inputs 299 | .push(quote!( 300 | extism_pdk::ManagedMemory::from(extism_pdk::ToMemory::to_memory(&&#i)?).offset() 301 | )); 302 | } 303 | _ => panic!("invalid host function argument"), 304 | } 305 | } 306 | _ => panic!("self arguments are not permitted in host functions"), 307 | } 308 | } 309 | 310 | let impl_name = syn::Ident::new(&format!("{name}_impl"), name.span()); 311 | let link_name = name.to_string(); 312 | let link_name = link_name.as_str(); 313 | 314 | let impl_block = quote! { 315 | #[link(wasm_import_module = #namespace)] 316 | extern "C" { 317 | #[link_name = #link_name] 318 | fn #impl_name(#(#converted_inputs),*) -> #converted_output; 319 | } 320 | }; 321 | 322 | let output = match output { 323 | syn::ReturnType::Default => quote!(()), 324 | syn::ReturnType::Type(_, ty) => quote!(#ty), 325 | }; 326 | 327 | if output_is_ptr { 328 | gen = quote! { 329 | #gen 330 | 331 | #impl_block 332 | 333 | #vis unsafe fn #name #generics (#original_inputs) -> core::result::Result<#output, extism_pdk::Error> { 334 | let res = extism_pdk::Memory::from(#impl_name(#(#into_inputs),*)); 335 | <#output as extism_pdk::FromBytes>::from_bytes(&res.to_vec()) 336 | } 337 | }; 338 | } else { 339 | gen = quote! { 340 | #gen 341 | 342 | #impl_block 343 | 344 | #vis unsafe fn #name #generics (#original_inputs) -> core::result::Result<#output, extism_pdk::Error> { 345 | let res = #impl_name(#(#into_inputs),*); 346 | core::result::Result::Ok(res) 347 | } 348 | }; 349 | } 350 | } 351 | } 352 | 353 | gen.into() 354 | } 355 | -------------------------------------------------------------------------------- /example-schema.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://xtp.dylibso.com/assets/wasm/schema.json 2 | # Learn more at https://docs.xtp.dylibso.com/docs/concepts/xtp-schema 3 | version: v1-draft 4 | exports: 5 | CountVowels: 6 | input: 7 | type: string 8 | contentType: text/plain; charset=utf-8 9 | output: 10 | $ref: "#/components/schemas/VowelReport" 11 | contentType: application/json 12 | components: 13 | schemas: 14 | VowelReport: 15 | description: The result of counting vowels on the Vowels input. 16 | properties: 17 | count: 18 | type: integer 19 | format: int32 20 | description: The count of vowels for input string. 21 | total: 22 | type: integer 23 | format: int32 24 | description: The cumulative amount of vowels counted, if this keeps state across multiple function calls. 25 | nullable: true 26 | vowels: 27 | type: string 28 | description: The set of vowels used to get the count, e.g. "aAeEiIoOuU" -------------------------------------------------------------------------------- /examples/count_vowels.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use extism_pdk::*; 4 | 5 | const VOWELS: &[char] = &['a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U']; 6 | 7 | #[derive(serde::Serialize, ToBytes)] 8 | #[encoding(Json)] 9 | struct TestOutput { 10 | pub count: i32, 11 | // pub config: String, 12 | // pub a: String, 13 | // pub b: &'a str, 14 | } 15 | 16 | #[plugin_fn] 17 | pub unsafe fn count_vowels<'a>(input: String) -> FnResult { 18 | let mut count = 0; 19 | for ch in input.chars() { 20 | if VOWELS.contains(&ch) { 21 | count += 1; 22 | } 23 | } 24 | 25 | // set_var!("a", "this is var a")?; 26 | 27 | // let a = var::get("a")?.expect("variable 'a' set"); 28 | // let a = String::from_utf8(a).expect("string from varible value"); 29 | // let config = config::get("thing")?.expect("'thing' key set in config"); 30 | // let b = "new_value"; 31 | 32 | let output = TestOutput { 33 | count, 34 | // config, 35 | // a, 36 | // b, 37 | }; 38 | Ok(output) 39 | } 40 | -------------------------------------------------------------------------------- /examples/host_function.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use extism_pdk::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | const VOWELS: &[char] = &['a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U']; 7 | 8 | #[derive(Serialize, Deserialize, ToBytes, FromBytes)] 9 | #[encoding(Json)] 10 | struct Output { 11 | pub count: i32, 12 | } 13 | 14 | #[host_fn("extism:host/user")] 15 | extern "ExtismHost" { 16 | fn hello_world(count: Output) -> Output; 17 | } 18 | 19 | #[plugin_fn] 20 | pub unsafe fn count_vowels<'a>(input: String) -> FnResult { 21 | let mut count = 0; 22 | for ch in input.chars() { 23 | if VOWELS.contains(&ch) { 24 | count += 1; 25 | } 26 | } 27 | 28 | let output = Output { count }; 29 | let output = unsafe { hello_world(output)? }; 30 | Ok(output) 31 | } 32 | -------------------------------------------------------------------------------- /examples/host_function_host.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use extism_pdk::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, ToBytes, FromBytes)] 7 | #[encoding(Json)] 8 | struct Output { 9 | pub count: i32, 10 | } 11 | 12 | #[extism_pdk::shared_fn] 13 | pub unsafe fn hello_world(mut input: Output) -> SharedFnResult { 14 | info!("Hello, world!"); 15 | input.count *= 10; 16 | Ok(input) 17 | } 18 | -------------------------------------------------------------------------------- /examples/http.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use extism_pdk::*; 4 | 5 | #[plugin_fn] 6 | pub fn http_get(Json(req): Json) -> FnResult { 7 | trace!("HTTP Request: {:?}", req); 8 | info!("Request to: {}", req.url); 9 | let res = http::request::<()>(&req, None)?; 10 | Ok(res.into_memory()) 11 | } 12 | -------------------------------------------------------------------------------- /examples/http_headers.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::collections::HashMap; 4 | 5 | use extism_pdk::*; 6 | 7 | #[plugin_fn] 8 | pub fn http_get(Json(req): Json) -> FnResult>> { 9 | info!("Request to: {}", req.url); 10 | let res = http::request::<()>(&req, None)?; 11 | Ok(Json(res.headers().clone())) 12 | } 13 | -------------------------------------------------------------------------------- /examples/reflect.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use extism_pdk::*; 4 | 5 | #[shared_fn] 6 | pub fn host_reflect(input: String) -> SharedFnResult> { 7 | Ok(input.to_lowercase().into_bytes()) 8 | } 9 | 10 | #[shared_fn] 11 | pub fn nothing() -> SharedFnResult<()> { 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /examples/sum.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use extism_pdk::*; 4 | 5 | #[derive(serde::Deserialize, FromBytes)] 6 | #[encoding(Json)] 7 | struct Add { 8 | a: u32, 9 | b: u32, 10 | } 11 | 12 | #[derive(serde::Serialize, ToBytes)] 13 | #[encoding(Json)] 14 | struct Sum { 15 | sum: u32, 16 | } 17 | 18 | #[plugin_fn] 19 | pub fn add(add: Add) -> FnResult { 20 | let sum = Sum { sum: add.a + add.b }; 21 | Ok(sum) 22 | } 23 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn get_memory(key: impl AsRef) -> Result, Error> { 4 | let mem = Memory::from_bytes(key.as_ref().as_bytes())?; 5 | 6 | let offset = unsafe { extism::config_get(mem.offset()) }; 7 | if offset == 0 { 8 | return Ok(None); 9 | } 10 | 11 | let len = unsafe { extism::length(offset) }; 12 | if len == 0 { 13 | return Ok(None); 14 | } 15 | 16 | Ok(Some(Memory(MemoryHandle { 17 | offset, 18 | length: len, 19 | }))) 20 | } 21 | 22 | /// Gets a config item passed in from the host. This item is read-only 23 | /// and static throughout the lifetime of the plug-in. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// * `key` - A unique string key to identify the variable 28 | /// 29 | /// # Examples 30 | /// 31 | /// ``` 32 | /// // let's assume we have a config object: { my_config: 42u32 } 33 | /// // which is a u32. We can default to 0 first time we fetch it if it's not present 34 | /// let my_config = config::get("my_config")?.unwrap_or(0u32); 35 | /// ``` 36 | pub fn get(key: impl AsRef) -> Result, Error> { 37 | Ok(get_memory(key)?.map(|x| x.to_string().expect("Config value is not a valid string"))) 38 | } 39 | -------------------------------------------------------------------------------- /src/extism.rs: -------------------------------------------------------------------------------- 1 | #[link(wasm_import_module = "extism:host/env")] 2 | extern "C" { 3 | pub fn input_length() -> u64; 4 | pub fn input_load_u8(offs: u64) -> u8; 5 | pub fn input_load_u64(offs: u64) -> u64; 6 | pub fn length(offs: u64) -> u64; 7 | pub fn length_unsafe(offs: u64) -> u64; 8 | pub fn alloc(length: u64) -> u64; 9 | pub fn free(offs: u64); 10 | pub fn output_set(offs: u64, length: u64); 11 | pub fn error_set(offs: u64); 12 | pub fn store_u8(offs: u64, data: u8); 13 | pub fn load_u8(offs: u64) -> u8; 14 | pub fn store_u64(offs: u64, data: u64); 15 | pub fn load_u64(offs: u64) -> u64; 16 | pub fn config_get(offs: u64) -> u64; 17 | pub fn var_get(offs: u64) -> u64; 18 | pub fn var_set(offs: u64, offs1: u64); 19 | pub fn http_request(req: u64, body: u64) -> u64; 20 | pub fn http_status_code() -> i32; 21 | pub fn http_headers() -> u64; 22 | pub fn log_info(offs: u64); 23 | pub fn log_debug(offs: u64); 24 | pub fn log_warn(offs: u64); 25 | pub fn log_error(offs: u64); 26 | pub fn log_trace(offs: u64); 27 | pub fn get_log_level() -> i32; 28 | } 29 | 30 | /// Loads a byte array from Extism's memory. Only use this if you 31 | /// have already considered the plugin_fn macro as well as the `extism_load_input` function. 32 | /// 33 | /// # Arguments 34 | /// 35 | /// * `offs` - The Extism offset pointer location to the memory 36 | /// * `data` - The pointer to byte slice result 37 | pub unsafe fn load(offs: u64, data: &mut [u8]) { 38 | let len = data.len(); 39 | // x >> 3 == x / 8 40 | let chunk_count = len >> 3; 41 | 42 | let mut_ptr = data.as_mut_ptr() as *mut u64; 43 | for chunk_idx in 0..chunk_count { 44 | let x = load_u64(offs + (chunk_idx << 3) as u64); 45 | mut_ptr.add(chunk_idx).write(x); 46 | } 47 | 48 | // x % 8 == x & 7 49 | let remainder = len & 7; 50 | let remainder_offset = chunk_count << 3; 51 | // Allow the needless range loop because clippy wants to turn this into 52 | // iter_mut().enumerate().skip().take(), which is less readable IMO! 53 | #[allow(clippy::needless_range_loop)] 54 | for index in remainder_offset..(remainder + remainder_offset) { 55 | data[index] = load_u8(offs + index as u64); 56 | } 57 | } 58 | 59 | /// Loads the input from the host as a raw byte vec. 60 | /// Consider using the plugin_fn macro to automatically 61 | /// handle inputs as function parameters. 62 | pub unsafe fn load_input() -> Vec { 63 | let len = input_length() as usize; 64 | let mut data = vec![0; len]; 65 | let chunk_count = len >> 3; 66 | 67 | let mut_ptr = data.as_mut_ptr() as *mut u64; 68 | for chunk_idx in 0..chunk_count { 69 | let x = input_load_u64((chunk_idx << 3) as u64); 70 | mut_ptr.add(chunk_idx).write(x); 71 | } 72 | 73 | let remainder = len & 7; 74 | let remainder_offset = chunk_count << 3; 75 | #[allow(clippy::needless_range_loop)] 76 | for index in remainder_offset..(remainder + remainder_offset) { 77 | data[index] = input_load_u8(index as u64); 78 | } 79 | 80 | data 81 | } 82 | 83 | /// Stores a byte array into Extism's memory. 84 | /// Only use this after considering [] 85 | /// 86 | /// # Arguments 87 | /// 88 | /// * `offs` - The Extism offset pointer location to store the memory 89 | /// * `data` - The byte array to store at that offset 90 | pub unsafe fn store(offs: u64, data: &[u8]) { 91 | let len = data.len(); 92 | let chunk_count = len >> 3; 93 | 94 | let ptr = data.as_ptr() as *const u64; 95 | for chunk_idx in 0..chunk_count { 96 | store_u64(offs + (chunk_idx << 3) as u64, ptr.add(chunk_idx).read()); 97 | } 98 | 99 | let remainder = len & 7; 100 | let remainder_offset = chunk_count << 3; 101 | #[allow(clippy::needless_range_loop)] 102 | for index in remainder_offset..(remainder + remainder_offset) { 103 | store_u8(offs + index as u64, data[index]); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::*; 4 | 5 | /// A HttpResponse is used to wrap the memory returned by 6 | /// `extism_pdk::http::request` 7 | pub struct HttpResponse { 8 | memory: Memory, 9 | status: u16, 10 | headers: HashMap, 11 | } 12 | 13 | impl HttpResponse { 14 | pub fn into_memory(self) -> Memory { 15 | self.memory 16 | } 17 | 18 | pub fn status_code(&self) -> u16 { 19 | self.status 20 | } 21 | 22 | pub fn as_memory(&self) -> &Memory { 23 | &self.memory 24 | } 25 | 26 | pub fn body(&self) -> Vec { 27 | self.memory.to_vec() 28 | } 29 | 30 | pub fn json(&self) -> Result { 31 | let x = serde_json::from_slice(&self.body())?; 32 | Ok(x) 33 | } 34 | 35 | pub fn headers(&self) -> &HashMap { 36 | &self.headers 37 | } 38 | 39 | pub fn header(&self, s: impl AsRef) -> Option<&str> { 40 | self.headers.get(s.as_ref()).map(|x| x.as_ref()) 41 | } 42 | } 43 | 44 | /// Execute `HttpRequest`, if `body` is not `None` then it will be sent as the 45 | /// request body. 46 | pub fn request( 47 | req: &extism_manifest::HttpRequest, 48 | body: Option, 49 | ) -> Result { 50 | let enc = serde_json::to_vec(req)?; 51 | let req = Memory::from_bytes(enc)?; 52 | let body = match body { 53 | Some(b) => Some(b.to_memory()?), 54 | None => None, 55 | }; 56 | let data = body.as_ref().map(|x| x.offset()).unwrap_or(0); 57 | let offs = unsafe { extism::http_request(req.offset(), data) }; 58 | let status = unsafe { extism::http_status_code() }; 59 | let len = unsafe { extism::length_unsafe(offs) }; 60 | 61 | let headers = unsafe { extism::http_headers() }; 62 | let headers = if headers == 0 { 63 | HashMap::new() 64 | } else { 65 | if let Some(h) = Memory::find(headers) { 66 | let Json(j) = h.to()?; 67 | h.free(); 68 | j 69 | } else { 70 | HashMap::new() 71 | } 72 | }; 73 | 74 | Ok(HttpResponse { 75 | memory: Memory(MemoryHandle { 76 | offset: offs, 77 | length: len, 78 | }), 79 | status: status as u16, 80 | headers, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_safety_doc)] 2 | 3 | #[cfg(target_arch = "wasm32")] 4 | pub use std::arch::wasm32::v128; 5 | 6 | mod macros; 7 | 8 | pub mod extism; 9 | pub mod memory; 10 | mod to_memory; 11 | 12 | /// Functions to read plug-in config 13 | pub mod config; 14 | 15 | /// Functions to manipulate plug-in variables 16 | pub mod var; 17 | 18 | #[cfg(feature = "http")] 19 | /// Types and functions for making HTTP requests 20 | pub mod http; 21 | 22 | pub use anyhow::Error; 23 | pub use extism_convert::*; 24 | pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes}; 25 | pub use extism_pdk_derive::{host_fn, plugin_fn, shared_fn}; 26 | pub use memory::{ManagedMemory, Memory, MemoryPointer}; 27 | pub use to_memory::ToMemory; 28 | 29 | #[cfg(feature = "http")] 30 | /// HTTP request type 31 | pub use extism_manifest::HttpRequest; 32 | 33 | #[cfg(feature = "http")] 34 | /// HTTP response type 35 | pub use http::HttpResponse; 36 | 37 | /// The return type of a plugin function 38 | pub type FnResult = Result>; 39 | 40 | /// The return type of a `shared_fn` 41 | pub type SharedFnResult = Result; 42 | 43 | /// Logging levels 44 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 45 | pub enum LogLevel { 46 | Trace, 47 | Debug, 48 | Info, 49 | Warn, 50 | Error, 51 | } 52 | 53 | impl LogLevel { 54 | pub const fn to_int(self) -> i32 { 55 | match self { 56 | LogLevel::Trace => 0, 57 | LogLevel::Debug => 1, 58 | LogLevel::Info => 2, 59 | LogLevel::Warn => 3, 60 | LogLevel::Error => 4, 61 | } 62 | } 63 | } 64 | 65 | /// Re-export of `serde_json` 66 | pub use serde_json as json; 67 | 68 | /// Base64 string 69 | pub struct Base64(pub String); 70 | 71 | /// Get input bytes from host 72 | pub fn input_bytes() -> Vec { 73 | unsafe { extism::load_input() } 74 | } 75 | 76 | /// Get input bytes from host and convert into `T` 77 | pub fn input() -> Result { 78 | let data = input_bytes(); 79 | T::from_bytes_owned(&data) 80 | } 81 | 82 | /// Set output for host 83 | pub fn output(data: T) -> Result<(), Error> { 84 | let data = data.to_memory()?; 85 | data.set_output(); 86 | Ok(()) 87 | } 88 | 89 | pub struct WithReturnCode(pub T, pub i32); 90 | 91 | impl> From for WithReturnCode { 92 | fn from(value: E) -> Self { 93 | WithReturnCode::new(value.into(), -1) 94 | } 95 | } 96 | 97 | impl std::fmt::Debug for WithReturnCode { 98 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 99 | self.0.fmt(f) 100 | } 101 | } 102 | 103 | impl ToMemory for WithReturnCode { 104 | fn to_memory(&self) -> Result { 105 | self.0.to_memory() 106 | } 107 | 108 | fn status(&self) -> i32 { 109 | self.1 110 | } 111 | } 112 | 113 | impl WithReturnCode { 114 | pub fn new(x: T, status: i32) -> Self { 115 | WithReturnCode(x, status) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! log { 3 | ($lvl:expr, $($arg:tt)+) => {{ 4 | let level = unsafe { $crate::extism::get_log_level() }; 5 | if $lvl.to_int() >= level && level != i32::MAX { 6 | let fmt = format!($($arg)+); 7 | let memory = $crate::Memory::from_bytes(&fmt).unwrap(); 8 | memory.log($lvl) 9 | } 10 | }} 11 | } 12 | 13 | #[macro_export] 14 | macro_rules! info { 15 | ($($arg:tt)+) => { 16 | $crate::log!($crate::LogLevel::Info, $($arg)+) 17 | } 18 | } 19 | 20 | #[macro_export] 21 | macro_rules! debug { 22 | ($($arg:tt)+) => { 23 | $crate::log!($crate::LogLevel::Debug, $($arg)+) 24 | } 25 | } 26 | 27 | #[macro_export] 28 | macro_rules! warn { 29 | ($($arg:tt)+) => { 30 | $crate::log!($crate::LogLevel::Warn, $($arg)+) 31 | } 32 | } 33 | 34 | #[macro_export] 35 | macro_rules! error { 36 | ($($arg:tt)+) => { 37 | $crate::log!($crate::LogLevel::Error, $($arg)+) 38 | } 39 | } 40 | 41 | #[macro_export] 42 | macro_rules! trace { 43 | ($($arg:tt)+) => { 44 | $crate::log!($crate::LogLevel::Trace, $($arg)+) 45 | } 46 | } 47 | 48 | #[macro_export] 49 | macro_rules! unwrap { 50 | ($x:expr) => { 51 | match $x { 52 | Ok(x) => x, 53 | Err(e) => { 54 | let err = format!("{:?}", e); 55 | let mut mem = $crate::Memory::from_bytes(&err).unwrap(); 56 | unsafe { 57 | $crate::extism::error_set(mem.offset()); 58 | } 59 | return -1; 60 | } 61 | } 62 | }; 63 | } 64 | 65 | #[macro_export] 66 | macro_rules! set_var { 67 | ($k:expr, $($arg:tt)+) => { 68 | $crate::var::set($k, &format!($($arg)+)) 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub struct Memory(pub MemoryHandle); 4 | 5 | pub struct ManagedMemory(pub Memory); 6 | 7 | pub mod internal { 8 | use super::*; 9 | 10 | pub fn memory_alloc(n: u64) -> MemoryHandle { 11 | let length = n; 12 | let offset = unsafe { extism::alloc(length) }; 13 | MemoryHandle { offset, length } 14 | } 15 | 16 | pub fn memory_free(handle: MemoryHandle) { 17 | unsafe { extism::free(handle.offset) } 18 | } 19 | 20 | pub fn memory_bytes(handle: MemoryHandle) -> Vec { 21 | let mut data = vec![0; handle.length as usize]; 22 | unsafe { extism::load(handle.offset, &mut data) }; 23 | data 24 | } 25 | 26 | /// Get the length of the memory handle stored at the given offset, this will return 0 if called on a non-handle pointer 27 | pub fn memory_length(offs: u64) -> u64 { 28 | unsafe { extism::length(offs) } 29 | } 30 | 31 | /// Get the length of the memory handle stored at the given offset, this may return garbage if called on a non-handle pointer 32 | pub fn memory_length_unsafe(offs: u64) -> u64 { 33 | unsafe { extism::length_unsafe(offs) } 34 | } 35 | 36 | /// Load data from memory into a `u8` slice 37 | pub fn load(handle: MemoryHandle, mut buf: impl AsMut<[u8]>) { 38 | let buf = buf.as_mut(); 39 | unsafe { 40 | extism::load(handle.offset, &mut buf[0..handle.length as usize]); 41 | } 42 | } 43 | 44 | /// Load data from `u8` slice into memory 45 | pub fn store(handle: MemoryHandle, buf: impl AsRef<[u8]>) { 46 | let buf = buf.as_ref(); 47 | unsafe { extism::store(handle.offset, &buf[0..handle.length as usize]) } 48 | } 49 | 50 | /// Find `Memory` by offset 51 | pub fn find(offset: u64) -> Option { 52 | let length = unsafe { extism::length(offset) }; 53 | 54 | if length == 0 { 55 | return None; 56 | } 57 | 58 | Some(MemoryHandle { offset, length }) 59 | } 60 | } 61 | 62 | impl Memory { 63 | pub fn offset(&self) -> u64 { 64 | self.0.offset 65 | } 66 | 67 | pub fn len(&self) -> usize { 68 | self.0.length as usize 69 | } 70 | 71 | pub fn is_empty(&self) -> bool { 72 | self.0.length == 0 73 | } 74 | 75 | pub fn null() -> Self { 76 | Memory(MemoryHandle { 77 | offset: 0, 78 | length: 0, 79 | }) 80 | } 81 | 82 | /// Allocate a new block with an encoded value 83 | pub fn new<'a, T: ToBytes<'a>>(x: &T) -> Result { 84 | let data = x.to_bytes()?; 85 | let data = data.as_ref(); 86 | let length = data.len() as u64; 87 | let offset = unsafe { extism::alloc(length) }; 88 | unsafe { extism::store(offset, data) }; 89 | Ok(Self(MemoryHandle { offset, length })) 90 | } 91 | 92 | /// Create a memory block and copy bytes from `u8` slice 93 | pub fn from_bytes(data: impl AsRef<[u8]>) -> Result { 94 | let memory = Memory::new(&data.as_ref())?; 95 | Ok(memory) 96 | } 97 | 98 | /// Copy data out of memory and into a vec 99 | pub fn to_vec(&self) -> Vec { 100 | let mut dest = vec![0u8; self.0.length as usize]; 101 | internal::load(self.0, &mut dest); 102 | dest 103 | } 104 | 105 | /// Copy data out of memory and convert to string 106 | pub fn to_string(&self) -> Result { 107 | let x = String::from_utf8(self.to_vec())?; 108 | Ok(x) 109 | } 110 | 111 | /// Store memory as function output 112 | pub fn set_output(self) { 113 | unsafe { 114 | extism::output_set(self.0.offset, self.0.length); 115 | } 116 | } 117 | 118 | /// Log memory 119 | pub fn log(&self, level: LogLevel) { 120 | unsafe { 121 | match level { 122 | LogLevel::Info => extism::log_info(self.0.offset), 123 | LogLevel::Debug => extism::log_debug(self.0.offset), 124 | LogLevel::Warn => extism::log_warn(self.0.offset), 125 | LogLevel::Error => extism::log_error(self.0.offset), 126 | LogLevel::Trace => extism::log_trace(self.0.offset), 127 | } 128 | } 129 | } 130 | 131 | /// Convert to a Rust value 132 | pub fn to(&self) -> Result { 133 | T::from_bytes_owned(&self.to_vec()) 134 | } 135 | 136 | /// Locate a memory block by offset 137 | pub fn find(offs: u64) -> Option { 138 | internal::find(offs).map(Memory) 139 | } 140 | 141 | /// Free a memory block, allowing for it to be re-used 142 | pub fn free(self) { 143 | internal::memory_free(self.0) 144 | } 145 | } 146 | 147 | impl From for () { 148 | fn from(_: Memory) {} 149 | } 150 | 151 | impl From<()> for Memory { 152 | fn from(_: ()) -> Memory { 153 | Memory(MemoryHandle::null()) 154 | } 155 | } 156 | 157 | impl From for i64 { 158 | fn from(m: Memory) -> Self { 159 | m.0.offset as i64 160 | } 161 | } 162 | 163 | impl From for u64 { 164 | fn from(m: Memory) -> Self { 165 | m.0.offset 166 | } 167 | } 168 | 169 | impl From for Memory { 170 | fn from(offset: u64) -> Memory { 171 | Memory::find(offset).unwrap_or_else(Memory::null) 172 | } 173 | } 174 | 175 | impl From for Memory { 176 | fn from(offset: i64) -> Memory { 177 | Memory::find(offset as u64).unwrap_or_else(Memory::null) 178 | } 179 | } 180 | 181 | #[repr(transparent)] 182 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 183 | pub struct MemoryPointer(u64, std::marker::PhantomData); 184 | 185 | impl MemoryPointer { 186 | pub unsafe fn new(x: u64) -> Self { 187 | MemoryPointer(x, Default::default()) 188 | } 189 | } 190 | 191 | impl MemoryPointer { 192 | pub fn get(&self) -> Result { 193 | let mem = Memory::find(self.0); 194 | match mem { 195 | Some(mem) => T::from_bytes_owned(&mem.to_vec()), 196 | None => anyhow::bail!("Invalid pointer offset {}", self.0), 197 | } 198 | } 199 | } 200 | 201 | impl Drop for ManagedMemory { 202 | fn drop(&mut self) { 203 | internal::memory_free((self.0).0) 204 | } 205 | } 206 | 207 | impl ManagedMemory { 208 | pub fn new(mem: Memory) -> Self { 209 | ManagedMemory(mem) 210 | } 211 | 212 | pub fn offset(&self) -> u64 { 213 | self.0.offset() 214 | } 215 | 216 | pub fn len(&self) -> usize { 217 | self.0.len() 218 | } 219 | } 220 | 221 | impl From for ManagedMemory { 222 | fn from(value: Memory) -> Self { 223 | ManagedMemory(value) 224 | } 225 | } 226 | 227 | impl AsRef for ManagedMemory { 228 | fn as_ref(&self) -> &Memory { 229 | &self.0 230 | } 231 | } 232 | 233 | impl AsMut for ManagedMemory { 234 | fn as_mut(&mut self) -> &mut Memory { 235 | &mut self.0 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/to_memory.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | use base64::Engine; 4 | 5 | pub trait ToMemory { 6 | fn to_memory(&self) -> Result; 7 | 8 | fn status(&self) -> i32 { 9 | 0 10 | } 11 | } 12 | 13 | impl ToMemory for Memory { 14 | fn to_memory(&self) -> Result { 15 | Ok(Memory(MemoryHandle { 16 | offset: self.offset(), 17 | length: self.len() as u64, 18 | })) 19 | } 20 | } 21 | 22 | impl<'a> ToMemory for &'a Memory { 23 | fn to_memory(&self) -> Result { 24 | Ok(Memory(MemoryHandle { 25 | offset: self.offset(), 26 | length: self.len() as u64, 27 | })) 28 | } 29 | } 30 | 31 | #[cfg(feature = "http")] 32 | impl ToMemory for HttpResponse { 33 | fn to_memory(&self) -> Result { 34 | self.as_memory().to_memory() 35 | } 36 | } 37 | 38 | impl<'a, T: ToBytes<'a>> ToMemory for T { 39 | fn to_memory(&self) -> Result { 40 | Memory::from_bytes(self.to_bytes()?) 41 | } 42 | } 43 | 44 | impl ToMemory for Base64 { 45 | fn to_memory(&self) -> Result { 46 | base64::engine::general_purpose::STANDARD 47 | .encode(&self.0) 48 | .as_str() 49 | .to_memory() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/var.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn get_memory(key: impl AsRef) -> Result, Error> { 4 | let mem = Memory::from_bytes(key.as_ref().as_bytes())?; 5 | 6 | let offset = unsafe { extism::var_get(mem.offset()) }; 7 | if offset == 0 { 8 | return Ok(None); 9 | } 10 | let length = unsafe { extism::length(offset) }; 11 | 12 | if length == 0 { 13 | return Ok(None); 14 | } 15 | 16 | let memory = MemoryHandle { offset, length }; 17 | Ok(Some(Memory(memory))) 18 | } 19 | 20 | /// Gets a variable in the plug-in. This variable lives as long as the 21 | /// plug-in is loaded. 22 | /// 23 | /// # Arguments 24 | /// 25 | /// * `key` - A unique string key to identify the variable 26 | /// 27 | /// # Examples 28 | /// 29 | /// ``` 30 | /// // let's assume we have a variable at `"my_var"` 31 | /// // which is a u32. We can default to 0 first time we fetch it: 32 | /// let my_var = var::get("my_var")?.unwrap_or(0u32); 33 | /// ``` 34 | pub fn get(key: impl AsRef) -> Result, Error> { 35 | match get_memory(key)?.map(|x| x.to_vec()) { 36 | Some(v) => Ok(Some(T::from_bytes(&v)?)), 37 | None => Ok(None), 38 | } 39 | } 40 | 41 | /// Set a variable in the plug-in. This variable lives as long as the 42 | /// plug-in is loaded. The value must have a [ToMemory] implementation. 43 | /// 44 | /// # Arguments 45 | /// 46 | /// * `key` - A unique string key to identify the variable 47 | /// * `val` - The value to set. Must have a [ToMemory] implementation 48 | /// 49 | /// # Examples 50 | /// 51 | /// ``` 52 | /// var::set("my_u32_var", 42u32)?; 53 | /// var::set("my_str_var", "Hello, World!")?; 54 | /// ``` 55 | pub fn set(key: impl AsRef, val: impl ToMemory) -> Result<(), Error> { 56 | let val = val.to_memory()?; 57 | let key = Memory::from_bytes(key.as_ref().as_bytes())?; 58 | unsafe { extism::var_set(key.offset(), val.offset()) } 59 | Ok(()) 60 | } 61 | 62 | /// Removes a variable from the plug-in. This variable normally lives as long as the 63 | /// plug-in is loaded. 64 | /// 65 | /// # Arguments 66 | /// 67 | /// * `key` - A unique string key to identify the variable 68 | /// 69 | /// # Examples 70 | /// 71 | /// ``` 72 | /// var::remove("my_var")?; 73 | /// ``` 74 | pub fn remove(key: impl AsRef) -> Result<(), Error> { 75 | let key = Memory::from_bytes(key.as_ref().as_bytes())?; 76 | unsafe { extism::var_set(key.offset(), 0) }; 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /test/code.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/rust-pdk/ad480f8d7d29a99466197fd08626d5e6945e082b/test/code.wasm -------------------------------------------------------------------------------- /test/host_function.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/rust-pdk/ad480f8d7d29a99466197fd08626d5e6945e082b/test/host_function.wasm -------------------------------------------------------------------------------- /test/http.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/rust-pdk/ad480f8d7d29a99466197fd08626d5e6945e082b/test/http.wasm -------------------------------------------------------------------------------- /test/http_headers.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/rust-pdk/ad480f8d7d29a99466197fd08626d5e6945e082b/test/http_headers.wasm --------------------------------------------------------------------------------