Skip to content

Commit 5b42678

Browse files
committed
api: hide Chicory low-level primitives, expose Extism function type
- Loosely follow the java-sdk, expose ExtismHostFunction and use an internal ExtismValTypeList that avoids wrapping as much as possible. - Hide the conversion between longs and high-level, types - Add test case following the java-sdk - Update the README Signed-off-by: Edoardo Vacchi <[email protected]>
1 parent 6c46c19 commit 5b42678

File tree

12 files changed

+545
-62
lines changed

12 files changed

+545
-62
lines changed

README.md

Lines changed: 182 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,187 @@ to complete a full Extism SDK. If anyone would like to work on it feel free to r
88
> **Note**: If you are interested in a solid and working Java SDK, see our [Extism Java SDK](https://github.com/extism/java-sdk).
99
> But if you have a need for pure Java solution, please reach out!
1010
11-
## Example
11+
## Installation
12+
13+
### Maven
14+
15+
To use the Chicory java-sdk with maven you need to add the following dependency to your `pom.xml` file:
16+
```xml
17+
<dependency>
18+
<groupId>org.extism.sdk</groupId>
19+
<artifactId>chicory-sdk</artifactId>
20+
<version>999-SNAPSHOT</version>
21+
</dependency>
22+
```
23+
24+
25+
### Gradle
26+
27+
To use the Chicory java-sdk with maven you need to add the following dependency to your `build.gradle` file:
28+
29+
```
30+
implementation 'org.extism.sdk:chicory-sdk:999-SNAPSHOT'
31+
```
32+
33+
## Getting Started
34+
35+
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.
36+
Since you may not have a Extism plug-in on hand to test, let's load a demo plug-in from the web:
37+
38+
```java
39+
var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm";
40+
var wasm = ManifestWasm.fromUrl(url).build();
41+
var manifest = Manifest.ofWasms(wasm).build();
42+
var plugin = Plugin.ofManifest(manifest).build();
43+
```
44+
45+
> **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.
46+
47+
### Calling A Plug-in's Exports
48+
49+
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`.
50+
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[]))
51+
52+
```java
53+
var output = plugin.call("count_vowels", "Hello, World!".getBytes(StandardCharsets.UTF_8));
54+
System.out.println(new String(output, StandardCharsets.UTF_8));
55+
// => "{"count": 3, "total": 3, "vowels": "aeiouAEIOU"}"
56+
```
57+
58+
All exports have a simple interface of bytes-in and bytes-out.
59+
This plug-in happens to take a string and return a JSON encoded string with a report of results.
60+
61+
62+
### Plug-in State
63+
64+
Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables.
65+
Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result.
66+
You can see this by making subsequent calls to the export:
67+
68+
```java
69+
var output = plugin.call("count_vowels","Hello, World!".getBytes(StandardCharsets.UTF_8));
70+
System.out.println(output);
71+
// => "{"count": 3, "total": 6, "vowels": "aeiouAEIOU"}"
72+
73+
var output = plugin.call("count_vowels", "Hello, World!".getBytes(StandardCharsets.UTF_8));
74+
System.out.println(output);
75+
// => "{"count": 3, "total": 9, "vowels": "aeiouAEIOU"}"
76+
```
77+
78+
These variables will persist until this plug-in is freed or you initialize a new one.
79+
80+
### Configuration
81+
82+
Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in.
83+
Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example:
1284

1385
```java
14-
var manifest =
15-
Manifest.ofWasms(
16-
ManifestWasm.fromUrl(
17-
"https://github.com/extism/plugins/releases/download/v1.1.0/greet.wasm")
18-
.build()).build();
19-
var plugin = Plugin.Builder.ofManifest(manifest).build();
20-
var input = "Benjamin";
21-
var result = new String(plugin.call("greet", input.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
22-
assertEquals("Hello, Benjamin!", result);
23-
```
86+
var plugin = new Plugin(manifest, false, null);
87+
var output = plugin.call("count_vowels", "Yellow, World!");
88+
System.out.println(output);
89+
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
90+
91+
// Let's change the vowels config it uses to determine what is a vowel:
92+
var config = Map.of("vowels", "aeiouyAEIOUY");
93+
var manifest2 = Manifest.ofWasms(wasm)
94+
.withOptions(new Manifest.Options().withConfig(config)).build();
95+
var plugin = Plugin.ofManifest(manifest2).build();
96+
var result = new String(plugin.call("count_vowels", "Yellow, World!".getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
97+
System.out.println(output);
98+
// => {"count": 4, "total": 4, "vowels": "aeiouyAEIOUY"}
99+
// ^ note count changed to 4 as we configured Y as a vowel this time
100+
```
101+
102+
### Host Functions
103+
104+
Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var,
105+
let's store it in a persistent key-value store!
106+
107+
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.
108+
109+
[Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application.
110+
They are simply some java methods you write which can be passed down and invoked from any language inside the plug-in.
111+
112+
Let's load the manifest like usual but load up this `count_vowels_kvstore` plug-in:
113+
114+
```java
115+
var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels_kvstore.wasm";
116+
var manifest = new Manifest(List.of(UrlWasmSource.fromUrl(url)));
117+
var plugin = new Plugin(manifest, false, null);
118+
```
119+
120+
> *Note*: The source code for this plug-in is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs)
121+
> and is written in rust, but it could be written in any of our PDK languages.
122+
123+
Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy its import interface for a KV store.
124+
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`.
125+
126+
```java
127+
// Our application KV store
128+
// Pretend this is redis or a database :)
129+
var kvStore = new HashMap<String, byte[]>();
130+
131+
ExtismFunction kvWrite = (plugin, params, returns) -> {
132+
System.out.println("Hello from kv_write Java Function!");
133+
var key = plugin.memory().readString(params.getRaw(0));
134+
var value = plugin.memory().readBytes(params.getRaw(1));
135+
System.out.println("Writing to key " + key);
136+
kvStore.put(key, value);
137+
};
138+
139+
ExtismFunction kvRead = (plugin, params, returns) -> {
140+
System.out.println("Hello from kv_read Java Function!");
141+
var key = plugin.memory().readString(params.getRaw(0));
142+
System.out.println("Reading from key " + key);
143+
var value = kvStore.get(key);
144+
if (value == null) {
145+
// default to zeroed bytes
146+
var zero = new byte[]{0, 0, 0, 0};
147+
returns.setRaw(0, plugin.memory().writeBytes(zero));
148+
} else {
149+
returns.setRaw(0, plugin.memory().writeBytes(value));
150+
}
151+
};
152+
153+
var kvWriteHostFn = ExtismHostFunction.of(
154+
"kv_write",
155+
List.of(ExtismValType.I64, ExtismValType.I64),
156+
List.of(),
157+
kvWrite
158+
);
159+
160+
var kvReadHostFn = ExtismHostFunction.of(
161+
"kv_read",
162+
List.of(ExtismValType.I64),
163+
List.of(ExtismValType.I64),
164+
kvRead
165+
);
166+
```
167+
168+
> *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.
169+
> The `plugin` parameter is an instance of this class.
170+
171+
Now we just need to pass in these function references when creating the plugin:.
172+
173+
```java
174+
var plugin = Plugin.ofManifest(manifest).withHostFunctions(kvReadHostFn, kvWriteHostFn).build();
175+
var output = plugin.call("count_vowels", "Yellow, World!".getBytes(StandardCharsets.UTF_8));
176+
var result = new String(output, StandardCharsets.UTF_8);
177+
// => Hello from kv_read Java Function!
178+
// => Reading from key count-vowels
179+
// => Hello from kv_write Java Function!
180+
// => Writing to key count-vowels
181+
System.out.println(output);
182+
// => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
183+
```
184+
185+
## Development
186+
187+
# Build
188+
189+
To build the Extism chicory-sdk run the following command:
190+
191+
```
192+
mvn clean verify
193+
```
194+

src/main/java/org/extism/chicory/sdk/CurrentPlugin.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,17 @@ public HostEnv.Memory memory() {
1515
return plugin.memory();
1616
}
1717

18+
19+
void setInput(byte[] input) {
20+
plugin.setInput(input);
21+
}
22+
23+
byte[] getOutput() {
24+
return plugin.getOutput();
25+
}
26+
27+
String getError() {
28+
return plugin.getError();
29+
}
30+
1831
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.extism.chicory.sdk;
2+
3+
@FunctionalInterface
4+
public interface ExtismFunction {
5+
void apply(CurrentPlugin currentPlugin, ExtismValueList args, ExtismValueList returns);
6+
}

src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import com.dylibso.chicory.runtime.HostFunction;
44
import com.dylibso.chicory.runtime.Instance;
5-
import com.dylibso.chicory.wasm.types.Value;
6-
import com.dylibso.chicory.wasm.types.ValueType;
75

86
import java.util.List;
97

@@ -12,39 +10,39 @@ public final class ExtismHostFunction {
1210

1311
public static ExtismHostFunction of(
1412
String name,
15-
List<ValueType> paramTypes,
16-
List<ValueType> returnTypes,
17-
Handle handle) {
18-
return new ExtismHostFunction(DEFAULT_NAMESPACE, name, handle, paramTypes, returnTypes);
13+
List<ExtismValType> paramTypes,
14+
List<ExtismValType> returnTypes,
15+
ExtismFunction extismFunction) {
16+
return new ExtismHostFunction(DEFAULT_NAMESPACE, name, paramTypes, returnTypes, extismFunction);
1917
}
2018

2119
public static ExtismHostFunction of(
2220
String module,
2321
String name,
24-
Handle handle,
25-
List<ValueType> paramTypes,
26-
List<ValueType> returnTypes) {
27-
return new ExtismHostFunction(module, name, handle, paramTypes, returnTypes);
22+
ExtismFunction extismFunction,
23+
List<ExtismValType> paramTypes,
24+
List<ExtismValType> returnTypes) {
25+
return new ExtismHostFunction(module, name, paramTypes, returnTypes, extismFunction);
2826
}
2927

3028
private final String module;
3129
private final String name;
32-
private final Handle handle;
33-
private final List<ValueType> paramTypes;
34-
private final List<ValueType> returnTypes;
30+
private final ExtismFunction extismFunction;
31+
private final ExtismValTypeList paramTypes;
32+
private final ExtismValTypeList returnTypes;
3533
private CurrentPlugin currentPlugin;
3634

37-
ExtismHostFunction(
35+
private ExtismHostFunction(
3836
String module,
3937
String name,
40-
Handle handle,
41-
List<ValueType> paramTypes,
42-
List<ValueType> returnTypes) {
38+
List<ExtismValType> paramTypes,
39+
List<ExtismValType> returnTypes,
40+
ExtismFunction extismFunction) {
4341
this.module = module;
4442
this.name = name;
45-
this.handle = handle;
46-
this.paramTypes = paramTypes;
47-
this.returnTypes = returnTypes;
43+
this.paramTypes = new ExtismValTypeList(paramTypes);
44+
this.returnTypes = new ExtismValTypeList(returnTypes);
45+
this.extismFunction = extismFunction;
4846
}
4947

5048
public void bind(CurrentPlugin p) {
@@ -58,12 +56,13 @@ public void bind(CurrentPlugin p) {
5856

5957
final HostFunction asHostFunction() {
6058
return new HostFunction(
61-
module, name, paramTypes, returnTypes,
62-
(Instance inst, long... args) -> handle.apply(this.currentPlugin, args));
59+
module, name, paramTypes.toChicoryTypes(), returnTypes.toChicoryTypes(),
60+
(Instance inst, long... args) -> {
61+
var params = paramTypes.toExtismValueList(args);
62+
var results = returnTypes.toExtismValueList();
63+
extismFunction.apply(this.currentPlugin, params, results);
64+
return results.unwrap();
65+
});
6366
}
6467

65-
@FunctionalInterface
66-
public interface Handle {
67-
long[] apply(CurrentPlugin currentPlugin, long... args);
68-
}
6968
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.extism.chicory.sdk;
2+
3+
public class ExtismTypeConversionException extends ExtismException {
4+
5+
public ExtismTypeConversionException(ExtismValType expected, ExtismValType given) {
6+
super(String.format("Illegal type conversion, wanted %s, given %s", expected.name(), given.name()));
7+
}
8+
9+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.extism.chicory.sdk;
2+
3+
import com.dylibso.chicory.wasm.types.ValueType;
4+
5+
public enum ExtismValType {
6+
I32(ValueType.I32),
7+
I64(ValueType.I64),
8+
F32(ValueType.F32),
9+
F64(ValueType.F64);
10+
11+
private final ValueType chicoryType;
12+
13+
ExtismValType(ValueType chicoryType) {
14+
this.chicoryType = chicoryType;
15+
}
16+
17+
ValueType toChicoryValueType() {
18+
return chicoryType;
19+
}
20+
21+
public ExtismValue toExtismValue(long v) {
22+
switch (this) {
23+
case I32:
24+
return ExtismValue.i32(v);
25+
case I64:
26+
return ExtismValue.i64(v);
27+
case F32:
28+
return ExtismValue.f32FromLongBits(v);
29+
case F64:
30+
return ExtismValue.f64FromLongBits(v);
31+
default:
32+
throw new IllegalArgumentException();
33+
}
34+
}
35+
36+
public long toChicoryValue(ExtismValue value) {
37+
return 0;
38+
}
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.extism.chicory.sdk;
2+
3+
import com.dylibso.chicory.wasm.types.ValueType;
4+
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import java.util.stream.Collectors;
8+
9+
class ExtismValTypeList {
10+
private final ExtismValType[] types;
11+
private final List<ValueType> chicoryTypes;
12+
13+
ExtismValTypeList(List<ExtismValType> types) {
14+
this.types = types.toArray(ExtismValType[]::new);
15+
this.chicoryTypes = types.stream().map(ExtismValType::toChicoryValueType)
16+
.collect(Collectors.toList());
17+
}
18+
19+
List<ValueType> toChicoryTypes() {
20+
return Arrays.stream(types)
21+
.map(ExtismValType::toChicoryValueType)
22+
.collect(Collectors.toList());
23+
}
24+
25+
public ExtismValueList toExtismValueList(long[] args) {
26+
return new ExtismValueList(this.types, args);
27+
}
28+
29+
public ExtismValueList toExtismValueList() {
30+
return new ExtismValueList(this.types, new long[this.types.length]);
31+
}
32+
33+
}

0 commit comments

Comments
 (0)