├── .github ├── CODEOWNERS ├── actions │ └── libextism │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── env.go ├── example-schema.yaml ├── example ├── countvowels │ ├── std_main.go │ └── tiny_main.go ├── http │ ├── std_main.go │ └── tiny_main.go ├── httptransport │ └── std_main.go └── reactor │ ├── README.md │ ├── test.txt │ └── tiny_main.go ├── extism_pdk.go ├── go.mod ├── go.sum ├── go.work ├── http └── httptransport.go ├── internal ├── http │ └── extism_http.go └── memory │ ├── allocate.go │ ├── extism.go │ ├── memory.go │ └── pointer.go └── wasi-reactor ├── extism_pdk_reactor.go └── go.mod /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @zshipko @nilslice @mhmd-azeez 2 | -------------------------------------------------------------------------------- /.github/actions/libextism/action.yml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | name: libextism 4 | 5 | inputs: 6 | token: 7 | description: 'A Github PAT' 8 | required: true 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.token }}" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 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 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | path: go-pdk 17 | - uses: ./go-pdk/.github/actions/libextism 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - run: cp go-pdk/go.sum . # Needed to get setup-go to work 21 | 22 | - name: Install Go 23 | uses: actions/setup-go@v3 24 | with: 25 | cache: true 26 | go-version: '1.21.3' 27 | 28 | - name: Install TinyGo 29 | uses: acifani/setup-tinygo@v1.1.0 30 | with: 31 | tinygo-version: 0.34.0 32 | binaryen-version: "116" 33 | 34 | - name: Compile example 35 | working-directory: go-pdk 36 | run: | 37 | ls -a example 38 | make -B example 39 | 40 | - name: Test example 41 | working-directory: go-pdk 42 | run: | 43 | # --wasi is needed as there is currently some issue compiling Go PDK plugins without wasi 44 | 45 | TEST=$(extism call example/tiny_countvowels.wasm --wasi --github-token="$GITHUB_TOKEN" --input "this is a test" --set-config='{"thing": "1", "a": "b"}' count_vowels) 46 | echo $TEST | grep '"count": 4' 47 | echo $TEST | grep '"config": "1"' 48 | echo $TEST | grep '"a": "this is var a"' 49 | 50 | extism call example/tiny_http.wasm --wasi http_get --github-token="$GITHUB_TOKEN" --allow-host "jsonplaceholder.typicode.com" | grep '"userId": 1' 51 | 52 | extism call example/tiny_reactor.wasm read_file --input "example/reactor/test.txt" --allow-path ./example/reactor --wasi --log-level info | grep 'Hello World!' 53 | 54 | # run all the tests 55 | make test 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.wasm -------------------------------------------------------------------------------- /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 | .PHONY: example 2 | example: 3 | tinygo build -o example/tiny_countvowels.wasm -target wasip1 -buildmode c-shared ./example/countvowels 4 | tinygo build -o example/tiny_http.wasm -target wasip1 -buildmode c-shared ./example/http 5 | tinygo build -o example/tiny_reactor.wasm -target wasip1 -buildmode c-shared ./example/reactor 6 | 7 | GOOS=wasip1 GOARCH=wasm go build -tags std -o example/std_countvowels.wasm ./example/countvowels 8 | GOOS=wasip1 GOARCH=wasm go build -tags std -o example/std_http.wasm ./example/http 9 | 10 | test: 11 | extism call example/tiny_countvowels.wasm count_vowels --wasi --input "this is a test" --set-config '{"thing": "1234"}' 12 | extism call example/tiny_http.wasm http_get --wasi --log-level info --allow-host "jsonplaceholder.typicode.com" 13 | extism call example/tiny_reactor.wasm read_file --input "example/reactor/test.txt" --allow-path ./example/reactor --wasi --log-level info 14 | extism call example/tiny_countvowels.wasm count_vowels_roundtrip_json_mem --wasi 15 | 16 | extism call example/std_countvowels.wasm _start --wasi --input "this is a test" --set-config '{"thing": "1234"}' 17 | extism call example/std_http.wasm _start --wasi --log-level info --allow-host "jsonplaceholder.typicode.com" 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism Go PDK 2 | 3 | This library can be used to write 4 | [Extism Plug-ins](https://extism.org/docs/concepts/plug-in) in Go. 5 | 6 | ## Install 7 | 8 | Include the library with Go get: 9 | 10 | ```bash 11 | go get github.com/extism/go-pdk 12 | ``` 13 | 14 | ## Reference Documentation 15 | 16 | You can find the reference documentation for this library on 17 | [pkg.go.dev](https://pkg.go.dev/github.com/extism/go-pdk). 18 | 19 | ## Getting Started 20 | 21 | The goal of writing an 22 | [Extism plug-in](https://extism.org/docs/concepts/plug-in) is to compile your Go 23 | code to a Wasm module with exported functions that the host application can 24 | invoke. The first thing you should understand is creating an export. Let's write 25 | a simple program that exports a `greet` function which will take a name as a 26 | string and return a greeting string. Paste this into your `main.go`: 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "github.com/extism/go-pdk" 33 | ) 34 | 35 | //go:wasmexport greet 36 | func greet() int32 { 37 | input := pdk.Input() 38 | greeting := `Hello, ` + string(input) + `!` 39 | pdk.OutputString(greeting) 40 | return 0 41 | } 42 | ``` 43 | 44 | Some things to note about this code: 45 | 46 | 1. The `//go:wasmexport greet` comment is required. This marks the greet function as an 47 | export with the name `greet` that can be called by the host. 48 | 2. Exports in the Go PDK are coded to the raw ABI. You get parameters from the 49 | host by calling 50 | [pdk.Input* functions](https://pkg.go.dev/github.com/extism/go-pdk#Input) and 51 | you send returns back with the 52 | [pdk.Output* functions](https://pkg.go.dev/github.com/extism/go-pdk#Output). 53 | 3. An Extism export expects an i32 return code. `0` is success and `1` is a 54 | failure. 55 | 56 | Install the `tinygo` compiler: 57 | 58 | See https://tinygo.org/getting-started/install/ for instructions for your 59 | platform. 60 | 61 | > Note: while the core Go toolchain has support to target WebAssembly, we find 62 | > `tinygo` to work well for plug-in code. Please open issues on this repository 63 | > if you try building with `go build` instead & have problems! 64 | 65 | Compile this with the command: 66 | 67 | ```bash 68 | tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared main.go 69 | ``` 70 | 71 | We can now test `plugin.wasm` using the 72 | [Extism CLI](https://github.com/extism/cli)'s `run` command: 73 | 74 | ```bash 75 | extism call plugin.wasm greet --input "Benjamin" --wasi 76 | # => Hello, Benjamin! 77 | ``` 78 | 79 | > **Note**: Currently `wasip1` must be provided for all Go plug-ins even if they 80 | > don't need system access, however this will eventually be optional. 81 | 82 | > **Note**: We also have a web-based, plug-in tester called the 83 | > [Extism Playground](https://playground.extism.org/) 84 | 85 | ### More Exports: Error Handling 86 | 87 | Suppose we want to re-write our greeting module to never greet Benjamins. We can 88 | use [pdk.SetError](https://pkg.go.dev/github.com/extism/go-pdk#SetError) or 89 | [pdk.SetErrorString](https://pkg.go.dev/github.com/extism/go-pdk#SetErrorString): 90 | 91 | ```go 92 | //go:wasmexport greet 93 | func greet() int32 { 94 | name := string(pdk.Input()) 95 | if name == "Benjamin" { 96 | pdk.SetError(errors.New("Sorry, we don't greet Benjamins!")) 97 | return 1 98 | } 99 | greeting := `Hello, ` + name + `!` 100 | pdk.OutputString(greeting) 101 | return 0 102 | } 103 | ``` 104 | 105 | Now when we try again: 106 | 107 | ```bash 108 | extism call plugin.wasm greet --input="Benjamin" --wasi 109 | # => Error: Sorry, we don't greet Benjamins! 110 | # => returned non-zero exit code: 1 111 | echo $? # print last status code 112 | # => 1 113 | extism call plugin.wasm greet --input="Zach" --wasi 114 | # => Hello, Zach! 115 | echo $? 116 | # => 0 117 | ``` 118 | 119 | ### Json 120 | 121 | Extism export functions simply take bytes in and bytes out. Those can be 122 | whatever you want them to be. A common and simple way to get more complex types 123 | to and from the host is with json: 124 | 125 | ```go 126 | type Add struct { 127 | A int `json:"a"` 128 | B int `json:"b"` 129 | } 130 | 131 | type Sum struct { 132 | Sum int `json:"sum"` 133 | } 134 | 135 | //go:wasmexport add 136 | func add() int32 { 137 | params := Add{} 138 | // use json input helper, which automatically unmarshals the plugin input into your struct 139 | err := pdk.InputJSON(¶ms) 140 | if err != nil { 141 | pdk.SetError(err) 142 | return 1 143 | } 144 | sum := Sum{Sum: params.A + params.B} 145 | // use json output helper, which automatically marshals your struct to the plugin output 146 | _, err := pdk.OutputJSON(sum) 147 | if err != nil { 148 | pdk.SetError(err) 149 | return 1 150 | } 151 | return 0 152 | } 153 | ``` 154 | 155 | ```bash 156 | extism call plugin.wasm add --input='{"a": 20, "b": 21}' --wasi 157 | # => {"sum":41} 158 | ``` 159 | 160 | ## Configs 161 | 162 | Configs are key-value pairs that can be passed in by the host when creating a 163 | plug-in. These can be useful to statically configure the plug-in with some data 164 | that exists across every function call. Here is a trivial example using 165 | [pdk.GetConfig](https://pkg.go.dev/github.com/extism/go-pdk#GetConfig): 166 | 167 | ```go 168 | //go:wasmexport greet 169 | func greet() int32 { 170 | user, ok := pdk.GetConfig("user") 171 | if !ok { 172 | pdk.SetErrorString("This plug-in requires a 'user' key in the config") 173 | return 1 174 | } 175 | greeting := `Hello, ` + user + `!` 176 | pdk.OutputString(greeting) 177 | return 0 178 | } 179 | ``` 180 | 181 | To test it, the [Extism CLI](https://github.com/extism/cli) has a `--config` 182 | option that lets you pass in `key=value` pairs: 183 | 184 | ```bash 185 | extism call plugin.wasm greet --config user=Benjamin 186 | # => Hello, Benjamin! 187 | ``` 188 | 189 | ## Variables 190 | 191 | Variables are another key-value mechanism but it's a mutable data store that 192 | will persist across function calls. These variables will persist as long as the 193 | host has loaded and not freed the plug-in. 194 | 195 | ```go 196 | //go:wasmexport count 197 | func count() int32 { 198 | count := pdk.GetVarInt("count") 199 | count = count + 1 200 | pdk.SetVarInt("count", count) 201 | pdk.OutputString(strconv.Itoa(count)) 202 | return 0 203 | } 204 | ``` 205 | 206 | > **Note**: Use the untyped variants 207 | > [pdk.SetVar(string, []byte)](https://pkg.go.dev/github.com/extism/go-pdk#SetVar) 208 | > and 209 | > [pdk.GetVar(string) []byte](https://pkg.go.dev/github.com/extism/go-pdk#GetVar) 210 | > to handle your own types. 211 | 212 | ## Logging 213 | 214 | Because Wasm modules by default do not have access to the system, printing to 215 | stdout won't work (unless you use WASI). Extism provides a simple 216 | [logging function](https://pkg.go.dev/github.com/extism/go-pdk#Log) that allows 217 | you to use the host application to log without having to give the plug-in 218 | permission to make syscalls. 219 | 220 | ```go 221 | //go:wasmexport log_stuff 222 | func logStuff() int32 { 223 | pdk.Log(pdk.LogInfo, "An info log!") 224 | pdk.Log(pdk.LogDebug, "A debug log!") 225 | pdk.Log(pdk.LogWarn, "A warn log!") 226 | pdk.Log(pdk.LogError, "An error log!") 227 | return 0 228 | } 229 | ``` 230 | 231 | From [Extism CLI](https://github.com/extism/cli): 232 | 233 | ```bash 234 | extism call plugin.wasm log_stuff --wasi --log-level=debug 235 | 2023/10/12 12:11:23 Calling function : log_stuff 236 | 2023/10/12 12:11:23 An info log! 237 | 2023/10/12 12:11:23 A debug log! 238 | 2023/10/12 12:11:23 A warn log! 239 | 2023/10/12 12:11:23 An error log! 240 | ``` 241 | 242 | > _Note_: From the CLI you need to pass a level with `--log-level`. If you are 243 | > running the plug-in in your own host using one of our SDKs, you need to make 244 | > sure that you call `set_log_file` to `"stdout"` or some file location. 245 | 246 | ## HTTP 247 | 248 | Sometimes it is useful to let a plug-in 249 | [make HTTP calls](https://pkg.go.dev/github.com/extism/go-pdk#HTTPRequest.Send). 250 | [See this example](example/http/tiny_main.go) 251 | 252 | ```go 253 | //go:wasmexport http_get 254 | func httpGet() int32 { 255 | // create an HTTP Request (withuot relying on WASI), set headers as needed 256 | req := pdk.NewHTTPRequest(pdk.MethodGet, "https://jsonplaceholder.typicode.com/todos/1") 257 | req.SetHeader("some-name", "some-value") 258 | req.SetHeader("another", "again") 259 | // send the request, get response back (can check status on response via res.Status()) 260 | res := req.Send() 261 | 262 | pdk.OutputMemory(res.Memory()) 263 | 264 | return 0 265 | } 266 | ``` 267 | 268 | By default, Extism modules cannot make HTTP requests unless you specify which 269 | hosts it can connect to. You can use `--alow-host` in the Extism CLI to set 270 | this: 271 | 272 | ``` 273 | extism call plugin.wasm http_get --wasi --allow-host='*.typicode.com' 274 | # => { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } 275 | ``` 276 | 277 | ## Imports (Host Functions) 278 | 279 | Like any other code module, Wasm not only let's you export functions to the 280 | outside world, you can import them too. Host Functions allow a plug-in to import 281 | functions defined in the host. For example, if you host application is written 282 | in Python, it can pass a Python function down to your Go plug-in where you can 283 | invoke it. 284 | 285 | This topic can get fairly complicated and we have not yet fully abstracted the 286 | Wasm knowledge you need to do this correctly. So we recommend reading our 287 | [concept doc on Host Functions](https://extism.org/docs/concepts/host-functions) 288 | before you get started. 289 | 290 | ### A Simple Example 291 | 292 | Host functions have a similar interface as exports. You just need to declare 293 | them as extern on the top of your main.go. You only declare the interface as it 294 | is the host's responsibility to provide the implementation: 295 | 296 | ```go 297 | //go:wasmimport extism:host/user a_python_func 298 | func aPythonFunc(uint64) uint64 299 | ``` 300 | 301 | We should be able to call this function as a normal Go function. Note that we 302 | need to manually handle the pointer casting: 303 | 304 | ```go 305 | //go:wasmexport hello_from_python 306 | func helloFromPython() int32 { 307 | msg := "An argument to send to Python" 308 | mem := pdk.AllocateString(msg) 309 | defer mem.Free() 310 | ptr := aPythonFunc(mem.Offset()) 311 | rmem := pdk.FindMemory(ptr) 312 | response := string(rmem.ReadBytes()) 313 | pdk.OutputString(response) 314 | return 0 315 | } 316 | ``` 317 | 318 | ### Testing it out 319 | 320 | We can't really test this from the Extism CLI as something must provide the 321 | implementation. So let's write out the Python side here. Check out the 322 | [docs for Host SDKs](https://extism.org/docs/concepts/host-sdk) to implement a 323 | host function in a language of your choice. 324 | 325 | ```python 326 | from extism import host_fn, Plugin 327 | 328 | @host_fn() 329 | def a_python_func(input: str) -> str: 330 | # just printing this out to prove we're in Python land 331 | print("Hello from Python!") 332 | 333 | # let's just add "!" to the input string 334 | # but you could imagine here we could add some 335 | # applicaiton code like query or manipulate the database 336 | # or our application APIs 337 | return input + "!" 338 | ``` 339 | 340 | Now when we load the plug-in we pass the host function: 341 | 342 | ```python 343 | manifest = {"wasm": [{"path": "/path/to/plugin.wasm"}]} 344 | plugin = Plugin(manifest, functions=[a_python_func], wasi=True) 345 | result = plugin.call('hello_from_python', b'').decode('utf-8') 346 | print(result) 347 | ``` 348 | 349 | ```bash 350 | python3 app.py 351 | # => Hello from Python! 352 | # => An argument to send to Python! 353 | ``` 354 | 355 | ## Reactor modules 356 | 357 | Since TinyGo version 0.34.0, the compiler has native support for 358 | [Reactor modules](https://dylibso.com/blog/wasi-command-reactor/). 359 | 360 | Make sure you invoke the compiler with the `-buildmode=c-shared` flag 361 | so that libc and the Go runtime are properly initialized: 362 | 363 | ```bash 364 | cd example/reactor 365 | tinygo build -target wasip1 -buildmode=c-shared -o reactor.wasm ./tiny_main.go 366 | extism call ./reactor.wasm read_file --input "./test.txt" --allow-path . --wasi --log-level info 367 | # => Hello World! 368 | ``` 369 | 370 | ### Note on TinyGo 0.33.0 and earlier 371 | 372 | TinyGo versions below 0.34.0 do not support 373 | [Reactor modules](https://dylibso.com/blog/wasi-command-reactor/). 374 | If you want to use WASI inside your Reactor module functions (exported functions other 375 | than `main`). You can however import the `wasi-reactor` module to ensure that libc 376 | and go runtime are initialized as expected: 377 | 378 | Moreover, older versions may not provide the special `//go:wasmexport` 379 | directive, and instead use `//export`. 380 | 381 | ```go 382 | package main 383 | 384 | import ( 385 | "os" 386 | 387 | "github.com/extism/go-pdk" 388 | _ "github.com/extism/go-pdk/wasi-reactor" 389 | ) 390 | 391 | //export read_file 392 | func read_file() { 393 | name := pdk.InputString() 394 | 395 | content, err := os.ReadFile(name) 396 | if err != nil { 397 | pdk.Log(pdk.LogError, err.Error()) 398 | return 399 | } 400 | 401 | pdk.Output(content) 402 | } 403 | 404 | func main() {} 405 | ``` 406 | 407 | ```bash 408 | tinygo build -target wasip1 -o reactor.wasm ./tiny_main.go 409 | extism call ./reactor.wasm read_file --input "./test.txt" --allow-path . --wasi --log-level info 410 | # => Hello World! 411 | ``` 412 | 413 | Note: this is not required if you only have the `main` function. 414 | 415 | ## Generating Bindings 416 | 417 | It's often very useful to define a schema to describe the function signatures 418 | and types you want to use between Extism SDK and PDK languages. 419 | 420 | [XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source 421 | framework to generate PDK bindings for Extism plug-ins. It's used by the 422 | [XTP Platform](https://www.getxtp.com/), but can be used outside of the platform 423 | to define any Extism compatible plug-in system. 424 | 425 | ### 1. Install the `xtp` CLI. 426 | 427 | See installation instructions 428 | [here](https://docs.xtp.dylibso.com/docs/cli#installation). 429 | 430 | ### 2. Create a schema using our OpenAPI-inspired IDL: 431 | 432 | ```yaml 433 | version: v1-draft 434 | exports: 435 | CountVowels: 436 | input: 437 | type: string 438 | contentType: text/plain; charset=utf-8 439 | output: 440 | $ref: "#/components/schemas/VowelReport" 441 | contentType: application/json 442 | # components.schemas defined in example-schema.yaml... 443 | ``` 444 | 445 | > See an example in [example-schema.yaml](./example-schema.yaml), or a full 446 | > "kitchen sink" example on 447 | > [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/). 448 | 449 | ### 3. Generate bindings to use from your plugins: 450 | 451 | ``` 452 | xtp plugin init --schema-file ./example-schema.yaml 453 | 1. TypeScript 454 | > 2. Go 455 | 3. Rust 456 | 4. Python 457 | 5. C# 458 | 6. Zig 459 | 7. C++ 460 | 8. GitHub Template 461 | 9. Local Template 462 | ``` 463 | 464 | This will create an entire boilerplate plugin project for you to get started 465 | with: 466 | 467 | ```go 468 | package main 469 | 470 | // returns VowelReport (The result of counting vowels on the Vowels input.) 471 | func CountVowels(input string) (VowelReport, error) { 472 | // TODO: fill out your implementation here 473 | panic("Function not implemented.") 474 | } 475 | ``` 476 | 477 | Implement the empty function(s), and run `xtp plugin build` to compile your 478 | plugin. 479 | 480 | > For more information about XTP Bindgen, see the 481 | > [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and 482 | > the official 483 | > [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema). 484 | 485 | ## Reach Out! 486 | 487 | Have a question or just want to drop in and say hi? 488 | [Hop on the Discord](https://extism.org/discord)! 489 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package pdk 2 | 3 | import ( 4 | "github.com/extism/go-pdk/internal/memory" 5 | ) 6 | 7 | // extismInputLength returns the number of bytes provided by the host via its input methods. 8 | // 9 | //go:wasmimport extism:host/env input_length 10 | func extismInputLength() uint64 11 | 12 | // extismInputLoadU8 returns the byte at location `offset` of the "input" data from the host. 13 | // 14 | //go:wasmimport extism:host/env input_load_u8 15 | func extismInputLoadU8_(offset memory.ExtismPointer) uint32 16 | func extismInputLoadU8(offset memory.ExtismPointer) uint8 { 17 | return uint8(extismInputLoadU8_(offset)) 18 | } 19 | 20 | // extismInputLoadU64 returns the 64-bit unsigned integer of the "input" data from the host. 21 | // Note that `offset` must lie on an 8-byte boundary. 22 | // 23 | //go:wasmimport extism:host/env input_load_u64 24 | func extismInputLoadU64(offset memory.ExtismPointer) uint64 25 | 26 | // extismOutputSet sets the "output" data from the plugin to the host to be the memory that 27 | // has been written at `offset` with the given `length`. 28 | // The memory can be immediately freed because the host makes a copy for its use. 29 | // 30 | //go:wasmimport extism:host/env output_set 31 | func extismOutputSet(offset memory.ExtismPointer, length uint64) 32 | 33 | // extismErrorSet sets the "error" data from the plugin to the host to be the memory that 34 | // has been written at `offset`. 35 | // The memory can be immediately freed because the host makes a copy for its use. 36 | // 37 | //go:wasmimport extism:host/env error_set 38 | func extismErrorSet(offset memory.ExtismPointer) 39 | 40 | // extismConfigGet returns the host memory block offset for the "config" data associated with 41 | // the key which is represented by the UTF-8 string which as been previously written at `offset`. 42 | // The memory for the key can be immediately freed because the host has its own copy. 43 | // 44 | //go:wasmimport extism:host/env config_get 45 | func extismConfigGet(offset memory.ExtismPointer) memory.ExtismPointer 46 | 47 | // extismVarGet returns the host memory block offset for the "var" data associated with 48 | // the key which is represented by the UTF-8 string which as been previously written at `offset`. 49 | // The memory for the key can be immediately freed because the host has its own copy. 50 | // 51 | //go:wasmimport extism:host/env var_get 52 | func extismVarGet(offset memory.ExtismPointer) memory.ExtismPointer 53 | 54 | // extismVarSet sets the host "var" memory keyed by the UTF-8 string located at `offset` 55 | // to be the value which has been previously written at `valueOffset`. 56 | // 57 | // A `valueOffset` of 0 causes the old value associated with this key to be freed on the host 58 | // and the association to be completely removed. 59 | // 60 | // The memory for the key can be immediately freed because the host has its own copy. 61 | // The memory for the value, however, should not be freed, as that erases the value from the host. 62 | // 63 | //go:wasmimport extism:host/env var_set 64 | func extismVarSet(offset, valueOffset memory.ExtismPointer) 65 | 66 | // extismLogInfo logs an "info" string to the host from the previously-written UTF-8 string written to `offset`. 67 | // 68 | //go:wasmimport extism:host/env log_info 69 | func extismLogInfo(offset memory.ExtismPointer) 70 | 71 | // extismLogDebug logs a "debug" string to the host from the previously-written UTF-8 string written to `offset`. 72 | // 73 | //go:wasmimport extism:host/env log_debug 74 | func extismLogDebug(offset memory.ExtismPointer) 75 | 76 | // extismLogWarn logs a "warning" string to the host from the previously-written UTF-8 string written to `offset`. 77 | // 78 | //go:wasmimport extism:host/env log_warn 79 | func extismLogWarn(offset memory.ExtismPointer) 80 | 81 | // extismLogError logs an "error" string to the host from the previously-written UTF-8 string written to `offset`. 82 | // 83 | //go:wasmimport extism:host/env log_error 84 | func extismLogError(offset memory.ExtismPointer) 85 | 86 | // extismLogTrace logs an "error" string to the host from the previously-written UTF-8 string written to `offset`. 87 | // 88 | //go:wasmimport extism:host/env log_error 89 | func extismLogTrace(offset memory.ExtismPointer) 90 | 91 | // extismGetLogLevel returns the configured log level 92 | // 93 | //go:wasmimport extism:host/env get_log_level 94 | func extismGetLogLevel() int32 95 | -------------------------------------------------------------------------------- /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" 29 | -------------------------------------------------------------------------------- /example/countvowels/std_main.go: -------------------------------------------------------------------------------- 1 | //go:build std 2 | // +build std 3 | 4 | package main 5 | 6 | import ( 7 | // "fmt" 8 | "strconv" 9 | 10 | "github.com/extism/go-pdk" 11 | ) 12 | 13 | // Currently, the standard Go compiler cannot export custom functions and is limited to exporting 14 | // `_start` via WASI. So, `main` functions should contain the plugin behavior, that the host will 15 | // invoke by explicitly calling `_start`. 16 | func main() { 17 | countVowels() 18 | countVowelsTyped() 19 | countVowelsJSONOutput() 20 | countVowelsJSONRoundtripMem() 21 | } 22 | 23 | // CountVowelsInput represents the JSON input provided by the host. 24 | type CountVowelsInput struct { 25 | Input string `json:"input"` 26 | } 27 | 28 | // CountVowelsOutput represents the JSON output sent to the host. 29 | type CountVowelsOuptut struct { 30 | Count int `json:"count"` 31 | Total int `json:"total"` 32 | Vowels string `json:"vowels"` 33 | } 34 | 35 | //export count_vowels_typed 36 | func countVowelsTyped() int32 { 37 | var input CountVowelsInput 38 | if err := pdk.InputJSON(&input); err != nil { 39 | pdk.SetError(err) 40 | return -1 41 | } 42 | 43 | pdk.OutputString(input.Input) 44 | return 0 45 | } 46 | 47 | //export count_vowels_json_output 48 | func countVowelsJSONOutput() int32 { 49 | output := CountVowelsOuptut{Count: 42, Total: 2.1e7, Vowels: "aAeEiIoOuUyY"} 50 | err := pdk.OutputJSON(output) 51 | if err != nil { 52 | pdk.SetError(err) 53 | return -1 54 | } 55 | return 0 56 | } 57 | 58 | //export count_vowels_roundtrip_json_mem 59 | func countVowelsJSONRoundtripMem() int32 { 60 | a := CountVowelsOuptut{Count: 42, Total: 2.1e7, Vowels: "aAeEiIoOuUyY"} 61 | mem, err := pdk.AllocateJSON(&a) 62 | if err != nil { 63 | pdk.SetError(err) 64 | return -1 65 | } 66 | 67 | // find the data in mem and ensure it's the same once decoded 68 | var b CountVowelsOuptut 69 | err = pdk.JSONFrom(mem.Offset(), &b) 70 | if err != nil { 71 | pdk.SetError(err) 72 | return -1 73 | } 74 | 75 | if a.Count != b.Count || a.Total != b.Total || a.Vowels != b.Vowels { 76 | pdk.SetErrorString("roundtrip JSON failed") 77 | return -1 78 | } 79 | 80 | pdk.OutputString("JSON roundtrip: a === b") 81 | return 0 82 | } 83 | 84 | func countVowels() int32 { 85 | input := pdk.Input() 86 | 87 | count := 0 88 | for _, a := range input { 89 | switch a { 90 | case 'A', 'I', 'E', 'O', 'U', 'a', 'e', 'i', 'o', 'u': 91 | count++ 92 | default: 93 | } 94 | } 95 | 96 | // test some extra pdk functionality 97 | if pdk.GetVar("a") == nil { 98 | pdk.SetVar("a", []byte("this is var a")) 99 | } 100 | varA := pdk.GetVar("a") 101 | thing, ok := pdk.GetConfig("thing") 102 | 103 | if !ok { 104 | thing = "" 105 | } 106 | 107 | output := `{"count": ` + strconv.Itoa(count) + `, "config": "` + thing + `", "a": "` + string(varA) + `"}` 108 | mem := pdk.AllocateString(output) 109 | 110 | // zero-copy output to host 111 | pdk.OutputMemory(mem) 112 | 113 | return 0 114 | } 115 | -------------------------------------------------------------------------------- /example/countvowels/tiny_main.go: -------------------------------------------------------------------------------- 1 | //go:build !std 2 | // +build !std 3 | 4 | package main 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/extism/go-pdk" 10 | ) 11 | 12 | // CountVowelsInput represents the JSON input provided by the host. 13 | type CountVowelsInput struct { 14 | Input string `json:"input"` 15 | } 16 | 17 | // CountVowelsOutput represents the JSON output sent to the host. 18 | type CountVowelsOuptut struct { 19 | Count int `json:"count"` 20 | Total int `json:"total"` 21 | Vowels string `json:"vowels"` 22 | } 23 | 24 | //go:wasmexport count_vowels_typed 25 | func countVowelsTyped() int32 { 26 | var input CountVowelsInput 27 | if err := pdk.InputJSON(&input); err != nil { 28 | pdk.SetError(err) 29 | return -1 30 | } 31 | 32 | pdk.OutputString(input.Input) 33 | return 0 34 | } 35 | 36 | //go:wasmexport count_vowels_json_output 37 | func countVowelsJSONOutput() int32 { 38 | output := CountVowelsOuptut{Count: 42, Total: 2.1e7, Vowels: "aAeEiIoOuUyY"} 39 | err := pdk.OutputJSON(output) 40 | if err != nil { 41 | pdk.SetError(err) 42 | return -1 43 | } 44 | return 0 45 | } 46 | 47 | //go:wasmexport count_vowels_roundtrip_json_mem 48 | func countVowelsJSONRoundtripMem() int32 { 49 | a := CountVowelsOuptut{Count: 42, Total: 2.1e7, Vowels: "aAeEiIoOuUyY"} 50 | mem, err := pdk.AllocateJSON(&a) 51 | if err != nil { 52 | pdk.SetError(err) 53 | return -1 54 | } 55 | 56 | // find the data in mem and ensure it's the same once decoded 57 | var b CountVowelsOuptut 58 | err = pdk.JSONFrom(mem.Offset(), &b) 59 | if err != nil { 60 | pdk.SetError(err) 61 | return -1 62 | } 63 | 64 | if a.Count != b.Count || a.Total != b.Total || a.Vowels != b.Vowels { 65 | pdk.SetErrorString("roundtrip JSON failed") 66 | return -1 67 | } 68 | 69 | pdk.OutputString("JSON roundtrip: a === b") 70 | return 0 71 | } 72 | 73 | //go:wasmexport count_vowels 74 | func countVowels() int32 { 75 | input := pdk.Input() 76 | 77 | count := 0 78 | for _, a := range input { 79 | switch a { 80 | case 'A', 'I', 'E', 'O', 'U', 'a', 'e', 'i', 'o', 'u': 81 | count++ 82 | default: 83 | } 84 | } 85 | 86 | // test some extra pdk functionality 87 | if pdk.GetVar("a") == nil { 88 | pdk.SetVar("a", []byte("this is var a")) 89 | } 90 | varA := pdk.GetVar("a") 91 | thing, ok := pdk.GetConfig("thing") 92 | 93 | if !ok { 94 | thing = "" 95 | } 96 | 97 | output := `{"count": ` + strconv.Itoa(count) + `, "config": "` + thing + `", "a": "` + string(varA) + `"}` 98 | mem := pdk.AllocateString(output) 99 | 100 | // zero-copy output to host 101 | pdk.OutputMemory(mem) 102 | 103 | return 0 104 | } 105 | -------------------------------------------------------------------------------- /example/http/std_main.go: -------------------------------------------------------------------------------- 1 | //go:build std 2 | // +build std 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/extism/go-pdk" 8 | ) 9 | 10 | // Currently, the standard Go compiler cannot export custom functions and is limited to exporting 11 | // `_start` via WASI. So, `main` functions should contain the plugin behavior, that the host will 12 | // invoke by explicitly calling `_start`. 13 | func main() { 14 | httpGet() 15 | } 16 | 17 | func httpGet() int32 { 18 | // create an HTTP Request (withuot relying on WASI), set headers as needed 19 | req := pdk.NewHTTPRequest(pdk.MethodGet, "https://jsonplaceholder.typicode.com/todos/1") 20 | req.SetHeader("some-name", "some-value") 21 | req.SetHeader("another", "again") 22 | // send the request, get response back (can check status on response via res.Status()) 23 | res := req.Send() 24 | 25 | // zero-copy output to host 26 | pdk.OutputMemory(res.Memory()) 27 | 28 | return 0 29 | } 30 | -------------------------------------------------------------------------------- /example/http/tiny_main.go: -------------------------------------------------------------------------------- 1 | //go:build !std 2 | // +build !std 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/extism/go-pdk" 8 | ) 9 | 10 | //go:wasmexport http_get 11 | func httpGet() int32 { 12 | // create an HTTP Request (withuot relying on WASI), set headers as needed 13 | req := pdk.NewHTTPRequest(pdk.MethodGet, "https://jsonplaceholder.typicode.com/todos/1") 14 | req.SetHeader("some-name", "some-value") 15 | req.SetHeader("another", "again") 16 | // send the request, get response back (can check status on response via res.Status()) 17 | res := req.Send() 18 | 19 | // zero-copy output to host 20 | pdk.OutputMemory(res.Memory()) 21 | 22 | return 0 23 | } 24 | -------------------------------------------------------------------------------- /example/httptransport/std_main.go: -------------------------------------------------------------------------------- 1 | //go:build std 2 | // +build std 3 | 4 | // Build the example, then invoke with: 5 | // GOOS=wasip1 GOARCH=wasm go build std_main.go 6 | // extism call --wasi --allow-host "jsonplaceholder.typicode.com" std_main _start 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "io" 13 | "net/http" 14 | "os" 15 | 16 | pdk "github.com/extism/go-pdk" 17 | pdkhttp "github.com/extism/go-pdk/http" 18 | ) 19 | 20 | // Currently, the standard Go compiler cannot export custom functions and is limited to exporting 21 | // `_start` via WASI. So, `main` functions should contain the plugin behavior, that the host will 22 | // invoke by explicitly calling `_start`. 23 | func main() { 24 | body, err := httpGet() 25 | if err != nil { 26 | pdk.SetError(err) 27 | os.Exit(1) 28 | } 29 | 30 | pdk.OutputString(string(body)) 31 | } 32 | 33 | func httpGet() ([]byte, error) { 34 | // Set the default transport to use Extism PDK HTTPTransport 35 | // 36 | // Alternatively, if using http.Client, specify the transport: 37 | // client := http.Client{ 38 | // Transport: &pdkhttp.HTTPTransport{}, 39 | // } 40 | http.DefaultTransport = &pdkhttp.HTTPTransport{} 41 | 42 | resp, err := http.Get("https://jsonplaceholder.typicode.com/todos/1") 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to make request: %q", err) 45 | } 46 | 47 | body, err := io.ReadAll(resp.Body) 48 | if err != nil { 49 | return nil, fmt.Errorf("failed to read request body: %q", err) 50 | } 51 | 52 | return body, nil 53 | } 54 | -------------------------------------------------------------------------------- /example/reactor/README.md: -------------------------------------------------------------------------------- 1 | ## Reactor module example 2 | By including this package, you'll turn your plugin into a [Reactor](https://dylibso.com/blog/wasi-command-reactor/) module. 3 | This makes sure that you can use WASI (e.g. File Access) in your exported functions. 4 | 5 | This is only required for TinyGo versions below 0.34.0 where native support for reactort-style modules 6 | was missing.To test this example, run: 7 | 8 | ```bash 9 | tinygo build -target wasi -o reactor.wasm ./tiny_main.go 10 | extism call ./reactor.wasm read_file --input "./test.txt" --allow-path . --wasi --log-level info 11 | # => Hello World! 12 | ``` 13 | 14 | If you don't include the pacakge, you'll see this output: 15 | ```bash 16 | extism call ./c.wasm read_file --input "./test.txt" --allow-path . --wasi --log-level info 17 | # => 2024/01/18 20:48:48 open ./test.txt: errno 76 18 | ``` 19 | -------------------------------------------------------------------------------- /example/reactor/test.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /example/reactor/tiny_main.go: -------------------------------------------------------------------------------- 1 | //go:build !std 2 | // +build !std 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/extism/go-pdk" 10 | ) 11 | 12 | //go:wasmexport read_file 13 | func readFile() { 14 | name := pdk.InputString() 15 | 16 | content, err := os.ReadFile(name) 17 | if err != nil { 18 | pdk.Log(pdk.LogError, err.Error()) 19 | return 20 | } 21 | 22 | pdk.Output(content) 23 | } 24 | -------------------------------------------------------------------------------- /extism_pdk.go: -------------------------------------------------------------------------------- 1 | package pdk 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | 7 | "github.com/extism/go-pdk/internal/http" 8 | "github.com/extism/go-pdk/internal/memory" 9 | ) 10 | 11 | // Memory represents memory allocated by (and shared with) the host. 12 | type Memory = memory.Memory 13 | 14 | func NewMemory(offset uint64, length uint64) Memory { 15 | return memory.NewMemory( 16 | memory.ExtismPointer(offset), 17 | length, 18 | ) 19 | } 20 | 21 | // LogLevel represents a logging level. 22 | type LogLevel int 23 | 24 | const ( 25 | LogTrace LogLevel = iota 26 | LogDebug 27 | LogInfo 28 | LogWarn 29 | LogError 30 | ) 31 | 32 | func loadInput() []byte { 33 | length := int(extismInputLength()) 34 | buf := make([]byte, length) 35 | 36 | chunkCount := length >> 3 37 | 38 | for chunkIdx := 0; chunkIdx < chunkCount; chunkIdx++ { 39 | i := chunkIdx << 3 40 | binary.LittleEndian.PutUint64(buf[i:i+8], extismInputLoadU64(memory.ExtismPointer(i))) 41 | } 42 | 43 | remainder := length & 7 44 | remainderOffset := chunkCount << 3 45 | for index := remainderOffset; index < (remainder + remainderOffset); index++ { 46 | buf[index] = extismInputLoadU8(memory.ExtismPointer(index)) 47 | } 48 | 49 | return buf 50 | } 51 | 52 | // Input returns a slice of bytes from the host. 53 | func Input() []byte { 54 | return loadInput() 55 | } 56 | 57 | // JSONFrom unmarshals a `Memory` block located at `offset` from the host 58 | // into the provided data `v`. 59 | func JSONFrom(offset uint64, v any) error { 60 | mem := FindMemory(offset) 61 | return json.Unmarshal(mem.ReadBytes(), v) 62 | } 63 | 64 | // InputJSON returns unmartialed JSON data from the host "input". 65 | func InputJSON(v any) error { 66 | return json.Unmarshal(Input(), v) 67 | } 68 | 69 | // OutputJSON marshals the provided data `v` as output to the host. 70 | func OutputJSON(v any) error { 71 | b, err := json.Marshal(v) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | mem := memory.AllocateBytes(b) 77 | // TODO: coordinate replacement of call to free based on SDK alignment 78 | // defer mem.Free() 79 | OutputMemory(mem) 80 | return nil 81 | } 82 | 83 | func Allocate(length int) Memory { 84 | return memory.Allocate(length) 85 | } 86 | 87 | func AllocateBytes(data []byte) Memory { 88 | return memory.AllocateBytes(data) 89 | } 90 | 91 | // AllocateString allocates and saves the UTF-8 string `data` into Memory on the host. 92 | func AllocateString(data string) Memory { 93 | return memory.AllocateBytes([]byte(data)) 94 | } 95 | 96 | // AllocateJSON allocates and saves the type `any` into Memory on the host. 97 | func AllocateJSON(v any) (Memory, error) { 98 | b, err := json.Marshal(v) 99 | if err != nil { 100 | return Memory{}, err 101 | } 102 | 103 | return AllocateBytes(b), nil 104 | } 105 | 106 | // InputString returns the input data from the host as a UTF-8 string. 107 | func InputString() string { 108 | return string(Input()) 109 | } 110 | 111 | // OutputMemory sends the `mem` Memory to the host output. 112 | // Note that the `mem` is _NOT_ freed and is your responsibility to free when finished with it. 113 | func OutputMemory(mem Memory) { 114 | extismOutputSet(memory.ExtismPointer(mem.Offset()), mem.Length()) 115 | } 116 | 117 | // Output sends the `data` slice of bytes to the host output. 118 | func Output(data []byte) { 119 | clength := uint64(len(data)) 120 | m := memory.AllocateBytes(data) 121 | 122 | extismOutputSet(memory.ExtismPointer(m.Offset()), clength) 123 | // TODO: coordinate replacement of call to free based on SDK alignment 124 | // extismFree(offset) 125 | } 126 | 127 | // OutputString sends the UTF-8 string `s` to the host output. 128 | func OutputString(s string) { 129 | Output([]byte(s)) 130 | } 131 | 132 | // SetError sets the host error string from `err`. 133 | func SetError(err error) { 134 | SetErrorString(err.Error()) 135 | } 136 | 137 | // SetErrorString sets the host error string from `err`. 138 | func SetErrorString(err string) { 139 | mem := AllocateString(err) 140 | // TODO: coordinate replacement of call to free based on SDK alignment 141 | // defer mem.Free() 142 | extismErrorSet(memory.ExtismPointer(mem.Offset())) 143 | } 144 | 145 | // GetConfig returns the config string associated with `key` (if any). 146 | func GetConfig(key string) (string, bool) { 147 | mem := AllocateBytes([]byte(key)) 148 | defer mem.Free() 149 | 150 | offset := extismConfigGet(memory.ExtismPointer(mem.Offset())) 151 | clength := memory.ExtismLength(offset) 152 | if offset == 0 || clength == 0 { 153 | return "", false 154 | } 155 | 156 | value := make([]byte, clength) 157 | memory.Load(offset, value) 158 | 159 | return string(value), true 160 | } 161 | 162 | // LogMemory logs the `memory` block on the host using the provided log `level`. 163 | func LogMemory(level LogLevel, m Memory) { 164 | configuredLevel := extismGetLogLevel() 165 | if level < LogLevel(configuredLevel) { 166 | return 167 | } 168 | switch level { 169 | case LogInfo: 170 | extismLogInfo(memory.ExtismPointer(m.Offset())) 171 | case LogDebug: 172 | extismLogDebug(memory.ExtismPointer(m.Offset())) 173 | case LogWarn: 174 | extismLogWarn(memory.ExtismPointer(m.Offset())) 175 | case LogError: 176 | extismLogError(memory.ExtismPointer(m.Offset())) 177 | case LogTrace: 178 | extismLogTrace(memory.ExtismPointer(m.Offset())) 179 | } 180 | } 181 | 182 | // Log logs the provided UTF-8 string `s` on the host using the provided log `level`. 183 | func Log(level LogLevel, s string) { 184 | mem := AllocateString(s) 185 | // TODO: coordinate replacement of call to free based on SDK alignment 186 | // defer mem.Free() 187 | 188 | LogMemory(level, mem) 189 | } 190 | 191 | // GetVar returns the byte slice (if any) associated with `key`. 192 | func GetVar(key string) []byte { 193 | mem := AllocateBytes([]byte(key)) 194 | defer mem.Free() 195 | 196 | offset := extismVarGet(memory.ExtismPointer(mem.Offset())) 197 | clength := memory.ExtismLength(offset) 198 | if offset == 0 || clength == 0 { 199 | return nil 200 | } 201 | 202 | value := make([]byte, clength) 203 | memory.Load(offset, value) 204 | 205 | return value 206 | } 207 | 208 | // SetVar sets the host variable associated with `key` to the `value` byte slice. 209 | func SetVar(key string, value []byte) { 210 | keyMem := AllocateBytes([]byte(key)) 211 | // TODO: coordinate replacement of call to free based on SDK alignment 212 | // defer keyMem.Free() 213 | 214 | valMem := AllocateBytes(value) 215 | // TODO: coordinate replacement of call to free based on SDK alignment 216 | // defer valMem.Free() 217 | 218 | extismVarSet( 219 | memory.ExtismPointer(keyMem.Offset()), 220 | memory.ExtismPointer(valMem.Offset()), 221 | ) 222 | } 223 | 224 | // GetVarInt returns the int associated with `key` (or 0 if none). 225 | func GetVarInt(key string) int { 226 | mem := AllocateBytes([]byte(key)) 227 | defer mem.Free() 228 | 229 | offset := extismVarGet(memory.ExtismPointer(mem.Offset())) 230 | clength := memory.ExtismLength(offset) 231 | if offset == 0 || clength == 0 { 232 | return 0 233 | } 234 | 235 | value := make([]byte, clength) 236 | memory.Load(offset, value) 237 | 238 | return int(binary.LittleEndian.Uint64(value)) 239 | } 240 | 241 | // SetVarInt sets the host variable associated with `key` to the `value` int. 242 | func SetVarInt(key string, value int) { 243 | keyMem := AllocateBytes([]byte(key)) 244 | // TODO: coordinate replacement of call to free based on SDK alignment 245 | // defer keyMem.Free() 246 | 247 | bytes := make([]byte, 8) 248 | binary.LittleEndian.PutUint64(bytes, uint64(value)) 249 | 250 | valMem := AllocateBytes(bytes) 251 | // TODO: coordinate replacement of call to free based on SDK alignment 252 | // defer valMem.Free() 253 | 254 | extismVarSet( 255 | memory.ExtismPointer(keyMem.Offset()), 256 | memory.ExtismPointer(valMem.Offset()), 257 | ) 258 | } 259 | 260 | // RemoveVar removes (and frees) the host variable associated with `key`. 261 | func RemoveVar(key string) { 262 | mem := AllocateBytes([]byte(key)) 263 | // TODO: coordinate replacement of call to free based on SDK alignment 264 | // defer mem.Free() 265 | extismVarSet(memory.ExtismPointer(mem.Offset()), 0) 266 | } 267 | 268 | // HTTPRequestMeta represents the metadata associated with an HTTP request on the host. 269 | type HTTPRequestMeta struct { 270 | URL string `json:"url"` 271 | Method string `json:"method"` 272 | Headers map[string]string `json:"headers"` 273 | } 274 | 275 | // HTTPRequest represents an HTTP request sent by the host. 276 | type HTTPRequest struct { 277 | meta HTTPRequestMeta 278 | body []byte 279 | } 280 | 281 | // HTTPResponse represents an HTTP response returned from the host. 282 | type HTTPResponse struct { 283 | memory memory.Memory 284 | status uint16 285 | headers map[string]string 286 | } 287 | 288 | // Memory returns the memory associated with the `HTTPResponse`. 289 | func (r HTTPResponse) Memory() Memory { 290 | return r.memory 291 | } 292 | 293 | // Body returns the body byte slice (if any) from the `HTTPResponse`. 294 | func (r HTTPResponse) Body() []byte { 295 | if r.memory.Length() == 0 { 296 | return nil 297 | } 298 | 299 | buf := make([]byte, r.memory.Length()) 300 | r.memory.Load(buf) 301 | return buf 302 | } 303 | 304 | // Status returns the status code from the `HTTPResponse`. 305 | func (r HTTPResponse) Status() uint16 { 306 | return r.status 307 | } 308 | 309 | // Headers returns a map containing the HTTP response headers 310 | func (r *HTTPResponse) Headers() map[string]string { 311 | return r.headers 312 | } 313 | 314 | // HTTPMethod represents an HTTP method. 315 | type HTTPMethod int32 316 | 317 | const ( 318 | MethodGet HTTPMethod = iota 319 | MethodHead 320 | MethodPost 321 | MethodPut 322 | MethodPatch // RFC 5789 323 | MethodDelete 324 | MethodConnect 325 | MethodOptions 326 | MethodTrace 327 | ) 328 | 329 | func (m HTTPMethod) String() string { 330 | switch m { 331 | case MethodGet: 332 | return "GET" 333 | case MethodHead: 334 | return "HEAD" 335 | case MethodPost: 336 | return "POST" 337 | case MethodPut: 338 | return "PUT" 339 | case MethodPatch: 340 | return "PATCH" 341 | case MethodDelete: 342 | return "DELETE" 343 | case MethodConnect: 344 | return "CONNECT" 345 | case MethodOptions: 346 | return "OPTIONS" 347 | case MethodTrace: 348 | return "TRACE" 349 | default: 350 | return "" 351 | } 352 | } 353 | 354 | // NewHTTPRequest returns a new `HTTPRequest`. 355 | func NewHTTPRequest(method HTTPMethod, url string) *HTTPRequest { 356 | return &HTTPRequest{ 357 | meta: HTTPRequestMeta{ 358 | URL: url, 359 | Headers: map[string]string{}, 360 | Method: method.String(), 361 | }, 362 | body: nil, 363 | } 364 | } 365 | 366 | // SetHeader sets an HTTP header `key` to `value`. 367 | func (r *HTTPRequest) SetHeader(key string, value string) *HTTPRequest { 368 | if r.meta.Headers == nil { 369 | r.meta.Headers = make(map[string]string) 370 | } 371 | r.meta.Headers[key] = value 372 | return r 373 | } 374 | 375 | // SetBody sets an HTTP request body to the provided byte slice. 376 | func (r *HTTPRequest) SetBody(body []byte) *HTTPRequest { 377 | r.body = body 378 | return r 379 | } 380 | 381 | // Send sends the `HTTPRequest` from the host and returns the `HTTPResponse`. 382 | func (r *HTTPRequest) Send() HTTPResponse { 383 | enc, _ := json.Marshal(r.meta) 384 | 385 | req := AllocateBytes(enc) 386 | defer req.Free() 387 | var dataOffset memory.ExtismPointer 388 | if len(r.body) > 0 { 389 | data := AllocateBytes(r.body) 390 | defer data.Free() 391 | dataOffset = memory.ExtismPointer(data.Offset()) 392 | } 393 | 394 | offset := http.ExtismHTTPRequest( 395 | memory.ExtismPointer(req.Offset()), 396 | dataOffset, 397 | ) 398 | length := memory.ExtismLengthUnsafe(offset) 399 | status := uint16(http.ExtismHTTPStatusCode()) 400 | 401 | headersOffs := http.ExtismHTTPHeaders() 402 | headers := map[string]string{} 403 | 404 | if headersOffs != 0 { 405 | length := memory.ExtismLengthUnsafe(headersOffs) 406 | mem := memory.NewMemory(headersOffs, length) 407 | defer mem.Free() 408 | json.Unmarshal(mem.ReadBytes(), &headers) 409 | } 410 | 411 | memory := memory.NewMemory(offset, length) 412 | 413 | return HTTPResponse{ 414 | memory, 415 | status, 416 | headers, 417 | } 418 | } 419 | 420 | // FindMemory finds the host memory block at the given `offset`. 421 | func FindMemory(offset uint64) Memory { 422 | length := memory.ExtismLength(memory.ExtismPointer(offset)) 423 | if length == 0 { 424 | return Memory{} 425 | } 426 | return NewMemory(offset, length) 427 | } 428 | 429 | // ParamBytes returns bytes from Extism host memory given an offset. 430 | func ParamBytes(offset uint64) []byte { 431 | mem := FindMemory(offset) 432 | return mem.ReadBytes() 433 | } 434 | 435 | // ParamString returns UTF-8 string data from Extism host memory given an offset. 436 | func ParamString(offset uint64) string { 437 | return string(ParamBytes(offset)) 438 | } 439 | 440 | // ParamU32 returns a uint32 from Extism host memory given an offset. 441 | func ParamU32(offset uint64) uint32 { 442 | return binary.LittleEndian.Uint32(ParamBytes(offset)) 443 | } 444 | 445 | // ParamU64 returns a uint64 from Extism host memory given an offset. 446 | func ParamU64(offset uint64) uint64 { 447 | return binary.LittleEndian.Uint64(ParamBytes(offset)) 448 | } 449 | 450 | // ResultBytes allocates bytes and returns the offset in Extism host memory. 451 | func ResultBytes(d []byte) uint64 { 452 | mem := AllocateBytes(d) 453 | return mem.Offset() 454 | } 455 | 456 | // ResultString allocates a UTF-8 string and returns the offset in Extism host memory. 457 | func ResultString(s string) uint64 { 458 | mem := AllocateString(s) 459 | return mem.Offset() 460 | } 461 | 462 | // ResultU32 allocates a uint32 and returns the offset in Extism host memory. 463 | func ResultU32(d uint32) uint64 { 464 | mem := AllocateBytes(binary.LittleEndian.AppendUint32([]byte{}, d)) 465 | return mem.Offset() 466 | } 467 | 468 | // ResultU64 allocates a uint64 and returns the offset in Extism host memory. 469 | func ResultU64(d uint64) uint64 { 470 | mem := AllocateBytes(binary.LittleEndian.AppendUint64([]byte{}, d)) 471 | return mem.Offset() 472 | } 473 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/extism/go-pdk 2 | 3 | go 1.21.0 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/go-pdk/12d6f9af953ad4d386b4171442dacdc49a19b777/go.sum -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.21.1 2 | 3 | use ( 4 | . 5 | ./wasi-reactor 6 | ) 7 | -------------------------------------------------------------------------------- /http/httptransport.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | 11 | pdk "github.com/extism/go-pdk" 12 | extismhttp "github.com/extism/go-pdk/internal/http" 13 | "github.com/extism/go-pdk/internal/memory" 14 | ) 15 | 16 | // HTTPTransport implement go's http.RoundTripper interface, enabling usage of standard go 17 | // http.Client within a plugin 18 | type HTTPTransport struct { 19 | } 20 | 21 | func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { 22 | 23 | convertRequestHeaders := func() map[string]string { 24 | result := map[string]string{} 25 | 26 | for name, values := range req.Header { 27 | result[name] = strings.Join(values, ",") 28 | 29 | } 30 | 31 | return result 32 | } 33 | 34 | meta := pdk.HTTPRequestMeta{ 35 | URL: req.URL.String(), 36 | Headers: convertRequestHeaders(), 37 | Method: req.Method, 38 | } 39 | 40 | metaData, err := json.Marshal(meta) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to encode request headers: %q", err) 43 | } 44 | 45 | metaMemory := pdk.AllocateBytes(metaData) 46 | defer metaMemory.Free() 47 | 48 | var bodyMemoryOffset memory.ExtismPointer 49 | if req.Body != nil { 50 | bodyData, err := io.ReadAll(req.Body) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to read body bytes: %q", err) 53 | } 54 | 55 | bodyMemory := pdk.AllocateBytes(bodyData) 56 | defer bodyMemory.Free() 57 | 58 | bodyMemoryOffset = memory.ExtismPointer(bodyMemory.Offset()) 59 | } 60 | 61 | respPointer := extismhttp.ExtismHTTPRequest( 62 | memory.ExtismPointer(metaMemory.Offset()), 63 | bodyMemoryOffset, 64 | ) 65 | respLength := memory.ExtismLengthUnsafe(respPointer) 66 | respStatus := extismhttp.ExtismHTTPStatusCode() 67 | 68 | headersPointer := extismhttp.ExtismHTTPHeaders() 69 | respHeaders := map[string]string{} 70 | 71 | if headersPointer != 0 { 72 | headersLength := memory.ExtismLengthUnsafe(headersPointer) 73 | headersMemory := memory.NewMemory(headersPointer, headersLength) 74 | defer headersMemory.Free() 75 | json.Unmarshal(headersMemory.ReadBytes(), &respHeaders) 76 | } 77 | 78 | convertResponseHeaders := func() http.Header { 79 | result := http.Header{} 80 | for key, value := range respHeaders { 81 | result.Add(key, value) 82 | } 83 | 84 | return result 85 | } 86 | 87 | resp := &http.Response{ 88 | Status: http.StatusText(int(respStatus)), 89 | StatusCode: int(respStatus), 90 | Proto: "HTTP/1.1", 91 | ProtoMajor: 1, 92 | ProtoMinor: 1, 93 | Header: convertResponseHeaders(), 94 | Body: nil, 95 | ContentLength: -1, 96 | Request: req, 97 | } 98 | 99 | hasBody := req.Method != "HEAD" && respLength > 0 100 | if hasBody { 101 | respMemory := memory.NewMemory(respPointer, respLength) 102 | respBuf := make([]byte, respMemory.Length()) 103 | respMemory.Load(respBuf) 104 | 105 | resp.Body = io.NopCloser(bytes.NewReader(respBuf)) 106 | resp.ContentLength = int64(respLength) 107 | } 108 | 109 | return resp, nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/http/extism_http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import "github.com/extism/go-pdk/internal/memory" 4 | 5 | // extismHTTPRequest sends the HTTP `request` to the Extism host with the provided `body` (0 means no body) 6 | // and returns back the memory offset to the response body. 7 | // 8 | //go:wasmimport extism:host/env http_request 9 | func ExtismHTTPRequest(request, body memory.ExtismPointer) memory.ExtismPointer 10 | 11 | // extismHTTPStatusCode returns the status code for the last-sent `extism_http_request` call. 12 | // 13 | //go:wasmimport extism:host/env http_status_code 14 | func ExtismHTTPStatusCode() int32 15 | 16 | // extismHTTPHeaders returns the response headers for the last-sent `extism_http_request` call. 17 | // 18 | //go:wasmimport extism:host/env http_headers 19 | func ExtismHTTPHeaders() memory.ExtismPointer 20 | -------------------------------------------------------------------------------- /internal/memory/allocate.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | // Allocate allocates `length` uninitialized bytes on the host. 4 | func Allocate(length int) Memory { 5 | clength := uint64(length) 6 | offset := ExtismAlloc(clength) 7 | 8 | return NewMemory(offset, clength) 9 | } 10 | 11 | // AllocateBytes allocates and saves the `data` into Memory on the host. 12 | func AllocateBytes(data []byte) Memory { 13 | clength := uint64(len(data)) 14 | offset := ExtismAlloc(clength) 15 | 16 | Store(offset, data) 17 | 18 | return NewMemory(offset, clength) 19 | } 20 | -------------------------------------------------------------------------------- /internal/memory/extism.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | // extismStoreU8 stores the byte `v` at location `offset` in the host memory block. 4 | // 5 | //go:wasmimport extism:host/env store_u8 6 | func extismStoreU8_(ExtismPointer, uint32) 7 | func ExtismStoreU8(offset ExtismPointer, v uint8) { 8 | extismStoreU8_(offset, uint32(v)) 9 | } 10 | 11 | // extismLoadU8 returns the byte located at `offset` in the host memory block. 12 | // 13 | //go:wasmimport extism:host/env load_u8 14 | func extismLoadU8_(offset ExtismPointer) uint32 15 | func ExtismLoadU8(offset ExtismPointer) uint8 { 16 | return uint8(extismLoadU8_(offset)) 17 | } 18 | 19 | // extismStoreU64 stores the 64-bit unsigned integer value `v` at location `offset` in the host memory block. 20 | // Note that `offset` must lie on an 8-byte boundary. 21 | // 22 | //go:wasmimport extism:host/env store_u64 23 | func ExtismStoreU64(offset ExtismPointer, v uint64) 24 | 25 | // extismLoadU64 returns the 64-bit unsigned integer at location `offset` in the host memory block. 26 | // Note that `offset` must lie on an 8-byte boundary. 27 | // 28 | //go:wasmimport extism:host/env load_u64 29 | func ExtismLoadU64(offset ExtismPointer) uint64 30 | 31 | //go:wasmimport extism:host/env length_unsafe 32 | func ExtismLengthUnsafe(ExtismPointer) uint64 33 | 34 | // extismLength returns the number of bytes associated with the block of host memory 35 | // located at `offset`. 36 | // 37 | //go:wasmimport extism:host/env length 38 | func ExtismLength(offset ExtismPointer) uint64 39 | 40 | // extismAlloc allocates `length` bytes of data with host memory for use by the plugin 41 | // and returns its offset within the host memory block. 42 | // 43 | //go:wasmimport extism:host/env alloc 44 | func ExtismAlloc(length uint64) ExtismPointer 45 | 46 | // extismFree releases the bytes previously allocated with `extism_alloc` at the given `offset`. 47 | // 48 | //go:wasmimport extism:host/env free 49 | func ExtismFree(offset ExtismPointer) 50 | -------------------------------------------------------------------------------- /internal/memory/memory.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import "encoding/binary" 4 | 5 | func Load(offset ExtismPointer, buf []byte) { 6 | length := len(buf) 7 | chunkCount := length >> 3 8 | 9 | for chunkIdx := 0; chunkIdx < chunkCount; chunkIdx++ { 10 | i := chunkIdx << 3 11 | binary.LittleEndian.PutUint64(buf[i:i+8], ExtismLoadU64(offset+ExtismPointer(i))) 12 | } 13 | 14 | remainder := length & 7 15 | remainderOffset := chunkCount << 3 16 | for index := remainderOffset; index < (remainder + remainderOffset); index++ { 17 | buf[index] = ExtismLoadU8(offset + ExtismPointer(index)) 18 | } 19 | } 20 | 21 | func Store(offset ExtismPointer, buf []byte) { 22 | length := len(buf) 23 | chunkCount := length >> 3 24 | 25 | for chunkIdx := 0; chunkIdx < chunkCount; chunkIdx++ { 26 | i := chunkIdx << 3 27 | x := binary.LittleEndian.Uint64(buf[i : i+8]) 28 | ExtismStoreU64(offset+ExtismPointer(i), x) 29 | } 30 | 31 | remainder := length & 7 32 | remainderOffset := chunkCount << 3 33 | for index := remainderOffset; index < (remainder + remainderOffset); index++ { 34 | ExtismStoreU8(offset+ExtismPointer(index), buf[index]) 35 | } 36 | } 37 | 38 | func NewMemory(offset ExtismPointer, length uint64) Memory { 39 | return Memory{ 40 | offset: offset, 41 | length: length, 42 | } 43 | } 44 | 45 | // Memory represents memory allocated by (and shared with) the host. 46 | type Memory struct { 47 | offset ExtismPointer 48 | length uint64 49 | } 50 | 51 | // Load copies the host memory block to the provided `buffer` byte slice. 52 | func (m *Memory) Load(buffer []byte) { 53 | Load(m.offset, buffer) 54 | } 55 | 56 | // Store copies the `data` byte slice into host memory. 57 | func (m *Memory) Store(data []byte) { 58 | Store(m.offset, data) 59 | } 60 | 61 | // Free frees the host memory block. 62 | func (m *Memory) Free() { 63 | ExtismFree(m.offset) 64 | 65 | } 66 | 67 | // Length returns the number of bytes in the host memory block. 68 | func (m *Memory) Length() uint64 { 69 | return m.length 70 | } 71 | 72 | // Offset returns the offset of the host memory block. 73 | func (m *Memory) Offset() uint64 { 74 | return uint64(m.offset) 75 | } 76 | 77 | // ReadBytes returns the host memory block as a slice of bytes. 78 | func (m *Memory) ReadBytes() []byte { 79 | buff := make([]byte, m.length) 80 | m.Load(buff) 81 | return buff 82 | } 83 | -------------------------------------------------------------------------------- /internal/memory/pointer.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | type ExtismPointer uint64 4 | -------------------------------------------------------------------------------- /wasi-reactor/extism_pdk_reactor.go: -------------------------------------------------------------------------------- 1 | package reactor 2 | 3 | //export __wasm_call_ctors 4 | func wasmCallCtors() 5 | 6 | //export _initialize 7 | func initialize() { 8 | wasmCallCtors() 9 | } 10 | -------------------------------------------------------------------------------- /wasi-reactor/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/extism/go-pdk/wasi-reactor 2 | 3 | go 1.21.1 4 | --------------------------------------------------------------------------------