+ * 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); + + } + } + + private void checkCollision(String moduleName, String symbol) { + if (symbol == null && this.registeredSymbols.containsKey(moduleName)) { + throw new ExtismException("Collision detected: a module with the given name already exists: " + moduleName); + } else if (this.registeredSymbols.containsKey(moduleName) && this.registeredSymbols.get(moduleName).contains(symbol)) { + throw new ExtismException("Collision detected: a symbol with the given name already exists: " + moduleName + "." + symbol); + } + } + + /** + * Register a Module with the given name. + */ + public void registerModule(String name, Module m) { + checkCollision(name, null); + + ExportSection exportSection = m.exportSection(); + for (int i = 0; i < exportSection.exportCount(); i++) { + Export export = exportSection.getExport(i); + String exportName = export.name(); + this.registerSymbol(name, exportName); + } + modules.put(name, m); + } + + public void registerSymbol(String name, String symbol) { + checkCollision(name, symbol); + registeredSymbols.computeIfAbsent(name, k -> new HashSet<>()).add(symbol); + } + + public boolean validate() { + boolean valid = true; + for (var kv : modules.entrySet()) { + Module m = kv.getValue(); + + ImportSection imports = m.importSection(); + for (int i = 0; i < imports.importCount(); i++) { + Import imp = imports.getImport(i); + String moduleName = imp.moduleName(); + String symbolName = imp.name(); + if (!registeredSymbols.containsKey(moduleName) || !registeredSymbols.get(moduleName).contains(symbolName)) { + logger.warnf("Cannot find symbol: %s.%s\n", moduleName, symbolName); + valid = false; + } + if (!modules.containsKey(moduleName) && !hostModules.contains(moduleName)) { + logger.warnf("Cannot find definition for the given symbol: %s.%s\n", moduleName, symbolName); + valid = false; + } + } + } + return valid; + } + + /** + * Instantiate is a breadth-first visit of the dependency graph, starting + * from the `main` module, and recursively instantiating the required dependencies. + *
+ * The method is idempotent, invoking it twice causes it to return the same instance.
+ *
+ * @return an instance of the main module.
+ */
+ public Instance instantiate() {
+ Instance mainInstance = this.getMainInstance();
+ if (mainInstance != null) {
+ return mainInstance;
+ }
+
+ if (!validate()) {
+ throw new ExtismException("Unresolved symbols");
+ }
+
+ Stack
+ * Returns 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;
+ }
+
+ public Plugin link() {
+
+ var dg = new DependencyGraph(logger);
+ dg.setOptions(manifest.options);
+
+ // Register the Kernel module, usually not present in the manifest.
+ dg.registerModule(Kernel.IMPORT_MODULE_NAME, Kernel.module());
+
+ // Register the WASI host functions.
+ dg.registerFunctions(new WasiPreview1(logger).toHostFunctions());
+
+ // Register the user-provided host functions.
+ dg.registerFunctions(this.hostFunctions);
+
+ // Register all the modules declared in the manifest.
+ dg.registerModules(manifest.wasms);
+
+ // Instantiate the main module, and, recursively, all of its dependencies.
+ Instance main = dg.instantiate();
+ // 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/ManifestModuleMapper.java b/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java
deleted file mode 100644
index c729ea1..0000000
--- a/src/main/java/org/extism/chicory/sdk/ManifestModuleMapper.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.extism.chicory.sdk;
-
-import com.dylibso.chicory.aot.AotMachine;
-import com.dylibso.chicory.runtime.Module;
-
-class ManifestModuleMapper {
- private final Manifest manifest;
-
- ManifestModuleMapper(Manifest manifest) {
- this.manifest = manifest;
- }
-
- Module.Builder toModuleBuilder() {
- if (manifest.wasms.length > 1) {
- throw new UnsupportedOperationException(
- "Manifests of multiple wasm files are not supported yet!");
- }
- Module.Builder mb = wasmToModuleBuilder(manifest.wasms[0]);
- return withOptions(mb, manifest.options);
- }
-
- private Module.Builder wasmToModuleBuilder(ManifestWasm m) {
- if (m instanceof ManifestWasmBytes) {
- ManifestWasmBytes mwb = (ManifestWasmBytes) m;
- return Module.builder(mwb.bytes);
- } else if (m instanceof ManifestWasmPath) {
- ManifestWasmPath mwp = (ManifestWasmPath) m;
- return Module.builder(mwp.path);
- } else if (m instanceof ManifestWasmFile) {
- ManifestWasmFile mwf = (ManifestWasmFile) m;
- return Module.builder(mwf.filePath);
- } else if (m instanceof ManifestWasmUrl) {
- ManifestWasmUrl mwu = (ManifestWasmUrl) m;
- return Module.builder(mwu.getUrlAsStream());
- } else {
- throw new IllegalArgumentException("Unknown ManifestWasm type " + m.getClass());
- }
- }
-
- private Module.Builder withOptions(Module.Builder mb, Manifest.Options opts) {
- if (opts == null) {
- return mb;
- }
- if (opts.aot) {
- mb.withMachineFactory(AotMachine::new);
- }
- if (!opts.validationFlags.isEmpty()) {
- throw new UnsupportedOperationException("Validation flags are not supported yet");
- }
- return mb;
- }
-
-}
diff --git a/src/main/java/org/extism/chicory/sdk/Plugin.java b/src/main/java/org/extism/chicory/sdk/Plugin.java
index 99b753c..ea5d374 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,24 @@ public Builder withLogger(Logger logger) {
}
public Plugin build() {
- return new Plugin(manifest, hostFunctions, logger);
+ var logger = this.logger == null ? new SystemLogger() : this.logger;
+ Linker linker = new Linker(this.manifest, this.hostFunctions, logger);
+ return linker.link();
}
}
- private final Manifest manifest;
- private final Instance instance;
- private final HostImports imports;
private final Kernel kernel;
- private 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();
+ private final Instance mainInstance;
- 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;
+ Plugin(Instance main, Kernel kernel) {
+ this.kernel = kernel;
+ this.mainInstance = main;
+ mainInstance.initialize(true);
}
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) {
@@ -91,5 +66,4 @@ public byte[] call(String funcName, byte[] input) {
throw new ExtismException("Failed");
}
}
-
}
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..ab9d305
--- /dev/null
+++ b/src/test/java/org/extism/chicory/sdk/DependencyGraphTest.java
@@ -0,0 +1,102 @@
+package org.extism.chicory.sdk;
+
+import com.dylibso.chicory.log.SystemLogger;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.wasi.WasiPreview1;
+import com.dylibso.chicory.wasm.Module;
+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;
+
+public class DependencyGraphTest extends TestCase {
+
+ public void testCircularDeps() throws IOException {
+ InputStream is1 = this.getClass().getResourceAsStream("/circular-import/circular-import-1.wasm");
+ InputStream is2 = this.getClass().getResourceAsStream("/circular-import/circular-import-2.wasm");
+ InputStream is3 = this.getClass().getResourceAsStream("/circular-import/circular-import-main.wasm");
+
+ DependencyGraph dg = new DependencyGraph(new SystemLogger());
+
+ dg.registerModule("env-1", 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();
+ assertEquals(60, result[0].asInt());
+ }
+
+ public void testCircularDepsMore() throws IOException {
+ InputStream addBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-add.wasm");
+ InputStream subBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-sub.wasm");
+ InputStream exprBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-expr.wasm");
+ InputStream mainBytes = this.getClass().getResourceAsStream("/circular-import-more/circular-import-main.wasm");
+
+
+ Module add = Parser.parse(addBytes.readAllBytes());
+ Module sub = Parser.parse(subBytes.readAllBytes());
+ Module expr = Parser.parse(exprBytes.readAllBytes());
+ Module main = Parser.parse(mainBytes.readAllBytes());
+
+ {
+ DependencyGraph dg = new DependencyGraph(new SystemLogger());
+ dg.registerModule("add", add);
+ dg.registerModule("sub", sub);
+ dg.registerModule("expr", expr);
+ dg.registerModule("main", main);
+
+ Instance mainInst = dg.instantiate();
+
+ Value[] result = mainInst.export("real_do_expr").apply();
+ assertEquals(60, result[0].asInt());
+ }
+
+ // Let's try to register them in a different order:
+ // it should never matter.
+ {
+ DependencyGraph dg = new DependencyGraph(new SystemLogger());
+ dg.registerModule("expr", expr);
+ dg.registerModule("main", main);
+ dg.registerModule("sub", sub);
+ dg.registerModule("add", add);
+
+ Instance mainInst = dg.instantiate();
+
+ Value[] result = mainInst.export("real_do_expr").apply();
+ assertEquals(60, result[0].asInt());
+ }
+ }
+
+ public void testHostFunctionDeps() throws IOException {
+ InputStream requireWasi = this.getClass().getResourceAsStream("/host-functions/import-wasi.wasm");
+ Module requireWasiM = 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);
+ }
+
+
+}
diff --git a/src/test/java/org/extism/chicory/sdk/PluginTest.java b/src/test/java/org/extism/chicory/sdk/PluginTest.java
index a1c7133..94dbb04 100644
--- a/src/test/java/org/extism/chicory/sdk/PluginTest.java
+++ b/src/test/java/org/extism/chicory/sdk/PluginTest.java
@@ -33,4 +33,5 @@ public void testGreetAoT() {
assertEquals("Hello, Benjamin!", result);
}
+
}
diff --git a/src/test/resources/circular-import-more/circular-import-add.wasm b/src/test/resources/circular-import-more/circular-import-add.wasm
new file mode 100644
index 0000000..10c439e
Binary files /dev/null and b/src/test/resources/circular-import-more/circular-import-add.wasm differ
diff --git a/src/test/resources/circular-import-more/circular-import-add.wat b/src/test/resources/circular-import-more/circular-import-add.wat
new file mode 100644
index 0000000..ff31e5f
--- /dev/null
+++ b/src/test/resources/circular-import-more/circular-import-add.wat
@@ -0,0 +1,15 @@
+(module
+
+ (import "expr" "expr" (func $expr (result i32)))
+
+ (func $add (export "add") (param i32 i32) (result i32)
+ (i32.add
+ (local.get 0)
+ (local.get 1))
+ )
+
+ (func $do_expr (export "do_expr") (result i32)
+ call $expr
+ )
+
+)
diff --git a/src/test/resources/circular-import-more/circular-import-expr.wasm b/src/test/resources/circular-import-more/circular-import-expr.wasm
new file mode 100644
index 0000000..0f36df6
Binary files /dev/null and b/src/test/resources/circular-import-more/circular-import-expr.wasm differ
diff --git a/src/test/resources/circular-import-more/circular-import-expr.wat b/src/test/resources/circular-import-more/circular-import-expr.wat
new file mode 100644
index 0000000..4a9d44f
--- /dev/null
+++ b/src/test/resources/circular-import-more/circular-import-expr.wat
@@ -0,0 +1,14 @@
+(module
+
+ (import "add" "add" (func $add (param i32 i32) (result i32)))
+ (import "sub" "sub" (func $sub (param i32 i32) (result i32)))
+
+ (func $expr (export "expr") (result i32)
+ (i32.const 20)
+ (i32.const 50)
+ (call $add)
+ (i32.const 10)
+ (call $sub)
+ )
+
+)
diff --git a/src/test/resources/circular-import-more/circular-import-main.wasm b/src/test/resources/circular-import-more/circular-import-main.wasm
new file mode 100644
index 0000000..df6f46a
Binary files /dev/null and b/src/test/resources/circular-import-more/circular-import-main.wasm differ
diff --git a/src/test/resources/circular-import-more/circular-import-main.wat b/src/test/resources/circular-import-more/circular-import-main.wat
new file mode 100644
index 0000000..294c258
--- /dev/null
+++ b/src/test/resources/circular-import-more/circular-import-main.wat
@@ -0,0 +1,8 @@
+(module
+
+ (import "add" "do_expr" (func $do_expr (result i32)))
+
+ (func $real_do_expr (export "real_do_expr") (result i32)
+ (call $do_expr)
+ )
+)
\ No newline at end of file
diff --git a/src/test/resources/circular-import-more/circular-import-sub.wasm b/src/test/resources/circular-import-more/circular-import-sub.wasm
new file mode 100644
index 0000000..6b242e2
Binary files /dev/null and b/src/test/resources/circular-import-more/circular-import-sub.wasm differ
diff --git a/src/test/resources/circular-import-more/circular-import-sub.wat b/src/test/resources/circular-import-more/circular-import-sub.wat
new file mode 100644
index 0000000..35d9a58
--- /dev/null
+++ b/src/test/resources/circular-import-more/circular-import-sub.wat
@@ -0,0 +1,9 @@
+(module
+
+ (func $sub (export "sub") (param i32 i32) (result i32)
+ (i32.sub
+ (local.get 0)
+ (local.get 1))
+ )
+
+)
diff --git a/src/test/resources/circular-import/circular-import-1.wasm b/src/test/resources/circular-import/circular-import-1.wasm
new file mode 100644
index 0000000..8266694
Binary files /dev/null and b/src/test/resources/circular-import/circular-import-1.wasm differ
diff --git a/src/test/resources/circular-import/circular-import-1.wat b/src/test/resources/circular-import/circular-import-1.wat
new file mode 100644
index 0000000..0aee507
--- /dev/null
+++ b/src/test/resources/circular-import/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 20)
+ (i32.const 50)
+ (call $add)
+ (i32.const 10)
+ (call $sub)
+ )
+
+)
diff --git a/src/test/resources/circular-import/circular-import-2.wasm b/src/test/resources/circular-import/circular-import-2.wasm
new file mode 100644
index 0000000..75b1eca
Binary files /dev/null and b/src/test/resources/circular-import/circular-import-2.wasm differ
diff --git a/src/test/resources/circular-import/circular-import-2.wat b/src/test/resources/circular-import/circular-import-2.wat
new file mode 100644
index 0000000..d83361f
--- /dev/null
+++ b/src/test/resources/circular-import/circular-import-2.wat
@@ -0,0 +1,22 @@
+(module
+
+ (import "env-1" "expr" (func $expr (result i32)))
+
+ (func $add (export "add") (param i32 i32) (result i32)
+ (i32.add
+ (local.get 0)
+ (local.get 1))
+ )
+
+ (func $sub (export "sub") (param i32 i32) (result i32)
+ (i32.sub
+ (local.get 0)
+ (local.get 1))
+ )
+
+ (func $do_expr (export "do_expr") (result i32)
+ call $expr
+ )
+
+
+)
diff --git a/src/test/resources/circular-import/circular-import-main.wasm b/src/test/resources/circular-import/circular-import-main.wasm
new file mode 100644
index 0000000..3fb28c6
Binary files /dev/null and b/src/test/resources/circular-import/circular-import-main.wasm differ
diff --git a/src/test/resources/circular-import/circular-import-main.wat b/src/test/resources/circular-import/circular-import-main.wat
new file mode 100644
index 0000000..c4fd33b
--- /dev/null
+++ b/src/test/resources/circular-import/circular-import-main.wat
@@ -0,0 +1,8 @@
+(module
+
+ (import "env-2" "do_expr" (func $do_expr (result i32)))
+
+ (func $real_do_expr (export "real_do_expr") (result i32)
+ (call $do_expr)
+ )
+)
\ No newline at end of file
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 0000000..8db9bec
Binary files /dev/null and b/src/test/resources/host-functions/import-wasi.wasm differ
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