├── .github ├── CODEOWNERS ├── actions │ └── libextism │ │ └── action.yaml └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── org │ └── extism │ └── sdk │ ├── CancelHandle.java │ ├── Extism.java │ ├── ExtismCurrentPlugin.java │ ├── ExtismException.java │ ├── ExtismFunction.java │ ├── HostFunction.java │ ├── HostUserData.java │ ├── LibExtism.java │ ├── Plugin.java │ ├── manifest │ ├── Manifest.java │ ├── ManifestHttpRequest.java │ └── MemoryOptions.java │ ├── support │ ├── Hashing.java │ └── JsonSerde.java │ └── wasm │ ├── ByteArrayWasmSource.java │ ├── PathWasmSource.java │ ├── UrlWasmSource.java │ ├── WasmSource.java │ └── WasmSourceResolver.java └── test ├── java └── org │ └── extism │ └── sdk │ ├── HostFunctionTests.java │ ├── ManifestTests.java │ ├── PluginTests.java │ └── TestWasmSources.java └── resources ├── code-functions.wasm └── code.wasm /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @evacchi @bhelx 2 | -------------------------------------------------------------------------------- /.github/actions/libextism/action.yaml: -------------------------------------------------------------------------------- 1 | on: [workflow_call] 2 | 3 | name: libextism 4 | 5 | inputs: 6 | gh-token: 7 | description: "A GitHub PAT" 8 | default: ${{ github.token }} 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | repository: extism/cli 16 | path: .extism-cli 17 | - uses: ./.extism-cli/.github/actions/extism-cli 18 | - name: Install 19 | shell: bash 20 | run: sudo extism lib install --version git --github-token ${{ inputs.gh-token }} 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | java: 11 | name: Java 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | version: [11, 17] 17 | rust: 18 | - stable 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v3 22 | - uses: ./.github/actions/libextism 23 | - name: Set up Java 24 | uses: actions/setup-java@v3 25 | with: 26 | distribution: 'temurin' 27 | java-version: '${{ matrix.version }}' 28 | - name: Test Java 29 | run: | 30 | mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn verify 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release-version: 7 | description: 'Version being released' 8 | required: true 9 | branch: 10 | description: 'Branch to release from' 11 | required: true 12 | default: 'main' 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | release: 19 | name: Release 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Install libextism 27 | uses: ./.github/actions/libextism 28 | 29 | - name: Setup Java 30 | uses: actions/setup-java@v4 31 | with: 32 | java-version: 21 33 | distribution: 'temurin' 34 | server-id: ossrh 35 | server-username: MAVEN_USERNAME 36 | server-password: MAVEN_PASSWORD 37 | gpg-private-key: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 38 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 39 | 40 | - id: install-secret-key 41 | name: Install gpg secret key 42 | run: | 43 | cat <(echo -e "${{ secrets.JRELEASER_GPG_SECRET_KEY }}") | gpg --batch --import 44 | gpg --list-secret-keys --keyid-format LONG 45 | 46 | - name: Compile 47 | run: mvn --batch-mode --no-transfer-progress verify 48 | 49 | - name: Setup Git 50 | run: | 51 | git config user.name "Extism BOT" 52 | git config user.email "oss@extism.org" 53 | 54 | - name: Set the version 55 | run: | 56 | mvn --batch-mode --no-transfer-progress versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} 57 | env: 58 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 59 | 60 | - name: Release to Maven Central 61 | run: | 62 | mvn --batch-mode --no-transfer-progress -Prelease clean verify deploy -X 63 | env: 64 | MAVEN_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }} 65 | MAVEN_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }} 66 | MAVEN_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 67 | 68 | - name: Commit tag, back to Snapshot and Push 69 | if: ${{ ! endsWith(github.event.inputs.release-version, '-SNAPSHOT') }} 70 | run: | 71 | git add . 72 | git commit -m "Release version update ${{ github.event.inputs.release-version }}" 73 | git tag ${{ github.event.inputs.release-version }} 74 | mvn --batch-mode --no-transfer-progress versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT 75 | git add . 76 | git commit -m "Snapshot version update" 77 | git push 78 | git push origin ${{ github.event.inputs.release-version }} 79 | env: 80 | GITHUB_TOKEN: ${{ github.token }} 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | .project 27 | .classpath 28 | .settings 29 | 30 | target/ 31 | .idea/ -------------------------------------------------------------------------------- /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 Java SDK 2 | 3 | Java SDK for the [Extism](https://extism.org/) WebAssembly Plugin-System. 4 | 5 | [![maven](https://img.shields.io/maven-central/v/org.extism.sdk/extism)](https://search.maven.org/artifact/org.extism.sdk/extism) 6 | [![javadoc](https://javadoc.io/badge2/org.extism.sdk/extism/javadoc.svg)](https://javadoc.io/doc/org.extism.sdk/extism) 7 | 8 | ## Installation 9 | 10 | ### Install the Extism Runtime Dependency 11 | 12 | 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: 13 | 14 | ```bash 15 | sudo extism lib install latest 16 | 17 | #=> Fetching https://github.com/extism/extism/releases/download/v0.5.2/libextism-aarch64-apple-darwin-v0.5.2.tar.gz 18 | #=> Copying libextism.dylib to /usr/local/lib/libextism.dylib 19 | #=> Copying extism.h to /usr/local/include/extism.h 20 | ``` 21 | 22 | ### Install Jar 23 | 24 | To use the Extism java-sdk you need to add the `org.extism.sdk` dependency to your dependency management system. 25 | 26 | #### Maven 27 | 28 | To use the Extism java-sdk with maven you need to add the following dependency to your `pom.xml` file: 29 | ```xml 30 | 31 | org.extism.sdk 32 | extism 33 | 1.1.0 34 | 35 | ``` 36 | 37 | #### Gradle 38 | 39 | To use the Extism java-sdk with maven you need to add the following dependency to your `build.gradle` file: 40 | 41 | ``` 42 | implementation 'org.extism.sdk:extism:1.1.0' 43 | ``` 44 | 45 | ## Getting Started 46 | 47 | This guide should walk you through some of the concepts in Extism and this java library. 48 | 49 | ### Creating A Plug-in 50 | 51 | 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. 52 | Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web: 53 | 54 | ```java 55 | import org.extism.sdk.manifest.Manifest; 56 | import org.extism.sdk.wasm.UrlWasmSource; 57 | import org.extism.sdk.Plugin; 58 | 59 | var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"; 60 | var manifest = new Manifest(List.of(UrlWasmSource.fromUrl(url))); 61 | var plugin = new Plugin(manifest, false, null); 62 | ``` 63 | 64 | > **Note**: See [the Manifest docs](https://www.javadoc.io/doc/org.extism.sdk/extism/latest/org/extism/sdk/manifest/Manifest.html) as it has a rich schema and a lot of options. 65 | 66 | ### Calling A Plug-in's Exports 67 | 68 | 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 [Plugin#call](https://www.javadoc.io/doc/org.extism.sdk/extism/latest/org/extism/sdk/Plugin.html#call(java.lang.String,byte[])) 69 | 70 | ```java 71 | var output = plugin.call("count_vowels", "Hello, World!"); 72 | System.out.println(output); 73 | // => "{"count": 3, "total": 3, "vowels": "aeiouAEIOU"}" 74 | ``` 75 | 76 | All exports have a simple interface of bytes-in and bytes-out. 77 | This plug-in happens to take a string and return a JSON encoded string with a report of results. 78 | 79 | ### Plug-in State 80 | 81 | Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. 82 | Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. 83 | You can see this by making subsequent calls to the export: 84 | 85 | ```java 86 | var output = plugin.call("count_vowels", "Hello, World!"); 87 | System.out.println(output); 88 | // => "{"count": 3, "total": 6, "vowels": "aeiouAEIOU"}" 89 | 90 | var output = plugin.call("count_vowels", "Hello, World!"); 91 | System.out.println(output); 92 | // => "{"count": 3, "total": 9, "vowels": "aeiouAEIOU"}" 93 | ``` 94 | 95 | These variables will persist until this plug-in is freed or you initialize a new one. 96 | 97 | ### Configuration 98 | 99 | Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. 100 | Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example: 101 | 102 | ```java 103 | var plugin = new Plugin(manifest, false, null); 104 | var output = plugin.call("count_vowels", "Yellow, World!"); 105 | System.out.println(output); 106 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 107 | 108 | // Let's change the vowels config it uses to determine what is a vowel: 109 | var config = Map.of("vowels", "aeiouyAEIOUY"); 110 | var manifest2 = new Manifest(List.of(UrlWasmSource.fromUrl(url)), null, config); 111 | var plugin = new Plugin(manifest2, false, null); 112 | var output = plugin.call("count_vowels", "Yellow, World!"); 113 | System.out.println(output); 114 | // => {"count": 4, "total": 4, "vowels": "aeiouyAEIOUY"} 115 | // ^ note count changed to 4 as we configured Y as a vowel this time 116 | ``` 117 | 118 | ### Host Functions 119 | 120 | Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, 121 | let's store it in a persistent key-value store! 122 | 123 | Wasm can't use our app's KV store on its own. This is where [Host Functions](https://extism.org/docs/concepts/host-functions) come in. 124 | 125 | [Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. 126 | They are simply some java methods you write which can be passed down and invoked from any language inside the plug-in. 127 | 128 | Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in: 129 | 130 | ```java 131 | var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"; 132 | var manifest = new Manifest(List.of(UrlWasmSource.fromUrl(url))); 133 | var plugin = new Plugin(manifest, false, null); 134 | ``` 135 | 136 | > *Note*: The source code for this plug-in is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) 137 | > and is written in rust, but it could be written in any of our PDK languages. 138 | 139 | Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy its import interface for a KV store. 140 | We want to expose two functions to our plugin, `kv_write(String key, Bytes value)` which writes a bytes value to a key and `Bytes kv_read(String key)` which reads the bytes at the given `key`. 141 | 142 | ```java 143 | // Our application KV store 144 | // Pretend this is redis or a database :) 145 | var kvStore = new HashMap(); 146 | 147 | ExtismFunction kvWrite = (plugin, params, returns, data) -> { 148 | System.out.println("Hello from kv_write Java Function!"); 149 | var key = plugin.inputString(params[0]); 150 | var value = plugin.inputBytes(params[1]); 151 | System.out.println("Writing to key " + key); 152 | kvStore.put(key, value); 153 | }; 154 | 155 | ExtismFunction kvRead = (plugin, params, returns, data) -> { 156 | System.out.println("Hello from kv_read Java Function!"); 157 | var key = plugin.inputString(params[0]); 158 | System.out.println("Reading from key " + key); 159 | var value = kvStore.get(key); 160 | if (value == null) { 161 | // default to zeroed bytes 162 | var zero = new byte[]{0,0,0,0}; 163 | plugin.returnBytes(returns[0], zero); 164 | } else { 165 | plugin.returnBytes(returns[0], value); 166 | } 167 | }; 168 | 169 | HostFunction kvWriteHostFn = new HostFunction<>( 170 | "kv_write", 171 | new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64, LibExtism.ExtismValType.I64}, 172 | new LibExtism.ExtismValType[0], 173 | kvWrite, 174 | Optional.empty() 175 | ); 176 | 177 | HostFunction kvReadHostFn = new HostFunction<>( 178 | "kv_read", 179 | new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}, 180 | new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}, 181 | kvRead, 182 | Optional.empty() 183 | ); 184 | 185 | ``` 186 | 187 | > *Note*: In order to write host functions you should get familiar with the methods on the [ExtismCurrentPlugin](https://www.javadoc.io/doc/org.extism.sdk/extism/latest/org/extism/sdk/ExtismCurrentPlugin.html) class. 188 | > The `plugin` parameter is an instance of this class. 189 | 190 | Now we just need to pass in these function references when creating the plugin:. 191 | 192 | ```java 193 | HostFunction[] functions = {kvWriteHostFn, kvReadHostFn}; 194 | var plugin = new Plugin(manifest, false, functions); 195 | var output = plugin.call("count_vowels", "Hello, World!"); 196 | // => Hello from kv_read Java Function! 197 | // => Reading from key count-vowels 198 | // => Hello from kv_write Java Function! 199 | // => Writing to key count-vowels 200 | System.out.println(output); 201 | // => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"} 202 | ``` 203 | 204 | ## Development 205 | 206 | # Build 207 | 208 | To build the Extism java-sdk run the following command: 209 | 210 | ``` 211 | mvn clean verify 212 | ``` 213 | 214 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | org.extism.sdk 6 | extism 7 | jar 8 | 999-SNAPSHOT 9 | extism 10 | https://github.com/extism/extism 11 | Java-SDK for Extism to use webassembly from Java 12 | 13 | 14 | 15 | BSD 3-Clause 16 | https://opensource.org/licenses/BSD-3-Clause 17 | 18 | 19 | 20 | 21 | Dylibso, Inc. 22 | https://dylib.so 23 | 24 | 25 | 26 | 27 | The Extism Authors 28 | oss@extism.org 29 | 30 | Maintainer 31 | 32 | Dylibso, Inc. 33 | https://dylib.so 34 | 35 | 36 | 37 | 38 | scm:git:git://github.com/extism/extism.git 39 | scm:git:ssh://git@github.com/extism/extism.git 40 | https://github.com/extism/extism/tree/main/java 41 | main 42 | 43 | 44 | 45 | Github 46 | https://github.com/extism/java-sdk/issues 47 | 48 | 49 | 50 | 51 | ossrh 52 | Central Repository OSSRH 53 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 54 | 55 | 56 | 57 | 58 | 11 59 | UTF-8 60 | 61 | 62 | 5.12.1 63 | 2.10 64 | 65 | 66 | 5.9.1 67 | 3.23.1 68 | 69 | 70 | 3.10.1 71 | 2.22.2 72 | 73 | 74 | 75 | 76 | 77 | release 78 | 79 | 80 | ossrh 81 | https://s01.oss.sonatype.org/content/repositories/snapshots 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-javadoc-plugin 90 | 3.4.1 91 | 92 | -Xdoclint:none 93 | 94 | 95 | 96 | attach-javadoc 97 | 98 | jar 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-source-plugin 106 | 3.2.1 107 | 108 | 109 | attach-source 110 | 111 | jar 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-gpg-plugin 119 | 3.2.7 120 | 121 | 122 | sign-artifacts 123 | verify 124 | 125 | sign 126 | 127 | 128 | 129 | 130 | 131 | org.sonatype.plugins 132 | nexus-staging-maven-plugin 133 | true 134 | 135 | ossrh 136 | https://s01.oss.sonatype.org/ 137 | true 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | org.sonatype.plugins 149 | nexus-staging-maven-plugin 150 | 1.7.0 151 | true 152 | 153 | ossrh 154 | https://s01.oss.sonatype.org/ 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-compiler-plugin 160 | ${maven-compiler-plugin.version} 161 | 162 | ${java.version} 163 | 164 | 165 | 166 | org.apache.maven.plugins 167 | maven-surefire-plugin 168 | ${maven-surefire-plugin.version} 169 | 170 | 171 | ../target/release 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | net.java.dev.jna 180 | jna 181 | ${jna.version} 182 | 183 | 184 | com.google.code.gson 185 | gson 186 | ${gson.version} 187 | 188 | 189 | org.junit.jupiter 190 | junit-jupiter-engine 191 | ${junit-jupiter-engine.version} 192 | test 193 | 194 | 195 | org.assertj 196 | assertj-core 197 | ${assertj-core.version} 198 | test 199 | 200 | 201 | 202 | uk.org.webcompere 203 | model-assert 204 | 1.0.0 205 | test 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/CancelHandle.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.Pointer; 4 | 5 | /** 6 | * CancelHandle is used to cancel a running Plugin 7 | */ 8 | public class CancelHandle { 9 | private Pointer handle; 10 | 11 | public CancelHandle(Pointer handle) { 12 | this.handle = handle; 13 | } 14 | 15 | /** 16 | * Cancel execution of the Plugin associated with the CancelHandle 17 | */ 18 | boolean cancel() { 19 | return LibExtism.INSTANCE.extism_plugin_cancel(this.handle); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/Extism.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import org.extism.sdk.manifest.Manifest; 4 | 5 | import java.nio.file.Path; 6 | import java.util.Objects; 7 | 8 | /** 9 | * Extism convenience functions. 10 | */ 11 | public class Extism { 12 | 13 | /** 14 | * Configure a log file with the given {@link Path} and configure the given {@link LogLevel}. 15 | * 16 | * @param path 17 | * @param level 18 | * 19 | * @deprecated will be replaced with better logging API. 20 | */ 21 | @Deprecated(forRemoval = true) 22 | public static void setLogFile(Path path, LogLevel level) { 23 | 24 | Objects.requireNonNull(path, "path"); 25 | Objects.requireNonNull(level, "level"); 26 | 27 | var result = LibExtism.INSTANCE.extism_log_file(path.toString(), level.getLevel()); 28 | if (!result) { 29 | var error = String.format("Could not set extism logger to %s with level %s", path, level); 30 | throw new ExtismException(error); 31 | } 32 | } 33 | 34 | /** 35 | * Invokes the named {@code function} from the {@link Manifest} with the given {@code input}. 36 | * This is a convenience method. Prefer initializing and using a {@link Plugin} where possible. 37 | * 38 | * @param manifest the manifest containing the function 39 | * @param function the name of the function to call 40 | * @param input the input as string 41 | * @return the output as string 42 | * @throws ExtismException if the call fails 43 | */ 44 | public static String invokeFunction(Manifest manifest, String function, String input) throws ExtismException { 45 | try (var plugin = new Plugin(manifest, false, null)) { 46 | return plugin.call(function, input); 47 | } 48 | } 49 | 50 | /** 51 | * Error levels for the Extism logging facility. 52 | * 53 | * @see Extism#setLogFile(Path, LogLevel) 54 | */ 55 | public enum LogLevel { 56 | 57 | INFO("info"), // 58 | 59 | DEBUG("debug"), // 60 | 61 | WARN("warn"), // 62 | 63 | TRACE("trace"); 64 | 65 | private final String level; 66 | 67 | LogLevel(String level) { 68 | this.level = level; 69 | } 70 | 71 | public String getLevel() { 72 | return level; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/ExtismCurrentPlugin.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.Pointer; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | 7 | public class ExtismCurrentPlugin { 8 | public Pointer pointer; 9 | 10 | public ExtismCurrentPlugin(Pointer pointer) { 11 | this.pointer = pointer; 12 | } 13 | 14 | public Pointer memory() { 15 | return LibExtism.INSTANCE.extism_current_plugin_memory(this.pointer); 16 | } 17 | 18 | public int alloc(int n) { 19 | return LibExtism.INSTANCE.extism_current_plugin_memory_alloc(this.pointer, n); 20 | } 21 | 22 | public void free(long offset) { 23 | LibExtism.INSTANCE.extism_current_plugin_memory_free(this.pointer, offset); 24 | } 25 | 26 | public long memoryLength(long offset) { 27 | return LibExtism.INSTANCE.extism_current_plugin_memory_length(this.pointer, offset); 28 | } 29 | 30 | /** 31 | * Return a string from a host function 32 | * @param output - The output to set 33 | * @param s - The string to return 34 | */ 35 | public void returnString(LibExtism.ExtismVal output, String s) { 36 | returnBytes(output, s.getBytes(StandardCharsets.UTF_8)); 37 | } 38 | 39 | /** 40 | * Return bytes from a host function 41 | * @param output - The output to set 42 | * @param b - The buffer to return 43 | */ 44 | public void returnBytes(LibExtism.ExtismVal output, byte[] b) { 45 | int offs = this.alloc(b.length); 46 | Pointer ptr = this.memory(); 47 | ptr.write(offs, b, 0, b.length); 48 | output.v.i64 = offs; 49 | } 50 | 51 | /** 52 | * Get bytes from host function parameter 53 | * @param input - The input to read 54 | */ 55 | public byte[] inputBytes(LibExtism.ExtismVal input) { 56 | switch (input.t) { 57 | case 0: 58 | return this.memory() 59 | .getByteArray(input.v.i32, 60 | LibExtism.INSTANCE.extism_current_plugin_memory_length(this.pointer, input.v.i32)); 61 | case 1: 62 | return this.memory() 63 | .getByteArray(input.v.i64, 64 | LibExtism.INSTANCE.extism_current_plugin_memory_length(this.pointer, input.v.i64)); 65 | default: 66 | throw new ExtismException("inputBytes error: ExtismValType " + LibExtism.ExtismValType.values()[input.t] + " not implemtented"); 67 | } 68 | } 69 | 70 | 71 | /** 72 | * Get string from host function parameter 73 | * @param input - The input to read 74 | */ 75 | public String inputString(LibExtism.ExtismVal input) { 76 | return new String(this.inputBytes(input)); 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/ExtismException.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | /** 4 | * Thrown when an exceptional condition has occurred. 5 | */ 6 | public class ExtismException extends RuntimeException { 7 | 8 | public ExtismException() { 9 | } 10 | 11 | public ExtismException(String message) { 12 | super(message); 13 | } 14 | 15 | public ExtismException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/ExtismFunction.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import java.util.Optional; 4 | 5 | public interface ExtismFunction { 6 | void invoke( 7 | ExtismCurrentPlugin plugin, 8 | LibExtism.ExtismVal[] params, 9 | LibExtism.ExtismVal[] returns, 10 | Optional data 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/HostFunction.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.Pointer; 4 | import com.sun.jna.PointerType; 5 | 6 | import java.util.Arrays; 7 | import java.util.Optional; 8 | 9 | public class HostFunction { 10 | 11 | private final LibExtism.InternalExtismFunction callback; 12 | 13 | private boolean freed; 14 | 15 | public final Pointer pointer; 16 | 17 | public final String name; 18 | 19 | public final LibExtism.ExtismValType[] params; 20 | 21 | public final LibExtism.ExtismValType[] returns; 22 | 23 | public HostFunction(String name, LibExtism.ExtismValType[] params, LibExtism.ExtismValType[] returns, ExtismFunction f, Optional userData) { 24 | this.freed = false; 25 | this.name = name; 26 | this.params = params; 27 | this.returns = returns; 28 | this.callback = new Callback(f, userData); 29 | 30 | this.pointer = LibExtism.INSTANCE.extism_function_new( 31 | this.name, 32 | Arrays.stream(this.params).mapToInt(r -> r.v).toArray(), 33 | this.params.length, 34 | Arrays.stream(this.returns).mapToInt(r -> r.v).toArray(), 35 | this.returns.length, 36 | this.callback, 37 | userData.map(PointerType::getPointer).orElse(null), 38 | null 39 | ); 40 | } 41 | 42 | static void convertOutput(LibExtism.ExtismVal original, LibExtism.ExtismVal fromHostFunction) { 43 | if (fromHostFunction.t != original.t) 44 | throw new ExtismException(String.format("Output type mismatch, got %d but expected %d", fromHostFunction.t, original.t)); 45 | 46 | if (fromHostFunction.t == LibExtism.ExtismValType.I32.v) { 47 | original.v.setType(Integer.TYPE); 48 | original.v.i32 = fromHostFunction.v.i32; 49 | } else if (fromHostFunction.t == LibExtism.ExtismValType.I64.v) { 50 | original.v.setType(Long.TYPE); 51 | // PTR is an alias for I64 52 | if (fromHostFunction.v.i64 == 0 && fromHostFunction.v.ptr > 0) { 53 | original.v.i64 = fromHostFunction.v.ptr; 54 | } else { 55 | original.v.i64 = fromHostFunction.v.i64; 56 | } 57 | } else if (fromHostFunction.t == LibExtism.ExtismValType.F32.v) { 58 | original.v.setType(Float.TYPE); 59 | original.v.f32 = fromHostFunction.v.f32; 60 | } else if (fromHostFunction.t == LibExtism.ExtismValType.F64.v) { 61 | original.v.setType(Double.TYPE); 62 | original.v.f64 = fromHostFunction.v.f64; 63 | } else 64 | throw new ExtismException(String.format("Unsupported return type: %s", original.t)); 65 | } 66 | 67 | public void setNamespace(String name) { 68 | if (this.pointer != null) { 69 | LibExtism.INSTANCE.extism_function_set_namespace(this.pointer, name); 70 | } 71 | } 72 | 73 | public HostFunction withNamespace(String name) { 74 | this.setNamespace(name); 75 | return this; 76 | } 77 | 78 | public void free() { 79 | if (!this.freed) { 80 | LibExtism.INSTANCE.extism_function_free(this.pointer); 81 | this.freed = true; 82 | } 83 | } 84 | 85 | static class Callback implements LibExtism.InternalExtismFunction { 86 | private final ExtismFunction f; 87 | private final Optional userData; 88 | 89 | public Callback(ExtismFunction f, Optional userData) { 90 | this.f = f; 91 | this.userData = userData; 92 | } 93 | 94 | @Override 95 | public void invoke(Pointer currentPlugin, LibExtism.ExtismVal ins, int nInputs, LibExtism.ExtismVal outs, int nOutputs, Pointer data) { 96 | 97 | LibExtism.ExtismVal[] inputs; 98 | LibExtism.ExtismVal[] outputs; 99 | 100 | if (outs == null) { 101 | if (nOutputs > 0) { 102 | throw new ExtismException("Output array is null but nOutputs is greater than 0"); 103 | } 104 | outputs = new LibExtism.ExtismVal[0]; 105 | } else { 106 | outputs = (LibExtism.ExtismVal[]) outs.toArray(nOutputs); 107 | } 108 | 109 | if (ins == null) { 110 | if (nInputs > 0) { 111 | throw new ExtismException("Input array is null but nInputs is greater than 0"); 112 | } 113 | inputs = new LibExtism.ExtismVal[0]; 114 | } else { 115 | inputs = (LibExtism.ExtismVal[]) ins.toArray(nInputs); 116 | } 117 | 118 | f.invoke(new ExtismCurrentPlugin(currentPlugin), inputs, outputs, userData); 119 | 120 | for (LibExtism.ExtismVal output : outputs) { 121 | convertOutput(output, output); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/HostUserData.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.PointerType; 4 | 5 | public class HostUserData extends PointerType { 6 | 7 | } -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/LibExtism.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.*; 4 | 5 | /** 6 | * Wrapper around the Extism library. 7 | */ 8 | public interface LibExtism extends Library { 9 | 10 | /** 11 | * Holds the extism library instance. 12 | * Resolves the extism library based on the resolution algorithm defined in {@link com.sun.jna.NativeLibrary}. 13 | */ 14 | LibExtism INSTANCE = Native.load("extism", LibExtism.class); 15 | 16 | interface InternalExtismFunction extends Callback { 17 | void invoke( 18 | Pointer currentPlugin, 19 | ExtismVal inputs, 20 | int nInputs, 21 | ExtismVal outputs, 22 | int nOutputs, 23 | Pointer data 24 | ); 25 | } 26 | 27 | @Structure.FieldOrder({"t", "v"}) 28 | class ExtismVal extends Structure { 29 | public int t; 30 | public ExtismValUnion v; 31 | } 32 | class ExtismValUnion extends Union { 33 | public int i32; 34 | public long i64; 35 | public long ptr; 36 | public float f32; 37 | public double f64; 38 | } 39 | 40 | enum ExtismValType { 41 | I32(0), 42 | I64(1), 43 | // PTR is an alias for I64 44 | PTR(1), 45 | F32(2), 46 | F64(3), 47 | V128(4), 48 | FuncRef(5), 49 | ExternRef(6); 50 | 51 | public final int v; 52 | 53 | ExtismValType(int value) { 54 | this.v = value; 55 | } 56 | } 57 | 58 | Pointer extism_function_new(String name, 59 | int[] inputs, 60 | int nInputs, 61 | int[] outputs, 62 | int nOutputs, 63 | InternalExtismFunction func, 64 | Pointer userData, 65 | Pointer freeUserData); 66 | 67 | void extism_function_free(Pointer function); 68 | 69 | 70 | /** 71 | * Get the length of an allocated block 72 | * NOTE: this should only be called from host functions. 73 | */ 74 | int extism_current_plugin_memory_length(Pointer plugin, long n); 75 | 76 | /** 77 | * Returns a pointer to the memory of the currently running plugin 78 | * NOTE: this should only be called from host functions. 79 | */ 80 | Pointer extism_current_plugin_memory(Pointer plugin); 81 | 82 | /** 83 | * Allocate a memory block in the currently running plugin 84 | * NOTE: this should only be called from host functions. 85 | */ 86 | int extism_current_plugin_memory_alloc(Pointer plugin, long n); 87 | 88 | /** 89 | * Free an allocated memory block 90 | * NOTE: this should only be called from host functions. 91 | */ 92 | void extism_current_plugin_memory_free(Pointer plugin, long ptr); 93 | 94 | /** 95 | * Sets the logger to the given path with the given level of verbosity 96 | * 97 | * @param path The file path of the logger 98 | * @param logLevel The level of the logger 99 | * @return true if successful 100 | */ 101 | boolean extism_log_file(String path, String logLevel); 102 | 103 | /** 104 | * Returns the error associated with a @{@link Plugin} 105 | * 106 | * @param pluginPointer 107 | * @return 108 | */ 109 | String extism_plugin_error(Pointer pluginPointer); 110 | 111 | /** 112 | * Create a new plugin. 113 | * 114 | * @param wasm is a WASM module (wat or wasm) or a JSON encoded manifest 115 | * @param wasmSize the length of the `wasm` parameter 116 | * @param functions host functions 117 | * @param nFunctions the number of host functions 118 | * @param withWASI enables/disables WASI 119 | * @param errmsg get the error message if the return value is null 120 | * @return pointer to the plugin, or null in case of error 121 | */ 122 | Pointer extism_plugin_new(byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI, Pointer[] errmsg); 123 | Pointer extism_plugin_new_with_fuel_limit(byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI, long fuelLimit, Pointer[] errmsg); 124 | 125 | 126 | /** 127 | * Free error message from `extism_plugin_new` 128 | */ 129 | void extism_plugin_new_error_free(Pointer errmsg); 130 | 131 | /** 132 | * Returns the Extism version string 133 | */ 134 | String extism_version(); 135 | 136 | 137 | /** 138 | * Calls a function from the @{@link Plugin} at the given {@code pluginIndex}. 139 | * 140 | * @param pluginPointer 141 | * @param function_name is the function to call 142 | * @param data is the data input data 143 | * @param dataLength is the data input data length 144 | * @return the result code of the plugin call. non-zero in case of error, {@literal 0} otherwise. 145 | */ 146 | int extism_plugin_call(Pointer pluginPointer, String function_name, byte[] data, int dataLength); 147 | 148 | /** 149 | * Returns 150 | * @return the length of the output data in bytes. 151 | */ 152 | int extism_plugin_output_length(Pointer pluginPointer); 153 | 154 | /** 155 | 156 | * @return 157 | */ 158 | Pointer extism_plugin_output_data(Pointer pluginPointer); 159 | 160 | /** 161 | * Remove a plugin from the 162 | */ 163 | void extism_plugin_free(Pointer pluginPointer); 164 | 165 | /** 166 | * Update plugin config values, this 167 | * @param json 168 | * @param jsonLength 169 | * @return {@literal true} if update was successful 170 | */ 171 | boolean extism_plugin_config(Pointer pluginPointer, byte[] json, int jsonLength); 172 | Pointer extism_plugin_cancel_handle(Pointer pluginPointer); 173 | boolean extism_plugin_cancel(Pointer cancelHandle); 174 | void extism_function_set_namespace(Pointer p, String name); 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/Plugin.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.Pointer; 4 | import org.extism.sdk.manifest.Manifest; 5 | import org.extism.sdk.support.JsonSerde; 6 | 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.Objects; 9 | 10 | /** 11 | * Represents a Extism plugin. 12 | */ 13 | public class Plugin implements AutoCloseable { 14 | 15 | /** 16 | * Holds the Extism plugin pointer 17 | */ 18 | private final Pointer pluginPointer; 19 | 20 | private final HostFunction[] functions; 21 | 22 | /** 23 | * @param manifestBytes The manifest for the plugin 24 | * @param functions The Host functions for th eplugin 25 | * @param withWASI Set to true to enable WASI 26 | */ 27 | public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) { 28 | 29 | Objects.requireNonNull(manifestBytes, "manifestBytes"); 30 | 31 | Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length]; 32 | 33 | if (functions != null) 34 | for (int i = 0; i < functions.length; i++) { 35 | ptrArr[i] = functions[i].pointer; 36 | } 37 | 38 | Pointer[] errormsg = new Pointer[1]; 39 | Pointer p = LibExtism.INSTANCE.extism_plugin_new(manifestBytes, manifestBytes.length, 40 | ptrArr, 41 | functions == null ? 0 : functions.length, 42 | withWASI, 43 | errormsg); 44 | if (p == null) { 45 | if (functions != null) { 46 | for (int i = 0; i < functions.length; i++) { 47 | LibExtism.INSTANCE.extism_function_free(functions[i].pointer); 48 | } 49 | } 50 | String msg = errormsg[0].getString(0); 51 | LibExtism.INSTANCE.extism_plugin_new_error_free(errormsg[0]); 52 | throw new ExtismException(msg); 53 | } 54 | 55 | this.functions = functions; 56 | this.pluginPointer = p; 57 | } 58 | 59 | 60 | public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions, long fuelLimit) { 61 | 62 | Objects.requireNonNull(manifestBytes, "manifestBytes"); 63 | 64 | Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length]; 65 | 66 | if (functions != null) 67 | for (int i = 0; i < functions.length; i++) { 68 | ptrArr[i] = functions[i].pointer; 69 | } 70 | 71 | Pointer[] errormsg = new Pointer[1]; 72 | Pointer p = LibExtism.INSTANCE.extism_plugin_new_with_fuel_limit(manifestBytes, manifestBytes.length, 73 | ptrArr, 74 | functions == null ? 0 : functions.length, 75 | withWASI, 76 | fuelLimit, 77 | errormsg); 78 | if (p == null) { 79 | if (functions != null) { 80 | for (int i = 0; i < functions.length; i++) { 81 | LibExtism.INSTANCE.extism_function_free(functions[i].pointer); 82 | } 83 | } 84 | String msg = errormsg[0].getString(0); 85 | LibExtism.INSTANCE.extism_plugin_new_error_free(errormsg[0]); 86 | throw new ExtismException(msg); 87 | } 88 | 89 | this.functions = functions; 90 | this.pluginPointer = p; 91 | } 92 | 93 | public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) { 94 | this(serialize(manifest), withWASI, functions); 95 | } 96 | 97 | 98 | public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions, long fuelLimit) { 99 | this(serialize(manifest), withWASI, functions, fuelLimit); 100 | } 101 | 102 | private static byte[] serialize(Manifest manifest) { 103 | Objects.requireNonNull(manifest, "manifest"); 104 | return JsonSerde.toJson(manifest).getBytes(StandardCharsets.UTF_8); 105 | } 106 | 107 | /** 108 | * Invoke a function with the given name and input. 109 | * 110 | * @param functionName The name of the exported function to invoke 111 | * @param inputData The raw bytes representing any input data 112 | * @return A byte array representing the raw output data 113 | * @throws ExtismException if the call fails 114 | */ 115 | public byte[] call(String functionName, byte[] inputData) { 116 | 117 | Objects.requireNonNull(functionName, "functionName"); 118 | 119 | int inputDataLength = inputData == null ? 0 : inputData.length; 120 | int exitCode = LibExtism.INSTANCE.extism_plugin_call(this.pluginPointer, functionName, inputData, inputDataLength); 121 | if (exitCode != 0) { 122 | String error = this.error(); 123 | throw new ExtismException(error); 124 | } 125 | 126 | int length = LibExtism.INSTANCE.extism_plugin_output_length(this.pluginPointer); 127 | Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(this.pluginPointer); 128 | return output.getByteArray(0, length); 129 | } 130 | 131 | 132 | /** 133 | * Invoke a function with the given name and input. 134 | * 135 | * @param functionName The name of the exported function to invoke 136 | * @param input The string representing the input data 137 | * @return A string representing the output data 138 | */ 139 | public String call(String functionName, String input) { 140 | 141 | Objects.requireNonNull(functionName, "functionName"); 142 | 143 | var inputBytes = input == null ? null : input.getBytes(StandardCharsets.UTF_8); 144 | var outputBytes = call(functionName, inputBytes); 145 | return new String(outputBytes, StandardCharsets.UTF_8); 146 | } 147 | 148 | /** 149 | * Get the error associated with a plugin 150 | * 151 | * @return the error message 152 | */ 153 | protected String error() { 154 | String error = LibExtism.INSTANCE.extism_plugin_error(this.pluginPointer); 155 | if (error == null){ 156 | return new String("Unknown error encountered when running Extism plugin function"); 157 | } 158 | return error; 159 | } 160 | 161 | /** 162 | * Frees a plugin from memory 163 | */ 164 | public void free() { 165 | if (this.functions != null){ 166 | for (int i = 0; i < this.functions.length; i++) { 167 | this.functions[i].free(); 168 | } 169 | } 170 | LibExtism.INSTANCE.extism_plugin_free(this.pluginPointer); 171 | } 172 | 173 | /** 174 | * Update plugin config values, this will merge with the existing values. 175 | * 176 | * @param json 177 | * @return 178 | */ 179 | public boolean updateConfig(String json) { 180 | Objects.requireNonNull(json, "json"); 181 | return updateConfig(json.getBytes(StandardCharsets.UTF_8)); 182 | } 183 | 184 | /** 185 | * Update plugin config values, this will merge with the existing values. 186 | * 187 | * @param jsonBytes 188 | * @return {@literal true} if update was successful 189 | */ 190 | public boolean updateConfig(byte[] jsonBytes) { 191 | Objects.requireNonNull(jsonBytes, "jsonBytes"); 192 | return LibExtism.INSTANCE.extism_plugin_config(this.pluginPointer, jsonBytes, jsonBytes.length); 193 | } 194 | 195 | /** 196 | * Calls {@link #free()} if used in the context of a TWR block. 197 | */ 198 | @Override 199 | public void close() { 200 | free(); 201 | } 202 | 203 | /** 204 | * Return a new `CancelHandle`, which can be used to cancel a running Plugin 205 | */ 206 | public CancelHandle cancelHandle() { 207 | Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.pluginPointer); 208 | return new CancelHandle(handle); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/manifest/Manifest.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.manifest; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import org.extism.sdk.wasm.WasmSource; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class Manifest { 12 | 13 | @SerializedName("wasm") 14 | private final List sources; 15 | 16 | @SerializedName("memory") 17 | private final MemoryOptions memoryOptions; 18 | 19 | // FIXME remove this and related stuff if not supported in java-sdk 20 | @SerializedName("allowed_hosts") 21 | private final List allowedHosts; 22 | 23 | @SerializedName("allowed_paths") 24 | private final Map allowedPaths; 25 | 26 | @SerializedName("config") 27 | private final Map config; 28 | 29 | public Manifest() { 30 | this(new ArrayList<>(), null, null, null, null); 31 | } 32 | 33 | public Manifest(WasmSource source) { 34 | this(List.of(source)); 35 | } 36 | 37 | public Manifest(List sources) { 38 | this(sources, null, null, null, null); 39 | } 40 | 41 | public Manifest(List sources, MemoryOptions memoryOptions) { 42 | this(sources, memoryOptions, null, null, null); 43 | } 44 | 45 | public Manifest(List sources, MemoryOptions memoryOptions, Map config) { 46 | this(sources, memoryOptions, config, null, null); 47 | } 48 | 49 | public Manifest(List sources, MemoryOptions memoryOptions, Map config, List allowedHosts) { 50 | this(sources, memoryOptions, config, allowedHosts, null); 51 | } 52 | 53 | public Manifest(List sources, MemoryOptions memoryOptions, Map config, List allowedHosts, Map allowedPaths) { 54 | this.sources = sources; 55 | this.memoryOptions = memoryOptions; 56 | this.config = config; 57 | this.allowedHosts = allowedHosts; 58 | this.allowedPaths = allowedPaths; 59 | } 60 | 61 | public void addSource(WasmSource source) { 62 | this.sources.add(source); 63 | } 64 | 65 | public List getSources() { 66 | return Collections.unmodifiableList(sources); 67 | } 68 | 69 | public MemoryOptions getMemoryOptions() { 70 | return memoryOptions; 71 | } 72 | 73 | public Map getConfig() { 74 | if (config == null || config.isEmpty()) { 75 | return Collections.emptyMap(); 76 | } 77 | return Collections.unmodifiableMap(config); 78 | } 79 | 80 | public List getAllowedHosts() { 81 | if (allowedHosts == null || allowedHosts.isEmpty()) { 82 | return Collections.emptyList(); 83 | } 84 | return Collections.unmodifiableList(allowedHosts); 85 | } 86 | 87 | public Map getAllowedPaths() { 88 | if (allowedPaths == null || allowedPaths.isEmpty()) { 89 | return Collections.emptyMap(); 90 | } 91 | return Collections.unmodifiableMap(allowedPaths); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/manifest/ManifestHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.manifest; 2 | 3 | import java.util.Map; 4 | 5 | // FIXME remove this and related stuff if not supported in java-sdk 6 | public class ManifestHttpRequest { 7 | 8 | private final String url; 9 | private final Map header; 10 | private final String method; 11 | 12 | public ManifestHttpRequest(String url, Map header, String method) { 13 | this.url = url; 14 | this.header = header; 15 | this.method = method; 16 | } 17 | 18 | public String url() { 19 | return url; 20 | } 21 | 22 | public Map header() { 23 | return header; 24 | } 25 | 26 | public String method() { 27 | return method; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/manifest/MemoryOptions.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.manifest; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * Configures memory for the Wasm runtime. 7 | * Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory. 8 | * 9 | * @param maxPages Max number of pages. 10 | * @param httpMax Max number of bytes returned by HTTP requests using extism_http_request 11 | */ 12 | public class MemoryOptions { 13 | @SerializedName("max_pages") 14 | private final Integer maxPages; 15 | 16 | @SerializedName("max_http_response_bytes") 17 | private final Integer maxHttpResponseBytes; 18 | 19 | public MemoryOptions(Integer maxPages, Integer httpMax) { 20 | this.maxPages = maxPages; 21 | this.maxHttpResponseBytes = httpMax; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/support/Hashing.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.support; 2 | 3 | import java.security.MessageDigest; 4 | 5 | public class Hashing { 6 | 7 | public static String sha256HexDigest(byte[] input) { 8 | try { 9 | var messageDigest = MessageDigest.getInstance("SHA-256"); 10 | var messageDigestBytes = messageDigest.digest(input); 11 | 12 | var hexString = new StringBuilder(); 13 | for (var b : messageDigestBytes) { 14 | hexString.append(String.format("%02x", b)); 15 | } 16 | return hexString.toString(); 17 | } catch (Exception e) { 18 | throw new RuntimeException(e); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/support/JsonSerde.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.support; 2 | 3 | import com.google.gson.*; 4 | import com.google.gson.stream.JsonReader; 5 | import com.google.gson.stream.JsonToken; 6 | import com.google.gson.stream.JsonWriter; 7 | import org.extism.sdk.manifest.Manifest; 8 | 9 | import java.io.IOException; 10 | import java.lang.reflect.Type; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.Base64; 13 | 14 | public class JsonSerde { 15 | 16 | private static final Gson GSON; 17 | 18 | static { 19 | GSON = new GsonBuilder() // 20 | .disableHtmlEscaping() // 21 | // needed to convert the byte[] to a base64 encoded String 22 | .registerTypeHierarchyAdapter(byte[].class, new ByteArrayAdapter()) // 23 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) // 24 | .setPrettyPrinting() // 25 | .create(); 26 | } 27 | 28 | public static String toJson(Manifest manifest) { 29 | return GSON.toJson(manifest); 30 | } 31 | 32 | private static class ByteArrayAdapter extends TypeAdapter { 33 | 34 | @Override 35 | public void write(JsonWriter out, byte[] byteValue) throws IOException { 36 | out.value(new String(Base64.getEncoder().encode(byteValue))); 37 | } 38 | 39 | @Override 40 | public byte[] read(JsonReader in) { 41 | try { 42 | if (in.peek() == JsonToken.NULL) { 43 | in.nextNull(); 44 | return new byte[]{}; 45 | } 46 | String byteValue = in.nextString(); 47 | if (byteValue != null) { 48 | return Base64.getDecoder().decode(byteValue); 49 | } 50 | return new byte[]{}; 51 | } catch (Exception e) { 52 | throw new JsonParseException(e); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/wasm/ByteArrayWasmSource.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.wasm; 2 | 3 | /** 4 | * WASM Source represented by raw bytes. 5 | */ 6 | public class ByteArrayWasmSource implements WasmSource { 7 | 8 | private final String name; 9 | private final byte[] data; 10 | private final String hash; 11 | 12 | 13 | /** 14 | * Constructor 15 | * @param name 16 | * @param data the byte array representing the WASM code 17 | * @param hash 18 | */ 19 | public ByteArrayWasmSource(String name, byte[] data, String hash) { 20 | this.name = name; 21 | this.data = data; 22 | this.hash = hash; 23 | } 24 | 25 | @Override 26 | public String name() { 27 | return name; 28 | } 29 | 30 | @Override 31 | public String hash() { 32 | return hash; 33 | } 34 | 35 | public byte[] data() { 36 | return data; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/wasm/PathWasmSource.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.wasm; 2 | 3 | /** 4 | * WASM Source represented by a file referenced by a path. 5 | */ 6 | public class PathWasmSource implements WasmSource { 7 | 8 | private final String name; 9 | 10 | private final String path; 11 | 12 | private final String hash; 13 | 14 | /** 15 | * Constructor 16 | * @param name 17 | * @param path 18 | * @param hash 19 | */ 20 | public PathWasmSource(String name, String path, String hash) { 21 | this.name = name; 22 | this.path = path; 23 | this.hash = hash; 24 | } 25 | 26 | @Override 27 | public String name() { 28 | return name; 29 | } 30 | 31 | @Override 32 | public String hash() { 33 | return hash; 34 | } 35 | 36 | public String path() { 37 | return path; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/wasm/UrlWasmSource.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.wasm; 2 | 3 | /** 4 | * WASM Source represented by a url. 5 | */ 6 | public class UrlWasmSource implements WasmSource { 7 | 8 | private final String name; 9 | 10 | private final String url; 11 | 12 | private final String hash; 13 | 14 | /** 15 | * Provides a quick way to instantiate with just a url 16 | * 17 | * @param url String url to the wasm file 18 | * @return 19 | */ 20 | public static UrlWasmSource fromUrl(String url) { 21 | return new UrlWasmSource(null, url, null); 22 | } 23 | 24 | /** 25 | * Constructor 26 | * @param name 27 | * @param url 28 | * @param hash 29 | */ 30 | public UrlWasmSource(String name, String url, String hash) { 31 | this.name = name; 32 | this.url = url; 33 | this.hash = hash; 34 | } 35 | 36 | @Override 37 | public String name() { 38 | return name; 39 | } 40 | 41 | @Override 42 | public String hash() { 43 | return hash; 44 | } 45 | 46 | public String url() { 47 | return url; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/wasm/WasmSource.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.wasm; 2 | 3 | /** 4 | * A named WASM source. 5 | */ 6 | public interface WasmSource { 7 | 8 | /** 9 | * Logical name of the WASM source 10 | * @return 11 | */ 12 | String name(); 13 | 14 | /** 15 | * Hash of the WASM source 16 | * @return 17 | */ 18 | String hash(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/extism/sdk/wasm/WasmSourceResolver.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk.wasm; 2 | 3 | import org.extism.sdk.ExtismException; 4 | import org.extism.sdk.support.Hashing; 5 | 6 | import java.io.IOException; 7 | import java.net.URL; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.Objects; 11 | 12 | /** 13 | * Resolves {@link WasmSource} from {@link Path Path's} or raw bytes. 14 | */ 15 | public class WasmSourceResolver { 16 | 17 | public PathWasmSource resolve(Path path) { 18 | return resolve(null, path); 19 | } 20 | 21 | public PathWasmSource resolve(String name, Path path) { 22 | Objects.requireNonNull(path, "path"); 23 | 24 | var wasmFile = path.toFile(); 25 | var hash = hash(path); 26 | var wasmName = name == null ? wasmFile.getName() : name; 27 | 28 | return new PathWasmSource(wasmName, wasmFile.getAbsolutePath(), hash); 29 | } 30 | 31 | public ByteArrayWasmSource resolve(String name, byte[] bytes) { 32 | return new ByteArrayWasmSource(name, bytes, hash(bytes)); 33 | } 34 | 35 | protected String hash(Path wasmFile) { 36 | try { 37 | return hash(Files.readAllBytes(wasmFile)); 38 | } catch (IOException ioe) { 39 | throw new ExtismException("Could not compute hash from path: " + wasmFile, ioe); 40 | } 41 | } 42 | 43 | protected String hash(byte[] bytes) { 44 | return Hashing.sha256HexDigest(bytes); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/extism/sdk/HostFunctionTests.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.Pointer; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | public class HostFunctionTests { 9 | @Test 10 | public void callbackShouldAcceptNullParameters() { 11 | var callback = new HostFunction.Callback<>( 12 | (plugin, params, returns, userData) -> {/* NOOP */}, null); 13 | callback.invoke(Pointer.NULL, null, 0, null, 0, Pointer.NULL); 14 | } 15 | 16 | @Test 17 | public void callbackShouldThrowOnNullParametersAndNonzeroCounts() { 18 | var callback = new HostFunction.Callback<>( 19 | (plugin, params, returns, userData) -> {/* NOOP */}, null); 20 | assertThrows(ExtismException.class, () -> 21 | callback.invoke(Pointer.NULL, null, 1, null, 0, Pointer.NULL)); 22 | assertThrows(ExtismException.class, () -> 23 | callback.invoke(Pointer.NULL, null, 0, null, 1, Pointer.NULL)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/org/extism/sdk/ManifestTests.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import org.extism.sdk.manifest.Manifest; 4 | import org.extism.sdk.manifest.MemoryOptions; 5 | import org.extism.sdk.support.JsonSerde; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | import java.util.HashMap; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.extism.sdk.TestWasmSources.CODE; 13 | import static org.junit.jupiter.api.Assertions.assertNotNull; 14 | import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; 15 | 16 | public class ManifestTests { 17 | 18 | @Test 19 | public void shouldSerializeManifestWithWasmSourceToJson() { 20 | var paths = new HashMap(); 21 | paths.put("/tmp/foo", "/tmp/extism-plugins/foo"); 22 | var manifest = new Manifest(List.of(CODE.pathWasmSource()), null, null, null, paths); 23 | var json = JsonSerde.toJson(manifest); 24 | assertNotNull(json); 25 | 26 | assertJson(json).at("/wasm").isArray(); 27 | assertJson(json).at("/wasm").hasSize(1); 28 | assertJson(json).at("/allowed_paths").isObject(); 29 | assertJson(json).at("/allowed_paths").hasSize(1); 30 | } 31 | 32 | @Test 33 | public void shouldSerializeManifestWithWasmSourceAndMemoryOptionsToJson() { 34 | 35 | var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(4, 1024 * 1024 * 10)); 36 | var json = JsonSerde.toJson(manifest); 37 | assertNotNull(json); 38 | 39 | assertJson(json).at("/wasm").isArray(); 40 | assertJson(json).at("/wasm").hasSize(1); 41 | assertJson(json).at("/memory/max_pages").isEqualTo(4); 42 | assertJson(json).at("/memory/max_http_response_bytes").isEqualTo(1024 * 1024 * 10); 43 | } 44 | 45 | @Test 46 | public void codeWasmFromFileAndBytesShouldProduceTheSameHash() { 47 | 48 | var byteHash = CODE.byteArrayWasmSource().hash(); 49 | var fileHash = CODE.pathWasmSource().hash(); 50 | 51 | assertThat(byteHash).isEqualTo(fileHash); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/org/extism/sdk/PluginTests.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import com.sun.jna.Pointer; 4 | import org.extism.sdk.manifest.Manifest; 5 | import org.extism.sdk.manifest.MemoryOptions; 6 | import org.extism.sdk.wasm.UrlWasmSource; 7 | import org.extism.sdk.wasm.WasmSourceResolver; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.*; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.extism.sdk.TestWasmSources.CODE; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | 16 | public class PluginTests { 17 | 18 | // static { 19 | // Extism.setLogFile(Paths.get("/tmp/extism.log"), Extism.LogLevel.TRACE); 20 | // } 21 | 22 | @Test 23 | public void shouldInvokeFunctionWithMemoryOptions() { 24 | var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(0, 0)); 25 | assertThrows(ExtismException.class, () -> { 26 | Extism.invokeFunction(manifest, "count_vowels", "Hello World"); 27 | }); 28 | } 29 | 30 | @Test 31 | public void shouldInvokeFunctionWithConfig() { 32 | //FIXME check if config options are available in wasm call 33 | var config = Map.of("key1", "value1"); 34 | var manifest = new Manifest(List.of(CODE.pathWasmSource()), null, config); 35 | var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); 36 | assertThat(output).isEqualTo("{\"count\":3,\"total\":3,\"vowels\":\"aeiouAEIOU\"}"); 37 | } 38 | 39 | @Test 40 | public void shouldInvokeFunctionFromFileWasmSource() { 41 | var manifest = new Manifest(CODE.pathWasmSource()); 42 | var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); 43 | assertThat(output).isEqualTo("{\"count\":3,\"total\":3,\"vowels\":\"aeiouAEIOU\"}"); 44 | } 45 | 46 | @Test 47 | public void shouldInvokeFunctionFromUrlWasmSource() { 48 | var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"; 49 | var config = Map.of("vowels", "aeiouyAEIOUY"); 50 | var manifest = new Manifest(List.of(UrlWasmSource.fromUrl(url)), null, config); 51 | var plugin = new Plugin(manifest, false, null); 52 | var output = plugin.call("count_vowels", "Yellow, World!"); 53 | assertThat(output).isEqualTo("{\"count\":4,\"total\":4,\"vowels\":\"aeiouyAEIOUY\"}"); 54 | } 55 | 56 | @Test 57 | public void shouldInvokeFunctionFromUrlWasmSourceHostFuncs() { 58 | var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm"; 59 | var manifest = new Manifest(List.of(UrlWasmSource.fromUrl(url))); 60 | 61 | // Our application KV store 62 | // Pretend this is redis or a database :) 63 | var kvStore = new HashMap(); 64 | 65 | ExtismFunction kvWrite = (plugin, params, returns, data) -> { 66 | System.out.println("Hello from Java Host Function!"); 67 | var key = plugin.inputString(params[0]); 68 | var value = plugin.inputBytes(params[1]); 69 | System.out.println("Writing to key " + key); 70 | kvStore.put(key, value); 71 | }; 72 | 73 | ExtismFunction kvRead = (plugin, params, returns, data) -> { 74 | System.out.println("Hello from Java Host Function!"); 75 | var key = plugin.inputString(params[0]); 76 | System.out.println("Reading from key " + key); 77 | var value = kvStore.get(key); 78 | if (value == null) { 79 | // default to zeroed bytes 80 | var zero = new byte[]{0,0,0,0}; 81 | plugin.returnBytes(returns[0], zero); 82 | } else { 83 | plugin.returnBytes(returns[0], value); 84 | } 85 | }; 86 | 87 | HostFunction kvWriteHostFn = new HostFunction<>( 88 | "kv_write", 89 | new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64, LibExtism.ExtismValType.I64}, 90 | new LibExtism.ExtismValType[0], 91 | kvWrite, 92 | Optional.empty() 93 | ); 94 | 95 | HostFunction kvReadHostFn = new HostFunction<>( 96 | "kv_read", 97 | new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}, 98 | new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}, 99 | kvRead, 100 | Optional.empty() 101 | ); 102 | 103 | HostFunction[] functions = {kvWriteHostFn, kvReadHostFn}; 104 | var plugin = new Plugin(manifest, false, functions); 105 | var output = plugin.call("count_vowels", "Hello, World!"); 106 | } 107 | 108 | @Test 109 | public void shouldInvokeFunctionFromByteArrayWasmSource() { 110 | var manifest = new Manifest(CODE.byteArrayWasmSource()); 111 | var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); 112 | assertThat(output).isEqualTo("{\"count\":3,\"total\":3,\"vowels\":\"aeiouAEIOU\"}"); 113 | } 114 | 115 | @Test 116 | public void shouldFailToInvokeUnknownFunction() { 117 | assertThrows(ExtismException.class, () -> { 118 | var manifest = new Manifest(CODE.pathWasmSource()); 119 | Extism.invokeFunction(manifest, "unknown", "dummy"); 120 | }, "Function not found: unknown"); 121 | } 122 | 123 | @Test 124 | public void shouldAllowInvokeFunctionFromFileWasmSourceApiUsageExample() { 125 | 126 | var wasmSourceResolver = new WasmSourceResolver(); 127 | var manifest = new Manifest(wasmSourceResolver.resolve(CODE.getWasmFilePath())); 128 | 129 | var functionName = "count_vowels"; 130 | var input = "Hello World"; 131 | 132 | try (var plugin = new Plugin(manifest, false, null)) { 133 | var output = plugin.call(functionName, input); 134 | assertThat(output).contains("\"count\":3"); 135 | } 136 | } 137 | 138 | @Test 139 | public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() { 140 | var manifest = new Manifest(CODE.pathWasmSource()); 141 | var functionName = "count_vowels"; 142 | var input = "Hello World"; 143 | 144 | try (var plugin = new Plugin(manifest, false, null)) { 145 | var output = plugin.call(functionName, input); 146 | assertThat(output).contains("\"count\":3"); 147 | 148 | output = plugin.call(functionName, input); 149 | assertThat(output).contains("\"count\":3"); 150 | } 151 | } 152 | 153 | @Test 154 | public void shouldAllowInvokeHostFunctionFromPDK() { 155 | var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; 156 | var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; 157 | 158 | class MyUserData extends HostUserData { 159 | private String data1; 160 | private int data2; 161 | 162 | public MyUserData(String data1, int data2) { 163 | super(); 164 | this.data1 = data1; 165 | this.data2 = data2; 166 | } 167 | } 168 | 169 | ExtismFunction helloWorldFunction = (ExtismFunction) (plugin, params, returns, data) -> { 170 | System.out.println("Hello from Java Host Function!"); 171 | System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0]))); 172 | 173 | int offs = plugin.alloc(4); 174 | Pointer mem = plugin.memory(); 175 | mem.write(offs, "test".getBytes(), 0, 4); 176 | returns[0].v.i64 = offs; 177 | 178 | data.ifPresent(d -> System.out.println(String.format("Host user data, %s, %d", d.data1, d.data2))); 179 | }; 180 | 181 | HostFunction helloWorld = new HostFunction<>( 182 | "hello_world", 183 | parametersTypes, 184 | resultsTypes, 185 | helloWorldFunction, 186 | Optional.of(new MyUserData("test", 2)) 187 | ); 188 | 189 | HostFunction[] functions = {helloWorld}; 190 | 191 | Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); 192 | String functionName = "count_vowels"; 193 | 194 | try (var plugin = new Plugin(manifest, true, functions)) { 195 | var output = plugin.call(functionName, "this is a test"); 196 | assertThat(output).isEqualTo("test"); 197 | } 198 | } 199 | 200 | @Test 201 | public void shouldAllowInvokeHostFunctionFromPDKUsingPTR() { 202 | var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.PTR}; 203 | var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.PTR}; 204 | 205 | class MyUserData extends HostUserData { 206 | private String data1; 207 | private int data2; 208 | 209 | public MyUserData(String data1, int data2) { 210 | super(); 211 | this.data1 = data1; 212 | this.data2 = data2; 213 | } 214 | } 215 | 216 | ExtismFunction helloWorldFunction = (ExtismFunction) (plugin, params, returns, data) -> { 217 | System.out.println("Hello from Java Host Function!"); 218 | System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0]))); 219 | 220 | int offs = plugin.alloc(4); 221 | Pointer mem = plugin.memory(); 222 | mem.write(offs, "test".getBytes(), 0, 4); 223 | returns[0].v.ptr = offs; 224 | 225 | data.ifPresent(d -> System.out.println(String.format("Host user data, %s, %d", d.data1, d.data2))); 226 | }; 227 | 228 | HostFunction helloWorld = new HostFunction<>( 229 | "hello_world", 230 | parametersTypes, 231 | resultsTypes, 232 | helloWorldFunction, 233 | Optional.of(new MyUserData("test", 2)) 234 | ); 235 | 236 | HostFunction[] functions = {helloWorld}; 237 | 238 | Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); 239 | String functionName = "count_vowels"; 240 | 241 | try (var plugin = new Plugin(manifest, true, functions)) { 242 | var output = plugin.call(functionName, "this is a test"); 243 | assertThat(output).isEqualTo("test"); 244 | } 245 | } 246 | 247 | @Test 248 | public void shouldAllowInvokeHostFunctionWithoutUserData() { 249 | 250 | var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; 251 | var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; 252 | 253 | ExtismFunction helloWorldFunction = (plugin, params, returns, data) -> { 254 | System.out.println("Hello from Java Host Function!"); 255 | System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0]))); 256 | 257 | int offs = plugin.alloc(4); 258 | Pointer mem = plugin.memory(); 259 | mem.write(offs, "test".getBytes(), 0, 4); 260 | returns[0].v.i64 = offs; 261 | 262 | assertThat(data.isEmpty()); 263 | }; 264 | 265 | HostFunction f = new HostFunction<>( 266 | "hello_world", 267 | parametersTypes, 268 | resultsTypes, 269 | helloWorldFunction, 270 | Optional.empty() 271 | ) 272 | .withNamespace("extism:host/user"); 273 | 274 | HostFunction g = new HostFunction<>( 275 | "hello_world", 276 | parametersTypes, 277 | resultsTypes, 278 | helloWorldFunction, 279 | Optional.empty() 280 | ) 281 | .withNamespace("test"); 282 | 283 | HostFunction[] functions = {f,g}; 284 | 285 | Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); 286 | String functionName = "count_vowels"; 287 | 288 | try (var plugin = new Plugin(manifest, true, functions)) { 289 | var output = plugin.call(functionName, "this is a test"); 290 | assertThat(output).isEqualTo("test"); 291 | } 292 | } 293 | 294 | 295 | @Test 296 | public void shouldFailToInvokeUnknownHostFunction() { 297 | Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); 298 | String functionName = "count_vowels"; 299 | 300 | try { 301 | var plugin = new Plugin(manifest, true, null); 302 | plugin.call(functionName, "this is a test"); 303 | } catch (ExtismException e) { 304 | assertThat(e.getMessage()).contains("unknown import: `extism:host/user::hello_world` has not been defined"); 305 | } 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /src/test/java/org/extism/sdk/TestWasmSources.java: -------------------------------------------------------------------------------- 1 | package org.extism.sdk; 2 | 3 | import org.extism.sdk.wasm.ByteArrayWasmSource; 4 | import org.extism.sdk.wasm.PathWasmSource; 5 | import org.extism.sdk.wasm.WasmSourceResolver; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.util.Arrays; 12 | 13 | public enum TestWasmSources { 14 | 15 | CODE { 16 | public Path getWasmFilePath() { 17 | return Paths.get(WASM_LOCATION, "code.wasm"); 18 | } 19 | public Path getWasmFunctionsFilePath() { 20 | return Paths.get(WASM_LOCATION, "code-functions.wasm"); 21 | } 22 | }; 23 | 24 | public static final String WASM_LOCATION = "src/test/resources"; 25 | 26 | public abstract Path getWasmFilePath(); 27 | 28 | public abstract Path getWasmFunctionsFilePath(); 29 | 30 | public PathWasmSource pathWasmSource() { 31 | return resolvePathWasmSource(getWasmFilePath()); 32 | } 33 | 34 | public PathWasmSource pathWasmFunctionsSource() { 35 | return resolvePathWasmSource(getWasmFunctionsFilePath()); 36 | } 37 | 38 | public ByteArrayWasmSource byteArrayWasmSource() { 39 | try { 40 | byte[] wasmBytes = Files.readAllBytes(getWasmFilePath()); 41 | return new WasmSourceResolver().resolve("wasm@" + Arrays.hashCode(wasmBytes), wasmBytes); 42 | } catch (IOException ioe) { 43 | throw new RuntimeException(ioe); 44 | } 45 | } 46 | 47 | public static PathWasmSource resolvePathWasmSource(Path path) { 48 | return new WasmSourceResolver().resolve(path); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/resources/code-functions.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/java-sdk/d31c1e27a5b15c70d54c29b8d8d2db1768eb0045/src/test/resources/code-functions.wasm -------------------------------------------------------------------------------- /src/test/resources/code.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extism/java-sdk/d31c1e27a5b15c70d54c29b8d8d2db1768eb0045/src/test/resources/code.wasm --------------------------------------------------------------------------------