From f4eed9a13e1f46a5cb2b33b7d2143df665680b7e Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Wed, 9 Oct 2024 19:24:12 +0200 Subject: [PATCH] Host SDK: add some host-side functions missing from the kernel. (#11) Signed-off-by: Edoardo Vacchi --- .../org/extism/chicory/sdk/CurrentPlugin.java | 18 ++ .../chicory/sdk/ExtismHostFunction.java | 59 +++++ .../java/org/extism/chicory/sdk/HostEnv.java | 247 ++++++++++++++++++ .../java/org/extism/chicory/sdk/Kernel.java | 39 +-- .../java/org/extism/chicory/sdk/LogLevel.java | 24 ++ .../java/org/extism/chicory/sdk/Plugin.java | 43 ++- .../chicory/sdk/ExtismHostFunctionTest.java | 31 +++ .../org/extism/chicory/sdk/HostEnvTest.java | 28 ++ 8 files changed, 462 insertions(+), 27 deletions(-) create mode 100644 src/main/java/org/extism/chicory/sdk/CurrentPlugin.java create mode 100644 src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java create mode 100644 src/main/java/org/extism/chicory/sdk/HostEnv.java create mode 100644 src/main/java/org/extism/chicory/sdk/LogLevel.java create mode 100644 src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java create mode 100644 src/test/java/org/extism/chicory/sdk/HostEnvTest.java diff --git a/src/main/java/org/extism/chicory/sdk/CurrentPlugin.java b/src/main/java/org/extism/chicory/sdk/CurrentPlugin.java new file mode 100644 index 0000000..3b7c68b --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/CurrentPlugin.java @@ -0,0 +1,18 @@ +package org.extism.chicory.sdk; + +public class CurrentPlugin { + private final Plugin plugin; + + public CurrentPlugin(Plugin plugin) { + this.plugin = plugin; + } + + public HostEnv.Log log() { + return plugin.log(); + } + + public HostEnv.Memory memory() { + return plugin.memory(); + } + +} diff --git a/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java b/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java new file mode 100644 index 0000000..547a208 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java @@ -0,0 +1,59 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.types.Value; +import com.dylibso.chicory.wasm.types.ValueType; + +import java.util.List; + +public final class ExtismHostFunction { + static final String DEFAULT_NAMESPACE = "extism:host/user"; + + public static ExtismHostFunction of( + String name, + List paramTypes, + List returnTypes, + Handle handle) { + return new ExtismHostFunction(DEFAULT_NAMESPACE, name, handle, paramTypes, returnTypes); + } + + public static ExtismHostFunction of( + String module, + String name, + Handle handle, + List paramTypes, + List returnTypes) { + return new ExtismHostFunction(module, name, handle, paramTypes, returnTypes); + } + + private final String module; + private final String name; + private final Handle handle; + private final List paramTypes; + private final List returnTypes; + + ExtismHostFunction( + String module, + String name, + Handle handle, + List paramTypes, + List returnTypes) { + this.module = module; + this.name = name; + this.handle = handle; + this.paramTypes = paramTypes; + this.returnTypes = returnTypes; + } + + final HostFunction toHostFunction(CurrentPlugin currentPlugin) { + return new HostFunction( + (Instance inst, Value... args) -> handle.apply(currentPlugin, args), + module, name, paramTypes, returnTypes); + } + + @FunctionalInterface + public interface Handle { + Value[] apply(CurrentPlugin currentPlugin, Value... args); + } +} diff --git a/src/main/java/org/extism/chicory/sdk/HostEnv.java b/src/main/java/org/extism/chicory/sdk/HostEnv.java new file mode 100644 index 0000000..5129d89 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/HostEnv.java @@ -0,0 +1,247 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.Logger; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.types.Value; +import com.dylibso.chicory.wasm.types.ValueType; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.dylibso.chicory.wasm.types.Value.i64; + +public class HostEnv { + + private final Kernel kernel; + private final Memory memory; + private final Logger logger; + private final Log log; + private final Var var; + private final Config config; + + public HostEnv(Kernel kernel, Map config, Logger logger) { + this.kernel = kernel; + this.memory = new Memory(); + this.logger = logger; + this.config = new Config(config); + this.var = new Var(); + this.log = new Log(); + } + + public Log log() { + return log; + } + + public Var var() { + return var; + } + + public Config config() { + return config; + } + + public HostFunction[] toHostFunctions() { + return concat( + kernel.toHostFunctions(), + log.toHostFunctions(), + var.toHostFunctions(), + config.toHostFunctions()); + } + + private HostFunction[] concat(HostFunction[]... hfs) { + return Arrays.stream(hfs).flatMap(Arrays::stream).toArray(HostFunction[]::new); + } + + public void setInput(byte[] input) { + kernel.setInput(input); + } + + public byte[] getOutput() { + return kernel.getOutput(); + } + + public Memory memory() { + return this.memory; + } + + public class Memory { + + public long length(long offset) { + return kernel.length.apply(i64(offset))[0].asLong(); + } + + public com.dylibso.chicory.runtime.Memory memory() { + return kernel.instanceMemory; + } + + public long alloc(long size) { + return kernel.alloc.apply(i64(size))[0].asLong(); + } + + byte[] readBytes(long offset) { + long length = length(offset); + return memory().readBytes((int) offset, (int) length); + } + + String readString(long offset) { + return new String(readBytes(offset), StandardCharsets.UTF_8); + } + + long writeBytes(byte[] bytes) { + long ptr = alloc(bytes.length); + memory().write((int) ptr, bytes); + return ptr; + } + + long writeString(String s) { + return writeBytes(s.getBytes(StandardCharsets.UTF_8)); + } + } + + public class Log { + private Log(){} + + public void log(LogLevel level, String message) { + logger.log(level.toChicoryLogLevel(), message, null); + } + + public void logf(LogLevel level, String format, Object args) { + logger.log(level.toChicoryLogLevel(), String.format(format, args), null); + } + + private Value[] logTrace(Instance instance, Value... args) { + return log(LogLevel.TRACE, args[0].asLong()); + } + + private Value[] logDebug(Instance instance, Value... args) { + return log(LogLevel.DEBUG, args[0].asLong()); + } + + private Value[] logInfo(Instance instance, Value... args) { + return log(LogLevel.INFO, args[0].asLong()); + } + + private Value[] logWarn(Instance instance, Value... args) { + return log(LogLevel.WARN, args[0].asLong()); + } + + private Value[] logError(Instance instance, Value... args) { + return log(LogLevel.ERROR, args[0].asLong()); + } + + + private Value[] log(LogLevel level, long offset) { + String msg = memory().readString(offset); + log(level, msg); + return new Value[0]; + } + + HostFunction[] toHostFunctions() { + return new HostFunction[]{ + new HostFunction(this::logTrace, Kernel.IMPORT_MODULE_NAME, "log_trace", List.of(ValueType.I64), List.of()), + new HostFunction(this::logDebug, Kernel.IMPORT_MODULE_NAME, "log_debug", List.of(ValueType.I64), List.of()), + new HostFunction(this::logInfo, Kernel.IMPORT_MODULE_NAME, "log_info", List.of(ValueType.I64), List.of()), + new HostFunction(this::logWarn, Kernel.IMPORT_MODULE_NAME, "log_warn", List.of(ValueType.I64), List.of()), + new HostFunction(this::logError, Kernel.IMPORT_MODULE_NAME, "log_error", List.of(ValueType.I64), List.of())}; + } + } + + public class Var { + private final Map vars = new ConcurrentHashMap<>(); + + private Var() {} + + public byte[] get(String key) { + return vars.get(key); + } + + public void set(String key, byte[] value) { + this.vars.put(key, value); + } + + private Value[] varGet(Instance instance, Value... args) { + // FIXME: should check MaxVarBytes to see if vars are disabled. + + long ptr = args[0].asLong(); + String key = memory().readString(ptr); + byte[] value = get(key); + Value result; + if (value == null) { + // Value not found + result = i64(0); + } else { + long rPtr = memory().writeBytes(value); + result = i64(rPtr); + } + return new Value[]{result}; + } + + private Value[] varSet(Instance instance, Value... args) { + // FIXME: should check MaxVarBytes before committing. + + long keyPtr = args[0].asLong(); + long valuePtr = args[1].asLong(); + String key = memory().readString(keyPtr); + + // Remove if the value offset is 0 + if (valuePtr == 0) { + vars.remove(key); + } else { + byte[] value = memory().readBytes(valuePtr); + set(key, value); + } + return new Value[0]; + } + + + HostFunction[] toHostFunctions() { + return new HostFunction[]{ + new HostFunction(this::varGet, Kernel.IMPORT_MODULE_NAME, "var_get", List.of(ValueType.I64), List.of(ValueType.I64)), + new HostFunction(this::varSet, Kernel.IMPORT_MODULE_NAME, "var_set", List.of(ValueType.I64, ValueType.I64), List.of()), + }; + } + } + + public class Config { + + private final Map config; + + private Config(Map config) { + this.config = config; + } + + public String get(String key) { + return config.get(key); + } + + private Value[] configGet(Instance instance, Value... args) { + long ptr = args[0].asLong(); + String key = memory().readString(ptr); + String value = get(key); + Value result; + if (value == null) { + // Value not found + result = i64(0); + } else { + long rPtr = memory().writeString(value); + result = i64(rPtr); + } + return new Value[]{result}; + } + + HostFunction[] toHostFunctions() { + return new HostFunction[]{ + new HostFunction(this::configGet, Kernel.IMPORT_MODULE_NAME, "config_get", List.of(ValueType.I64), List.of(ValueType.I64)) + }; + } + + } + + + + +} diff --git a/src/main/java/org/extism/chicory/sdk/Kernel.java b/src/main/java/org/extism/chicory/sdk/Kernel.java index 7bbd2b3..d805041 100644 --- a/src/main/java/org/extism/chicory/sdk/Kernel.java +++ b/src/main/java/org/extism/chicory/sdk/Kernel.java @@ -1,8 +1,5 @@ package org.extism.chicory.sdk; -import static com.dylibso.chicory.wasm.types.Value.*; - -import com.dylibso.chicory.aot.AotMachine; import com.dylibso.chicory.log.Logger; import com.dylibso.chicory.log.SystemLogger; import com.dylibso.chicory.runtime.ExportFunction; @@ -11,16 +8,18 @@ import com.dylibso.chicory.runtime.Module; import com.dylibso.chicory.wasm.types.Value; import com.dylibso.chicory.wasm.types.ValueType; -import com.dylibso.chicory.runtime.Memory; + import java.util.HashMap; import java.util.List; +import static com.dylibso.chicory.wasm.types.Value.i64; + public class Kernel { - private static final String IMPORT_MODULE_NAME = "extism:host/env"; - private final Memory memory; - private final ExportFunction alloc; + static final String IMPORT_MODULE_NAME = "extism:host/env"; + final com.dylibso.chicory.runtime.Memory instanceMemory; + final ExportFunction alloc; private final ExportFunction free; - private final ExportFunction length; + final ExportFunction length; private final ExportFunction lengthUnsafe; private final ExportFunction loadU8; private final ExportFunction loadU64; @@ -28,11 +27,11 @@ public class Kernel { private final ExportFunction inputLoadU64; private final ExportFunction storeU8; private final ExportFunction storeU64; - private final ExportFunction inputSet; + final ExportFunction inputSet; private final ExportFunction inputLen; private final ExportFunction inputOffset; - private final ExportFunction outputLen; - private final ExportFunction outputOffset; + final ExportFunction outputLen; + final ExportFunction outputOffset; private final ExportFunction outputSet; private final ExportFunction reset; private final ExportFunction errorSet; @@ -52,7 +51,7 @@ public Kernel(Logger logger) { //moduleBuilder = moduleBuilder.withMachineFactory(AotMachine::new); Instance kernel = moduleBuilder.build().instantiate(); - memory = kernel.memory(); + instanceMemory = kernel.memory(); alloc = kernel.export("alloc"); free = kernel.export("free"); length = kernel.export("length"); @@ -73,21 +72,22 @@ public Kernel(Logger logger) { errorSet = kernel.export("error_set"); errorGet = kernel.export("error_get"); memoryBytes = kernel.export("memory_bytes"); + } - public void setInput(byte[] input) { + void setInput(byte[] input) { var ptr = alloc.apply(i64(input.length))[0]; - memory.write(ptr.asInt(), input); + instanceMemory.write(ptr.asInt(), input); inputSet.apply(ptr, i64(input.length)); } - public byte[] getOutput() { + byte[] getOutput() { var ptr = outputOffset.apply()[0]; var len = outputLen.apply()[0]; - return memory.readBytes(ptr.asInt(), len.asInt()); + return instanceMemory.readBytes(ptr.asInt(), len.asInt()); } - public HostFunction[] toHostFunctions() { + HostFunction[] toHostFunctions() { var hostFunctions = new HostFunction[23]; int count = 0; @@ -261,7 +261,7 @@ public HostFunction[] toHostFunctions() { // var key = memory.getString(args[0].asInt(), // keyLen.asInt()); // var value = vars.get(key); - return new Value[] {i64(0)}; + return new Value[]{i64(0)}; }, IMPORT_MODULE_NAME, "var_get", @@ -291,7 +291,7 @@ public HostFunction[] toHostFunctions() { // var key = memory.getString(args[0].asInt(), // keyLen.asInt()); // var value = vars.get(key); - return new Value[] {i64(0)}; + return new Value[]{i64(0)}; }, IMPORT_MODULE_NAME, "config_get", @@ -300,4 +300,5 @@ public HostFunction[] toHostFunctions() { return hostFunctions; } + } diff --git a/src/main/java/org/extism/chicory/sdk/LogLevel.java b/src/main/java/org/extism/chicory/sdk/LogLevel.java new file mode 100644 index 0000000..bdbe549 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/LogLevel.java @@ -0,0 +1,24 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.Logger; + +public enum LogLevel { + TRACE, DEBUG, INFO, WARN, ERROR; + + Logger.Level toChicoryLogLevel() { + switch (this) { + case TRACE: + return Logger.Level.TRACE; + case DEBUG: + return Logger.Level.DEBUG; + case INFO: + return Logger.Level.INFO; + case WARN: + return Logger.Level.WARNING; + case ERROR: + return Logger.Level.ERROR; + } + throw new IllegalArgumentException("unknown type " + this); + } +} + diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java index 99b753c..3ae5112 100644 --- a/src/main/java/org/extism/chicory/sdk/Plugin.java +++ b/src/main/java/org/extism/chicory/sdk/Plugin.java @@ -8,7 +8,11 @@ import com.dylibso.chicory.wasi.WasiOptions; import com.dylibso.chicory.wasi.WasiPreview1; +import java.util.Arrays; +import java.util.Map; + public class Plugin { + public static Builder ofManifest(Manifest manifest) { return new Builder(manifest); } @@ -16,14 +20,14 @@ public static Builder ofManifest(Manifest manifest) { public static class Builder { private final Manifest manifest; - private HostFunction[] hostFunctions = new HostFunction[0]; + private ExtismHostFunction[] hostFunctions = new ExtismHostFunction[0]; private Logger logger; private Builder(Manifest manifest) { this.manifest = manifest; } - public Builder withHostFunctions(HostFunction... hostFunctions) { + public Builder withHostFunctions(ExtismHostFunction... hostFunctions) { this.hostFunctions = hostFunctions; return this; } @@ -42,17 +46,19 @@ public Plugin build() { private final Instance instance; private final HostImports imports; private final Kernel kernel; + private final HostEnv hostEnv; private Plugin(Manifest manifest) { - this(manifest, new HostFunction[]{}, null); + this(manifest, new ExtismHostFunction[]{}, null); } - private Plugin(Manifest manifest, HostFunction[] hostFunctions, Logger logger) { + private Plugin(Manifest manifest, ExtismHostFunction[] hostFunctions, Logger logger) { if (logger == null) { logger = new SystemLogger(); } this.kernel = new Kernel(logger); + this.hostEnv = new HostEnv(kernel, Map.of(), logger); this.manifest = manifest; // TODO: Expand WASI Support here @@ -60,7 +66,7 @@ private Plugin(Manifest manifest, HostFunction[] hostFunctions, Logger logger) { var wasi = new WasiPreview1(logger, options); var wasiHostFunctions = wasi.toHostFunctions(); - var hostFuncList = getHostFunctions(kernel.toHostFunctions(), hostFunctions, wasiHostFunctions); + var hostFuncList = getHostFunctions(hostEnv.toHostFunctions(), lower(hostFunctions), wasiHostFunctions); this.imports = new HostImports(hostFuncList); var moduleBuilder = new ManifestModuleMapper(manifest) @@ -71,10 +77,31 @@ private Plugin(Manifest manifest, HostFunction[] hostFunctions, Logger logger) { this.instance = moduleBuilder.build().instantiate(); } + public HostEnv.Log log() { + return hostEnv.log(); + } + + public HostEnv.Var var() { + return hostEnv.var(); + } + + public HostEnv.Config config() { + return hostEnv.config(); + } + + public HostEnv.Memory memory() { + return hostEnv.memory(); + } + + private HostFunction[] lower(ExtismHostFunction[] fns) { + var currentPlugin = new CurrentPlugin(this); + return Arrays.stream(fns).map(fn -> fn.toHostFunction(currentPlugin)).toArray(HostFunction[]::new); + } + private static HostFunction[] getHostFunctions( HostFunction[] kernelFuncs, HostFunction[] hostFunctions, HostFunction[] wasiHostFunctions) { // concat list of host functions - var hostFuncList = new HostFunction[hostFunctions.length + kernelFuncs.length + wasiHostFunctions.length]; + var hostFuncList = new HostFunction[ kernelFuncs.length + hostFunctions.length + wasiHostFunctions.length]; System.arraycopy(kernelFuncs, 0, hostFuncList, 0, kernelFuncs.length); System.arraycopy(hostFunctions, 0, hostFuncList, kernelFuncs.length, hostFunctions.length); System.arraycopy(wasiHostFunctions, 0, hostFuncList, kernelFuncs.length + hostFunctions.length, wasiHostFunctions.length); @@ -83,10 +110,10 @@ private static HostFunction[] getHostFunctions( public byte[] call(String funcName, byte[] input) { var func = instance.export(funcName); - kernel.setInput(input); + hostEnv.setInput(input); var result = func.apply()[0].asInt(); if (result == 0) { - return kernel.getOutput(); + return hostEnv.getOutput(); } else { throw new ExtismException("Failed"); } diff --git a/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java b/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java new file mode 100644 index 0000000..ab92613 --- /dev/null +++ b/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java @@ -0,0 +1,31 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.SystemLogger; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.types.Value; +import junit.framework.TestCase; + +import java.util.List; + +public class ExtismHostFunctionTest extends TestCase { + public void testFunction() { + var f = ExtismHostFunction.of("myfunc", List.of(), List.of(), + (CurrentPlugin p, Value... args) -> { + p.log().log(LogLevel.INFO, "hello world"); + return null; + }); + + var manifest = Manifest.ofWasms( + ManifestWasm.fromUrl( + "https://github.com/extism/plugins/releases/download/v1.1.0/greet.wasm") + .build()).build(); + var plugin = Plugin.ofManifest(manifest).withLogger(new SystemLogger()).build(); + + + HostFunction hostFunction = f.toHostFunction(new CurrentPlugin(plugin)); + Instance instance = null; + Value[] args = null; + hostFunction.handle().apply(instance, args); + } +} diff --git a/src/test/java/org/extism/chicory/sdk/HostEnvTest.java b/src/test/java/org/extism/chicory/sdk/HostEnvTest.java new file mode 100644 index 0000000..6cb0f5c --- /dev/null +++ b/src/test/java/org/extism/chicory/sdk/HostEnvTest.java @@ -0,0 +1,28 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.SystemLogger; +import junit.framework.TestCase; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class HostEnvTest extends TestCase { + public void testShowcase() { + var logger = new SystemLogger(); + + var config = Map.of("key", "value"); + var hostEnv = new HostEnv(new Kernel(logger), config, logger); + + assertEquals(hostEnv.config().get("key"), "value"); + + byte[] bytes = "extism".getBytes(StandardCharsets.UTF_8); + hostEnv.var().set("test", bytes); + assertSame(hostEnv.var().get("test"), bytes); + + hostEnv.log().log(LogLevel.INFO, "hello world"); + + int size = 100; + long ptr = hostEnv.memory().alloc(size); + assertEquals(hostEnv.memory().length(ptr), size); + } +}