├── .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 | [](https://search.maven.org/artifact/org.extism.sdk/extism)
6 | [](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
--------------------------------------------------------------------------------