From dd18544f745ec2972871da86ec8ee6516c1f5613 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Mon, 19 Aug 2024 16:36:08 +0200 Subject: [PATCH 01/12] Sketch Linker implementation. Signed-off-by: Edoardo Vacchi --- ...stModuleMapper.java => ChicoryModule.java} | 22 +-- .../java/org/extism/chicory/sdk/Kernel.java | 14 +- .../java/org/extism/chicory/sdk/Linker.java | 141 ++++++++++++++++++ .../java/org/extism/chicory/sdk/Plugin.java | 68 +++------ 4 files changed, 177 insertions(+), 68 deletions(-) rename src/main/java/org/extism/chicory/sdk/{ManifestModuleMapper.java => ChicoryModule.java} (65%) create mode 100644 src/main/java/org/extism/chicory/sdk/Linker.java diff --git a/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java similarity index 65% rename from src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java rename to src/main/java/org/extism/chicory/sdk/ChicoryModule.java index c729ea1..517b1b7 100644 --- a/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -3,23 +3,14 @@ import com.dylibso.chicory.aot.AotMachine; import com.dylibso.chicory.runtime.Module; -class ManifestModuleMapper { - private final Manifest manifest; +public class ChicoryModule { - ManifestModuleMapper(Manifest manifest) { - this.manifest = manifest; + public static Module.Builder builderFrom(ManifestWasm m, Manifest.Options opts) { + Module.Builder mb = fromWasm(m); + return withOptions(mb, opts); } - 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) { + private static Module.Builder fromWasm(ManifestWasm m) { if (m instanceof ManifestWasmBytes) { ManifestWasmBytes mwb = (ManifestWasmBytes) m; return Module.builder(mwb.bytes); @@ -37,7 +28,7 @@ private Module.Builder wasmToModuleBuilder(ManifestWasm m) { } } - private Module.Builder withOptions(Module.Builder mb, Manifest.Options opts) { + private static Module.Builder withOptions(Module.Builder mb, Manifest.Options opts) { if (opts == null) { return mb; } @@ -49,5 +40,4 @@ private Module.Builder withOptions(Module.Builder mb, Manifest.Options opts) { } return mb; } - } diff --git a/src/main/java/org/extism/chicory/sdk/Kernel.java b/src/main/java/org/extism/chicory/sdk/Kernel.java index 7bbd2b3..ffa5b6e 100644 --- a/src/main/java/org/extism/chicory/sdk/Kernel.java +++ b/src/main/java/org/extism/chicory/sdk/Kernel.java @@ -12,6 +12,7 @@ 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; @@ -39,17 +40,16 @@ public class Kernel { private final ExportFunction errorGet; private final ExportFunction memoryBytes; - public Kernel() { - this(new SystemLogger()); - } - - public Kernel(Logger logger) { + public Kernel(Logger logger, Manifest.Options opts) { var kernelStream = getClass().getClassLoader().getResourceAsStream("extism-runtime.wasm"); var moduleBuilder = Module.builder(kernelStream).withLogger(logger); - // uncomment for AOT mode - //moduleBuilder = moduleBuilder.withMachineFactory(AotMachine::new); + if (opts != null) { + if (opts.aot) { + moduleBuilder = moduleBuilder.withMachineFactory(AotMachine::new); + } + } Instance kernel = moduleBuilder.build().instantiate(); memory = kernel.memory(); 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..9067b97 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -0,0 +1,141 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.Logger; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.HostImports; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.Module; +import com.dylibso.chicory.wasi.WasiOptions; +import com.dylibso.chicory.wasi.WasiPreview1; + +/** + * 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 LinkedModules} instance, which in turn can be converted + * into a {@link Plugin}. + */ +class Linker { + public static final String EXTISM_NS = "extism:host/env"; + private final Manifest manifest; + private final HostFunction[] hostFunctions; + private final Logger logger; + + Linker(Manifest manifest, HostFunction[] hostFunctions, Logger logger) { + this.manifest = manifest; + this.hostFunctions = hostFunctions; + this.logger = logger; + } + + /* + * 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 LinkedModules link() { + WasiPreview1 wasip1 = wasip1(); + Kernel kernel = kernel(); + + HostFunction[] allHostFs = concat(kernel.toHostFunctions(), this.hostFunctions, wasip1.toHostFunctions()); + HostImports hostImports = new HostImports(allHostFs); + + + ManifestWasm[] wasms = this.manifest.wasms; + Module.Builder[] moduleBuilders = new Module.Builder[wasms.length]; + int mainModule = -1; + + for (int i = 0; i < wasms.length; i++) { + ManifestWasm wasm = wasms[i]; + boolean isLast = i == wasms.length - 1; + String moduleName = wasm.name; + Module.Builder mb = ChicoryModule.builderFrom(wasm, this.manifest.options) + .withLogger(logger) + .withHostImports(hostImports); + + if ((moduleName == null || moduleName.isEmpty() || isLast) && mainModule < 0) { + moduleName = "main"; + mainModule = i; + } + + checkCollision(moduleName, wasms); + checkHash(moduleName, wasm); + + moduleBuilders[i] = mb; + } + + return new LinkedModules(kernel, moduleBuilders, mainModule); + } + + private WasiPreview1 wasip1() { + var options = WasiOptions.builder().build(); + var wasi = new WasiPreview1(logger, options); + return wasi; + } + + private Kernel kernel() { + var kernel = new Kernel(logger, manifest.options); + return kernel; + } + + private static HostFunction[] concat( + HostFunction[] kernelFuncs, HostFunction[] hostFunctions, HostFunction[] wasiHostFunctions) { + // concat list of host functions + var hostFuncList = new HostFunction[hostFunctions.length + kernelFuncs.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; + } + + + /** + * @throws ExtismException on name collision. + */ + private void checkCollision(String moduleName, ManifestWasm[] wasms) { + // FIXME: check both host imports and modules. + if (moduleName.equals(EXTISM_NS)) { + throw new ExtismException(String.format("Module name collision: %s", moduleName)); + } + + // FIXME: check collision on already processed modules + } + + private void checkHash(String moduleName, ManifestWasm wasm) { + // FIXME: add hash check. + } + + +} + +/** + * A collection of modules that have been successfully linked together. + * LinkedModules can be converted to a Plugin using the {@link #toPlugin()} + * method. + */ +class LinkedModules { + + private final Kernel kernel; + private final Module.Builder[] moduleBuilders; + private final int mainModule; + + LinkedModules(Kernel kernel, Module.Builder[] moduleBuilders, int mainModule) { + this.kernel = kernel; + this.moduleBuilders = moduleBuilders; + this.mainModule = mainModule; + } + + public Plugin toPlugin() { + Instance[] instances = new Instance[moduleBuilders.length]; + Module.Builder[] builders = this.moduleBuilders; + for (int i = 0; i < builders.length; i++) { + Module.Builder mb = builders[i]; + Module m = mb.build(); + instances[i] = m.instantiate(); + } + + return new Plugin(kernel, instances, mainModule); + } +} diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java index 99b753c..b4d679f 100644 --- a/src/main/java/org/extism/chicory/sdk/Plugin.java +++ b/src/main/java/org/extism/chicory/sdk/Plugin.java @@ -3,12 +3,18 @@ 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; +/** + * 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) { return new Builder(manifest); } @@ -34,55 +40,27 @@ 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); + LinkedModules linked = linker.link(); + return linked.toPlugin(); } } - private final Manifest manifest; - private final Instance instance; - private final HostImports imports; private final Kernel kernel; - - private Plugin(Manifest manifest) { - this(manifest, new HostFunction[]{}, null); - } - - private Plugin(Manifest manifest, HostFunction[] hostFunctions, Logger logger) { - if (logger == null) { - logger = new SystemLogger(); - } - - this.kernel = new Kernel(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(kernel.toHostFunctions(), hostFunctions, wasiHostFunctions); - this.imports = new HostImports(hostFuncList); - - var moduleBuilder = new ManifestModuleMapper(manifest) - .toModuleBuilder() - .withLogger(logger) - .withHostImports(imports); - - this.instance = moduleBuilder.build().instantiate(); - } - - 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]; - 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; + private final Instance[] instances; + private final int mainModule; + private final Instance mainInstance; + + Plugin(Kernel kernel, Instance[] instances, int mainModule) { + this.kernel = kernel; + this.instances = instances; + this.mainModule = mainModule; + this.mainInstance = instances[mainModule]; } public byte[] call(String funcName, byte[] input) { - var func = instance.export(funcName); + var func = mainInstance.export(funcName); kernel.setInput(input); var result = func.apply()[0].asInt(); if (result == 0) { From e82f27d69379e6fbc1b7b9960aa2a58648961816 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Fri, 23 Aug 2024 18:01:30 +0200 Subject: [PATCH 02/12] Sketching a Store implementation (not finished) Signed-off-by: Edoardo Vacchi --- pom.xml | 8 +- .../org/extism/chicory/sdk/ChicoryModule.java | 50 +++- .../java/org/extism/chicory/sdk/Kernel.java | 276 +++++++----------- .../java/org/extism/chicory/sdk/Linker.java | 58 ++-- .../java/org/extism/chicory/sdk/Plugin.java | 1 + .../java/org/extism/chicory/sdk/Store.java | 117 ++++++++ 6 files changed, 288 insertions(+), 222 deletions(-) create mode 100644 src/main/java/org/extism/chicory/sdk/Store.java diff --git a/pom.xml b/pom.xml index 3542f94..e53e767 100644 --- a/pom.xml +++ b/pom.xml @@ -18,23 +18,25 @@ 11 11 11 + + 999-SNAPSHOT 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 index 517b1b7..5af2b43 100644 --- a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -1,43 +1,67 @@ package org.extism.chicory.sdk; import com.dylibso.chicory.aot.AotMachine; -import com.dylibso.chicory.runtime.Module; +import com.dylibso.chicory.runtime.FunctionSignature; +import com.dylibso.chicory.runtime.FunctionSignatureBundle; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.Module; +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.types.ExternalType; +import com.dylibso.chicory.wasm.types.FunctionType; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; public class ChicoryModule { - public static Module.Builder builderFrom(ManifestWasm m, Manifest.Options opts) { - Module.Builder mb = fromWasm(m); - return withOptions(mb, opts); + public static Instance.Builder builderFrom(ManifestWasm mw, Manifest.Options opts) { + Module m = fromWasm(mw); + return withOptions(m, opts); } - private static Module.Builder fromWasm(ManifestWasm m) { + public static Module fromWasm(ManifestWasm m) { if (m instanceof ManifestWasmBytes) { ManifestWasmBytes mwb = (ManifestWasmBytes) m; - return Module.builder(mwb.bytes); + return Parser.parse(mwb.bytes); } else if (m instanceof ManifestWasmPath) { ManifestWasmPath mwp = (ManifestWasmPath) m; - return Module.builder(mwp.path); + return Parser.parse(Path.of(mwp.path)); } else if (m instanceof ManifestWasmFile) { ManifestWasmFile mwf = (ManifestWasmFile) m; - return Module.builder(mwf.filePath); + return Parser.parse(mwf.filePath); } else if (m instanceof ManifestWasmUrl) { ManifestWasmUrl mwu = (ManifestWasmUrl) m; - return Module.builder(mwu.getUrlAsStream()); + return Parser.parse(mwu.getUrlAsStream()); } else { throw new IllegalArgumentException("Unknown ManifestWasm type " + m.getClass()); } } - private static Module.Builder withOptions(Module.Builder mb, Manifest.Options opts) { + public static FunctionSignatureBundle toSignatureBundle(String name, Module module) { + List signatures = new ArrayList<>(); + var exportSection = module.exportSection(); + for (int i = 0; i < exportSection.exportCount(); i++) { + var export = exportSection.getExport(i); + if (export.exportType() == ExternalType.FUNCTION) { + var type = module.functionSection().getFunctionType(export.index(), module.typeSection()); + signatures.add(new FunctionSignature(name, export.name(), type.params(), type.returns())); + } + } + return new FunctionSignatureBundle(name, signatures.toArray(new FunctionSignature[signatures.size()])); + } + + private static Instance.Builder withOptions(Module m, Manifest.Options opts) { + Instance.Builder builder = Instance.builder(m); if (opts == null) { - return mb; + return builder; } if (opts.aot) { - mb.withMachineFactory(AotMachine::new); + builder.withMachineFactory(AotMachine::new); } if (!opts.validationFlags.isEmpty()) { throw new UnsupportedOperationException("Validation flags are not supported yet"); } - return mb; + return builder; } } diff --git a/src/main/java/org/extism/chicory/sdk/Kernel.java b/src/main/java/org/extism/chicory/sdk/Kernel.java index ffa5b6e..87849f5 100644 --- a/src/main/java/org/extism/chicory/sdk/Kernel.java +++ b/src/main/java/org/extism/chicory/sdk/Kernel.java @@ -1,23 +1,19 @@ 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; -import com.dylibso.chicory.runtime.HostFunction; -import com.dylibso.chicory.runtime.Instance; -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.*; import com.dylibso.chicory.runtime.Memory; +import com.dylibso.chicory.wasm.Module; +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.types.*; -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"; + public static final String IMPORT_MODULE_NAME = "extism:host/env"; private final Memory memory; private final ExportFunction alloc; private final ExportFunction free; @@ -39,19 +35,21 @@ public class Kernel { private final ExportFunction errorSet; private final ExportFunction errorGet; private final ExportFunction memoryBytes; + private final Module module; public Kernel(Logger logger, Manifest.Options opts) { var kernelStream = getClass().getClassLoader().getResourceAsStream("extism-runtime.wasm"); + this.module = Parser.parse(kernelStream); - var moduleBuilder = Module.builder(kernelStream).withLogger(logger); + var instanceBuilder = Instance.builder(module); if (opts != null) { if (opts.aot) { - moduleBuilder = moduleBuilder.withMachineFactory(AotMachine::new); + instanceBuilder = instanceBuilder.withMachineFactory(AotMachine::new); } } - Instance kernel = moduleBuilder.build().instantiate(); + var kernel = instanceBuilder.build().initialize(true); memory = kernel.memory(); alloc = kernel.export("alloc"); free = kernel.export("free"); @@ -75,6 +73,10 @@ public Kernel(Logger logger, Manifest.Options opts) { memoryBytes = kernel.export("memory_bytes"); } + public Module module() { + return module; + } + public void setInput(byte[] input) { var ptr = alloc.apply(i64(input.length))[0]; memory.write(ptr.asInt(), input); @@ -87,217 +89,135 @@ public byte[] getOutput() { return memory.readBytes(ptr.asInt(), len.asInt()); } - public HostFunction[] toHostFunctions() { - var hostFunctions = new HostFunction[23]; - int count = 0; + public HostModule toHostModule() { + FunctionSection functionSection = module.functionSection(); + for (int i = 0; i < module.exportSection().exportCount(); i++) { + + Export export = module.exportSection().getExport(i); + if (export.exportType() == ExternalType.FUNCTION) { + throw new UnsupportedOperationException("todo"); + } + + FunctionType functionType = functionSection.getFunctionType(i, module.typeSection()); + } - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> alloc.apply(args), - IMPORT_MODULE_NAME, + return HostModule.builder(IMPORT_MODULE_NAME) + .withFunctionSignature( "alloc", List.of(ValueType.I64), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> free.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "free", List.of(ValueType.I64), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> length.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "length", List.of(ValueType.I64), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> lengthUnsafe.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "length_unsafe", List.of(ValueType.I64), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> loadU8.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "load_u8", List.of(ValueType.I64), - List.of(ValueType.I32)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> loadU64.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I32)) + .withFunctionSignature( "load_u64", List.of(ValueType.I64), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> inputLoadU8.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "input_load_u8", List.of(ValueType.I64), - List.of(ValueType.I32)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> inputLoadU64.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I32)) + .withFunctionSignature( "input_load_u64", List.of(ValueType.I64), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> storeU8.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "store_u8", List.of(ValueType.I64, ValueType.I32), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> storeU64.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "store_u64", List.of(ValueType.I64, ValueType.I64), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> inputSet.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "input_set", List.of(ValueType.I64, ValueType.I64), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> inputLen.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "input_length", List.of(), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> inputOffset.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "input_offset", List.of(), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> outputSet.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "output_set", List.of(ValueType.I64, ValueType.I64), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> outputLen.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "output_length", List.of(), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> outputOffset.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "output_offset", List.of(), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> reset.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "reset", List.of(), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> errorSet.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "error_set", List.of(ValueType.I64), - List.of()); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> errorGet.apply(args), - IMPORT_MODULE_NAME, + List.of()) + .withFunctionSignature( "error_get", List.of(), - List.of(ValueType.I64)); - - hostFunctions[count++] = - new HostFunction( - (Instance instance, Value... args) -> memoryBytes.apply(args), - IMPORT_MODULE_NAME, + List.of(ValueType.I64)) + .withFunctionSignature( "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, + List.of(ValueType.I64)) + .withFunctionSignature( "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, + List.of(ValueType.I64)) + .withFunctionSignature( "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, + List.of()) + .withFunctionSignature( "config_get", List.of(ValueType.I64), - List.of(ValueType.I64)); + List.of(ValueType.I64)).build(); + } + + public HostFunction[] toHostFunctions() { - return hostFunctions; + return HostModuleInstance.builder(toHostModule()) + .bind("alloc", (Instance inst, Value[] args) -> alloc.apply(args)) + .bind("free", (Instance inst, Value[] args) -> free.apply(args)) + .bind("length", (Instance inst, Value[] args) -> length.apply(args)) + .bind("length_unsafe", (Instance inst, Value[] args) -> lengthUnsafe.apply(args)) + .bind("load_u8", (Instance inst, Value[] args) -> loadU8.apply(args)) + .bind("load_u64", (Instance inst, Value[] args) -> loadU64.apply(args)) + .bind("input_load_u8", (Instance inst, Value[] args) -> inputLoadU8.apply(args)) + .bind("input_load_u64", (Instance inst, Value[] args) -> inputLoadU64.apply(args)) + .bind("store_u8", (Instance inst, Value[] args) -> storeU8.apply(args)) + .bind("store_u64", (Instance inst, Value[] args) -> storeU64.apply(args)) + .bind("input_set", (Instance inst, Value[] args) -> inputSet.apply(args)) + .bind("input_len", (Instance inst, Value[] args) -> inputLen.apply(args)) + .bind("input_offset", (Instance inst, Value[] args) -> inputOffset.apply(args)) + .bind("output_len", (Instance inst, Value[] args) -> outputLen.apply(args)) + .bind("output_offset", (Instance inst, Value[] args) -> outputOffset.apply(args)) + .bind("output_set", (Instance inst, Value[] args) -> outputSet.apply(args)) + .bind("reset", (Instance inst, Value[] args) -> reset.apply(args)) + .bind("error_set", (Instance inst, Value[] args) -> errorSet.apply(args)) + .bind("error_set", (Instance inst, Value[] args) -> errorGet.apply(args)) + .bind("memory_bytes", (Instance inst, Value[] args) -> memoryBytes.apply(args)).build().hostFunctions(); } } diff --git a/src/main/java/org/extism/chicory/sdk/Linker.java b/src/main/java/org/extism/chicory/sdk/Linker.java index 9067b97..62f3b13 100644 --- a/src/main/java/org/extism/chicory/sdk/Linker.java +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -2,11 +2,11 @@ import com.dylibso.chicory.log.Logger; import com.dylibso.chicory.runtime.HostFunction; -import com.dylibso.chicory.runtime.HostImports; +import com.dylibso.chicory.runtime.HostModule; import com.dylibso.chicory.runtime.Instance; -import com.dylibso.chicory.runtime.Module; -import com.dylibso.chicory.wasi.WasiOptions; import com.dylibso.chicory.wasi.WasiPreview1; +import com.dylibso.chicory.wasm.Module; + /** * Links together the modules in the given manifest with the given host functions @@ -36,24 +36,23 @@ class Linker { * */ public LinkedModules link() { - WasiPreview1 wasip1 = wasip1(); + var wasip1 = wasip1(); Kernel kernel = kernel(); - HostFunction[] allHostFs = concat(kernel.toHostFunctions(), this.hostFunctions, wasip1.toHostFunctions()); - HostImports hostImports = new HostImports(allHostFs); + Store store = new Store(); + + store.register(wasip1); + store.register(Kernel.IMPORT_MODULE_NAME, kernel.module()); ManifestWasm[] wasms = this.manifest.wasms; - Module.Builder[] moduleBuilders = new Module.Builder[wasms.length]; int mainModule = -1; for (int i = 0; i < wasms.length; i++) { ManifestWasm wasm = wasms[i]; boolean isLast = i == wasms.length - 1; String moduleName = wasm.name; - Module.Builder mb = ChicoryModule.builderFrom(wasm, this.manifest.options) - .withLogger(logger) - .withHostImports(hostImports); + Module m = ChicoryModule.fromWasm(wasm); if ((moduleName == null || moduleName.isEmpty() || isLast) && mainModule < 0) { moduleName = "main"; @@ -63,16 +62,16 @@ public LinkedModules link() { checkCollision(moduleName, wasms); checkHash(moduleName, wasm); - moduleBuilders[i] = mb; + store.register(moduleName, m); } - return new LinkedModules(kernel, moduleBuilders, mainModule); + store.resolve(); + + return new LinkedModules(kernel, store, mainModule); } - private WasiPreview1 wasip1() { - var options = WasiOptions.builder().build(); - var wasi = new WasiPreview1(logger, options); - return wasi; + private HostModule wasip1() { + return WasiPreview1.toHostModule(); } private Kernel kernel() { @@ -118,24 +117,27 @@ private void checkHash(String moduleName, ManifestWasm wasm) { class LinkedModules { private final Kernel kernel; - private final Module.Builder[] moduleBuilders; + private final Store store; private final int mainModule; - LinkedModules(Kernel kernel, Module.Builder[] moduleBuilders, int mainModule) { + LinkedModules(Kernel kernel, Store store, int mainModule) { this.kernel = kernel; - this.moduleBuilders = moduleBuilders; + this.store = store; this.mainModule = mainModule; } public Plugin toPlugin() { - Instance[] instances = new Instance[moduleBuilders.length]; - Module.Builder[] builders = this.moduleBuilders; - for (int i = 0; i < builders.length; i++) { - Module.Builder mb = builders[i]; - Module m = mb.build(); - instances[i] = m.instantiate(); - } - - return new Plugin(kernel, instances, mainModule); + throw new UnsupportedOperationException("todo"); + +// Instance[] instances = null; +// Instance[] instances = new Instance[moduleBuilders.length]; +// Instance.Builder[] builders = this.moduleBuilders; +// for (int i = 0; i < builders.length; i++) { +// Instance.Builder mb = builders[i]; +// Instance m = mb.build(); +// instances[i] = m.initialize(false); +// } +// +// return new Plugin(kernel, instances, mainModule); } } diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java index b4d679f..f1f3887 100644 --- a/src/main/java/org/extism/chicory/sdk/Plugin.java +++ b/src/main/java/org/extism/chicory/sdk/Plugin.java @@ -57,6 +57,7 @@ public Plugin build() { this.instances = instances; this.mainModule = mainModule; this.mainInstance = instances[mainModule]; + mainInstance.initialize(true); } public byte[] call(String funcName, byte[] input) { 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..8aedf91 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/Store.java @@ -0,0 +1,117 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.runtime.FunctionSignature; +import com.dylibso.chicory.runtime.FunctionSignatureBundle; +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.HostModule; +import com.dylibso.chicory.wasm.Module; +import com.dylibso.chicory.wasm.types.Export; +import com.dylibso.chicory.wasm.types.ExternalType; +import com.dylibso.chicory.wasm.types.Import; + +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +// TODO the store should also store the Modules. +public class Store { + private final Map> exportedFunctions = new HashMap<>(); + private final Map functionSignatures = new HashMap<>(); + private final Map> imports = new HashMap<>(); + private final Map> resolvedImports = new HashMap<>(); + + + public Store register(String name, Module module) { + var signatureBundle = ChicoryModule.toSignatureBundle(name, module); + this.register(signatureBundle); + + var importSection = module.importSection(); + for (int i = 0; i < importSection.importCount(); i++) { + Import ii = importSection.getImport(i); + if (ii.importType() == ExternalType.FUNCTION) { + this.imports.computeIfAbsent( + name, k -> new ArrayList<>()) + .add(ii); + } else { + // ignore for now + } + } + return this; + } + + + public void register(HostModule hostModule) { + this.functionSignatures.put(hostModule.name(), (FunctionSignatureBundle) hostModule); // FIXME + } + + public void resolve() { + for (var module : imports.entrySet()) { + // Name of the module importing the symbol. + String moduleName = module.getKey(); + var resolvedImports = new ArrayList(); + this.resolvedImports.put(moduleName, resolvedImports); + List imports = module.getValue(); + for (Import ii : imports) { + // Name of the module whose symbol is being imported. + String importedModuleName = ii.moduleName(); + String symbolName = ii.name(); + boolean found = lookupSymbol(exportedFunctions, importedModuleName, symbolName); + if (found) { + Logger.getAnonymousLogger().log(Level.INFO, String.format("Found HOST symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); + continue; + } + + found = lookupSymbolInBundle(functionSignatures, importedModuleName, symbolName); + if (found) { + Logger.getAnonymousLogger().log(Level.INFO, String.format("Found EXPORTED symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); + continue; + } + + Logger.getAnonymousLogger().log(Level.WARNING, String.format("NOT FOUND: symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); + } + } + } + + private boolean lookupSymbol(Map> nss, String importedModuleName, String symbolName) { + if (nss.containsKey(importedModuleName)) { + var ns = nss.get(importedModuleName); + return ns.lookup(symbolName).isPresent(); + } + return false; + } + + private boolean lookupSymbolInBundle(Map nss, String importedModuleName, String symbolName) { + if (nss.containsKey(importedModuleName)) { + var ns = nss.get(importedModuleName); + for (FunctionSignature signature : ns.signatures()) { + if (signature.name().equals(symbolName)) { + return true; + } + } + return false; + } + return false; + } + +} + + +class Namespace { + private final String name; + private final Map items; + + Namespace(String name) { + this.name = name; + this.items = new HashMap<>(); + } + + public Namespace register(String itemId, T value) { + items.put(itemId, value); + return this; + } + + public Optional lookup(String itemId) { + return Optional.ofNullable(items.get(itemId)); + } + +} \ No newline at end of file From 98b3e0812929712b78c055c0cc2e9049e02ecc91 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Sat, 24 Aug 2024 17:39:47 +0200 Subject: [PATCH 03/12] Working Store / Linker (PoC, unfinished) Signed-off-by: Edoardo Vacchi --- .../org/extism/chicory/sdk/ChicoryModule.java | 12 +- .../java/org/extism/chicory/sdk/Kernel.java | 152 +----------------- .../java/org/extism/chicory/sdk/Linker.java | 57 +------ .../java/org/extism/chicory/sdk/Plugin.java | 17 +- .../java/org/extism/chicory/sdk/Store.java | 138 ++++++++-------- .../org/extism/chicory/sdk/TInstance.java | 44 +++++ .../java/org/extism/chicory/sdk/TModule.java | 48 ++++++ 7 files changed, 191 insertions(+), 277 deletions(-) create mode 100644 src/main/java/org/extism/chicory/sdk/TInstance.java create mode 100644 src/main/java/org/extism/chicory/sdk/TModule.java diff --git a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java index 5af2b43..df3c135 100644 --- a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -1,13 +1,10 @@ package org.extism.chicory.sdk; import com.dylibso.chicory.aot.AotMachine; -import com.dylibso.chicory.runtime.FunctionSignature; -import com.dylibso.chicory.runtime.FunctionSignatureBundle; -import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.*; import com.dylibso.chicory.wasm.Module; import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.types.ExternalType; -import com.dylibso.chicory.wasm.types.FunctionType; import java.nio.file.Path; import java.util.ArrayList; @@ -51,6 +48,13 @@ public static FunctionSignatureBundle toSignatureBundle(String name, Module modu return new FunctionSignatureBundle(name, signatures.toArray(new FunctionSignature[signatures.size()])); } + public static HostFunction bind(FunctionSignature fsig, WasmFunctionHandle handle) { + return new HostFunction(handle, fsig.moduleName(), fsig.name(), fsig.paramTypes(), fsig.returnTypes()); + } + + public static WasmFunctionHandle asHandle(ExportFunction ef) { + return (inst, args) -> ef.apply(args); + } private static Instance.Builder withOptions(Module m, Manifest.Options opts) { Instance.Builder builder = Instance.builder(m); if (opts == null) { diff --git a/src/main/java/org/extism/chicory/sdk/Kernel.java b/src/main/java/org/extism/chicory/sdk/Kernel.java index 87849f5..b75339e 100644 --- a/src/main/java/org/extism/chicory/sdk/Kernel.java +++ b/src/main/java/org/extism/chicory/sdk/Kernel.java @@ -35,21 +35,8 @@ public class Kernel { private final ExportFunction errorSet; private final ExportFunction errorGet; private final ExportFunction memoryBytes; - private final Module module; - public Kernel(Logger logger, Manifest.Options opts) { - var kernelStream = getClass().getClassLoader().getResourceAsStream("extism-runtime.wasm"); - this.module = Parser.parse(kernelStream); - - var instanceBuilder = Instance.builder(module); - - if (opts != null) { - if (opts.aot) { - instanceBuilder = instanceBuilder.withMachineFactory(AotMachine::new); - } - } - - var kernel = instanceBuilder.build().initialize(true); + public Kernel(Instance kernel) { memory = kernel.memory(); alloc = kernel.export("alloc"); free = kernel.export("free"); @@ -73,8 +60,9 @@ public Kernel(Logger logger, Manifest.Options opts) { memoryBytes = kernel.export("memory_bytes"); } - public Module module() { - return module; + public static Module module() { + var kernelStream = Kernel.class.getClassLoader().getResourceAsStream("extism-runtime.wasm"); + return Parser.parse(kernelStream); } public void setInput(byte[] input) { @@ -88,136 +76,4 @@ public byte[] getOutput() { var len = outputLen.apply()[0]; return memory.readBytes(ptr.asInt(), len.asInt()); } - - public HostModule toHostModule() { - FunctionSection functionSection = module.functionSection(); - for (int i = 0; i < module.exportSection().exportCount(); i++) { - - Export export = module.exportSection().getExport(i); - if (export.exportType() == ExternalType.FUNCTION) { - throw new UnsupportedOperationException("todo"); - } - - FunctionType functionType = functionSection.getFunctionType(i, module.typeSection()); - } - - return HostModule.builder(IMPORT_MODULE_NAME) - .withFunctionSignature( - "alloc", - List.of(ValueType.I64), - List.of(ValueType.I64)) - .withFunctionSignature( - "free", - List.of(ValueType.I64), - List.of()) - .withFunctionSignature( - "length", - List.of(ValueType.I64), - List.of(ValueType.I64)) - .withFunctionSignature( - "length_unsafe", - List.of(ValueType.I64), - List.of(ValueType.I64)) - .withFunctionSignature( - "load_u8", - List.of(ValueType.I64), - List.of(ValueType.I32)) - .withFunctionSignature( - "load_u64", - List.of(ValueType.I64), - List.of(ValueType.I64)) - .withFunctionSignature( - "input_load_u8", - List.of(ValueType.I64), - List.of(ValueType.I32)) - .withFunctionSignature( - "input_load_u64", - List.of(ValueType.I64), - List.of(ValueType.I64)) - .withFunctionSignature( - "store_u8", - List.of(ValueType.I64, ValueType.I32), - List.of()) - .withFunctionSignature( - "store_u64", - List.of(ValueType.I64, ValueType.I64), - List.of()) - .withFunctionSignature( - "input_set", - List.of(ValueType.I64, ValueType.I64), - List.of()) - .withFunctionSignature( - "input_length", - List.of(), - List.of(ValueType.I64)) - .withFunctionSignature( - "input_offset", - List.of(), - List.of(ValueType.I64)) - .withFunctionSignature( - "output_set", - List.of(ValueType.I64, ValueType.I64), - List.of()) - .withFunctionSignature( - "output_length", - List.of(), - List.of(ValueType.I64)) - .withFunctionSignature( - "output_offset", - List.of(), - List.of(ValueType.I64)) - .withFunctionSignature( - "reset", - List.of(), - List.of()) - .withFunctionSignature( - "error_set", - List.of(ValueType.I64), - List.of()) - .withFunctionSignature( - "error_get", - List.of(), - List.of(ValueType.I64)) - .withFunctionSignature( - "memory_bytes", - List.of(), - List.of(ValueType.I64)) - .withFunctionSignature( - "var_get", - List.of(ValueType.I64, ValueType.I64), - List.of(ValueType.I64)) - .withFunctionSignature( - "var_set", - List.of(ValueType.I64, ValueType.I64), - List.of()) - .withFunctionSignature( - "config_get", - List.of(ValueType.I64), - List.of(ValueType.I64)).build(); - } - - public HostFunction[] toHostFunctions() { - - return HostModuleInstance.builder(toHostModule()) - .bind("alloc", (Instance inst, Value[] args) -> alloc.apply(args)) - .bind("free", (Instance inst, Value[] args) -> free.apply(args)) - .bind("length", (Instance inst, Value[] args) -> length.apply(args)) - .bind("length_unsafe", (Instance inst, Value[] args) -> lengthUnsafe.apply(args)) - .bind("load_u8", (Instance inst, Value[] args) -> loadU8.apply(args)) - .bind("load_u64", (Instance inst, Value[] args) -> loadU64.apply(args)) - .bind("input_load_u8", (Instance inst, Value[] args) -> inputLoadU8.apply(args)) - .bind("input_load_u64", (Instance inst, Value[] args) -> inputLoadU64.apply(args)) - .bind("store_u8", (Instance inst, Value[] args) -> storeU8.apply(args)) - .bind("store_u64", (Instance inst, Value[] args) -> storeU64.apply(args)) - .bind("input_set", (Instance inst, Value[] args) -> inputSet.apply(args)) - .bind("input_len", (Instance inst, Value[] args) -> inputLen.apply(args)) - .bind("input_offset", (Instance inst, Value[] args) -> inputOffset.apply(args)) - .bind("output_len", (Instance inst, Value[] args) -> outputLen.apply(args)) - .bind("output_offset", (Instance inst, Value[] args) -> outputOffset.apply(args)) - .bind("output_set", (Instance inst, Value[] args) -> outputSet.apply(args)) - .bind("reset", (Instance inst, Value[] args) -> reset.apply(args)) - .bind("error_set", (Instance inst, Value[] args) -> errorSet.apply(args)) - .bind("error_set", (Instance inst, Value[] args) -> errorGet.apply(args)) - .bind("memory_bytes", (Instance inst, Value[] args) -> memoryBytes.apply(args)).build().hostFunctions(); - } } diff --git a/src/main/java/org/extism/chicory/sdk/Linker.java b/src/main/java/org/extism/chicory/sdk/Linker.java index 62f3b13..d49922e 100644 --- a/src/main/java/org/extism/chicory/sdk/Linker.java +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -2,7 +2,6 @@ import com.dylibso.chicory.log.Logger; import com.dylibso.chicory.runtime.HostFunction; -import com.dylibso.chicory.runtime.HostModule; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.wasi.WasiPreview1; import com.dylibso.chicory.wasm.Module; @@ -12,8 +11,7 @@ * 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 LinkedModules} instance, which in turn can be converted - * into a {@link Plugin}. + * Returns a {@link Plugin}. */ class Linker { public static final String EXTISM_NS = "extism:host/env"; @@ -35,15 +33,14 @@ class Linker { * - Otherwise the last module listed is the main module * */ - public LinkedModules link() { - var wasip1 = wasip1(); - Kernel kernel = kernel(); + public Plugin link() { + var wasip1 = WasiPreview1.toHostModule(); + var kernel = Kernel.module(); Store store = new Store(); store.register(wasip1); - store.register(Kernel.IMPORT_MODULE_NAME, kernel.module()); - + store.register(Kernel.IMPORT_MODULE_NAME, kernel); ManifestWasm[] wasms = this.manifest.wasms; int mainModule = -1; @@ -67,16 +64,10 @@ public LinkedModules link() { store.resolve(); - return new LinkedModules(kernel, store, mainModule); - } - - private HostModule wasip1() { - return WasiPreview1.toHostModule(); - } + Instance kernelInstance = store.instantiate(Kernel.IMPORT_MODULE_NAME); + Instance main = store.instantiate("main"); + return new Plugin(main, new Kernel(kernelInstance)); - private Kernel kernel() { - var kernel = new Kernel(logger, manifest.options); - return kernel; } private static HostFunction[] concat( @@ -109,35 +100,3 @@ private void checkHash(String moduleName, ManifestWasm wasm) { } -/** - * A collection of modules that have been successfully linked together. - * LinkedModules can be converted to a Plugin using the {@link #toPlugin()} - * method. - */ -class LinkedModules { - - private final Kernel kernel; - private final Store store; - private final int mainModule; - - LinkedModules(Kernel kernel, Store store, int mainModule) { - this.kernel = kernel; - this.store = store; - this.mainModule = mainModule; - } - - public Plugin toPlugin() { - throw new UnsupportedOperationException("todo"); - -// Instance[] instances = null; -// Instance[] instances = new Instance[moduleBuilders.length]; -// Instance.Builder[] builders = this.moduleBuilders; -// for (int i = 0; i < builders.length; i++) { -// Instance.Builder mb = builders[i]; -// Instance m = mb.build(); -// instances[i] = m.initialize(false); -// } -// -// return new Plugin(kernel, instances, mainModule); - } -} diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java index f1f3887..f358df5 100644 --- a/src/main/java/org/extism/chicory/sdk/Plugin.java +++ b/src/main/java/org/extism/chicory/sdk/Plugin.java @@ -2,8 +2,12 @@ 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; +import com.dylibso.chicory.runtime.Memory; + +import static com.dylibso.chicory.wasm.types.Value.i64; /** * A Plugin instance. @@ -42,21 +46,17 @@ public Builder withLogger(Logger logger) { public Plugin build() { var logger = this.logger == null ? new SystemLogger() : this.logger; Linker linker = new Linker(this.manifest, this.hostFunctions, logger); - LinkedModules linked = linker.link(); - return linked.toPlugin(); + return linker.link(); } } private final Kernel kernel; - private final Instance[] instances; - private final int mainModule; + private final Instance mainInstance; - Plugin(Kernel kernel, Instance[] instances, int mainModule) { + Plugin(Instance main, Kernel kernel) { this.kernel = kernel; - this.instances = instances; - this.mainModule = mainModule; - this.mainInstance = instances[mainModule]; + this.mainInstance = main; mainInstance.initialize(true); } @@ -70,5 +70,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 index 8aedf91..9ba3315 100644 --- a/src/main/java/org/extism/chicory/sdk/Store.java +++ b/src/main/java/org/extism/chicory/sdk/Store.java @@ -1,117 +1,121 @@ package org.extism.chicory.sdk; -import com.dylibso.chicory.runtime.FunctionSignature; -import com.dylibso.chicory.runtime.FunctionSignatureBundle; -import com.dylibso.chicory.runtime.HostFunction; -import com.dylibso.chicory.runtime.HostModule; +import com.dylibso.chicory.runtime.*; import com.dylibso.chicory.wasm.Module; -import com.dylibso.chicory.wasm.types.Export; -import com.dylibso.chicory.wasm.types.ExternalType; -import com.dylibso.chicory.wasm.types.Import; +import com.dylibso.chicory.wasm.types.*; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; +import static java.util.stream.Collectors.groupingBy; +import static org.extism.chicory.sdk.ChicoryModule.bind; + // TODO the store should also store the Modules. public class Store { - private final Map> exportedFunctions = new HashMap<>(); - private final Map functionSignatures = new HashMap<>(); - private final Map> imports = new HashMap<>(); + static Logger logger = Logger.getAnonymousLogger(); + // private final Map> exportedFunctions = new HashMap<>(); + private final Map modules = new HashMap<>(); + private final Map instances = new HashMap<>(); + private final Map> importedFunctions = new HashMap<>(); private final Map> resolvedImports = new HashMap<>(); public Store register(String name, Module module) { - var signatureBundle = ChicoryModule.toSignatureBundle(name, module); - this.register(signatureBundle); + // Exports are converted into a signature bundle. + this.modules.put(name, new TModule.ChicoryModule(name, module)); var importSection = module.importSection(); for (int i = 0; i < importSection.importCount(); i++) { Import ii = importSection.getImport(i); if (ii.importType() == ExternalType.FUNCTION) { - this.imports.computeIfAbsent( - name, k -> new ArrayList<>()) - .add(ii); - } else { - // ignore for now - } + FunctionImport fi = (FunctionImport) ii; + var tidx = fi.typeIndex(); + FunctionType type = module.typeSection().getType(tidx); + FunctionSignature fsig = new FunctionSignature(fi.moduleName(), fi.name(), type.params(), type.returns()); + this.importedFunctions.computeIfAbsent(name, k -> new ArrayList<>()).add(fsig); + } // else ignore for now } return this; } public void register(HostModule hostModule) { - this.functionSignatures.put(hostModule.name(), (FunctionSignatureBundle) hostModule); // FIXME + // HostModules to not declare imports. + this.modules.put(hostModule.name(), new TModule.ChicoryHostModule(hostModule)); } public void resolve() { - for (var module : imports.entrySet()) { + for (var module : importedFunctions.entrySet()) { // Name of the module importing the symbol. String moduleName = module.getKey(); var resolvedImports = new ArrayList(); this.resolvedImports.put(moduleName, resolvedImports); - List imports = module.getValue(); - for (Import ii : imports) { + var imports = module.getValue(); + for (var ii : imports) { // Name of the module whose symbol is being imported. String importedModuleName = ii.moduleName(); String symbolName = ii.name(); - boolean found = lookupSymbol(exportedFunctions, importedModuleName, symbolName); - if (found) { - Logger.getAnonymousLogger().log(Level.INFO, String.format("Found HOST symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); + TModule tModule = modules.get(importedModuleName); + if (tModule.lookup(symbolName) != null) { + logger.log(Level.INFO, String.format("Found EXPORTED symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); continue; } - - found = lookupSymbolInBundle(functionSignatures, importedModuleName, symbolName); - if (found) { - Logger.getAnonymousLogger().log(Level.INFO, String.format("Found EXPORTED symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); - continue; - } - - Logger.getAnonymousLogger().log(Level.WARNING, String.format("NOT FOUND: symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); + logger.log(Level.WARNING, String.format("NOT FOUND: symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); } } } - private boolean lookupSymbol(Map> nss, String importedModuleName, String symbolName) { - if (nss.containsKey(importedModuleName)) { - var ns = nss.get(importedModuleName); - return ns.lookup(symbolName).isPresent(); - } - return false; + public Instance instantiate(String name) { + return instantiate(modules.get(name)).asInstance(); } - private boolean lookupSymbolInBundle(Map nss, String importedModuleName, String symbolName) { - if (nss.containsKey(importedModuleName)) { - var ns = nss.get(importedModuleName); - for (FunctionSignature signature : ns.signatures()) { - if (signature.name().equals(symbolName)) { - return true; - } - } - return false; - } - return false; + public Instance instantiate(String name, Module m) { + return instantiate(new TModule.ChicoryModule(name, m)).asInstance(); } -} - - -class Namespace { - private final String name; - private final Map items; + public TInstance instantiate(TModule m) { + var importedSigs = this.importedFunctions.get(m.name()); + TInstance tInstance = instances.get(m.name()); + if (tInstance != null) { + return tInstance; + } - Namespace(String name) { - this.name = name; - this.items = new HashMap<>(); - } + // If all imports are satisfied, then we can instantiate. + if (importedSigs == null) { + if (m.isModule()) { + // No imports: trivially satisfied. + var instance = new TInstance.ChicoryModule( + m.name(), Instance.builder(m.asModule()).build().initialize(true)); + this.instances.put(m.name(), instance); + return instance; + } else { + throw new UnsupportedOperationException("Cannot instantiate host modules yet"); + } + } else { + var sigsByModule = importedSigs.stream().collect(groupingBy(FunctionSignature::moduleName)); + for (String mname : sigsByModule.keySet()) { + TModule tModule = this.modules.get(mname); + instantiate(tModule); + } + // All imports have been now satisfied (or they failed), we can now instantiate. + List satisfiedImports = new ArrayList<>(); + for (String mname : sigsByModule.keySet()) { + var instance = this.instances.get(mname); + for (var sig : sigsByModule.get(mname)) { + HostFunction f = bind(sig, ChicoryModule.asHandle(instance.asInstance().export(sig.name()))); + satisfiedImports.add(f); + } + } - public Namespace register(String itemId, T value) { - items.put(itemId, value); - return this; - } + Instance instance = Instance.builder(m.asModule()) + .withHostImports(new HostImports(satisfiedImports.toArray(new HostFunction[0]))).build() + .initialize(true); + TInstance.ChicoryModule tinstance = new TInstance.ChicoryModule(m.name(), instance); + this.instances.put(m.name(), tinstance); + return tinstance; + } - public Optional lookup(String itemId) { - return Optional.ofNullable(items.get(itemId)); } -} \ No newline at end of file +} diff --git a/src/main/java/org/extism/chicory/sdk/TInstance.java b/src/main/java/org/extism/chicory/sdk/TInstance.java new file mode 100644 index 0000000..d366e41 --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/TInstance.java @@ -0,0 +1,44 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.runtime.*; +import com.dylibso.chicory.wasm.Module; + +import java.util.Arrays; + +import static org.extism.chicory.sdk.ChicoryModule.*; + +public interface TInstance { + default boolean isModule() { return this instanceof TInstance.ChicoryModule; } + default Instance asInstance() { return ((ChicoryModule) this).instance; } + default boolean isHostModule() { return this instanceof TInstance.ChicoryHostModule; } + default HostModuleInstance asHostModuleInstance() { return ((ChicoryHostModule) this).instance; } + + HostFunction lookup(String symbolName); + + class ChicoryModule implements TInstance { + public final Instance instance; + public final FunctionSignatureBundle bundle; + public ChicoryModule(String name, Instance instance) { + this.instance = instance; + this.bundle = toSignatureBundle(name, instance.module()); + } + public HostFunction lookup(String symbolName) { + ExportFunction export = instance.export(symbolName); + FunctionSignature sig = Arrays.stream(bundle.signatures()) + .filter(s -> s.name().equals(symbolName)).findFirst().orElse(null); + if (sig == null) { + return null; + } + return bind(sig, asHandle(export)); + } + } + class ChicoryHostModule implements TInstance { + public final HostModuleInstance instance; + public ChicoryHostModule(HostModuleInstance instance) {this.instance = instance;} + @Override + public HostFunction lookup(String symbolName) { + return Arrays.stream(instance.hostFunctions()) + .filter(s -> s.fieldName().equals(symbolName)).findFirst().orElse(null); + } + } +} diff --git a/src/main/java/org/extism/chicory/sdk/TModule.java b/src/main/java/org/extism/chicory/sdk/TModule.java new file mode 100644 index 0000000..0b7552c --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/TModule.java @@ -0,0 +1,48 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.runtime.FunctionSignature; +import com.dylibso.chicory.runtime.FunctionSignatureBundle; +import com.dylibso.chicory.wasm.Module; +import com.dylibso.chicory.runtime.HostModule; + +import java.util.Arrays; + +public interface TModule { + default boolean isModule() { return this instanceof TModule.ChicoryModule; } + default Module asModule() { return ((ChicoryModule) this).module; } + default boolean isHostModule() { return this instanceof TModule.ChicoryHostModule; } + default HostModule asHostModule() { return ((ChicoryHostModule) this).module; } + + FunctionSignature lookup(String symbolName); + + String name(); + + class ChicoryModule implements TModule { + private final String name; + public final Module module; + public final FunctionSignatureBundle signatures; + public ChicoryModule(String name, Module module) { + this.name = name; + this.module = module; + this.signatures = org.extism.chicory.sdk.ChicoryModule.toSignatureBundle(name, module); + } + @Override + public String name() {return name;} + @Override + public FunctionSignature lookup(String symbolName) { + return Arrays.stream(signatures.signatures()).filter(s -> s.name().equals(symbolName)).findFirst().orElse(null); + } + } + class ChicoryHostModule implements TModule { + public final HostModule module; + public ChicoryHostModule(HostModule module) {this.module = module;} + @Override + public FunctionSignature lookup(String symbolName) { + return Arrays.stream(module.signatures()).filter(s -> s.name().equals(symbolName)).findFirst().orElse(null); + } + @Override + public String name() { + return module.name(); + } + } +} From 5312ce064fe3295de1996a67cbdec78c762a5173 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Thu, 5 Sep 2024 12:57:45 +0200 Subject: [PATCH 04/12] wip Signed-off-by: Edoardo Vacchi --- .../extism/chicory/sdk/DependencyGraph.java | 94 +++++++ .../java/org/extism/chicory/sdk/Kernel.java | 8 +- .../java/org/extism/chicory/sdk/Linker.java | 40 ++- .../java/org/extism/chicory/sdk/Store.java | 243 +++++++++--------- .../org/extism/chicory/sdk/TInstance.java | 44 ---- .../java/org/extism/chicory/sdk/TModule.java | 48 ---- 6 files changed, 237 insertions(+), 240 deletions(-) create mode 100644 src/main/java/org/extism/chicory/sdk/DependencyGraph.java delete mode 100644 src/main/java/org/extism/chicory/sdk/TInstance.java delete mode 100644 src/main/java/org/extism/chicory/sdk/TModule.java 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..ae7fbff --- /dev/null +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -0,0 +1,94 @@ +package org.extism.chicory.sdk; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.dylibso.chicory.runtime.HostFunction; +import com.dylibso.chicory.runtime.HostImports; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.Store; +import com.dylibso.chicory.runtime.WasmFunctionHandle; +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.Import; +import com.dylibso.chicory.wasm.types.ImportSection; +import com.dylibso.chicory.wasm.types.Value; + +public class DependencyGraph { + private String mainId; + + Map> edges = new HashMap<>(); + Map modules = new HashMap<>(); + Map instances = new HashMap<>(); + Map trampolines = new HashMap<>(); + + Store store = new Store(); + + public void setMain(String mainId) { + this.mainId = mainId; + } + + public void registerSymbol(String name, String symbol) { + edges.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); + } + + public void register(String name, Module m) { + ExportSection exportSection = m.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 boolean validate() { + boolean valid = true; + for (var kv : modules.entrySet()) { + String name = kv.getKey(); + Module m = kv.getValue(); + + ImportSection imports = m.importSection(); + for (int i = 0; i < imports.importCount(); i++) { + Import imp = imports.getImport(i); + String moduleName = imp.moduleName(); + if (!edges.containsKey(moduleName) || !edges.get(moduleName).contains(imp.name())) { + System.err.printf("Cannot find symbol: %s.%s\n", moduleName, name); + } + } + } + return valid; + } + + public void resolve() { + if (!validate()) { + throw new ExtismException("Unresolved symbols"); + } + + String moduleId = mainId; + while (true) { + Module m = modules.get(mainId); + Instance.builder(m).withHostImports() + } + } + +} + +class Trampoline implements WasmFunctionHandle { + WasmFunctionHandle f = + (Instance instance, Value... args) -> { + throw new ExtismException("Unresolved trampoline"); + }; + + public void resolveFunction(HostFunction hf) { + this.f = hf.handle(); + } + + @Override + public Value[] apply(Instance instance, Value... args) { + return f.apply(instance, args); + } +} \ No newline at end of file diff --git a/src/main/java/org/extism/chicory/sdk/Kernel.java b/src/main/java/org/extism/chicory/sdk/Kernel.java index b75339e..7379e50 100644 --- a/src/main/java/org/extism/chicory/sdk/Kernel.java +++ b/src/main/java/org/extism/chicory/sdk/Kernel.java @@ -1,14 +1,10 @@ package org.extism.chicory.sdk; -import com.dylibso.chicory.aot.AotMachine; -import com.dylibso.chicory.log.Logger; -import com.dylibso.chicory.runtime.*; +import com.dylibso.chicory.runtime.ExportFunction; +import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.Memory; import com.dylibso.chicory.wasm.Module; import com.dylibso.chicory.wasm.Parser; -import com.dylibso.chicory.wasm.types.*; - -import java.util.List; import static com.dylibso.chicory.wasm.types.Value.i64; diff --git a/src/main/java/org/extism/chicory/sdk/Linker.java b/src/main/java/org/extism/chicory/sdk/Linker.java index d49922e..113cacd 100644 --- a/src/main/java/org/extism/chicory/sdk/Linker.java +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -3,6 +3,7 @@ import com.dylibso.chicory.log.Logger; import com.dylibso.chicory.runtime.HostFunction; import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.Store; import com.dylibso.chicory.wasi.WasiPreview1; import com.dylibso.chicory.wasm.Module; @@ -34,52 +35,49 @@ class Linker { * */ public Plugin link() { - var wasip1 = WasiPreview1.toHostModule(); + var wasip1 = new WasiPreview1(logger); var kernel = Kernel.module(); Store store = new Store(); - store.register(wasip1); - store.register(Kernel.IMPORT_MODULE_NAME, kernel); + store.addFunction(wasip1.toHostFunctions()); + Instance kernelInstance = + store.instantiate(Kernel.IMPORT_MODULE_NAME, kernel); ManifestWasm[] wasms = this.manifest.wasms; - int mainModule = -1; + Instance mainModule = null; + + sortWasms(wasms); for (int i = 0; i < wasms.length; i++) { + boolean isMain = false; ManifestWasm wasm = wasms[i]; boolean isLast = i == wasms.length - 1; String moduleName = wasm.name; Module m = ChicoryModule.fromWasm(wasm); - if ((moduleName == null || moduleName.isEmpty() || isLast) && mainModule < 0) { + if ((moduleName == null || moduleName.isEmpty() || isLast) && mainModule == null) { moduleName = "main"; - mainModule = i; + isMain = true; } checkCollision(moduleName, wasms); checkHash(moduleName, wasm); - store.register(moduleName, m); + Instance instance = store.instantiate(moduleName, m); + if (isMain) { + mainModule = instance; + } } - store.resolve(); - - Instance kernelInstance = store.instantiate(Kernel.IMPORT_MODULE_NAME); - Instance main = store.instantiate("main"); - return new Plugin(main, new Kernel(kernelInstance)); + return new Plugin(mainModule, new Kernel(kernelInstance)); } - private static HostFunction[] concat( - HostFunction[] kernelFuncs, HostFunction[] hostFunctions, HostFunction[] wasiHostFunctions) { - // concat list of host functions - var hostFuncList = new HostFunction[hostFunctions.length + kernelFuncs.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; - } + private void sortWasms(ManifestWasm[] wasms) { + Store store = new Store(); + } /** * @throws ExtismException on name collision. diff --git a/src/main/java/org/extism/chicory/sdk/Store.java b/src/main/java/org/extism/chicory/sdk/Store.java index 9ba3315..5f20c28 100644 --- a/src/main/java/org/extism/chicory/sdk/Store.java +++ b/src/main/java/org/extism/chicory/sdk/Store.java @@ -1,121 +1,122 @@ -package org.extism.chicory.sdk; - -import com.dylibso.chicory.runtime.*; -import com.dylibso.chicory.wasm.Module; -import com.dylibso.chicory.wasm.types.*; - -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static java.util.stream.Collectors.groupingBy; -import static org.extism.chicory.sdk.ChicoryModule.bind; - -// TODO the store should also store the Modules. -public class Store { - static Logger logger = Logger.getAnonymousLogger(); - // private final Map> exportedFunctions = new HashMap<>(); - private final Map modules = new HashMap<>(); - private final Map instances = new HashMap<>(); - private final Map> importedFunctions = new HashMap<>(); - private final Map> resolvedImports = new HashMap<>(); - - - public Store register(String name, Module module) { - // Exports are converted into a signature bundle. - this.modules.put(name, new TModule.ChicoryModule(name, module)); - - var importSection = module.importSection(); - for (int i = 0; i < importSection.importCount(); i++) { - Import ii = importSection.getImport(i); - if (ii.importType() == ExternalType.FUNCTION) { - FunctionImport fi = (FunctionImport) ii; - var tidx = fi.typeIndex(); - FunctionType type = module.typeSection().getType(tidx); - FunctionSignature fsig = new FunctionSignature(fi.moduleName(), fi.name(), type.params(), type.returns()); - this.importedFunctions.computeIfAbsent(name, k -> new ArrayList<>()).add(fsig); - } // else ignore for now - } - return this; - } - - - public void register(HostModule hostModule) { - // HostModules to not declare imports. - this.modules.put(hostModule.name(), new TModule.ChicoryHostModule(hostModule)); - } - - public void resolve() { - for (var module : importedFunctions.entrySet()) { - // Name of the module importing the symbol. - String moduleName = module.getKey(); - var resolvedImports = new ArrayList(); - this.resolvedImports.put(moduleName, resolvedImports); - var imports = module.getValue(); - for (var ii : imports) { - // Name of the module whose symbol is being imported. - String importedModuleName = ii.moduleName(); - String symbolName = ii.name(); - TModule tModule = modules.get(importedModuleName); - if (tModule.lookup(symbolName) != null) { - logger.log(Level.INFO, String.format("Found EXPORTED symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); - continue; - } - logger.log(Level.WARNING, String.format("NOT FOUND: symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); - } - } - } - - public Instance instantiate(String name) { - return instantiate(modules.get(name)).asInstance(); - } - - public Instance instantiate(String name, Module m) { - return instantiate(new TModule.ChicoryModule(name, m)).asInstance(); - } - - public TInstance instantiate(TModule m) { - var importedSigs = this.importedFunctions.get(m.name()); - TInstance tInstance = instances.get(m.name()); - if (tInstance != null) { - return tInstance; - } - - // If all imports are satisfied, then we can instantiate. - if (importedSigs == null) { - if (m.isModule()) { - // No imports: trivially satisfied. - var instance = new TInstance.ChicoryModule( - m.name(), Instance.builder(m.asModule()).build().initialize(true)); - this.instances.put(m.name(), instance); - return instance; - } else { - throw new UnsupportedOperationException("Cannot instantiate host modules yet"); - } - } else { - var sigsByModule = importedSigs.stream().collect(groupingBy(FunctionSignature::moduleName)); - for (String mname : sigsByModule.keySet()) { - TModule tModule = this.modules.get(mname); - instantiate(tModule); - } - // All imports have been now satisfied (or they failed), we can now instantiate. - List satisfiedImports = new ArrayList<>(); - for (String mname : sigsByModule.keySet()) { - var instance = this.instances.get(mname); - for (var sig : sigsByModule.get(mname)) { - HostFunction f = bind(sig, ChicoryModule.asHandle(instance.asInstance().export(sig.name()))); - satisfiedImports.add(f); - } - } - - Instance instance = Instance.builder(m.asModule()) - .withHostImports(new HostImports(satisfiedImports.toArray(new HostFunction[0]))).build() - .initialize(true); - TInstance.ChicoryModule tinstance = new TInstance.ChicoryModule(m.name(), instance); - this.instances.put(m.name(), tinstance); - return tinstance; - } - - } - -} +//package org.extism.chicory.sdk; +// +//import com.dylibso.chicory.runtime.*; +//import com.dylibso.chicory.wasm.Module; +//import com.dylibso.chicory.wasm.types.*; +// +//import java.util.*; +//import java.util.logging.Level; +//import java.util.logging.Logger; +// +//import static java.util.stream.Collectors.groupingBy; +//import static org.extism.chicory.sdk.ChicoryModule.bind; +// +//// TODO the store should also store the Modules. +//public class Store { +// static Logger logger = Logger.getAnonymousLogger(); +// // private final Map> exportedFunctions = new HashMap<>(); +// private final Map modules = new HashMap<>(); +// private final Map instances = new HashMap<>(); +// private final Map> importedFunctions = new HashMap<>(); +//// private final Map> resolvedImports = new HashMap<>(); +// +// +// public Store register(String name, Module module) { +// // Exports are converted into a signature bundle. +// this.modules.put(name, new TModule.ChicoryModule(name, module)); +// +// var importSection = module.importSection(); +// for (int i = 0; i < importSection.importCount(); i++) { +// Import ii = importSection.getImport(i); +// if (ii.importType() == ExternalType.FUNCTION) { +// FunctionImport fi = (FunctionImport) ii; +// var tidx = fi.typeIndex(); +// FunctionType type = module.typeSection().getType(tidx); +// FunctionSignature fsig = new FunctionSignature(fi.moduleName(), fi.name(), type.params(), type.returns()); +// this.importedFunctions.computeIfAbsent(name, k -> new ArrayList<>()).add(fsig); +// } // else ignore for now +// } +// return this; +// } +// +// +// public void register(HostModule hostModule) { +// // HostModules to not declare imports. +// this.modules.put(hostModule.name(), new TModule.ChicoryHostModule(hostModule)); +// } +// +// public void resolve() { +// for (var module : importedFunctions.entrySet()) { +// // Name of the module importing the symbol. +// String moduleName = module.getKey(); +// var resolvedImports = new ArrayList(); +//// this.resolvedImports.put(moduleName, resolvedImports); +// var imports = module.getValue(); +// for (var ii : imports) { +// // Name of the module whose symbol is being imported. +// String importedModuleName = ii.moduleName(); +// String symbolName = ii.name(); +// TModule tModule = modules.get(importedModuleName); +// if (tModule.lookup(symbolName) != null) { +// logger.log(Level.INFO, String.format("Found EXPORTED symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); +// continue; +// } +// logger.log(Level.WARNING, String.format("NOT FOUND: symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); +// } +// } +// } +// +// public Instance instantiate(String name) { +// return instantiate(modules.get(name)).asInstance(); +// } +// +// public Instance instantiate(String name, Module m) { +// return instantiate(new TModule.ChicoryModule(name, m)).asInstance(); +// } +// +// public TInstance instantiate(TModule m) { +// var importedSigs = this.importedFunctions.get(m.name()); +// TInstance tInstance = instances.get(m.name()); +// if (tInstance != null) { +// return tInstance; +// } +// +// // If all imports are satisfied, then we can instantiate. +// // The empty set is trivially satisfied. +// if (importedSigs == null) { +// if (m.isModule()) { +// // No imports: trivially satisfied. +// var instance = new TInstance.ChicoryModule( +// m.name(), Instance.builder(m.asModule()).build().initialize(true)); +// this.instances.put(m.name(), instance); +// return instance; +// } else { +// throw new UnsupportedOperationException("Cannot instantiate host modules yet"); +// } +// } else { +// var sigsByModule = importedSigs.stream().collect(groupingBy(FunctionSignature::moduleName)); +// for (String mname : sigsByModule.keySet()) { +// TModule tModule = this.modules.get(mname); +// instantiate(tModule); +// } +// // All imports have been now satisfied (or they failed), we can now instantiate. +// List satisfiedImports = new ArrayList<>(); +// for (String mname : sigsByModule.keySet()) { +// var instance = this.instances.get(mname); +// for (var sig : sigsByModule.get(mname)) { +// HostFunction f = bind(sig, ChicoryModule.asHandle(instance.asInstance().export(sig.name()))); +// satisfiedImports.add(f); +// } +// } +// +// Instance instance = Instance.builder(m.asModule()) +// .withHostImports(new HostImports(satisfiedImports.toArray(new HostFunction[0]))).build() +// .initialize(true); +// TInstance.ChicoryModule tinstance = new TInstance.ChicoryModule(m.name(), instance); +// this.instances.put(m.name(), tinstance); +// return tinstance; +// } +// +// } +// +//} diff --git a/src/main/java/org/extism/chicory/sdk/TInstance.java b/src/main/java/org/extism/chicory/sdk/TInstance.java deleted file mode 100644 index d366e41..0000000 --- a/src/main/java/org/extism/chicory/sdk/TInstance.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.extism.chicory.sdk; - -import com.dylibso.chicory.runtime.*; -import com.dylibso.chicory.wasm.Module; - -import java.util.Arrays; - -import static org.extism.chicory.sdk.ChicoryModule.*; - -public interface TInstance { - default boolean isModule() { return this instanceof TInstance.ChicoryModule; } - default Instance asInstance() { return ((ChicoryModule) this).instance; } - default boolean isHostModule() { return this instanceof TInstance.ChicoryHostModule; } - default HostModuleInstance asHostModuleInstance() { return ((ChicoryHostModule) this).instance; } - - HostFunction lookup(String symbolName); - - class ChicoryModule implements TInstance { - public final Instance instance; - public final FunctionSignatureBundle bundle; - public ChicoryModule(String name, Instance instance) { - this.instance = instance; - this.bundle = toSignatureBundle(name, instance.module()); - } - public HostFunction lookup(String symbolName) { - ExportFunction export = instance.export(symbolName); - FunctionSignature sig = Arrays.stream(bundle.signatures()) - .filter(s -> s.name().equals(symbolName)).findFirst().orElse(null); - if (sig == null) { - return null; - } - return bind(sig, asHandle(export)); - } - } - class ChicoryHostModule implements TInstance { - public final HostModuleInstance instance; - public ChicoryHostModule(HostModuleInstance instance) {this.instance = instance;} - @Override - public HostFunction lookup(String symbolName) { - return Arrays.stream(instance.hostFunctions()) - .filter(s -> s.fieldName().equals(symbolName)).findFirst().orElse(null); - } - } -} diff --git a/src/main/java/org/extism/chicory/sdk/TModule.java b/src/main/java/org/extism/chicory/sdk/TModule.java deleted file mode 100644 index 0b7552c..0000000 --- a/src/main/java/org/extism/chicory/sdk/TModule.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.extism.chicory.sdk; - -import com.dylibso.chicory.runtime.FunctionSignature; -import com.dylibso.chicory.runtime.FunctionSignatureBundle; -import com.dylibso.chicory.wasm.Module; -import com.dylibso.chicory.runtime.HostModule; - -import java.util.Arrays; - -public interface TModule { - default boolean isModule() { return this instanceof TModule.ChicoryModule; } - default Module asModule() { return ((ChicoryModule) this).module; } - default boolean isHostModule() { return this instanceof TModule.ChicoryHostModule; } - default HostModule asHostModule() { return ((ChicoryHostModule) this).module; } - - FunctionSignature lookup(String symbolName); - - String name(); - - class ChicoryModule implements TModule { - private final String name; - public final Module module; - public final FunctionSignatureBundle signatures; - public ChicoryModule(String name, Module module) { - this.name = name; - this.module = module; - this.signatures = org.extism.chicory.sdk.ChicoryModule.toSignatureBundle(name, module); - } - @Override - public String name() {return name;} - @Override - public FunctionSignature lookup(String symbolName) { - return Arrays.stream(signatures.signatures()).filter(s -> s.name().equals(symbolName)).findFirst().orElse(null); - } - } - class ChicoryHostModule implements TModule { - public final HostModule module; - public ChicoryHostModule(HostModule module) {this.module = module;} - @Override - public FunctionSignature lookup(String symbolName) { - return Arrays.stream(module.signatures()).filter(s -> s.name().equals(symbolName)).findFirst().orElse(null); - } - @Override - public String name() { - return module.name(); - } - } -} From e692659c26255cd9da4d93901c6c56b3deea505e Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Thu, 5 Sep 2024 16:40:47 +0200 Subject: [PATCH 05/12] Improve over the last version by depending on the latest snapshot. The latest SNAPSHOT on main includes a `Store` facility that does not track direct dependencies between components; it does not recursively instantiate required modules, it just assumes that instantiating a given module at a given moment means that all the requirements are satisfied at that time. The missing bit to turning the Store into a linker is a layer that tracks dependencies, linearizes them into a sequence, and then instantiate them in order. We also track cycles, introducing an indirection (a trampoline) to break out of them. Signed-off-by: Edoardo Vacchi --- .../org/extism/chicory/sdk/ChicoryModule.java | 32 +-- .../extism/chicory/sdk/DependencyGraph.java | 263 +++++++++++++++--- .../java/org/extism/chicory/sdk/Linker.java | 60 +--- .../java/org/extism/chicory/sdk/Plugin.java | 4 - .../java/org/extism/chicory/sdk/Store.java | 122 -------- 5 files changed, 244 insertions(+), 237 deletions(-) delete mode 100644 src/main/java/org/extism/chicory/sdk/Store.java diff --git a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java index df3c135..8a71a9c 100644 --- a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -1,22 +1,14 @@ package org.extism.chicory.sdk; import com.dylibso.chicory.aot.AotMachine; -import com.dylibso.chicory.runtime.*; +import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.wasm.Module; import com.dylibso.chicory.wasm.Parser; -import com.dylibso.chicory.wasm.types.ExternalType; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; public class ChicoryModule { - public static Instance.Builder builderFrom(ManifestWasm mw, Manifest.Options opts) { - Module m = fromWasm(mw); - return withOptions(m, opts); - } - public static Module fromWasm(ManifestWasm m) { if (m instanceof ManifestWasmBytes) { ManifestWasmBytes mwb = (ManifestWasmBytes) m; @@ -35,27 +27,7 @@ public static Module fromWasm(ManifestWasm m) { } } - public static FunctionSignatureBundle toSignatureBundle(String name, Module module) { - List signatures = new ArrayList<>(); - var exportSection = module.exportSection(); - for (int i = 0; i < exportSection.exportCount(); i++) { - var export = exportSection.getExport(i); - if (export.exportType() == ExternalType.FUNCTION) { - var type = module.functionSection().getFunctionType(export.index(), module.typeSection()); - signatures.add(new FunctionSignature(name, export.name(), type.params(), type.returns())); - } - } - return new FunctionSignatureBundle(name, signatures.toArray(new FunctionSignature[signatures.size()])); - } - - public static HostFunction bind(FunctionSignature fsig, WasmFunctionHandle handle) { - return new HostFunction(handle, fsig.moduleName(), fsig.name(), fsig.paramTypes(), fsig.returnTypes()); - } - - public static WasmFunctionHandle asHandle(ExportFunction ef) { - return (inst, args) -> ef.apply(args); - } - private static Instance.Builder withOptions(Module m, Manifest.Options opts) { + public static Instance.Builder instanceWithOptions(Module m, Manifest.Options opts) { Instance.Builder builder = Instance.builder(m); if (opts == null) { return builder; diff --git a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java index ae7fbff..4fcdf45 100644 --- a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -1,41 +1,93 @@ package org.extism.chicory.sdk; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - +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.Store; import com.dylibso.chicory.runtime.WasmFunctionHandle; 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.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.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.Stack; + +import static java.util.stream.Collectors.groupingBy; + public class DependencyGraph { - private String mainId; + public static final String MAIN_MODULE_NAME = "main"; - Map> edges = new HashMap<>(); - Map modules = new HashMap<>(); - Map instances = new HashMap<>(); - Map trampolines = new HashMap<>(); + private final Logger logger; - Store store = new Store(); + private final Map> edges = new HashMap<>(); + private final Map modules = new HashMap<>(); + private final Map instances = new HashMap<>(); + private final Map trampolines = new HashMap<>(); - public void setMain(String mainId) { - this.mainId = mainId; + private final Store store = new Store(); + private Manifest.Options options; + + public DependencyGraph(Logger logger) { + this.logger = logger; } - public void registerSymbol(String name, String symbol) { - edges.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); + /** + * 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; + Module m = 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, m); + + } } - public void register(String name, Module m) { + private void checkCollision(String moduleName, String symbol) { + if (symbol == null && this.edges.containsKey(moduleName)) { + throw new ExtismException("Collision detected: a module with the given name already exists: " + moduleName); + } else if (this.edges.containsKey(moduleName) && this.edges.get(moduleName).contains(symbol)) { + throw new ExtismException("Collision detected: a symbol with the given name already exists: " + moduleName + "." + symbol); + } + } + + public void registerModule(String name, Module m) { + checkCollision(name, null); + ExportSection exportSection = m.exportSection(); for (int i = 0; i < exportSection.exportCount(); i++) { Export export = exportSection.getExport(i); @@ -45,50 +97,189 @@ public void register(String name, Module m) { modules.put(name, m); } + public void registerSymbol(String name, String symbol) { + checkCollision(name, symbol); + edges.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); + } + public boolean validate() { boolean valid = true; for (var kv : modules.entrySet()) { - String name = kv.getKey(); Module m = kv.getValue(); ImportSection imports = m.importSection(); for (int i = 0; i < imports.importCount(); i++) { Import imp = imports.getImport(i); String moduleName = imp.moduleName(); - if (!edges.containsKey(moduleName) || !edges.get(moduleName).contains(imp.name())) { - System.err.printf("Cannot find symbol: %s.%s\n", moduleName, name); + String symbolName = imp.name(); + if (!edges.containsKey(moduleName) || !edges.get(moduleName).contains(symbolName)) { + logger.debug(String.format("Cannot find symbol: %s.%s\n", moduleName, symbolName)); } } } return valid; } - public void resolve() { + /** + * Instantiate is a breadth-first visit of the dependency graph, starting + * from the `main` module, and recursively instantiating the required dependencies. + * + * @return an instance of the main module. + */ + public Instance instantiate() { if (!validate()) { throw new ExtismException("Unresolved symbols"); } - String moduleId = mainId; - while (true) { - Module m = modules.get(mainId); - Instance.builder(m).withHostImports() + 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; + + ImportSection imports = m.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. + registerTrampoline((FunctionImport) mi, m); + } else { + throw new ExtismException("cycle detected on a non-function"); + } + } + } else if (!this.instances.containsKey(requiredModule)) { + // No such instance, 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(); + Instance instance = + ChicoryModule.instanceWithOptions(m, this.options) + .withHostImports(store.toHostImports()) + .build(); + this.store.register(moduleId, instance); + this.instances.put(moduleId, instance); + } + } + + // 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); + } + + return this.getMainInstance(); } -} + private void registerTrampoline(FunctionImport f, Module m) { + var trampoline = new Trampoline(); + var functionType = m.typeSection().getType(f.typeIndex()); -class Trampoline implements WasmFunctionHandle { - WasmFunctionHandle f = - (Instance instance, Value... args) -> { - throw new ExtismException("Unresolved trampoline"); - }; + this.trampolines.put(new QualifiedName(f.moduleName(), f.name()), trampoline); + this.store.addFunction(trampoline.asHostFunction(f.moduleName(), f.name(), functionType)); + } - public void resolveFunction(HostFunction hf) { - this.f = hf.handle(); + /** + * Register the given host functions in the store. + */ + public void registerFunctions(HostFunction... functions) { + store.addFunction(functions); + for (HostFunction f : functions) { + registerSymbol(f.moduleName(), f.fieldName()); + } + } + + /** + * @return a named instance with the given name. + */ + public Instance getInstance(String moduleName) { + return instances.get(moduleName); } - @Override - public Value[] apply(Instance instance, Value... args) { - return f.apply(instance, args); + /** + * @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()); + } + } + + 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/Linker.java b/src/main/java/org/extism/chicory/sdk/Linker.java index 113cacd..a90a715 100644 --- a/src/main/java/org/extism/chicory/sdk/Linker.java +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -3,9 +3,7 @@ import com.dylibso.chicory.log.Logger; import com.dylibso.chicory.runtime.HostFunction; import com.dylibso.chicory.runtime.Instance; -import com.dylibso.chicory.runtime.Store; import com.dylibso.chicory.wasi.WasiPreview1; -import com.dylibso.chicory.wasm.Module; /** @@ -26,57 +24,29 @@ class Linker { this.logger = logger; } - /* - * 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 Plugin link() { - var wasip1 = new WasiPreview1(logger); - var kernel = Kernel.module(); - - Store store = new Store(); - - store.addFunction(wasip1.toHostFunctions()); - Instance kernelInstance = - store.instantiate(Kernel.IMPORT_MODULE_NAME, kernel); - - ManifestWasm[] wasms = this.manifest.wasms; - Instance mainModule = null; - sortWasms(wasms); + var dg = new DependencyGraph(logger); + dg.setOptions(manifest.options); - for (int i = 0; i < wasms.length; i++) { - boolean isMain = false; - ManifestWasm wasm = wasms[i]; - boolean isLast = i == wasms.length - 1; - String moduleName = wasm.name; - Module m = ChicoryModule.fromWasm(wasm); + // Register the Kernel module, usually not present in the manifest. + dg.registerModule(Kernel.IMPORT_MODULE_NAME, Kernel.module()); - if ((moduleName == null || moduleName.isEmpty() || isLast) && mainModule == null) { - moduleName = "main"; - isMain = true; - } + // Register the WASI host functions. + dg.registerFunctions(new WasiPreview1(logger).toHostFunctions()); - checkCollision(moduleName, wasms); - checkHash(moduleName, wasm); + // Register the user-provided host functions. + dg.registerFunctions(this.hostFunctions); - Instance instance = store.instantiate(moduleName, m); - if (isMain) { - mainModule = instance; - } - } - - return new Plugin(mainModule, new Kernel(kernelInstance)); - - } + // Register all the modules declared in the manifest. + dg.registerModules(manifest.wasms); - private void sortWasms(ManifestWasm[] wasms) { - Store store = new Store(); + // Instantiate the main module, and, recursively, all of its dependencies. + Instance main = dg.instantiate(); + // The kernel has been now instantiated, get a handle for it. + Instance kernelInstance = dg.getInstance(Kernel.IMPORT_MODULE_NAME); + return new Plugin(main, new Kernel(kernelInstance)); } /** diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java index f358df5..ea5d374 100644 --- a/src/main/java/org/extism/chicory/sdk/Plugin.java +++ b/src/main/java/org/extism/chicory/sdk/Plugin.java @@ -2,12 +2,8 @@ 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; -import com.dylibso.chicory.runtime.Memory; - -import static com.dylibso.chicory.wasm.types.Value.i64; /** * A Plugin instance. diff --git a/src/main/java/org/extism/chicory/sdk/Store.java b/src/main/java/org/extism/chicory/sdk/Store.java deleted file mode 100644 index 5f20c28..0000000 --- a/src/main/java/org/extism/chicory/sdk/Store.java +++ /dev/null @@ -1,122 +0,0 @@ -//package org.extism.chicory.sdk; -// -//import com.dylibso.chicory.runtime.*; -//import com.dylibso.chicory.wasm.Module; -//import com.dylibso.chicory.wasm.types.*; -// -//import java.util.*; -//import java.util.logging.Level; -//import java.util.logging.Logger; -// -//import static java.util.stream.Collectors.groupingBy; -//import static org.extism.chicory.sdk.ChicoryModule.bind; -// -//// TODO the store should also store the Modules. -//public class Store { -// static Logger logger = Logger.getAnonymousLogger(); -// // private final Map> exportedFunctions = new HashMap<>(); -// private final Map modules = new HashMap<>(); -// private final Map instances = new HashMap<>(); -// private final Map> importedFunctions = new HashMap<>(); -//// private final Map> resolvedImports = new HashMap<>(); -// -// -// public Store register(String name, Module module) { -// // Exports are converted into a signature bundle. -// this.modules.put(name, new TModule.ChicoryModule(name, module)); -// -// var importSection = module.importSection(); -// for (int i = 0; i < importSection.importCount(); i++) { -// Import ii = importSection.getImport(i); -// if (ii.importType() == ExternalType.FUNCTION) { -// FunctionImport fi = (FunctionImport) ii; -// var tidx = fi.typeIndex(); -// FunctionType type = module.typeSection().getType(tidx); -// FunctionSignature fsig = new FunctionSignature(fi.moduleName(), fi.name(), type.params(), type.returns()); -// this.importedFunctions.computeIfAbsent(name, k -> new ArrayList<>()).add(fsig); -// } // else ignore for now -// } -// return this; -// } -// -// -// public void register(HostModule hostModule) { -// // HostModules to not declare imports. -// this.modules.put(hostModule.name(), new TModule.ChicoryHostModule(hostModule)); -// } -// -// public void resolve() { -// for (var module : importedFunctions.entrySet()) { -// // Name of the module importing the symbol. -// String moduleName = module.getKey(); -// var resolvedImports = new ArrayList(); -//// this.resolvedImports.put(moduleName, resolvedImports); -// var imports = module.getValue(); -// for (var ii : imports) { -// // Name of the module whose symbol is being imported. -// String importedModuleName = ii.moduleName(); -// String symbolName = ii.name(); -// TModule tModule = modules.get(importedModuleName); -// if (tModule.lookup(symbolName) != null) { -// logger.log(Level.INFO, String.format("Found EXPORTED symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); -// continue; -// } -// logger.log(Level.WARNING, String.format("NOT FOUND: symbol %s.%s for module %s", importedModuleName, symbolName, moduleName)); -// } -// } -// } -// -// public Instance instantiate(String name) { -// return instantiate(modules.get(name)).asInstance(); -// } -// -// public Instance instantiate(String name, Module m) { -// return instantiate(new TModule.ChicoryModule(name, m)).asInstance(); -// } -// -// public TInstance instantiate(TModule m) { -// var importedSigs = this.importedFunctions.get(m.name()); -// TInstance tInstance = instances.get(m.name()); -// if (tInstance != null) { -// return tInstance; -// } -// -// // If all imports are satisfied, then we can instantiate. -// // The empty set is trivially satisfied. -// if (importedSigs == null) { -// if (m.isModule()) { -// // No imports: trivially satisfied. -// var instance = new TInstance.ChicoryModule( -// m.name(), Instance.builder(m.asModule()).build().initialize(true)); -// this.instances.put(m.name(), instance); -// return instance; -// } else { -// throw new UnsupportedOperationException("Cannot instantiate host modules yet"); -// } -// } else { -// var sigsByModule = importedSigs.stream().collect(groupingBy(FunctionSignature::moduleName)); -// for (String mname : sigsByModule.keySet()) { -// TModule tModule = this.modules.get(mname); -// instantiate(tModule); -// } -// // All imports have been now satisfied (or they failed), we can now instantiate. -// List satisfiedImports = new ArrayList<>(); -// for (String mname : sigsByModule.keySet()) { -// var instance = this.instances.get(mname); -// for (var sig : sigsByModule.get(mname)) { -// HostFunction f = bind(sig, ChicoryModule.asHandle(instance.asInstance().export(sig.name()))); -// satisfiedImports.add(f); -// } -// } -// -// Instance instance = Instance.builder(m.asModule()) -// .withHostImports(new HostImports(satisfiedImports.toArray(new HostFunction[0]))).build() -// .initialize(true); -// TInstance.ChicoryModule tinstance = new TInstance.ChicoryModule(m.name(), instance); -// this.instances.put(m.name(), tinstance); -// return tinstance; -// } -// -// } -// -//} From 1a8297b92dedbaf3ca6ec871c9b450dc5c05b2b5 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Thu, 5 Sep 2024 18:30:31 +0200 Subject: [PATCH 06/12] wip Signed-off-by: Edoardo Vacchi --- .../extism/chicory/sdk/DependencyGraph.java | 34 +++++++++++++----- .../org/extism/chicory/sdk/PluginTest.java | 18 ++++++++++ src/test/resources/circular-import-1.wasm | Bin 0 -> 78 bytes src/test/resources/circular-import-1.wat | 14 ++++++++ src/test/resources/circular-import-2.wasm | Bin 0 -> 86 bytes src/test/resources/circular-import-2.wat | 22 ++++++++++++ src/test/resources/circular-import-main.wasm | Bin 0 -> 64 bytes src/test/resources/circular-import-main.wat | 8 +++++ 8 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/circular-import-1.wasm create mode 100644 src/test/resources/circular-import-1.wat create mode 100644 src/test/resources/circular-import-2.wasm create mode 100644 src/test/resources/circular-import-2.wat create mode 100644 src/test/resources/circular-import-main.wasm create mode 100644 src/test/resources/circular-import-main.wat diff --git a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java index 4fcdf45..adbd1c6 100644 --- a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -113,7 +113,8 @@ public boolean validate() { String moduleName = imp.moduleName(); String symbolName = imp.name(); if (!edges.containsKey(moduleName) || !edges.get(moduleName).contains(symbolName)) { - logger.debug(String.format("Cannot find symbol: %s.%s\n", moduleName, symbolName)); + logger.info(String.format("Cannot find symbol: %s.%s\n", moduleName, symbolName)); + valid = false; } } } @@ -175,12 +176,7 @@ public Instance instantiate() { // and instantiate. if (satisfied) { unresolved.pop(); - Instance instance = - ChicoryModule.instanceWithOptions(m, this.options) - .withHostImports(store.toHostImports()) - .build(); - this.store.register(moduleId, instance); - this.instances.put(moduleId, instance); + instantiate(moduleId); } } @@ -194,9 +190,27 @@ public Instance instantiate() { 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) { + Module m = this.modules.get(moduleId); + Objects.requireNonNull(m); + Instance instance = + ChicoryModule.instanceWithOptions(m, this.options) + .withHostImports(store.toHostImports()) + .withStart(false) + .build(); + this.store.register(moduleId, instance); + this.instances.put(moduleId, instance); + return instance; + } + private void registerTrampoline(FunctionImport f, Module m) { var trampoline = new Trampoline(); var functionType = m.typeSection().getType(f.typeIndex()); @@ -219,7 +233,11 @@ public void registerFunctions(HostFunction... functions) { * @return a named instance with the given name. */ public Instance getInstance(String moduleName) { - return instances.get(moduleName); + if (instances.containsKey(moduleName)) { + return instances.get(moduleName); + } else { + return instantiate(moduleName); + } } /** diff --git a/src/test/java/org/extism/chicory/sdk/PluginTest.java b/src/test/java/org/extism/chicory/sdk/PluginTest.java index a1c7133..dbd9fd7 100644 --- a/src/test/java/org/extism/chicory/sdk/PluginTest.java +++ b/src/test/java/org/extism/chicory/sdk/PluginTest.java @@ -2,7 +2,10 @@ import junit.framework.TestCase; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; public class PluginTest extends TestCase { @@ -33,4 +36,19 @@ public void testGreetAoT() { assertEquals("Hello, Benjamin!", result); } + + public void testCircularDeps() throws IOException { + InputStream is1 = this.getClass().getResourceAsStream("/circular-import-1.wasm"); + InputStream is2 = this.getClass().getResourceAsStream("/circular-import-2.wasm"); + InputStream is3 = this.getClass().getResourceAsStream("/circular-import-main.wasm"); + var manifest = + Manifest.ofWasms( + ManifestWasm.fromBytes(is1.readAllBytes()).withName("env-1").build(), + ManifestWasm.fromBytes(is2.readAllBytes()).withName("env-2").build(), + ManifestWasm.fromBytes(is3.readAllBytes()).withName("main").build()).build(); + var plugin = Plugin.ofManifest(manifest).build(); + byte[] result = plugin.call("real_do_expr", new byte[0]); + System.out.println(Arrays.toString(result)); + } + } diff --git a/src/test/resources/circular-import-1.wasm b/src/test/resources/circular-import-1.wasm new file mode 100644 index 0000000000000000000000000000000000000000..1dcf74e62e24f124df75dc59a5f76770a81c59f6 GIT binary patch literal 78 zcmZQbEY4+QU|?Y6W=deHuV<`JU|_6gl4N2{%`4M2Vopp+VPJr=i%XLj7?_zD8QD1) bSyC$siWr!<_!xN@9Jw4t9E}7R1Q@viLtza; literal 0 HcmV?d00001 diff --git a/src/test/resources/circular-import-1.wat b/src/test/resources/circular-import-1.wat new file mode 100644 index 0000000..6be095c --- /dev/null +++ b/src/test/resources/circular-import-1.wat @@ -0,0 +1,14 @@ +(module + + (import "env-2" "add" (func $add (param i32 i32) (result i32))) + (import "env-2" "sub" (func $sub (param i32 i32) (result i32))) + + (func $expr (export "expr") (result i32) + (i32.const 10) + (i32.const 20) + (i32.const 50) + (call $add) + (call $sub) + ) + +) diff --git a/src/test/resources/circular-import-2.wasm b/src/test/resources/circular-import-2.wasm new file mode 100644 index 0000000000000000000000000000000000000000..04f74909f020f5a0de8a50921a7a73fae7599034 GIT binary patch literal 86 zcmZQbEY4+QU|?WmWlUgTtY_k5WKGR0(=}vCttcpBU|?op21>GvGczZqq%bft7nde6 gFtMlP$AhJrxx|>+863GBMY6adbT&5&g8&0J0Byq!#{d8T literal 0 HcmV?d00001 diff --git a/src/test/resources/circular-import-2.wat b/src/test/resources/circular-import-2.wat new file mode 100644 index 0000000..32f4423 --- /dev/null +++ b/src/test/resources/circular-import-2.wat @@ -0,0 +1,22 @@ +(module + + (import "env-1" "expr" (func $expr (result i32))) + + (func $add (export "add") (result i32) + (i32.add + (i32.const 10) + (i32.const 20)) + ) + + (func $sub (export "sub") (result i32) + (i32.sub + (i32.const 10) + (i32.const 20)) + ) + + (func $do_expr (export "do_expr") (result i32) + call $expr + ) + + +) diff --git a/src/test/resources/circular-import-main.wasm b/src/test/resources/circular-import-main.wasm new file mode 100644 index 0000000000000000000000000000000000000000..42b9cec20da9ad214255efb69b827589aa03ca86 GIT binary patch literal 64 zcmZQbEY4+QU|?WmWlUgTtY;ErWKGR0(=}pG$&XL1C@5lJU}j=uU>9KIDN0SuiHC_Y Na Date: Fri, 6 Sep 2024 09:42:06 +0200 Subject: [PATCH 07/12] fix test case Signed-off-by: Edoardo Vacchi --- .../chicory/sdk/DependencyGraphTest.java | 32 ++++++++++++++++++ src/test/resources/circular-import-1.wasm | Bin 78 -> 78 bytes src/test/resources/circular-import-1.wat | 2 +- src/test/resources/circular-import-2.wasm | Bin 86 -> 92 bytes src/test/resources/circular-import-2.wat | 12 +++---- src/test/resources/circular-import-main.wasm | Bin 64 -> 64 bytes src/test/resources/circular-import-main.wat | 2 +- 7 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java 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..5a35130 --- /dev/null +++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java @@ -0,0 +1,32 @@ +package org.extism.chicory.sdk; + +import com.dylibso.chicory.log.SystemLogger; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.types.Value; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class DependencyGraphTest { + + public void testCircularDeps() throws IOException { + InputStream is1 = this.getClass().getResourceAsStream("/circular-import-1.wasm"); + InputStream is2 = this.getClass().getResourceAsStream("/circular-import-2.wasm"); + InputStream is3 = this.getClass().getResourceAsStream("/circular-import-main.wasm"); + + + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + + + dg.registerModule("wasm1", Parser.parse(is1.readAllBytes())); + dg.registerModule("wasm2", Parser.parse(is2.readAllBytes())); + dg.registerModule("main", Parser.parse(is3.readAllBytes())); + + Instance main = dg.instantiate(); + + Value[] result = main.export("real_do_expr").apply(); + System.out.println(Arrays.toString(result)); + } +} diff --git a/src/test/resources/circular-import-1.wasm b/src/test/resources/circular-import-1.wasm index 1dcf74e62e24f124df75dc59a5f76770a81c59f6..8266694cc5ef8d5231cdc97d31b1e9bd9c84ac95 100644 GIT binary patch delta 15 WcmebCo8ZDF;%Fqm;K(Jw$PEA*jskfA delta 15 WcmebCo8ZF5veke0}w4FOe+#%1m(O&C=<8gYbhHsaB!UL?`$<{*cbk`4)Op1 delta 62 zcmazEQ(;If&Shj^U|?irOkiNFXX0aIP0cIQHDpPxC@5lJU}j-vV3?>SFYL(WD3Zm^ O&H$pbxmg$l7`Opa?+M5N diff --git a/src/test/resources/circular-import-2.wat b/src/test/resources/circular-import-2.wat index 32f4423..d83361f 100644 --- a/src/test/resources/circular-import-2.wat +++ b/src/test/resources/circular-import-2.wat @@ -2,16 +2,16 @@ (import "env-1" "expr" (func $expr (result i32))) - (func $add (export "add") (result i32) + (func $add (export "add") (param i32 i32) (result i32) (i32.add - (i32.const 10) - (i32.const 20)) + (local.get 0) + (local.get 1)) ) - (func $sub (export "sub") (result i32) + (func $sub (export "sub") (param i32 i32) (result i32) (i32.sub - (i32.const 10) - (i32.const 20)) + (local.get 0) + (local.get 1)) ) (func $do_expr (export "do_expr") (result i32) diff --git a/src/test/resources/circular-import-main.wasm b/src/test/resources/circular-import-main.wasm index 42b9cec20da9ad214255efb69b827589aa03ca86..3fb28c6489b581cb9a50a8761239c3bfe9adfab8 100644 GIT binary patch delta 7 OcmZ>8m|(}mzzqNgAOTze delta 7 OcmZ>8m|(}m$PEAoA^}|h diff --git a/src/test/resources/circular-import-main.wat b/src/test/resources/circular-import-main.wat index 19a5289..c4fd33b 100644 --- a/src/test/resources/circular-import-main.wat +++ b/src/test/resources/circular-import-main.wat @@ -3,6 +3,6 @@ (import "env-2" "do_expr" (func $do_expr (result i32))) (func $real_do_expr (export "real_do_expr") (result i32) - (call $real_do_expr) + (call $do_expr) ) ) \ No newline at end of file From 27cee590fe7c30cfd4bab659107e1a7b1aa96803 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Fri, 6 Sep 2024 09:52:59 +0200 Subject: [PATCH 08/12] Add test case for DependencyGraph Signed-off-by: Edoardo Vacchi --- .../chicory/sdk/DependencyGraphTest.java | 38 +++++++++++++----- .../org/extism/chicory/sdk/PluginTest.java | 14 ------- .../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-1.wasm | Bin .../circular-import-1.wat | 0 .../circular-import-2.wasm | Bin .../circular-import-2.wat | 0 .../circular-import-main.wasm | Bin .../circular-import-main.wat | 0 16 files changed, 74 insertions(+), 24 deletions(-) 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 rename src/test/resources/{ => circular-import}/circular-import-1.wasm (100%) rename src/test/resources/{ => circular-import}/circular-import-1.wat (100%) rename src/test/resources/{ => circular-import}/circular-import-2.wasm (100%) rename src/test/resources/{ => circular-import}/circular-import-2.wat (100%) rename src/test/resources/{ => circular-import}/circular-import-main.wasm (100%) rename src/test/resources/{ => circular-import}/circular-import-main.wat (100%) diff --git a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java index 5a35130..c00b8d1 100644 --- a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java +++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java @@ -4,29 +4,47 @@ import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.types.Value; +import junit.framework.TestCase; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -public class DependencyGraphTest { +public class DependencyGraphTest extends TestCase { public void testCircularDeps() throws IOException { - InputStream is1 = this.getClass().getResourceAsStream("/circular-import-1.wasm"); - InputStream is2 = this.getClass().getResourceAsStream("/circular-import-2.wasm"); - InputStream is3 = this.getClass().getResourceAsStream("/circular-import-main.wasm"); - + 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("wasm1", Parser.parse(is1.readAllBytes())); - dg.registerModule("wasm2", Parser.parse(is2.readAllBytes())); + dg.registerModule("env-1", Parser.parse(is1.readAllBytes())); + dg.registerModule("env-2", Parser.parse(is2.readAllBytes())); dg.registerModule("main", Parser.parse(is3.readAllBytes())); Instance main = dg.instantiate(); Value[] result = main.export("real_do_expr").apply(); - System.out.println(Arrays.toString(result)); + assertEquals(60, result[0].asInt()); + } + + public void testCircularDepsMore() throws IOException { + InputStream add = this.getClass().getResourceAsStream("/circular-import-more/circular-import-add.wasm"); + InputStream sub = this.getClass().getResourceAsStream("/circular-import-more/circular-import-sub.wasm"); + InputStream expr = this.getClass().getResourceAsStream("/circular-import-more/circular-import-expr.wasm"); + InputStream main = this.getClass().getResourceAsStream("/circular-import-more/circular-import-main.wasm"); + + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + + dg.registerModule("add", Parser.parse(add.readAllBytes())); + dg.registerModule("sub", Parser.parse(sub.readAllBytes())); + dg.registerModule("expr", Parser.parse(expr.readAllBytes())); + dg.registerModule("main", Parser.parse(main.readAllBytes())); + + Instance mainInst = dg.instantiate(); + + Value[] result = mainInst.export("real_do_expr").apply(); + assertEquals(60, result[0].asInt()); } + } diff --git a/src/test/java/org/extism/chicory/sdk/PluginTest.java b/src/test/java/org/extism/chicory/sdk/PluginTest.java index dbd9fd7..f3f9a8e 100644 --- a/src/test/java/org/extism/chicory/sdk/PluginTest.java +++ b/src/test/java/org/extism/chicory/sdk/PluginTest.java @@ -37,18 +37,4 @@ public void testGreetAoT() { } - public void testCircularDeps() throws IOException { - InputStream is1 = this.getClass().getResourceAsStream("/circular-import-1.wasm"); - InputStream is2 = this.getClass().getResourceAsStream("/circular-import-2.wasm"); - InputStream is3 = this.getClass().getResourceAsStream("/circular-import-main.wasm"); - var manifest = - Manifest.ofWasms( - ManifestWasm.fromBytes(is1.readAllBytes()).withName("env-1").build(), - ManifestWasm.fromBytes(is2.readAllBytes()).withName("env-2").build(), - ManifestWasm.fromBytes(is3.readAllBytes()).withName("main").build()).build(); - var plugin = Plugin.ofManifest(manifest).build(); - byte[] result = plugin.call("real_do_expr", new byte[0]); - System.out.println(Arrays.toString(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(HrDu8i Date: Fri, 6 Sep 2024 17:47:51 +0200 Subject: [PATCH 09/12] refactor trampolines Signed-off-by: Edoardo Vacchi --- .../extism/chicory/sdk/DependencyGraph.java | 50 +++++++++++++++---- .../chicory/sdk/DependencyGraphTest.java | 47 ++++++++++++----- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java index adbd1c6..2d9d2ae 100644 --- a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -3,6 +3,7 @@ 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.Store; import com.dylibso.chicory.runtime.WasmFunctionHandle; @@ -16,8 +17,10 @@ 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; @@ -139,6 +142,7 @@ public Instance instantiate() { String moduleId = unresolved.peek(); Module m = this.modules.get(moduleId); boolean satisfied = true; + List trampolines = new ArrayList<>(); ImportSection imports = m.importSection(); // We assume that each unique `name` in an import of the form `name.symbol` @@ -158,7 +162,7 @@ public Instance instantiate() { 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. - registerTrampoline((FunctionImport) mi, m); + trampolines.add(registerTrampoline((FunctionImport) mi, m)); } else { throw new ExtismException("cycle detected on a non-function"); } @@ -176,9 +180,8 @@ public Instance instantiate() { // and instantiate. if (satisfied) { unresolved.pop(); - instantiate(moduleId); + instantiate(moduleId, trampolines); } - } // We are now ready to resolve all the trampolines. @@ -198,12 +201,16 @@ public Instance instantiate() { return this.getMainInstance(); } - private Instance instantiate(String moduleId) { + 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(m, this.options) - .withHostImports(store.toHostImports()) + .withHostImports(extendedHostImports) .withStart(false) .build(); this.store.register(moduleId, instance); @@ -211,12 +218,33 @@ private Instance instantiate(String moduleId) { return instance; } - private void registerTrampoline(FunctionImport f, Module m) { - var trampoline = new Trampoline(); - var functionType = m.typeSection().getType(f.typeIndex()); + 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()); + } - this.trampolines.put(new QualifiedName(f.moduleName(), f.name()), trampoline); - this.store.addFunction(trampoline.asHostFunction(f.moduleName(), f.name(), functionType)); + 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.typeSection().getType(f.typeIndex()); + return trampoline.asHostFunction(f.moduleName(), f.name(), functionType); } /** @@ -236,7 +264,7 @@ public Instance getInstance(String moduleName) { if (instances.containsKey(moduleName)) { return instances.get(moduleName); } else { - return instantiate(moduleName); + return instantiate(moduleName, List.of()); } } diff --git a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java index c00b8d1..0e6b59f 100644 --- a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java +++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java @@ -2,6 +2,7 @@ import com.dylibso.chicory.log.SystemLogger; import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.Module; import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.types.Value; import junit.framework.TestCase; @@ -29,22 +30,44 @@ public void testCircularDeps() throws IOException { } public void testCircularDepsMore() throws IOException { - InputStream add = this.getClass().getResourceAsStream("/circular-import-more/circular-import-add.wasm"); - InputStream sub = this.getClass().getResourceAsStream("/circular-import-more/circular-import-sub.wasm"); - InputStream expr = this.getClass().getResourceAsStream("/circular-import-more/circular-import-expr.wasm"); - InputStream main = this.getClass().getResourceAsStream("/circular-import-more/circular-import-main.wasm"); + 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"); - DependencyGraph dg = new DependencyGraph(new SystemLogger()); - dg.registerModule("add", Parser.parse(add.readAllBytes())); - dg.registerModule("sub", Parser.parse(sub.readAllBytes())); - dg.registerModule("expr", Parser.parse(expr.readAllBytes())); - dg.registerModule("main", Parser.parse(main.readAllBytes())); + Module add = Parser.parse(addBytes.readAllBytes()); + Module sub = Parser.parse(subBytes.readAllBytes()); + Module expr = Parser.parse(exprBytes.readAllBytes()); + Module main = Parser.parse(mainBytes.readAllBytes()); - Instance mainInst = dg.instantiate(); + { + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerModule("add", add); + dg.registerModule("sub", sub); + dg.registerModule("expr", expr); + dg.registerModule("main", main); - Value[] result = mainInst.export("real_do_expr").apply(); - assertEquals(60, result[0].asInt()); + 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()); + } } } From 37a7a6e52fabce01fcafee4091f6cd0e6d1b52b5 Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Wed, 11 Sep 2024 16:16:40 +0200 Subject: [PATCH 10/12] should ensure dependencies are considered resolved on host function Signed-off-by: Edoardo Vacchi --- .../org/extism/chicory/sdk/ChicoryModule.java | 5 ++++- .../org/extism/chicory/sdk/DependencyGraph.java | 7 ++++--- .../java/org/extism/chicory/sdk/Linker.java | 17 ----------------- .../java/org/extism/chicory/sdk/PluginTest.java | 3 --- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java index 8a71a9c..61c8c01 100644 --- a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -9,6 +9,8 @@ public class ChicoryModule { + static final boolean IS_NATIVE_IMAGE_AOT = Boolean.getBoolean("com.oracle.graalvm.isaot"); + public static Module fromWasm(ManifestWasm m) { if (m instanceof ManifestWasmBytes) { ManifestWasmBytes mwb = (ManifestWasmBytes) m; @@ -32,7 +34,8 @@ public static Instance.Builder instanceWithOptions(Module m, Manifest.Options op if (opts == null) { return builder; } - if (opts.aot) { + // This feature is not compatibly with the native-image builder. + if (opts.aot && !IS_NATIVE_IMAGE_AOT) { builder.withMachineFactory(AotMachine::new); } if (!opts.validationFlags.isEmpty()) { diff --git a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java index 2d9d2ae..09dc0d9 100644 --- a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -35,6 +35,7 @@ public class DependencyGraph { private final Map> edges = 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<>(); @@ -143,7 +144,6 @@ public Instance instantiate() { Module m = this.modules.get(moduleId); boolean satisfied = true; List trampolines = new ArrayList<>(); - ImportSection imports = m.importSection(); // We assume that each unique `name` in an import of the form `name.symbol` // is registered as a module with that name @@ -167,8 +167,8 @@ public Instance instantiate() { throw new ExtismException("cycle detected on a non-function"); } } - } else if (!this.instances.containsKey(requiredModule)) { - // No such instance, we schedule this module for visiting. + } 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); } @@ -253,6 +253,7 @@ private HostFunction registerTrampoline(FunctionImport f, Module m) { public void registerFunctions(HostFunction... functions) { store.addFunction(functions); for (HostFunction f : functions) { + this.hostModules.add(f.moduleName()); registerSymbol(f.moduleName(), f.fieldName()); } } diff --git a/src/main/java/org/extism/chicory/sdk/Linker.java b/src/main/java/org/extism/chicory/sdk/Linker.java index a90a715..0cf1b65 100644 --- a/src/main/java/org/extism/chicory/sdk/Linker.java +++ b/src/main/java/org/extism/chicory/sdk/Linker.java @@ -49,22 +49,5 @@ public Plugin link() { return new Plugin(main, new Kernel(kernelInstance)); } - /** - * @throws ExtismException on name collision. - */ - private void checkCollision(String moduleName, ManifestWasm[] wasms) { - // FIXME: check both host imports and modules. - if (moduleName.equals(EXTISM_NS)) { - throw new ExtismException(String.format("Module name collision: %s", moduleName)); - } - - // FIXME: check collision on already processed modules - } - - private void checkHash(String moduleName, ManifestWasm wasm) { - // FIXME: add hash check. - } - - } diff --git a/src/test/java/org/extism/chicory/sdk/PluginTest.java b/src/test/java/org/extism/chicory/sdk/PluginTest.java index f3f9a8e..94dbb04 100644 --- a/src/test/java/org/extism/chicory/sdk/PluginTest.java +++ b/src/test/java/org/extism/chicory/sdk/PluginTest.java @@ -2,10 +2,7 @@ import junit.framework.TestCase; -import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.Arrays; public class PluginTest extends TestCase { From cca2ecb08a492126dc0ca36138c7a91676c5507b Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Wed, 11 Sep 2024 16:27:31 +0200 Subject: [PATCH 11/12] add test case Signed-off-by: Edoardo Vacchi --- .../org/extism/chicory/sdk/DependencyGraph.java | 4 ++++ .../extism/chicory/sdk/DependencyGraphTest.java | 12 ++++++++++++ src/test/resources/host-functions/import-wasi.wasm | Bin 0 -> 96 bytes src/test/resources/host-functions/import-wasi.wat | 7 +++++++ 4 files changed, 23 insertions(+) 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/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java index 09dc0d9..ab6e6f7 100644 --- a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -120,6 +120,10 @@ public boolean validate() { logger.info(String.format("Cannot find symbol: %s.%s\n", moduleName, symbolName)); valid = false; } + if (!modules.containsKey(moduleName) && !hostModules.contains(moduleName)) { + logger.info(String.format("Cannot find definition for the given symbol: %s.%s\n", moduleName, symbolName)); + valid = false; + } } } return valid; diff --git a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java index 0e6b59f..e85bb50 100644 --- a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java +++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java @@ -2,6 +2,7 @@ import com.dylibso.chicory.log.SystemLogger; import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasi.WasiPreview1; import com.dylibso.chicory.wasm.Module; import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.types.Value; @@ -70,4 +71,15 @@ public void testCircularDepsMore() throws IOException { } } + public void testHostFunctionDeps() throws IOException { + InputStream requireWasi = this.getClass().getResourceAsStream("/host-functions/import-wasi.wasm"); + Module requireWasiM = Parser.parse(requireWasi.readAllBytes()); + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerFunctions(WasiPreview1.builder().build().toHostFunctions()); + dg.registerModule("main", requireWasiM); + Instance mainInst = dg.instantiate(); + assertNotNull(mainInst); + + } + } diff --git a/src/test/resources/host-functions/import-wasi.wasm b/src/test/resources/host-functions/import-wasi.wasm new file mode 100644 index 0000000000000000000000000000000000000000..8db9bec97355a6398220fe6b62294ea9f9e0b0fa GIT binary patch literal 96 zcmWN;K@Ni;5Czcx2SroeHSq$TpeI6-(#V2J7*f)eSJ(I8H4y-3*}C(_a|2}M(!55e rU!Qe9s>>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 From 75d15c0edf2362cb61a4bf12cbc95cd2cfe8886c Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Mon, 16 Sep 2024 10:42:51 +0200 Subject: [PATCH 12/12] cleanup Signed-off-by: Edoardo Vacchi --- .../org/extism/chicory/sdk/ChicoryModule.java | 6 +-- .../extism/chicory/sdk/DependencyGraph.java | 47 ++++++++++++------- .../chicory/sdk/DependencyGraphTest.java | 17 +++++++ 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java index 61c8c01..4562b79 100644 --- a/src/main/java/org/extism/chicory/sdk/ChicoryModule.java +++ b/src/main/java/org/extism/chicory/sdk/ChicoryModule.java @@ -7,11 +7,11 @@ import java.nio.file.Path; -public class ChicoryModule { +class ChicoryModule { static final boolean IS_NATIVE_IMAGE_AOT = Boolean.getBoolean("com.oracle.graalvm.isaot"); - public static Module fromWasm(ManifestWasm m) { + static Module fromWasm(ManifestWasm m) { if (m instanceof ManifestWasmBytes) { ManifestWasmBytes mwb = (ManifestWasmBytes) m; return Parser.parse(mwb.bytes); @@ -29,7 +29,7 @@ public static Module fromWasm(ManifestWasm m) { } } - public static Instance.Builder instanceWithOptions(Module m, Manifest.Options opts) { + static Instance.Builder instanceWithOptions(Module m, Manifest.Options opts) { Instance.Builder builder = Instance.builder(m); if (opts == null) { return builder; diff --git a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java index ab6e6f7..e8ae4a1 100644 --- a/src/main/java/org/extism/chicory/sdk/DependencyGraph.java +++ b/src/main/java/org/extism/chicory/sdk/DependencyGraph.java @@ -28,12 +28,12 @@ import static java.util.stream.Collectors.groupingBy; -public class DependencyGraph { +class DependencyGraph { public static final String MAIN_MODULE_NAME = "main"; private final Logger logger; - private final Map> edges = new HashMap<>(); + private final Map> registeredSymbols = new HashMap<>(); private final Map modules = new HashMap<>(); private final Set hostModules = new HashSet<>(); private final Map instances = new HashMap<>(); @@ -55,13 +55,12 @@ public void setOptions(Manifest.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 - * + * - 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++) { @@ -82,13 +81,16 @@ public void registerModules(ManifestWasm... wasms) { } private void checkCollision(String moduleName, String symbol) { - if (symbol == null && this.edges.containsKey(moduleName)) { + if (symbol == null && this.registeredSymbols.containsKey(moduleName)) { throw new ExtismException("Collision detected: a module with the given name already exists: " + moduleName); - } else if (this.edges.containsKey(moduleName) && this.edges.get(moduleName).contains(symbol)) { + } 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); @@ -103,7 +105,7 @@ public void registerModule(String name, Module m) { public void registerSymbol(String name, String symbol) { checkCollision(name, symbol); - edges.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); + registeredSymbols.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); } public boolean validate() { @@ -116,12 +118,12 @@ public boolean validate() { Import imp = imports.getImport(i); String moduleName = imp.moduleName(); String symbolName = imp.name(); - if (!edges.containsKey(moduleName) || !edges.get(moduleName).contains(symbolName)) { - logger.info(String.format("Cannot find symbol: %s.%s\n", moduleName, symbolName)); + 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.info(String.format("Cannot find definition for the given symbol: %s.%s\n", moduleName, symbolName)); + logger.warnf("Cannot find definition for the given symbol: %s.%s\n", moduleName, symbolName); valid = false; } } @@ -132,10 +134,17 @@ public boolean validate() { /** * 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"); } @@ -252,7 +261,9 @@ private HostFunction registerTrampoline(FunctionImport f, Module m) { } /** - * Register the given host functions in the store. + * 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); @@ -263,7 +274,8 @@ public void registerFunctions(HostFunction... functions) { } /** - * @return a named instance with the given name. + * @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)) { @@ -305,6 +317,9 @@ public HostFunction asHostFunction(String moduleName, String name, FunctionType } } + /** + * A pair moduleName, symbol name. + */ static final class QualifiedName { final String moduleName; final String fieldName; diff --git a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java index e85bb50..ab9d305 100644 --- a/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java +++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java @@ -74,12 +74,29 @@ public void testCircularDepsMore() throws IOException { public void testHostFunctionDeps() throws IOException { InputStream requireWasi = this.getClass().getResourceAsStream("/host-functions/import-wasi.wasm"); Module requireWasiM = Parser.parse(requireWasi.readAllBytes()); + DependencyGraph dg = new DependencyGraph(new SystemLogger()); dg.registerFunctions(WasiPreview1.builder().build().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 = Parser.parse(requireWasi.readAllBytes()); + DependencyGraph dg = new DependencyGraph(new SystemLogger()); + dg.registerFunctions(WasiPreview1.builder().build().toHostFunctions()); + dg.registerModule("main", requireWasiM); + + Instance mainInst = dg.instantiate(); + Instance mainInst2 = dg.instantiate(); + assertSame("when invoked twice, instantiate() returns the same instance", mainInst, mainInst2); } + }