From 967a52a96390a89e7c0f695e4d8f2fb6501d22dd Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Wed, 16 Oct 2024 15:48:20 +0200 Subject: [PATCH] Backport Linker API (#10) Signed-off-by: Edoardo Vacchi --- pom.xml | 8 +- .../org/extism/chicory/sdk/ChicoryModule.java | 43 +++ .../extism/chicory/sdk/DependencyGraph.java | 351 ++++++++++++++++++ .../chicory/sdk/ExtismHostFunction.java | 14 +- .../java/org/extism/chicory/sdk/Kernel.java | 104 ++---- .../java/org/extism/chicory/sdk/Linker.java | 73 ++++ .../java/org/extism/chicory/sdk/Manifest.java | 7 + .../chicory/sdk/ManifestModuleMapper.java | 53 --- .../java/org/extism/chicory/sdk/Plugin.java | 80 +--- .../java/org/extism/chicory/sdk/Store.java | 173 +++++++++ .../chicory/sdk/DependencyGraphTest.java | 109 ++++++ .../chicory/sdk/ExtismHostFunctionTest.java | 3 +- .../org/extism/chicory/sdk/HostEnvTest.java | 2 +- .../org/extism/chicory/sdk/PluginTest.java | 1 + .../circular-import-add.wasm | Bin 0 -> 76 bytes .../circular-import-add.wat | 15 + .../circular-import-expr.wasm | Bin 0 -> 74 bytes .../circular-import-expr.wat | 14 + .../circular-import-main.wasm | Bin 0 -> 62 bytes .../circular-import-main.wat | 8 + .../circular-import-sub.wasm | Bin 0 -> 41 bytes .../circular-import-sub.wat | 9 + .../circular-import/circular-import-1.wasm | Bin 0 -> 78 bytes .../circular-import/circular-import-1.wat | 14 + .../circular-import/circular-import-2.wasm | Bin 0 -> 92 bytes .../circular-import/circular-import-2.wat | 22 ++ .../circular-import/circular-import-main.wasm | Bin 0 -> 64 bytes .../circular-import/circular-import-main.wat | 8 + .../resources/host-functions/import-wasi.wasm | Bin 0 -> 96 bytes .../resources/host-functions/import-wasi.wat | 7 + 30 files changed, 914 insertions(+), 204 deletions(-) create mode 100644 src/main/java/org/extism/chicory/sdk/ChicoryModule.java create mode 100644 src/main/java/org/extism/chicory/sdk/DependencyGraph.java create mode 100644 src/main/java/org/extism/chicory/sdk/Linker.java delete mode 100644 src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java create mode 100644 src/main/java/org/extism/chicory/sdk/Store.java create mode 100644 src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java create mode 100644 src/test/resources/circular-import-more/circular-import-add.wasm create mode 100644 src/test/resources/circular-import-more/circular-import-add.wat create mode 100644 src/test/resources/circular-import-more/circular-import-expr.wasm create mode 100644 src/test/resources/circular-import-more/circular-import-expr.wat create mode 100644 src/test/resources/circular-import-more/circular-import-main.wasm create mode 100644 src/test/resources/circular-import-more/circular-import-main.wat create mode 100644 src/test/resources/circular-import-more/circular-import-sub.wasm create mode 100644 src/test/resources/circular-import-more/circular-import-sub.wat create mode 100644 src/test/resources/circular-import/circular-import-1.wasm create mode 100644 src/test/resources/circular-import/circular-import-1.wat create mode 100644 src/test/resources/circular-import/circular-import-2.wasm create mode 100644 src/test/resources/circular-import/circular-import-2.wat create mode 100644 src/test/resources/circular-import/circular-import-main.wasm create mode 100644 src/test/resources/circular-import/circular-import-main.wat create mode 100644 src/test/resources/host-functions/import-wasi.wasm create mode 100644 src/test/resources/host-functions/import-wasi.wat diff --git a/pom.xml b/pom.xml index 3542f94..a680e25 100644 --- a/pom.xml +++ b/pom.xml @@ -18,23 +18,25 @@ 11 11 11 + + 0.0.12 com.dylibso.chicory runtime - 0.0.12 + ${chicory.version} com.dylibso.chicory wasi - 0.0.12 + ${chicory.version} com.dylibso.chicory aot - 0.0.12 + ${chicory.version} junit diff --git a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java new file mode 100644 index 0000000..73b5b00 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -0,0 +1,43 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.aot.AotMachine; +import com.dylibso.chicory.runtime.Module; + +import java.nio.file.Path; + +class ChicoryModule { + + static final boolean IS_NATIVE_IMAGE_AOT = Boolean.getBoolean("com.oracle.graalvm.isaot"); + + static Module fromWasm(ManifestWasm m) { + if (m instanceof ManifestWasmBytes) { + ManifestWasmBytes mwb = (ManifestWasmBytes) m; + return Module.builder(mwb.bytes).build(); + } else if (m instanceof ManifestWasmPath) { + ManifestWasmPath mwp = (ManifestWasmPath) m; + return Module.builder(Path.of(mwp.path)).build(); + } else if (m instanceof ManifestWasmFile) { + ManifestWasmFile mwf = (ManifestWasmFile) m; + return Module.builder(mwf.filePath).build(); + } else if (m instanceof ManifestWasmUrl) { + ManifestWasmUrl mwu = (ManifestWasmUrl) m; + return Module.builder(mwu.getUrlAsStream()).build(); + } else { + throw new IllegalArgumentException("Unknown ManifestWasm type " + m.getClass()); + } + } + + static Module.Builder instanceWithOptions(Module.Builder m, Manifest.Options opts) { + if (opts == null) { + return m; + } + // This feature is not compatibly with the native-image builder. + if (opts.aot && !IS_NATIVE_IMAGE_AOT) { + m.withMachineFactory(AotMachine::new); + } + if (!opts.validationFlags.isEmpty()) { + throw new UnsupportedOperationException("Validation flags are not supported yet"); + } + return m; + } +} diff --git a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java new file mode 100644 index 0000000..75b0376 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -0,0 +1,351 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.Logger; +import com.dylibso.chicory.runtime.ExportFunction; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.HostImports; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.WasmFunctionHandle; +import com.dylibso.chicory.runtime.Module; +import com.dylibso.chicory.wasm.types.Export; +import com.dylibso.chicory.wasm.types.ExportSection; +import com.dylibso.chicory.wasm.types.ExternalType; +import com.dylibso.chicory.wasm.types.FunctionImport; +import com.dylibso.chicory.wasm.types.FunctionType; +import com.dylibso.chicory.wasm.types.Import; +import com.dylibso.chicory.wasm.types.ImportSection; +import com.dylibso.chicory.wasm.types.Value; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.Stack; + +import static java.util.stream.Collectors.groupingBy; + +class DependencyGraph { + public static final String MAIN_MODULE_NAME = "main"; + + private final Logger logger; + + private final Map> registeredSymbols = new HashMap<>(); + private final Map modules = new HashMap<>(); + private final Set hostModules = new HashSet<>(); + private final Map instances = new HashMap<>(); + private final Map trampolines = new HashMap<>(); + + private final Store store = new Store(); + private Manifest.Options options; + + public DependencyGraph(Logger logger) { + this.logger = logger; + } + + /** + * Set the instantiation options. + */ + public void setOptions(Manifest.Options options) { + this.options = options; + } + + /** + * Registers all the given named modules, and tries to look for a `main`. + *

+ * Try to find the main module: + * - There is always one main module + * - If a Wasm value has the Name field set to "main" then use that module + * - If there is only one module in the manifest then that is the main module by default + * - Otherwise the last module listed is the main module + */ + public void registerModules(ManifestWasm... wasms) { + for (int i = 0; i < wasms.length; i++) { + ManifestWasm wasm = wasms[i]; + boolean isLast = i == wasms.length - 1; + String moduleName = wasm.name; + var mb = ChicoryModule.fromWasm(wasm); + + if ((moduleName == null || moduleName.isEmpty() || isLast) + && !this.modules.containsKey(MAIN_MODULE_NAME)) { + moduleName = MAIN_MODULE_NAME; + } + + // TODO: checkHash(moduleName, wasm); + registerModule(moduleName, mb); + + } + } + + private void checkCollision(String moduleName, String symbol) { + if (symbol == null && this.registeredSymbols.containsKey(moduleName)) { + throw new ExtismException("Collision detected: a module with the given name already exists: " + moduleName); + } else if (this.registeredSymbols.containsKey(moduleName) && this.registeredSymbols.get(moduleName).contains(symbol)) { + throw new ExtismException("Collision detected: a symbol with the given name already exists: " + moduleName + "." + symbol); + } + } + + /** + * Register a Module with the given name. + */ + public void registerModule(String name, Module m) { + checkCollision(name, null); + + ExportSection exportSection = m.wasmModule().exportSection(); + for (int i = 0; i < exportSection.exportCount(); i++) { + Export export = exportSection.getExport(i); + String exportName = export.name(); + this.registerSymbol(name, exportName); + } + modules.put(name, m); + } + + public void registerSymbol(String name, String symbol) { + checkCollision(name, symbol); + registeredSymbols.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); + } + + public boolean validate() { + boolean valid = true; + for (var kv : modules.entrySet()) { + Module m = kv.getValue(); + + ImportSection imports = m.wasmModule().importSection(); + for (int i = 0; i < imports.importCount(); i++) { + Import imp = imports.getImport(i); + String moduleName = imp.moduleName(); + String symbolName = imp.name(); + if (!registeredSymbols.containsKey(moduleName) || !registeredSymbols.get(moduleName).contains(symbolName)) { + logger.warnf("Cannot find symbol: %s.%s\n", moduleName, symbolName); + valid = false; + } + if (!modules.containsKey(moduleName) && !hostModules.contains(moduleName)) { + logger.warnf("Cannot find definition for the given symbol: %s.%s\n", moduleName, symbolName); + valid = false; + } + } + } + return valid; + } + + /** + * Instantiate is a breadth-first visit of the dependency graph, starting + * from the `main` module, and recursively instantiating the required dependencies. + *

+ * The method is idempotent, invoking it twice causes it to return the same instance. + * + * @return an instance of the main module. + */ + public Instance instantiate() { + Instance mainInstance = this.getMainInstance(); + if (mainInstance != null) { + return mainInstance; + } + + if (!validate()) { + throw new ExtismException("Unresolved symbols"); + } + + Stack unresolved = new Stack<>(); + unresolved.push(MAIN_MODULE_NAME); + + while (!unresolved.isEmpty()) { + String moduleId = unresolved.peek(); + Module m = this.modules.get(moduleId); + boolean satisfied = true; + List trampolines = new ArrayList<>(); + ImportSection imports = m.wasmModule().importSection(); + // We assume that each unique `name` in an import of the form `name.symbol` + // is registered as a module with that name + // + // FIXME: this is actually a strong assumption, because we could + // define "overrides" by overwriting individual `name.symbol` in our table. + var requiredModules = imports.stream().collect(groupingBy(Import::moduleName)); + + if (!requiredModules.isEmpty()) { + // We need to check whether the given import is available + for (String requiredModule : requiredModules.keySet()) { + if (unresolved.contains(requiredModule)) { + // This is a cycle! + var moduleImports = requiredModules.get(requiredModule); + for (Import mi : moduleImports) { + if (mi.importType() == ExternalType.FUNCTION) { + // It's ok, we just add one little indirection. + // This will be resolved at the end, when everything is settled. + trampolines.add(registerTrampoline((FunctionImport) mi, m)); + } else { + throw new ExtismException("cycle detected on a non-function"); + } + } + } else if (!this.instances.containsKey(requiredModule) && !this.hostModules.contains(requiredModule)) { + // No such instance nor registered host function; we schedule this module for visiting. + satisfied = false; + unresolved.push(requiredModule); + } + } + } + + // The store already contains everything we need, + // we can proceed with pop the name from the stack + // and instantiate. + if (satisfied) { + unresolved.pop(); + instantiate(moduleId, trampolines); + } + } + + // We are now ready to resolve all the trampolines. + for (var t : trampolines.entrySet()) { + QualifiedName name = t.getKey(); + Trampoline trampoline = t.getValue(); + + ExportFunction ef = instances.get(name.moduleName).export(name.fieldName); + trampoline.resolveFunction(ef); + } + + // We can now initialize all modules. + for (var inst : this.instances.values()) { + inst.initialize(true); + } + + return this.getMainInstance(); + } + + private Instance instantiate(String moduleId, List moreHostFunctions) { + Module m = this.modules.get(moduleId); + Objects.requireNonNull(m); + + HostImports extendedHostImports = + mergeHostImports(store.toHostImports(), moreHostFunctions); + + Instance instance = + ChicoryModule.instanceWithOptions( + Module.builder(m.wasmModule()), this.options) + .withHostImports(extendedHostImports) + .withStart(false) + .build().instantiate(); + this.store.register(moduleId, instance); + this.instances.put(moduleId, instance); + return instance; + } + + private HostImports mergeHostImports(HostImports hostImports, List trampolines) { + HostFunction[] hostFunctions = hostImports.functions(); + List mergedList = new ArrayList<>(trampolines); + for (HostFunction fn : hostFunctions) { + for (HostFunction t : trampolines) { + if (t.moduleName().equals(fn.fieldName()) && t.fieldName().equals(fn.fieldName())) { + // If one such case exists, the "proper" function takes precedence over the trampoline. + mergedList.remove(t); + } + } + mergedList.add(fn); + } + return new HostImports( + mergedList.toArray(new HostFunction[mergedList.size()]), + hostImports.globals(), + hostImports.memories(), + hostImports.tables()); + } + + private HostFunction registerTrampoline(FunctionImport f, Module m) { + // Trampolines are singletons for each pair. + // Trampolines are not registered into the store, as they are not "real" functions. + // They are instead kept separately and passed explicitly to the instance. + Trampoline trampoline = this.trampolines.computeIfAbsent( + new QualifiedName(f.moduleName(), f.name()), k -> new Trampoline()); + var functionType = m.wasmModule().typeSection().getType(f.typeIndex()); + return trampoline.asHostFunction(f.moduleName(), f.name(), functionType); + } + + /** + * Register the given host functions in the store. Each host function + * has a "module name" and a symbol name, thus we register each module name + * in the "hostModules" set. + */ + public void registerFunctions(HostFunction... functions) { + store.addFunction(functions); + for (HostFunction f : functions) { + this.hostModules.add(f.moduleName()); + registerSymbol(f.moduleName(), f.fieldName()); + } + } + + /** + * @return a named instance with the given name. The method is idempotent, + * invoking it twice causes it to return the same instance. + */ + public Instance getInstance(String moduleName) { + if (instances.containsKey(moduleName)) { + return instances.get(moduleName); + } else { + return instantiate(moduleName, List.of()); + } + } + + /** + * @return the main instance. + */ + private Instance getMainInstance() { + return this.instances.get(MAIN_MODULE_NAME); + } + + static final class Trampoline implements WasmFunctionHandle { + WasmFunctionHandle f = + (Instance instance, Value... args) -> { + throw new ExtismException("Unresolved trampoline"); + }; + + public void resolveFunction(HostFunction hf) { + this.f = hf.handle(); + } + + public void resolveFunction(ExportFunction ef) { + this.f = (Instance instance, Value... args) -> ef.apply(args); + } + + @Override + public Value[] apply(Instance instance, Value... args) { + return f.apply(instance, args); + } + + public HostFunction asHostFunction(String moduleName, String name, FunctionType functionType) { + return new HostFunction(this, moduleName, name, + functionType.params(), functionType.returns()); + } + } + + /** + * A pair moduleName, symbol name. + */ + static final class QualifiedName { + final String moduleName; + final String fieldName; + + public QualifiedName(String moduleName, String fieldName) { + this.moduleName = moduleName; + this.fieldName = fieldName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof QualifiedName)) { + return false; + } + QualifiedName qualifiedName = (QualifiedName) o; + return Objects.equals(moduleName, qualifiedName.moduleName) + && Objects.equals(fieldName, qualifiedName.fieldName); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, fieldName); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java b/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java index 547a208..17dddc2 100644 --- a/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java +++ b/src/main/java/org/extism/chicory/sdk/ExtismHostFunction.java @@ -32,6 +32,7 @@ public static ExtismHostFunction of( private final Handle handle; private final List paramTypes; private final List returnTypes; + private CurrentPlugin currentPlugin; ExtismHostFunction( String module, @@ -46,9 +47,18 @@ public static ExtismHostFunction of( this.returnTypes = returnTypes; } - final HostFunction toHostFunction(CurrentPlugin currentPlugin) { + public void bind(CurrentPlugin p) { + if (currentPlugin != null) { + throw new IllegalArgumentException( + String.format("Function '%s.%s' is already bound to %s.", + module, name, currentPlugin)); + } + this.currentPlugin = p; + } + + final HostFunction asHostFunction() { return new HostFunction( - (Instance inst, Value... args) -> handle.apply(currentPlugin, args), + (Instance inst, Value... args) -> handle.apply(this.currentPlugin, args), module, name, paramTypes, returnTypes); } diff --git a/src/main/java/org/extism/chicory/sdk/Kernel.java b/src/main/java/org/extism/chicory/sdk/Kernel.java index d805041..80a2f1f 100644 --- a/src/main/java/org/extism/chicory/sdk/Kernel.java +++ b/src/main/java/org/extism/chicory/sdk/Kernel.java @@ -1,7 +1,5 @@ package org.extism.chicory.sdk; -import com.dylibso.chicory.log.Logger; -import com.dylibso.chicory.log.SystemLogger; import com.dylibso.chicory.runtime.ExportFunction; import com.dylibso.chicory.runtime.HostFunction; import com.dylibso.chicory.runtime.Instance; @@ -9,7 +7,6 @@ import com.dylibso.chicory.wasm.types.Value; import com.dylibso.chicory.wasm.types.ValueType; -import java.util.HashMap; import java.util.List; import static com.dylibso.chicory.wasm.types.Value.i64; @@ -18,39 +15,28 @@ public class Kernel { static final String IMPORT_MODULE_NAME = "extism:host/env"; final com.dylibso.chicory.runtime.Memory instanceMemory; final ExportFunction alloc; - private final ExportFunction free; + final ExportFunction free; final ExportFunction length; - private final ExportFunction lengthUnsafe; - private final ExportFunction loadU8; - private final ExportFunction loadU64; - private final ExportFunction inputLoadU8; - private final ExportFunction inputLoadU64; - private final ExportFunction storeU8; - private final ExportFunction storeU64; + final ExportFunction lengthUnsafe; + final ExportFunction loadU8; + final ExportFunction loadU64; + final ExportFunction inputLoadU8; + final ExportFunction inputLoadU64; + final ExportFunction storeU8; + final ExportFunction storeU64; final ExportFunction inputSet; - private final ExportFunction inputLen; - private final ExportFunction inputOffset; + final ExportFunction inputLen; + final ExportFunction inputOffset; final ExportFunction outputLen; final ExportFunction outputOffset; - private final ExportFunction outputSet; - private final ExportFunction reset; - private final ExportFunction errorSet; - private final ExportFunction errorGet; - private final ExportFunction memoryBytes; + final ExportFunction outputSet; + final ExportFunction reset; + final ExportFunction errorSet; + final ExportFunction errorGet; + final ExportFunction memoryBytes; public Kernel() { - this(new SystemLogger()); - } - - public Kernel(Logger logger) { - var kernelStream = getClass().getClassLoader().getResourceAsStream("extism-runtime.wasm"); - - var moduleBuilder = Module.builder(kernelStream).withLogger(logger); - - // uncomment for AOT mode - //moduleBuilder = moduleBuilder.withMachineFactory(AotMachine::new); - - Instance kernel = moduleBuilder.build().instantiate(); + Instance kernel = module().instantiate(); instanceMemory = kernel.memory(); alloc = kernel.export("alloc"); free = kernel.export("free"); @@ -72,10 +58,14 @@ public Kernel(Logger logger) { errorSet = kernel.export("error_set"); errorGet = kernel.export("error_get"); memoryBytes = kernel.export("memory_bytes"); + } + public static Module module() { + var kernelStream = Kernel.class.getClassLoader().getResourceAsStream("extism-runtime.wasm"); + return Module.builder(kernelStream).build(); } - void setInput(byte[] input) { + public void setInput(byte[] input) { var ptr = alloc.apply(i64(input.length))[0]; instanceMemory.write(ptr.asInt(), input); inputSet.apply(ptr, i64(input.length)); @@ -87,8 +77,9 @@ byte[] getOutput() { return instanceMemory.readBytes(ptr.asInt(), len.asInt()); } + HostFunction[] toHostFunctions() { - var hostFunctions = new HostFunction[23]; + var hostFunctions = new HostFunction[20]; int count = 0; hostFunctions[count++] = @@ -250,55 +241,6 @@ HostFunction[] toHostFunctions() { "memory_bytes", List.of(), List.of(ValueType.I64)); - - var vars = new HashMap(); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> { - // System.out.println("_var_get " + args); - // var keyLen = Length.apply(args[0])[0]; - // var key = memory.getString(args[0].asInt(), - // keyLen.asInt()); - // var value = vars.get(key); - return new Value[]{i64(0)}; - }, - IMPORT_MODULE_NAME, - "var_get", - List.of(ValueType.I64, ValueType.I64), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> { - // System.out.println("_var_set" + args); - // var keyLen = Length.apply(args[0])[0]; - // var key = memory.getString(args[0].asInt(), - // keyLen.asInt()); - // var value = vars.get(key); - return null; - }, - IMPORT_MODULE_NAME, - "var_set", - List.of(ValueType.I64, ValueType.I64), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> { - // System.out.println("_config_get" + args); - // var keyLen = Length.apply(args[0])[0]; - // var key = memory.getString(args[0].asInt(), - // keyLen.asInt()); - // var value = vars.get(key); - return new Value[]{i64(0)}; - }, - IMPORT_MODULE_NAME, - "config_get", - List.of(ValueType.I64), - List.of(ValueType.I64)); - return hostFunctions; } - } diff --git a/src/main/java/org/extism/chicory/sdk/Linker.java b/src/main/java/org/extism/chicory/sdk/Linker.java new file mode 100644 index 0000000..802dbcf --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -0,0 +1,73 @@ +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.wasi.WasiOptions; +import com.dylibso.chicory.wasi.WasiPreview1; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + + +/** + * Links together the modules in the given manifest with the given host functions + * and predefined support modules (e.g. the {@link Kernel}. + *

+ * Returns a {@link Plugin}. + */ +class Linker { + private final Manifest manifest; + private final ExtismHostFunction[] hostFunctions; + private final Logger logger; + + Linker(Manifest manifest, ExtismHostFunction[] hostFunctions, Logger logger) { + this.manifest = manifest; + this.hostFunctions = hostFunctions; + this.logger = logger; + } + + public Plugin link() { + + var dg = new DependencyGraph(logger); + dg.setOptions(manifest.options); + + // Register the HostEnv exports. + Map config = + manifest.options != null ? manifest.options.config : Map.of(); + var hostEnv = new HostEnv(new Kernel(), config, logger); + dg.registerFunctions(hostEnv.toHostFunctions()); + + // Register the WASI host functions. + dg.registerFunctions(new WasiPreview1(logger, + WasiOptions.builder() + .withArguments(List.of("main")) + .withStdout(System.out) + .withStderr(System.err) + .build()).toHostFunctions()); + + // Register the user-provided host functions. + dg.registerFunctions(Arrays.stream(this.hostFunctions) + .map(ExtismHostFunction::asHostFunction) + .toArray(HostFunction[]::new)); + + // Register all the modules declared in the manifest. + dg.registerModules(manifest.wasms); + + // Instantiate the main module, and, recursively, all of its dependencies. + Instance main = dg.instantiate(); + + Plugin p = new Plugin(main, hostEnv); + CurrentPlugin curr = new CurrentPlugin(p); + + // Bind all host functions to a CurrentPlugin wrapper for this Plugin. + for (ExtismHostFunction fn : this.hostFunctions) { + fn.bind(curr); + } + + return p; + } + +} + diff --git a/src/main/java/org/extism/chicory/sdk/Manifest.java b/src/main/java/org/extism/chicory/sdk/Manifest.java index 88d8153..7cd77a3 100644 --- a/src/main/java/org/extism/chicory/sdk/Manifest.java +++ b/src/main/java/org/extism/chicory/sdk/Manifest.java @@ -2,6 +2,7 @@ import java.util.EnumSet; import java.util.List; +import java.util.Map; public class Manifest { @@ -12,12 +13,18 @@ public enum Validation { public static class Options { boolean aot; EnumSet validationFlags = EnumSet.noneOf(Validation.class); + Map config; public Options withAoT() { this.aot = true; return this; } + public Options withConfig(Map config) { + this.config = config; + return this; + } + public Options withValidation(Validation... vs) { this.validationFlags.addAll(List.of(vs)); return this; diff --git a/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java b/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java deleted file mode 100644 index c729ea1..0000000 --- a/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.extism.chicory.sdk; - -import com.dylibso.chicory.aot.AotMachine; -import com.dylibso.chicory.runtime.Module; - -class ManifestModuleMapper { - private final Manifest manifest; - - ManifestModuleMapper(Manifest manifest) { - this.manifest = manifest; - } - - Module.Builder toModuleBuilder() { - if (manifest.wasms.length > 1) { - throw new UnsupportedOperationException( - "Manifests of multiple wasm files are not supported yet!"); - } - Module.Builder mb = wasmToModuleBuilder(manifest.wasms[0]); - return withOptions(mb, manifest.options); - } - - private Module.Builder wasmToModuleBuilder(ManifestWasm m) { - if (m instanceof ManifestWasmBytes) { - ManifestWasmBytes mwb = (ManifestWasmBytes) m; - return Module.builder(mwb.bytes); - } else if (m instanceof ManifestWasmPath) { - ManifestWasmPath mwp = (ManifestWasmPath) m; - return Module.builder(mwp.path); - } else if (m instanceof ManifestWasmFile) { - ManifestWasmFile mwf = (ManifestWasmFile) m; - return Module.builder(mwf.filePath); - } else if (m instanceof ManifestWasmUrl) { - ManifestWasmUrl mwu = (ManifestWasmUrl) m; - return Module.builder(mwu.getUrlAsStream()); - } else { - throw new IllegalArgumentException("Unknown ManifestWasm type " + m.getClass()); - } - } - - private Module.Builder withOptions(Module.Builder mb, Manifest.Options opts) { - if (opts == null) { - return mb; - } - if (opts.aot) { - mb.withMachineFactory(AotMachine::new); - } - if (!opts.validationFlags.isEmpty()) { - throw new UnsupportedOperationException("Validation flags are not supported yet"); - } - return mb; - } - -} diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java index 3ae5112..69ad9c7 100644 --- a/src/main/java/org/extism/chicory/sdk/Plugin.java +++ b/src/main/java/org/extism/chicory/sdk/Plugin.java @@ -3,14 +3,16 @@ import com.dylibso.chicory.log.Logger; import com.dylibso.chicory.log.SystemLogger; import com.dylibso.chicory.runtime.HostFunction; -import com.dylibso.chicory.runtime.HostImports; import com.dylibso.chicory.runtime.Instance; -import com.dylibso.chicory.wasi.WasiOptions; -import com.dylibso.chicory.wasi.WasiPreview1; - -import java.util.Arrays; -import java.util.Map; +/** + * A Plugin instance. + * + * Plugins can be instantiated using a {@link Plugin.Builder}, returned + * by {@link Plugin#ofManifest(Manifest)}. The Builder allows to set options + * on the Plugin, such as {@link HostFunction}s and the {@link Logger}. + * + */ public class Plugin { public static Builder ofManifest(Manifest manifest) { @@ -38,78 +40,31 @@ public Builder withLogger(Logger logger) { } public Plugin build() { - return new Plugin(manifest, hostFunctions, logger); + var logger = this.logger == null ? new SystemLogger() : this.logger; + Linker linker = new Linker(this.manifest, this.hostFunctions, logger); + return linker.link(); } } - private final Manifest manifest; - private final Instance instance; - private final HostImports imports; - private final Kernel kernel; + private final Instance mainInstance; private final HostEnv hostEnv; - private Plugin(Manifest manifest) { - this(manifest, new ExtismHostFunction[]{}, null); - } - - 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 - var options = WasiOptions.builder().build(); - var wasi = new WasiPreview1(logger, options); - var wasiHostFunctions = wasi.toHostFunctions(); - - var hostFuncList = getHostFunctions(hostEnv.toHostFunctions(), lower(hostFunctions), wasiHostFunctions); - this.imports = new HostImports(hostFuncList); - - var moduleBuilder = new ManifestModuleMapper(manifest) - .toModuleBuilder() - .withLogger(logger) - .withHostImports(imports); - - this.instance = moduleBuilder.build().instantiate(); + Plugin(Instance main, HostEnv hostEnv) { + this.mainInstance = main; + this.hostEnv = hostEnv; + mainInstance.initialize(true); } 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[ 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); - return hostFuncList; - } - public byte[] call(String funcName, byte[] input) { - var func = instance.export(funcName); + var func = mainInstance.export(funcName); hostEnv.setInput(input); var result = func.apply()[0].asInt(); if (result == 0) { @@ -118,5 +73,4 @@ public byte[] call(String funcName, byte[] input) { throw new ExtismException("Failed"); } } - } diff --git a/src/main/java/org/extism/chicory/sdk/Store.java b/src/main/java/org/extism/chicory/sdk/Store.java new file mode 100644 index 0000000..9ced1e7 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/Store.java @@ -0,0 +1,173 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.runtime.ExportFunction; +import com.dylibso.chicory.runtime.GlobalInstance; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.HostGlobal; +import com.dylibso.chicory.runtime.HostImports; +import com.dylibso.chicory.runtime.HostMemory; +import com.dylibso.chicory.runtime.HostTable; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.Module; +import com.dylibso.chicory.wasm.types.Export; +import com.dylibso.chicory.wasm.types.ExportSection; +import com.dylibso.chicory.wasm.types.FunctionType; +import java.util.LinkedHashMap; +import java.util.Objects; + +/** + * The runtime storage for all function, global, memory, table instances. + */ +class Store { + final LinkedHashMap functions = new LinkedHashMap<>(); + final LinkedHashMap globals = new LinkedHashMap<>(); + final LinkedHashMap memories = new LinkedHashMap<>(); + final LinkedHashMap tables = new LinkedHashMap<>(); + + public Store() {} + + /** + * Add a function to the store. + */ + public Store addFunction(HostFunction... function) { + for (var f : function) { + functions.put(new QualifiedName(f.moduleName(), f.fieldName()), f); + } + return this; + } + + /** + * Add a global to the store. + */ + public Store addGlobal(HostGlobal... global) { + for (var g : global) { + globals.put(new QualifiedName(g.moduleName(), g.fieldName()), g); + } + return this; + } + + /** + * Add a memory to the store. + */ + public Store addMemory(HostMemory... memory) { + for (var m : memory) { + memories.put(new QualifiedName(m.moduleName(), m.fieldName()), m); + } + return this; + } + + /** + * Add a table to the store. + */ + public Store addTable(HostTable... table) { + for (var t : table) { + tables.put(new QualifiedName(t.moduleName(), t.fieldName()), t); + } + return this; + } + + /** + * Add the contents of a {@link HostImports} instance to the store. + */ + public Store addHostImports(HostImports hostImports) { + return this.addGlobal(hostImports.globals()) + .addFunction(hostImports.functions()) + .addMemory(hostImports.memories()) + .addTable(hostImports.tables()); + } + + /** + * Convert the contents of a store to a {@link HostImports} instance. + */ + public HostImports toHostImports() { + return new HostImports( + functions.values().toArray(new HostFunction[0]), + globals.values().toArray(new HostGlobal[0]), + memories.values().toArray(new HostMemory[0]), + tables.values().toArray(new HostTable[0])); + } + + /** + * Register an instance in the store with the given name. + * All the exported functions, globals, memories, and tables are added to the store + * with the given name. + * + * For instance, if a module named "myModule" exports a function + * named "myFunction", the function will be added to the store with the name "myFunction.myModule". + * + */ + public Store register(String name, Instance instance) { + var exportSection = instance.module().exports().values(); + for (var export : exportSection) { + String exportName = export.name(); + switch (export.exportType()) { + case FUNCTION: + ExportFunction f = instance.export(exportName); + FunctionType ftype = instance.type(instance.functionType(export.index())); + this.addFunction( + new HostFunction( + (inst, args) -> f.apply(args), + name, + exportName, + ftype.params(), + ftype.returns())); + break; + + case TABLE: + this.addTable(new HostTable(name, exportName, instance.table(export.index()))); + break; + + case MEMORY: + this.addMemory(new HostMemory(name, exportName, instance.memory())); + break; + + case GLOBAL: + GlobalInstance g = instance.global(export.index()); + this.addGlobal(new HostGlobal(name, exportName, g)); + break; + } + } + return this; + } + + /** + * A shorthand for instantiating a module and registering it in the store. + */ + public Instance instantiate(String name, Module m) { + HostImports hostImports = toHostImports(); + Instance instance = com.dylibso.chicory.runtime.Module.builder(m).withHostImports(hostImports).build().instantiate(); + register(name, instance); + return instance; + } + + /** + * QualifiedName is internally used to use pairs (moduleName, name) as keys in the store. + */ + static class QualifiedName { + private final String moduleName; + private final String fieldName; + + public QualifiedName(String moduleName, String fieldName) { + this.moduleName = moduleName; + this.fieldName = fieldName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof QualifiedName)) { + return false; + } + QualifiedName qualifiedName = (QualifiedName) o; + return Objects.equals(moduleName, qualifiedName.moduleName) + && Objects.equals(fieldName, qualifiedName.fieldName); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, fieldName); + } + } +} diff --git a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java new file mode 100644 index 0000000..6d99d11 --- /dev/null +++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java @@ -0,0 +1,109 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.SystemLogger; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasi.WasiPreview1; +import com.dylibso.chicory.runtime.Module; +import com.dylibso.chicory.wasm.types.Value; +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.InputStream; + +public class DependencyGraphTest extends TestCase { + + public void testCircularDeps() throws IOException { + InputStream is1 = this.getClass().getResourceAsStream("/circular-import/circular-import-1.wasm"); + InputStream is2 = this.getClass().getResourceAsStream("/circular-import/circular-import-2.wasm"); + InputStream is3 = this.getClass().getResourceAsStream("/circular-import/circular-import-main.wasm"); + + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + + dg.registerModule("env-1", parse(is1)); + dg.registerModule("env-2", parse(is2)); + dg.registerModule("main", parse(is3)); + + Instance main = dg.instantiate(); + + Value[] result = main.export("real_do_expr").apply(); + assertEquals(60, result[0].asInt()); + } + + + public void testCircularDepsMore() throws IOException { + InputStream addBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-add.wasm"); + InputStream subBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-sub.wasm"); + InputStream exprBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-expr.wasm"); + InputStream mainBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-main.wasm"); + + + Module add = parse(addBytes); + Module sub = parse(subBytes); + Module expr = parse(exprBytes); + Module main = parse(mainBytes); + + { + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerModule("add", add); + dg.registerModule("sub", sub); + dg.registerModule("expr", expr); + dg.registerModule("main", main); + + Instance mainInst = dg.instantiate(); + + Value[] result = mainInst.export("real_do_expr").apply(); + assertEquals(60, result[0].asInt()); + } + + // Let's try to register them in a different order: + // it should never matter. + { + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerModule("expr", expr); + dg.registerModule("main", main); + dg.registerModule("sub", sub); + dg.registerModule("add", add); + + Instance mainInst = dg.instantiate(); + + Value[] result = mainInst.export("real_do_expr").apply(); + assertEquals(60, result[0].asInt()); + } + } + + public void testHostFunctionDeps() throws IOException { + InputStream requireWasi = this.getClass().getResourceAsStream("/host-functions/import-wasi.wasm"); + Module requireWasiM = parse(requireWasi); + + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerFunctions(wasiPreview1().toHostFunctions()); + dg.registerModule("main", requireWasiM); + + // The host functions should be found, thus the module should not be further searched in the DependencyGraph. + // If the search did not stop, it would cause an error, because there is no actual module to instantiate. + Instance mainInst = dg.instantiate(); + assertNotNull(mainInst); + } + + public void testInstantiate() throws IOException { + InputStream requireWasi = this.getClass().getResourceAsStream("/host-functions/import-wasi.wasm"); + Module requireWasiM = parse(requireWasi); + + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerFunctions(wasiPreview1().toHostFunctions()); + dg.registerModule("main", requireWasiM); + + Instance mainInst = dg.instantiate(); + Instance mainInst2 = dg.instantiate(); + assertSame("when invoked twice, instantiate() returns the same instance", mainInst, mainInst2); + } + + private Module parse(InputStream is1) throws IOException { + return Module.builder(is1.readAllBytes()).build(); + } + + private WasiPreview1 wasiPreview1() { + return new WasiPreview1(new SystemLogger()); + } + +} diff --git a/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java b/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java index ab92613..0d12e90 100644 --- a/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java +++ b/src/test/java/org/extism/chicory/sdk/ExtismHostFunctionTest.java @@ -23,7 +23,8 @@ public void testFunction() { var plugin = Plugin.ofManifest(manifest).withLogger(new SystemLogger()).build(); - HostFunction hostFunction = f.toHostFunction(new CurrentPlugin(plugin)); + HostFunction hostFunction = f.asHostFunction(); + f.bind(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 index 6cb0f5c..e12b382 100644 --- a/src/test/java/org/extism/chicory/sdk/HostEnvTest.java +++ b/src/test/java/org/extism/chicory/sdk/HostEnvTest.java @@ -11,7 +11,7 @@ public void testShowcase() { var logger = new SystemLogger(); var config = Map.of("key", "value"); - var hostEnv = new HostEnv(new Kernel(logger), config, logger); + var hostEnv = new HostEnv(new Kernel(), config, logger); assertEquals(hostEnv.config().get("key"), "value"); diff --git a/src/test/java/org/extism/chicory/sdk/PluginTest.java b/src/test/java/org/extism/chicory/sdk/PluginTest.java index a1c7133..94dbb04 100644 --- a/src/test/java/org/extism/chicory/sdk/PluginTest.java +++ b/src/test/java/org/extism/chicory/sdk/PluginTest.java @@ -33,4 +33,5 @@ public void testGreetAoT() { assertEquals("Hello, Benjamin!", result); } + } diff --git a/src/test/resources/circular-import-more/circular-import-add.wasm b/src/test/resources/circular-import-more/circular-import-add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..10c439e5a1bbd3940eb6fb4765a5be8047604b3c GIT binary patch literal 76 zcmW;BF$#b%5JkcFH-Ugb(HrDu8iveke0}w4FOe+#%1m(O&C=<8gYbhHsaB!UL?`$<{*cbk`4)Op1 literal 0 HcmV?d00001 diff --git a/src/test/resources/circular-import/circular-import-2.wat b/src/test/resources/circular-import/circular-import-2.wat new file mode 100644 index 0000000..d83361f --- /dev/null +++ b/src/test/resources/circular-import/circular-import-2.wat @@ -0,0 +1,22 @@ +(module + + (import "env-1" "expr" (func $expr (result i32))) + + (func $add (export "add") (param i32 i32) (result i32) + (i32.add + (local.get 0) + (local.get 1)) + ) + + (func $sub (export "sub") (param i32 i32) (result i32) + (i32.sub + (local.get 0) + (local.get 1)) + ) + + (func $do_expr (export "do_expr") (result i32) + call $expr + ) + + +) diff --git a/src/test/resources/circular-import/circular-import-main.wasm b/src/test/resources/circular-import/circular-import-main.wasm new file mode 100644 index 0000000000000000000000000000000000000000..3fb28c6489b581cb9a50a8761239c3bfe9adfab8 GIT binary patch literal 64 zcmZQbEY4+QU|?WmWlUgTtY;ErWKGR0(=}pG$&XL1C@5lJU}j=uU>9KIDN0SuiHC_Y Na>z(VrZ}N^LDMo8UQ0?ZwL-gu_>gZ`~}u&^`(N3o(7yQqG}ev literal 0 HcmV?d00001 diff --git a/src/test/resources/host-functions/import-wasi.wat b/src/test/resources/host-functions/import-wasi.wat new file mode 100644 index 0000000..98c5271 --- /dev/null +++ b/src/test/resources/host-functions/import-wasi.wat @@ -0,0 +1,7 @@ +(module + (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32))) + + (memory 1) + (export "memory" (memory 0)) + + (func $main (export "_start") nop)) \ No newline at end of file