├── .github ├── CODEOWNERS ├── actions │ └── libextism │ │ └── action.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples └── basic.zig ├── src ├── cancel_handle.zig ├── compiled_plugin.zig ├── current_plugin.zig ├── ffi.zig ├── function.zig ├── main.zig ├── manifest.zig └── plugin.zig ├── test.zig └── wasm ├── code-functions.wasm └── loop.wasm /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nilslice 2 | -------------------------------------------------------------------------------- /.github/actions/libextism/action.yaml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | name: libextism 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | repository: extism/cli 11 | path: .extism-cli 12 | - uses: ./.extism-cli/.github/actions/extism-cli 13 | - name: Install 14 | shell: bash 15 | run: sudo extism lib install --version git 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Zig CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | zig: 11 | name: Zig CI 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | zig_version: ["0.14.0"] # eventually use multiple versions once stable 16 | rust: 17 | - stable 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v3 21 | - uses: ./.github/actions/libextism 22 | - name: Setup Zig env 23 | uses: goto-bus-stop/setup-zig@v2 24 | with: 25 | version: ${{ matrix.zig_version }} 26 | - name: Test Zig Host SDK 27 | run: | 28 | zig version 29 | zig build test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is for zig-specific build artifacts. 2 | # If you have OS-specific or editor-specific files to ignore, 3 | # such as *.swp or .DS_Store, put those in your global 4 | # ~/.gitignore and put this in your ~/.gitconfig: 5 | # 6 | # [core] 7 | # excludesfile = ~/.gitignore 8 | # 9 | # Cheers! 10 | # -andrewrk 11 | 12 | .zig-cache/ 13 | zig-out/ 14 | /release/ 15 | /debug/ 16 | /build/ 17 | /build-*/ 18 | /docgen_tmp/ 19 | *.log 20 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extism Zig Host SDK 2 | 3 | This repo contains the Zig code for integrating with the [Extism](https://extism.org/) runtime. Install this library into your host Zig application to run Extism plug-ins. 4 | 5 | > **Note**: If you're unsure what Extism is or what an SDK is see our homepage: [https://extism.org](https://extism.org). 6 | 7 | ## Installation 8 | 9 | ### Install the Extism Runtime Dependency 10 | 11 | For this library, you first need to install the Extism Runtime. You can [download the shared object directly from a release](https://github.com/extism/extism/releases) or use the [Extism CLI](https://github.com/extism/cli) to install it: 12 | 13 | ```bash 14 | sudo extism lib install latest 15 | 16 | #=> Fetching https://github.com/extism/extism/releases/download/v0.5.2/libextism-aarch64-apple-darwin-v0.5.2.tar.gz 17 | #=> Copying libextism.dylib to /usr/local/lib/libextism.dylib 18 | #=> Copying extism.h to /usr/local/include/extism.h 19 | ``` 20 | 21 | # within your Zig project directory: 22 | 23 | ``` 24 | zig fetch --save https://github.com/extism/zig-sdk/archive/.tar.gz 25 | ``` 26 | 27 | And in your `build.zig`: 28 | ```zig 29 | // to use the build script util, import extism: 30 | const extism = @import("extism"); 31 | 32 | // inside your `build` function, after you've created tests or an executable step: 33 | extism.addLibrary(exe, b); 34 | ``` 35 | 36 | ## Getting Started 37 | 38 | This guide should walk you through some of the concepts in Extism and this Zig library. 39 | 40 | ### Creating A Plug-in 41 | 42 | The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). You can think of a plug-in as a code module stored in a `.wasm` file. 43 | 44 | Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web: 45 | 46 | ```zig 47 | // First require the library 48 | const extism = @import("extism"); 49 | const std = @import("std"); 50 | 51 | const wasm_url = extism.manifest.WasmUrl{ .url = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm" }; 52 | const manifest = extism.manifest.Manifest{ .wasm = &[_]extism.manifest.Wasm{.{ .wasm_url= wasm_url }} }; 53 | 54 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 55 | defer std.debug.assert(gpa.deinit() == .ok); 56 | const allocator = gpa.allocator(); 57 | 58 | var plugin = try extism.Plugin.initFromManifest( 59 | allocator, 60 | manifest, 61 | &[_]extism.Function{}, 62 | false, 63 | ); 64 | 65 | defer plugin.deinit(); 66 | ``` 67 | 68 | > **Note**: See [the Manifest docs](https://github.com/extism/zig-sdk/blob/main/src/manifest.zig#L32) as it has a rich schema and a lot of options. 69 | 70 | ### Calling A Plug-in's Exports 71 | 72 | This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: `count_vowels`. We can call exports using [Extism::Plugin#call](https://github.com/extism/zig-sdk/blob/main/src/plugin.zig#L61): 73 | 74 | ```zig 75 | try plugin.call("count_vowels", "Hello, World!"); 76 | # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 77 | ``` 78 | 79 | All exports have a simple interface of bytes-in and bytes-out. This plug-in happens to take a string and return a JSON encoded string with a report of results. 80 | 81 | ### Plug-in State 82 | 83 | Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export: 84 | 85 | ```zig 86 | try plugin.call("count_vowels", "Hello, World!"); 87 | # => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 88 | try plugin.call("count_vowels", "Hello, World!"); 89 | # => {"count": 3, "total": 9, "vowels": "aeiouAEIOU"} 90 | ``` 91 | 92 | These variables will persist until this plug-in is freed or you initialize a new one. 93 | 94 | ### Configuration 95 | 96 | Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example: 97 | 98 | ```zig 99 | try plugin.call("count_vowels", "Yellow, World!"); 100 | # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 101 | 102 | var config = std.json.ArrayHashMap([]const u8){}; 103 | defer config.deinit(allocator); 104 | 105 | try config.map.put(allocator, "vowels", "aeiouyAEIOUY"); 106 | try plugin.setConfig(allocator, config); 107 | 108 | try plugin.call("count_vowels", "Yellow, World!"); 109 | # => {"count": 4, "total": 4, "vowels": "aeiouAEIOUY"} 110 | ``` 111 | 112 | ### Host Functions 113 | 114 | Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, let's store it in a persistent key-value store! 115 | 116 | Wasm can't use our KV store on it's own. This is where [Host Functions](https://extism.org/docs/concepts/host-functions) come in. 117 | 118 | [Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. They are simply some zig methods you write which can be passed down and invoked from any language inside the plug-in. 119 | 120 | Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in: 121 | 122 | ```zig 123 | const wasm_url = extism.manifest.WasmUrl{ .url = "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm" }; 124 | const manifest = extism.manifest.Manifest{ .wasm = &[_]extism.manifest.Wasm{.{ .wasm_url= wasm_url }} }; 125 | ``` 126 | 127 | > *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages. 128 | 129 | Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy our its import interface for a KV store. 130 | 131 | We want to expose two functions to our plugin, `kv_write(key: String, value: Bytes)` which writes a bytes value to a key and `kv_read(key: String) -> Bytes` which reads the bytes at the given `key`. 132 | 133 | ```zig 134 | // pretend this is Redis or something 135 | var KV_STORE: std.StringHashMap(u32) = undefined; 136 | 137 | export fn kv_read(caller: ?*extism.c.ExtismCurrentPlugin, inputs: [*c]const extism.c.ExtismVal, n_inputs: u64, outputs: [*c]extism.c.ExtismVal, n_outputs: u64, user_data: ?*anyopaque) callconv(.C) void { 138 | _ = user_data; 139 | var curr_plugin = extism.CurrentPlugin.getCurrentPlugin(caller orelse unreachable); 140 | 141 | // retrieve the key from the plugin 142 | var input_slice = inputs[0..n_inputs]; 143 | const key = curr_plugin.inputBytes(&input_slice[0]); 144 | 145 | var out = outputs[0..n_outputs]; 146 | // Try to get the value from KV_STORE 147 | if (KV_STORE.get(key)) |val| { 148 | // return the value to the plugin 149 | var data: [4]u8 = undefined; 150 | std.mem.writeInt(u32, &data, val, .little); 151 | curr_plugin.returnBytes(&out[0], &data); 152 | } else { 153 | KV_STORE.put(key, 0) catch unreachable; 154 | curr_plugin.returnBytes(&out[0], &[4]u8{ 0, 0, 0, 0 }); 155 | } 156 | } 157 | 158 | export fn kv_write(caller: ?*extism.c.ExtismCurrentPlugin, inputs: [*c]const extism.c.ExtismVal, n_inputs: u64, outputs: [*c]extism.c.ExtismVal, n_outputs: u64, user_data: ?*anyopaque) callconv(.C) void { 159 | _ = user_data; 160 | _ = outputs; 161 | _ = n_outputs; 162 | var curr_plugin = extism.CurrentPlugin.getCurrentPlugin(caller orelse unreachable); 163 | 164 | // retrieve key and value from the plugin 165 | var in = inputs[0..n_inputs]; 166 | const key = curr_plugin.inputBytes(&in[0]); 167 | const val = curr_plugin.inputBytes(&in[1]); 168 | 169 | // write to the KV 170 | KV_STORE.put(key, std.mem.readInt(u32, val[0..4], .little)) catch unreachable; 171 | } 172 | 173 | ``` 174 | 175 | Now we just need to create a new host environment and pass it in when loading the plug-in. Here our environment initializer takes no arguments, but you could imagine putting some customer specific instance variables in there: 176 | 177 | ```zig 178 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 179 | const allocator = gpa.allocator(); 180 | 181 | KV_STORE = std.StringHashMap([]const u8).init(allocator); 182 | defer KV_STORE.deinit(); 183 | 184 | var f_read = extism.Function.init( 185 | "kv_read", 186 | &[_]extism.c.ExtismValType{extism.PTR}, 187 | &[_]extism.c.ExtismValType{extism.PTR}, 188 | &kv_read, 189 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 190 | ); 191 | defer f_read.deinit(); 192 | 193 | var f_write = extism.Function.init( 194 | "kv_write", 195 | &[_]extism.c.ExtismValType{extism.PTR, extism.PTR}, 196 | &[_]extism.c.ExtismValType{}, 197 | &kv_write, 198 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 199 | ); 200 | defer f_write.deinit(); 201 | 202 | var plugin = try extism.Plugin.initFromManifest( 203 | allocator, 204 | manifest, 205 | &[_]extism.Function{f_read, f_write}, 206 | false, 207 | ); 208 | defer plugin.deinit(); 209 | ``` 210 | 211 | Now we can invoke the event: 212 | 213 | ```zig 214 | try plugin.call("count_vowels", "Hello, World!"); 215 | # => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 216 | 217 | try plugin.call("count_vowels", "Hello, World!"); 218 | # => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"} 219 | ``` 220 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | pub fn build(b: *std.Build) void { 5 | comptime { 6 | const current_zig = builtin.zig_version; 7 | const min_zig = std.SemanticVersion.parse("0.14.0") catch unreachable; // https://ziglang.org/download/0.14.0/release-notes.html 8 | if (current_zig.order(min_zig) == .lt) { 9 | @compileError(std.fmt.comptimePrint("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig })); 10 | } 11 | } 12 | 13 | const target = b.standardTargetOptions(.{}); 14 | const optimize = b.standardOptimizeOption(.{}); 15 | 16 | const extism_module = b.addModule("extism", .{ 17 | .root_source_file = b.path("src/main.zig"), 18 | }); 19 | extism_module.addIncludePath(.{ .cwd_relative = "/usr/local/include" }); 20 | extism_module.addLibraryPath(.{ .cwd_relative = "/usr/local/lib" }); 21 | 22 | var tests = b.addTest(.{ 23 | .name = "Library Tests", 24 | .root_source_file = b.path("test.zig"), 25 | .target = target, 26 | .optimize = optimize, 27 | }); 28 | 29 | tests.root_module.addImport("extism", extism_module); 30 | tests.linkLibC(); 31 | tests.linkSystemLibrary("extism"); 32 | const tests_run_step = b.addRunArtifact(tests); 33 | 34 | const test_step = b.step("test", "Run library tests"); 35 | test_step.dependOn(&tests_run_step.step); 36 | 37 | var example = b.addExecutable(.{ 38 | .name = "Example", 39 | .root_source_file = b.path("examples/basic.zig"), 40 | .target = target, 41 | .optimize = optimize, 42 | }); 43 | 44 | example.root_module.addImport("extism", extism_module); 45 | example.linkLibC(); 46 | example.linkSystemLibrary("extism"); 47 | const example_run_step = b.addRunArtifact(example); 48 | 49 | const example_step = b.step("run_example", "Run the basic example"); 50 | example_step.dependOn(&example_run_step.step); 51 | } 52 | 53 | pub fn addLibrary(to: *std.Build.Step.Compile, b: *std.Build) void { 54 | to.root_module.addImport("extism", b.dependency("extism", .{}).module("extism")); 55 | to.linkLibC(); 56 | to.linkSystemLibrary("extism"); 57 | } 58 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .extism, 3 | .version = "1.0.0-rc3", 4 | .minimum_zig_version = "0.14.0", 5 | .fingerprint = 0xb41364f00afbe9db, 6 | .paths = .{""}, 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const sdk = @import("extism"); 3 | const Plugin = sdk.Plugin; 4 | const CurrentPlugin = sdk.CurrentPlugin; 5 | const CompiledPlugin = sdk.CompiledPlugin; 6 | const Function = sdk.Function; 7 | const manifest = sdk.manifest; 8 | 9 | export fn hello_world(plugin_ptr: ?*sdk.c.ExtismCurrentPlugin, inputs: [*c]const sdk.c.ExtismVal, n_inputs: u64, outputs: [*c]sdk.c.ExtismVal, n_outputs: u64, user_data: ?*anyopaque) callconv(.C) void { 10 | std.debug.print("Hello from Zig!\n", .{}); 11 | const str_ud = @as([*:0]const u8, @ptrCast(user_data orelse unreachable)); 12 | std.debug.print("User data: {s}\n", .{str_ud}); 13 | var input_slice = inputs[0..n_inputs]; 14 | var output_slice = outputs[0..n_outputs]; 15 | var curr_plugin = CurrentPlugin.getCurrentPlugin(plugin_ptr orelse unreachable); 16 | const input = curr_plugin.inputBytes(&input_slice[0]); 17 | std.debug.print("input: {s}\n", .{input}); 18 | curr_plugin.returnBytes(&output_slice[0], input); 19 | } 20 | 21 | pub fn main() !void { 22 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 23 | defer std.debug.assert(gpa.deinit() == .ok); 24 | const allocator = gpa.allocator(); 25 | _ = sdk.setLogFile("extism.log", .Debug); 26 | 27 | const wasmfile_manifest = manifest.WasmFile{ .path = "wasm/code-functions.wasm" }; 28 | const man = manifest.Manifest{ .wasm = &[_]manifest.Wasm{.{ .wasm_file = wasmfile_manifest }} }; 29 | var f = Function.init( 30 | "hello_world", 31 | &[_]sdk.c.ExtismValType{sdk.PTR}, 32 | &[_]sdk.c.ExtismValType{sdk.PTR}, 33 | &hello_world, 34 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 35 | ); 36 | defer f.deinit(); 37 | var c = try CompiledPlugin.initFromManifest(allocator, man, &[_]Function{f}, true); 38 | defer c.deinit(); 39 | 40 | var my_plugin = try Plugin.initFromCompiled(c); 41 | defer my_plugin.deinit(); 42 | 43 | var config = std.json.ArrayHashMap([]const u8){}; 44 | defer config.deinit(allocator); 45 | try config.map.put(allocator, "thing", "this is a really important thing"); 46 | try my_plugin.setConfig(allocator, config); 47 | 48 | const input = "aeiouAEIOU____________________________________&smtms_y?" ** 1182; 49 | if (my_plugin.call("count_vowels", input)) |data| { 50 | std.debug.print("plugin output: {s}\n", .{data}); 51 | } else |err| switch (err) { 52 | error.PluginCallFailed => { 53 | std.debug.print("plugin returned error: {s}\n", .{my_plugin.error_info.?}); 54 | }, 55 | } 56 | std.debug.print("extism version: {s}\n", .{sdk.extismVersion()}); 57 | std.debug.print("has count_vowels: {}\n", .{my_plugin.hasFunction("count_vowels")}); 58 | } 59 | -------------------------------------------------------------------------------- /src/cancel_handle.zig: -------------------------------------------------------------------------------- 1 | const c = @import("ffi.zig"); 2 | const Self = @This(); 3 | 4 | handle: ?*const c.ExtismCancelHandle, 5 | 6 | pub fn cancel(self: *Self) bool { 7 | return c.extism_plugin_cancel(self.handle); 8 | } 9 | -------------------------------------------------------------------------------- /src/compiled_plugin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Manifest = @import("manifest.zig").Manifest; 3 | const Function = @import("function.zig"); 4 | const CancelHandle = @import("cancel_handle.zig"); 5 | const c = @import("ffi.zig"); 6 | 7 | const Self = @This(); 8 | 9 | ptr: *c.ExtismCompiledPlugin, 10 | 11 | // We have to use this until ziglang/zig#2647 is resolved. 12 | error_info: ?[]const u8, 13 | 14 | /// Create a new plugin from a WASM module 15 | pub fn init(allocator: std.mem.Allocator, data: []const u8, functions: []const Function, wasi: bool) !Self { 16 | var plugin: ?*c.ExtismCompiledPlugin = null; 17 | var errmsg: [*c]u8 = null; 18 | if (functions.len > 0) { 19 | const funcPtrs = try allocator.alloc(?*const c.ExtismFunction, functions.len); 20 | defer allocator.free(funcPtrs); 21 | for (functions, 0..) |function, i| { 22 | funcPtrs[i] = function.c_func; 23 | } 24 | plugin = c.extism_compiled_plugin_new(data.ptr, @as(u64, data.len), &funcPtrs[0], functions.len, wasi, &errmsg); 25 | } else { 26 | plugin = c.extism_compiled_plugin_new(data.ptr, @as(u64, data.len), null, 0, wasi, &errmsg); 27 | } 28 | 29 | if (plugin == null) { 30 | // TODO: figure out what to do with this error 31 | std.debug.print("extism_compiled_plugin_new: {s}\n", .{ 32 | errmsg, 33 | }); 34 | c.extism_plugin_new_error_free(errmsg); 35 | return error.PluginLoadFailed; 36 | } 37 | return Self{ 38 | .ptr = plugin.?, 39 | .error_info = null, 40 | }; 41 | } 42 | 43 | /// Create a new plugin from the given manifest 44 | pub fn initFromManifest(allocator: std.mem.Allocator, manifest: Manifest, functions: []const Function, wasi: bool) !Self { 45 | const json = try std.json.stringifyAlloc(allocator, manifest, .{ .emit_null_optional_fields = false }); 46 | defer allocator.free(json); 47 | return init(allocator, json, functions, wasi); 48 | } 49 | 50 | pub fn deinit(self: *Self) void { 51 | c.extism_compiled_plugin_free(self.ptr); 52 | } 53 | -------------------------------------------------------------------------------- /src/current_plugin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("ffi.zig"); 3 | 4 | c_currplugin: *c.ExtismCurrentPlugin, 5 | 6 | const Self = @This(); 7 | const MemoryHandle = u64; 8 | 9 | pub fn getCurrentPlugin(ptr: *c.ExtismCurrentPlugin) Self { 10 | return .{ .c_currplugin = ptr }; 11 | } 12 | 13 | pub fn getMemory(self: Self, offset: MemoryHandle) []u8 { 14 | const len = c.extism_current_plugin_memory_length(self.c_currplugin, offset); 15 | const c_data = c.extism_current_plugin_memory(self.c_currplugin); 16 | const data: [*:0]u8 = std.mem.span(c_data); 17 | return data[offset .. offset + len]; 18 | } 19 | 20 | pub fn alloc(self: *Self, n: u64) MemoryHandle { 21 | return c.extism_current_plugin_memory_alloc(self.c_currplugin, n); 22 | } 23 | 24 | pub fn free(self: *Self, offset: MemoryHandle) void { 25 | c.extism_current_plugin_memory_free(self.c_currplugin, offset); 26 | } 27 | 28 | pub fn length(self: *Self, offset: MemoryHandle) u64 { 29 | return c.extism_current_plugin_memory_length(self.c_currplugin, offset); 30 | } 31 | 32 | pub fn returnBytes(self: *Self, val: *c.ExtismVal, data: []const u8) void { 33 | const mem = self.alloc(@as(u64, data.len)); 34 | const ptr = self.getMemory(mem); 35 | @memcpy(ptr, data); 36 | val.v.i64 = @intCast(mem); 37 | } 38 | 39 | pub fn inputBytes(self: *Self, val: *const c.ExtismVal) []const u8 { 40 | return self.getMemory(@intCast(val.v.i64)); 41 | } 42 | 43 | pub fn hostContext(self: *Self) ?*anyopaque { 44 | return c.extism_current_plugin_host_context(self.c_currplugin); 45 | } 46 | -------------------------------------------------------------------------------- /src/ffi.zig: -------------------------------------------------------------------------------- 1 | pub usingnamespace @cImport({ 2 | @cInclude("extism.h"); 3 | }); 4 | -------------------------------------------------------------------------------- /src/function.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @import("ffi.zig"); 3 | 4 | const Self = @This(); 5 | c_func: ?*c.ExtismFunction, 6 | 7 | pub fn init(name: []const u8, inputs: []const c.ExtismValType, outputs: []const c.ExtismValType, f: c.ExtismFunctionType, user_data: ?*anyopaque) Self { 8 | var inputsPtr: ?*const c.ExtismValType = null; 9 | if (inputs.len > 0) { 10 | inputsPtr = &inputs[0]; 11 | } 12 | var outputsPtr: ?*const c.ExtismValType = null; 13 | if (outputs.len > 0) { 14 | outputsPtr = &outputs[0]; 15 | } 16 | const ptr = c.extism_function_new(name.ptr, inputsPtr, @as(u64, inputs.len), outputsPtr, @as(u64, outputs.len), f, user_data, null); 17 | 18 | return .{ .c_func = ptr }; 19 | } 20 | 21 | pub fn deinit(self: *Self) void { 22 | c.extism_function_free(self.c_func); 23 | self.c_func = null; 24 | } 25 | 26 | pub fn setNamespace(self: *Self, namespace: []const u8) void { 27 | c.extism_function_set_namespace(self.c_func, namespace.ptr); 28 | } 29 | 30 | pub fn withNamespace(self: Self, namespace: []const u8) Self { 31 | var not_so_self = self; 32 | not_so_self.setNamespace(namespace); 33 | return not_so_self; 34 | } 35 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | pub const c = @import("ffi.zig"); 4 | 5 | pub const Plugin = @import("plugin.zig"); 6 | pub const CompiledPlugin = @import("compiled_plugin.zig"); 7 | pub const CurrentPlugin = @import("current_plugin.zig"); 8 | pub const CancelHandle = @import("cancel_handle.zig"); 9 | pub const Function = @import("function.zig"); 10 | pub const manifest = @import("manifest.zig"); 11 | pub const LogLevel = enum { 12 | Error, 13 | Warn, 14 | Info, 15 | Debug, 16 | Trace, 17 | }; 18 | 19 | pub fn setLogFile(file_name: []const u8, level: LogLevel) bool { 20 | const res = c.extism_log_file(file_name.ptr, @tagName(level)); 21 | return res; 22 | } 23 | 24 | pub fn extismVersion() []const u8 { 25 | const c_version = c.extism_version(); 26 | const version = std.mem.span(c_version); 27 | return version; 28 | } 29 | 30 | pub const PTR = c.ExtismValType_I64; 31 | -------------------------------------------------------------------------------- /src/manifest.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const WasmData = struct { 4 | data: []const u8, 5 | hash: ?[]const u8 = null, 6 | name: ?[]const u8 = null, 7 | }; 8 | 9 | pub const WasmFile = struct { path: []const u8, hash: ?[]const u8 = null, name: ?[]const u8 = null }; 10 | 11 | pub const WasmUrl = struct { 12 | url: []const u8, 13 | hash: ?[]const u8 = null, 14 | name: ?[]const u8 = null, 15 | method: ?[]const u8 = null, 16 | headers: ?std.json.ArrayHashMap([]const u8) = null, 17 | }; 18 | 19 | pub const Wasm = union(enum) { 20 | wasm_data: WasmData, 21 | wasm_file: WasmFile, 22 | wasm_url: WasmUrl, 23 | pub fn jsonStringify(self: @This(), jws: anytype) !void { 24 | switch (self) { 25 | inline else => |value| { 26 | try jws.write(value); 27 | }, 28 | } 29 | } 30 | }; 31 | 32 | pub const Manifest = struct { 33 | wasm: []const Wasm, 34 | memory: ?struct { max_pages: ?u32, max_http_response_bytes: ?u64 } = null, 35 | config: ?std.json.ArrayHashMap([]const u8) = null, 36 | allowed_hosts: ?[]const []const u8 = null, 37 | allowed_paths: ?std.json.ArrayHashMap([]const u8) = null, 38 | timeout: ?usize = null, 39 | }; 40 | -------------------------------------------------------------------------------- /src/plugin.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Manifest = @import("manifest.zig").Manifest; 3 | const Function = @import("function.zig"); 4 | const CancelHandle = @import("cancel_handle.zig"); 5 | const c = @import("ffi.zig"); 6 | const CompiledPlugin = @import("compiled_plugin.zig"); 7 | 8 | const Self = @This(); 9 | 10 | ptr: *c.ExtismPlugin, 11 | 12 | // We have to use this until ziglang/zig#2647 is resolved. 13 | error_info: ?[]const u8, 14 | 15 | /// Create a new plugin from a WASM module 16 | pub fn init(allocator: std.mem.Allocator, data: []const u8, functions: []const Function, wasi: bool) !Self { 17 | var plugin: ?*c.ExtismPlugin = null; 18 | var errmsg: [*c]u8 = null; 19 | if (functions.len > 0) { 20 | const funcPtrs = try allocator.alloc(?*const c.ExtismFunction, functions.len); 21 | defer allocator.free(funcPtrs); 22 | for (functions, 0..) |function, i| { 23 | funcPtrs[i] = function.c_func; 24 | } 25 | plugin = c.extism_plugin_new(data.ptr, @as(u64, data.len), &funcPtrs[0], functions.len, wasi, &errmsg); 26 | } else { 27 | plugin = c.extism_plugin_new(data.ptr, @as(u64, data.len), null, 0, wasi, &errmsg); 28 | } 29 | 30 | if (plugin == null) { 31 | // TODO: figure out what to do with this error 32 | std.debug.print("extism_plugin_new: {s}", .{ 33 | errmsg, 34 | }); 35 | c.extism_plugin_new_error_free(errmsg); 36 | return error.PluginLoadFailed; 37 | } 38 | return Self{ 39 | .ptr = plugin.?, 40 | .error_info = null, 41 | }; 42 | } 43 | 44 | /// Create a new plugin from the given manifest 45 | pub fn initFromManifest(allocator: std.mem.Allocator, manifest: Manifest, functions: []const Function, wasi: bool) !Self { 46 | const json = try std.json.stringifyAlloc(allocator, manifest, .{ .emit_null_optional_fields = false }); 47 | defer allocator.free(json); 48 | return init(allocator, json, functions, wasi); 49 | } 50 | 51 | /// Create a new plugin from a pre-compiled plugin 52 | pub fn initFromCompiled(compiled: *CompiledPlugin) !Self { 53 | var errmsg: [*c]u8 = null; 54 | const plugin = c.extism_plugin_new_from_compiled(compiled.ptr, &errmsg); 55 | if (plugin == null) { 56 | // TODO: figure out what to do with this error 57 | std.debug.print("extism_plugin_new: {s}\n", .{ 58 | errmsg, 59 | }); 60 | c.extism_plugin_new_error_free(errmsg); 61 | return error.PluginLoadFailed; 62 | } 63 | return plugin; 64 | } 65 | 66 | pub fn deinit(self: *Self) void { 67 | c.extism_plugin_free(self.ptr); 68 | } 69 | 70 | pub fn cancelHandle(self: *Self) CancelHandle { 71 | const ptr = c.extism_plugin_cancel_handle(self.ptr); 72 | return .{ .handle = ptr }; 73 | } 74 | 75 | fn handleCall(self: *Self, res: i32) ![]const u8 { 76 | if (res != 0) { 77 | const err_c = c.extism_plugin_error(self.ptr); 78 | const err = std.mem.span(err_c); 79 | 80 | if (!std.mem.eql(u8, err, "")) { 81 | self.error_info = err; 82 | } 83 | self.error_info = ""; 84 | return error.PluginCallFailed; 85 | } 86 | 87 | const len = c.extism_plugin_output_length(self.ptr); 88 | 89 | if (len > 0) { 90 | const output_data = c.extism_plugin_output_data(self.ptr); 91 | return output_data[0..len]; 92 | } 93 | return ""; 94 | } 95 | 96 | /// Call a function with the given input 97 | pub fn call(self: *Self, function_name: []const u8, input: []const u8) ![]const u8 { 98 | const res = c.extism_plugin_call(self.ptr, function_name.ptr, input.ptr, @as(u64, input.len)); 99 | return self.handleCall(res); 100 | } 101 | 102 | /// Call a function with the given input and host context 103 | pub fn callWithContext(self: *Self, function_name: []const u8, input: []const u8, host_context: *anyopaque) ![]const u8 { 104 | const res = c.extism_plugin_call_with_host_context(self.ptr, function_name.ptr, input.ptr, @as(u64, input.len), host_context); 105 | return self.handleCall(res); 106 | } 107 | 108 | /// Set configuration values 109 | pub fn setConfig(self: *Self, allocator: std.mem.Allocator, config: std.json.ArrayHashMap([]const u8)) !void { 110 | const config_json = try std.json.stringifyAlloc(allocator, config, .{ .emit_null_optional_fields = false }); 111 | defer allocator.free(config_json); 112 | _ = c.extism_plugin_config(self.ptr, config_json.ptr, @as(u64, config_json.len)); 113 | } 114 | 115 | /// Returns true if the plugin has a function matching `function_name` 116 | pub fn hasFunction(self: Self, function_name: []const u8) bool { 117 | const res = c.extism_plugin_function_exists(self.ptr, function_name.ptr); 118 | return res; 119 | } 120 | -------------------------------------------------------------------------------- /test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const sdk = @import("extism"); 4 | const Plugin = sdk.Plugin; 5 | const CurrentPlugin = sdk.CurrentPlugin; 6 | const Function = sdk.Function; 7 | const manifest = sdk.manifest; 8 | 9 | export fn hello_world(plugin_ptr: ?*sdk.c.ExtismCurrentPlugin, inputs: [*c]const sdk.c.ExtismVal, n_inputs: u64, outputs: [*c]sdk.c.ExtismVal, n_outputs: u64, user_data: ?*anyopaque) callconv(.C) void { 10 | std.debug.print("Hello from Zig!\n", .{}); 11 | const str_ud = @as([*:0]const u8, @ptrCast(user_data orelse unreachable)); 12 | std.debug.print("User data: {s}\n", .{str_ud}); 13 | var input_slice = inputs[0..n_inputs]; 14 | var output_slice = outputs[0..n_outputs]; 15 | var curr_plugin = CurrentPlugin.getCurrentPlugin(plugin_ptr orelse unreachable); 16 | const input = curr_plugin.inputBytes(&input_slice[0]); 17 | std.debug.print("input: {s}\n", .{input}); 18 | if (curr_plugin.hostContext()) |ctx_ptr| { 19 | const ctx: *u64 = @alignCast(@ptrCast(ctx_ptr)); 20 | std.debug.print("Host context={}\n", .{ctx.*}); 21 | } 22 | output_slice[0] = input_slice[0]; 23 | } 24 | 25 | const wasmfile_manifest = manifest.WasmFile{ .path = "wasm/code-functions.wasm" }; 26 | const man = manifest.Manifest{ .wasm = &[_]manifest.Wasm{.{ .wasm_file = wasmfile_manifest }} }; 27 | 28 | test "Single threaded tests" { 29 | var wasm_start = try std.time.Timer.start(); 30 | _ = sdk.setLogFile("test.log", .Debug); 31 | 32 | var f = Function.init( 33 | "hello_world", 34 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 35 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 36 | &hello_world, 37 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 38 | ); 39 | defer f.deinit(); 40 | 41 | var plugin = try Plugin.initFromManifest(testing.allocator, man, &[_]Function{f}, true); 42 | defer plugin.deinit(); 43 | 44 | std.debug.print("\nregister loaded plugin: {}\n", .{std.fmt.fmtDuration(wasm_start.read())}); 45 | const repeat = 1182; 46 | const input = "aeiouAEIOU____________________________________&smtms_y?" ** repeat; 47 | var data = try plugin.call("count_vowels", input); 48 | try testing.expectEqualStrings("{\"count\": 11820}", data); 49 | std.debug.print("register plugin + function call: {}, sent input size: {} bytes\n", .{ std.fmt.fmtDuration(wasm_start.read()), input.len }); 50 | var ctx: u64 = 12345; 51 | data = try plugin.callWithContext("count_vowels", input, @ptrCast(&ctx)); 52 | try testing.expectEqualStrings("{\"count\": 11820}", data); 53 | std.debug.print("--------------\n", .{}); 54 | var i: usize = 0; 55 | var wasm_elapsed: u64 = 0; 56 | while (i < 100) : (i += 1) { 57 | var call_start = try std.time.Timer.start(); 58 | _ = try plugin.call("count_vowels", input); 59 | wasm_elapsed += call_start.read(); 60 | } 61 | const wasm_avg = wasm_elapsed / i; 62 | 63 | i = 0; 64 | var native_elapsed: u64 = 0; 65 | var native_count: u32 = 0; 66 | while (i < 100) : (i += 1) { 67 | var native_start = try std.time.Timer.start(); 68 | for (input) |char| { 69 | switch (char) { 70 | 'A', 'I', 'E', 'O', 'U', 'a', 'e', 'i', 'o', 'u' => native_count += 1, 71 | else => {}, 72 | } 73 | } 74 | native_elapsed += native_start.read(); 75 | } 76 | const native_avg = native_elapsed / i; 77 | std.debug.print("native function call (avg, N = {}): {}\n", .{ i, std.fmt.fmtDuration(native_avg) }); 78 | std.debug.print("wasm function call (avg, N = {}): {}\n", .{ i, std.fmt.fmtDuration(wasm_avg) }); 79 | } 80 | 81 | test "Multi threaded tests" { 82 | const S = struct { 83 | fn _test() !void { 84 | var f = Function.init( 85 | "hello_world", 86 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 87 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 88 | &hello_world, 89 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 90 | ); 91 | defer f.deinit(); 92 | var plugin = try Plugin.initFromManifest(testing.allocator, man, &[_]Function{f}, true); 93 | defer plugin.deinit(); 94 | const output = try plugin.call("count_vowels", "this is a test"); 95 | std.debug.print("{s}\n", .{output}); 96 | } 97 | }; 98 | const t1 = try std.Thread.spawn(.{}, S._test, .{}); 99 | const t2 = try std.Thread.spawn(.{}, S._test, .{}); 100 | t1.join(); 101 | t2.join(); 102 | _ = sdk.setLogFile("test.log", .Debug); 103 | 104 | var f = Function.init( 105 | "hello_world", 106 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 107 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 108 | &hello_world, 109 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 110 | ); 111 | defer f.deinit(); 112 | 113 | var plugin = try Plugin.initFromManifest(testing.allocator, man, &[_]Function{f}, true); 114 | defer plugin.deinit(); 115 | 116 | const output = try plugin.call("count_vowels", "this is a test"); 117 | std.debug.print("{s}\n", .{output}); 118 | } 119 | 120 | test "Plugin Cancellation" { 121 | const loop_manifest = manifest.WasmFile{ .path = "wasm/loop.wasm" }; 122 | const loop_man = manifest.Manifest{ .wasm = &[_]manifest.Wasm{.{ .wasm_file = loop_manifest }} }; 123 | _ = sdk.setLogFile("test.log", .Debug); 124 | var f = Function.init( 125 | "hello_world", 126 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 127 | &[_]sdk.c.ExtismValType{sdk.c.ExtismValType_I64}, 128 | &hello_world, 129 | @constCast(@as(*const anyopaque, @ptrCast("user data"))), 130 | ); 131 | defer f.deinit(); 132 | 133 | var plugin = try Plugin.initFromManifest(testing.allocator, loop_man, &[_]Function{f}, true); 134 | defer plugin.deinit(); 135 | var handle = plugin.cancelHandle(); 136 | const S = struct { 137 | fn _test(h: *sdk.CancelHandle) void { 138 | std.time.sleep(1 * std.time.ns_per_s); 139 | _ = h.cancel(); 140 | } 141 | }; 142 | _ = try std.Thread.spawn(.{}, S._test, .{&handle}); 143 | var call_start = try std.time.Timer.start(); 144 | const output = plugin.call("infinite_loop", "abc123"); 145 | const call_end = call_start.read(); 146 | try std.testing.expectError(error.PluginCallFailed, output); 147 | std.debug.print("Cancelled plugin ran for {}\n", .{std.fmt.fmtDuration(call_end)}); 148 | } 149 | -------------------------------------------------------------------------------- /wasm/code-functions.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/zig-sdk/00991b0dbf1ca4b7654216e0e18a4cff7bba8c3d/wasm/code-functions.wasm -------------------------------------------------------------------------------- /wasm/loop.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/zig-sdk/00991b0dbf1ca4b7654216e0e18a4cff7bba8c3d/wasm/loop.wasm --------------------------------------------------------------------------------