diff --git a/annotations/annotations/pom.xml b/annotations/annotations/pom.xml index 97c9f7ab9..9de7b48d6 100644 --- a/annotations/annotations/pom.xml +++ b/annotations/annotations/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../../pom.xml annotations diff --git a/annotations/it/pom.xml b/annotations/it/pom.xml index f8db9a014..06e34437a 100644 --- a/annotations/it/pom.xml +++ b/annotations/it/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../../pom.xml annotations-it diff --git a/annotations/processor/pom.xml b/annotations/processor/pom.xml index 0966cba80..22da37246 100644 --- a/annotations/processor/pom.xml +++ b/annotations/processor/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../../pom.xml annotations-processor diff --git a/bom/pom.xml b/bom/pom.xml index afe9217ed..a2a833da8 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 bom pom diff --git a/build-time-compiler-cli/pom.xml b/build-time-compiler-cli/pom.xml index 4c4fb01b3..63cff6d66 100644 --- a/build-time-compiler-cli/pom.xml +++ b/build-time-compiler-cli/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 build-time-compiler-cli-experimental jar diff --git a/build-time-compiler/pom.xml b/build-time-compiler/pom.xml index 89bf66cef..2ac621b1b 100644 --- a/build-time-compiler/pom.xml +++ b/build-time-compiler/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml build-time-compiler diff --git a/cli/pom.xml b/cli/pom.xml index 75fa48fd6..a5c094171 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 cli-experimental jar diff --git a/codegen/pom.xml b/codegen/pom.xml index 721667b6d..8f812bc7f 100644 --- a/codegen/pom.xml +++ b/codegen/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml codegen diff --git a/compiler-maven-plugin/pom.xml b/compiler-maven-plugin/pom.xml index a7ea02a6a..b81ded880 100644 --- a/compiler-maven-plugin/pom.xml +++ b/compiler-maven-plugin/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml endive-compiler-maven-plugin diff --git a/compiler-tests/pom.xml b/compiler-tests/pom.xml index 3301170be..da36a47d3 100644 --- a/compiler-tests/pom.xml +++ b/compiler-tests/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml compiler-tests diff --git a/compiler/pom.xml b/compiler/pom.xml index f59d81546..d23d04ede 100644 --- a/compiler/pom.xml +++ b/compiler/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml compiler diff --git a/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java b/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java index f4a1e543b..fda5b22f5 100644 --- a/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java +++ b/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -18,6 +19,9 @@ import run.endive.runtime.Machine; import run.endive.wasm.WasmEngineException; import run.endive.wasm.WasmModule; +import run.endive.wasm.types.CodeSection; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.FunctionBody; /** * Compiles WASM function bodies to JVM byte code that can be used as a machine factory for {@link Instance}'s. @@ -85,6 +89,7 @@ public static Builder builder(WasmModule module) { public static final class Builder { private final WasmModule module; private final run.endive.compiler.internal.Compiler.Builder compilerBuilder; + private boolean releaseFunctionBodies; private Cache cache; private Builder(WasmModule module) { @@ -121,6 +126,20 @@ public Builder withMethodPrefixer(MethodPrefixer methodPrefixer) { return this; } + /** + * Drops the parsed instructions of every compiled function once compilation is done. The + * module handed to instances keeps each function's local types, and the functions left to + * the interpreter keep their instructions, but the compiled ones no longer hold their + * parsed code for as long as the module lives. Compiled machines never read it. + * + *

Modules loaded from a {@link #withCache(Cache) cache} keep their instructions, since + * the cache does not record which functions were interpreted. + */ + public Builder withReleasedFunctionBodies() { + this.releaseFunctionBodies = true; + return this; + } + public Builder withCache(Cache cache) { this.cache = cache; return this; @@ -152,13 +171,32 @@ public Function compile() { cache.putIfAbsent(module.digest(), storeClassLoadingCollector(collector)); } - return new MachineFactory(module, collector.machineFactory()); + WasmModule compiledModule = module; + if (releaseFunctionBodies) { + compiledModule = withoutCompiledBodies(module, result.interpretedFunctions()); + } + return new MachineFactory(compiledModule, collector.machineFactory()); } catch (IOException e) { throw new WasmEngineException(e); } } } + private static WasmModule withoutCompiledBodies(WasmModule module, Set interpreted) { + int imports = module.importSection().count(ExternalType.FUNCTION); + var code = module.codeSection(); + var stripped = CodeSection.builder().setRequiresDataCount(code.isRequiresDataCount()); + for (int i = 0; i < code.functionBodyCount(); i++) { + FunctionBody body = code.getFunctionBody(i); + if (interpreted.contains(imports + i)) { + stripped.addFunctionBody(body); + } else { + stripped.addFunctionBody(new FunctionBody(body.localTypes(), List.of())); + } + } + return module.withCodeSection(stripped.build()); + } + private static byte[] storeClassLoadingCollector(ClassLoadingCollector collector) { try { // Create JAR in memory diff --git a/compiler/src/main/java/run/endive/compiler/internal/CallCycles.java b/compiler/src/main/java/run/endive/compiler/internal/CallCycles.java new file mode 100644 index 000000000..6e86c8818 --- /dev/null +++ b/compiler/src/main/java/run/endive/compiler/internal/CallCycles.java @@ -0,0 +1,141 @@ +package run.endive.compiler.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.OpCode; + +/** + * Finds the direct calls that can take part in unbounded execution without a backward branch. + * + *

A guest can only run without end by branching backwards, by calling through a table or a + * reference, by calling into the host (which may call back), or by calling around a cycle of the + * direct call graph (for example {@code f(n) = f(n - 1) + f(n - 1)}, whose depth stays small). + * Direct calls to a function outside every cycle always return after a bounded amount of work + * unless they branch backwards, call indirectly or call the host themselves, all of which are + * checked, so they need no interruption check of their own. + */ +final class CallCycles { + + private CallCycles() {} + + /** + * Returns, for each function index (imports first), whether a direct call to it must check + * for thread interruption. + */ + static boolean[] checkedCallees(WasmModule module) { + int imports = module.importSection().count(ExternalType.FUNCTION); + int defined = module.functionSection().functionCount(); + boolean[] checked = new boolean[imports + defined]; + Arrays.fill(checked, 0, imports, true); + + List edges = new ArrayList<>(defined); + for (int i = 0; i < defined; i++) { + edges.add(directCallees(module, i, imports)); + } + + boolean[] cyclic = cyclicFunctions(edges); + for (int i = 0; i < defined; i++) { + checked[imports + i] = cyclic[i]; + } + return checked; + } + + private static int[] directCallees(WasmModule module, int function, int imports) { + var body = module.codeSection().getFunctionBody(function); + int[] callees = new int[8]; + int count = 0; + for (var ins : body.instructions()) { + if (ins.opcode() != OpCode.CALL && ins.opcode() != OpCode.RETURN_CALL) { + continue; + } + int callee = (int) ins.operand(0) - imports; + if (callee < 0) { + continue; + } + if (count == callees.length) { + callees = Arrays.copyOf(callees, count * 2); + } + callees[count++] = callee; + } + return Arrays.copyOf(callees, count); + } + + /** Tarjan's strongly connected components, iterative so deep call graphs cannot overflow. */ + private static boolean[] cyclicFunctions(List edges) { + int count = edges.size(); + int[] index = new int[count]; + int[] lowLink = new int[count]; + boolean[] onStack = new boolean[count]; + boolean[] cyclic = new boolean[count]; + Arrays.fill(index, -1); + + int[] stack = new int[count]; + int stackSize = 0; + int[] callStack = new int[count]; + int[] edgeCursor = new int[count]; + int nextIndex = 0; + + for (int root = 0; root < count; root++) { + if (index[root] != -1) { + continue; + } + int depth = 0; + callStack[depth] = root; + edgeCursor[root] = 0; + index[root] = nextIndex; + lowLink[root] = nextIndex; + nextIndex++; + stack[stackSize++] = root; + onStack[root] = true; + + while (depth >= 0) { + int node = callStack[depth]; + int[] callees = edges.get(node); + if (edgeCursor[node] < callees.length) { + int callee = callees[edgeCursor[node]++]; + if (callee == node) { + cyclic[node] = true; + } + if (index[callee] == -1) { + index[callee] = nextIndex; + lowLink[callee] = nextIndex; + nextIndex++; + stack[stackSize++] = callee; + onStack[callee] = true; + edgeCursor[callee] = 0; + callStack[++depth] = callee; + } else if (onStack[callee]) { + lowLink[node] = Math.min(lowLink[node], index[callee]); + } + continue; + } + + if (lowLink[node] == index[node]) { + int size = 0; + int member; + int first = stackSize; + do { + member = stack[--stackSize]; + onStack[member] = false; + size++; + } while (member != node); + if (size > 1) { + for (int i = stackSize; i < first; i++) { + cyclic[stack[i]] = true; + } + } + } + + depth--; + if (depth >= 0) { + int parent = callStack[depth]; + lowLink[parent] = Math.min(lowLink[parent], lowLink[node]); + } + } + } + return cyclic; + } +} diff --git a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java index 0d874286d..c42c96d32 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java @@ -158,6 +158,7 @@ public final class Compiler { private int maxFunctionsPerClass; private final HashSet interpretedFunctions; private final Set callRefTypeIds; + private final boolean[] checkedCallees; private final boolean[] tailCallFunctions; private final boolean[] tailCallTypes; private final boolean moduleHasTailCalls; @@ -198,6 +199,7 @@ private Compiler( this.functionTypes = analyzer.functionTypes(); this.callRefTypeIds = collectCallRefTypeIds(); + this.checkedCallees = CallCycles.checkedCallees(module); this.tailCallFunctions = analyzer.tailCallFunctions(); this.tailCallTypes = analyzer.tailCallTypes(); this.moduleHasTailCalls = analyzer.hasTailCalls(); @@ -2147,6 +2149,7 @@ private void compileFunction( body, tailCallFunctions, tailCallTypes, + checkedCallees, useBridgeClasses ? callIndirectClassResolver : typeId -> internalClassName, analysis.maxTempSlots(), this::methodName); diff --git a/compiler/src/main/java/run/endive/compiler/internal/Context.java b/compiler/src/main/java/run/endive/compiler/internal/Context.java index bdbd6b09f..d7dc2f69e 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Context.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Context.java @@ -27,6 +27,7 @@ final class Context { private final FunctionBody body; private final boolean[] tailCallFunctions; private final boolean[] tailCallTypes; + private final boolean[] checkedCallees; private final List slots; private final int memorySlot; private final int instanceSlot; @@ -46,6 +47,7 @@ public Context( FunctionBody body, boolean[] tailCallFunctions, boolean[] tailCallTypes, + boolean[] checkedCallees, IntFunction callIndirectClassResolver, int maxTempSlots, IntFunction methodNames) { @@ -59,6 +61,7 @@ public Context( this.body = body; this.tailCallFunctions = tailCallFunctions; this.tailCallTypes = tailCallTypes; + this.checkedCallees = checkedCallees; this.callIndirectClassResolver = callIndirectClassResolver; this.methodNames = methodNames; @@ -184,4 +187,8 @@ public FunctionType tagFunctionType(int tagId) { } return type; } + + public boolean checksInterruptionOnCall(int funcId) { + return checkedCallees[funcId]; + } } diff --git a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java index c4e767ad0..d05a81aa7 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java @@ -387,7 +387,9 @@ public static void CALL(Context ctx, CompilerInstruction ins, InstructionAdapter int funcId = (int) ins.operand(0); FunctionType functionType = ctx.functionTypes().get(funcId); - emitInvokeStatic(asm, ShadedRefs.CHECK_INTERRUPTION); + if (ctx.checksInterruptionOnCall(funcId)) { + emitInvokeStatic(asm, ShadedRefs.CHECK_INTERRUPTION); + } if (hasTooManyParameters(functionType)) { emitBoxValuesOnStack(ctx, asm, functionType.params()); } diff --git a/compiler/src/test/java/run/endive/compiler/internal/ReleasedFunctionBodiesTest.java b/compiler/src/test/java/run/endive/compiler/internal/ReleasedFunctionBodiesTest.java new file mode 100644 index 000000000..b8308ddaf --- /dev/null +++ b/compiler/src/test/java/run/endive/compiler/internal/ReleasedFunctionBodiesTest.java @@ -0,0 +1,60 @@ +package run.endive.compiler.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; +import org.junit.jupiter.api.Test; +import run.endive.compiler.InterpreterFallback; +import run.endive.compiler.MachineFactoryCompiler; +import run.endive.corpus.CorpusResources; +import run.endive.runtime.CompiledModule; +import run.endive.runtime.Instance; +import run.endive.wasm.Parser; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.ExternalType; + +public class ReleasedFunctionBodiesTest { + + @Test + public void compiledFunctionsRunWithoutTheirInstructions() { + WasmModule module = Parser.parse(CorpusResources.getResource("compiled/power.c.wasm")); + var factory = MachineFactoryCompiler.builder(module).withReleasedFunctionBodies().compile(); + WasmModule released = ((CompiledModule) factory).wasmModule(); + + for (var body : released.codeSection().functionBodies()) { + assertTrue(body.instructions().isEmpty()); + } + var instance = Instance.builder(released).withMachineFactory(factory).build(); + assertEquals(16, instance.export("run").apply(4)[0]); + } + + @Test + public void interpretedFunctionsKeepTheirInstructions() { + WasmModule module = Parser.parse(CorpusResources.getResource("compiled/power.c.wasm")); + int run = exportedFunction(module, "run"); + int imports = module.importSection().count(ExternalType.FUNCTION); + var factory = + MachineFactoryCompiler.builder(module) + .withInterpreterFallback(InterpreterFallback.FAIL) + .withInterpretedFunctions(Set.of(run)) + .withReleasedFunctionBodies() + .compile(); + WasmModule released = ((CompiledModule) factory).wasmModule(); + + assertFalse(released.codeSection().getFunctionBody(run - imports).instructions().isEmpty()); + var instance = Instance.builder(released).withMachineFactory(factory).build(); + assertEquals(16, instance.export("run").apply(4)[0]); + } + + private static int exportedFunction(WasmModule module, String name) { + var exports = module.exportSection(); + for (int i = 0; i < exports.exportCount(); i++) { + if (exports.getExport(i).name().equals(name)) { + return (int) exports.getExport(i).index(); + } + } + throw new IllegalArgumentException("no export " + name); + } +} diff --git a/dircache/pom.xml b/dircache/pom.xml index af43833e0..eb0173b6d 100644 --- a/dircache/pom.xml +++ b/dircache/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 dircache-experimental jar diff --git a/docs-lib/pom.xml b/docs-lib/pom.xml index 62220f060..a44f1ede6 100644 --- a/docs-lib/pom.xml +++ b/docs-lib/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml docs-lib diff --git a/fuzz/pom.xml b/fuzz/pom.xml index a306e665d..f57279e27 100644 --- a/fuzz/pom.xml +++ b/fuzz/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 fuzz jar diff --git a/jmh/pom.xml b/jmh/pom.xml index 32a7d3806..d03fcf21d 100644 --- a/jmh/pom.xml +++ b/jmh/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml jmh diff --git a/log/pom.xml b/log/pom.xml index be6a21a27..bc3db08eb 100644 --- a/log/pom.xml +++ b/log/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 log jar diff --git a/machine-tests/pom.xml b/machine-tests/pom.xml index c8570d7d6..5fdfeec13 100644 --- a/machine-tests/pom.xml +++ b/machine-tests/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 machine-tests jar diff --git a/pom.xml b/pom.xml index f442ad071..fb9199e13 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 pom Endive Native JVM WebAssembly runtime diff --git a/redline/api/pom.xml b/redline/api/pom.xml index 3b0717413..a769838f2 100644 --- a/redline/api/pom.xml +++ b/redline/api/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-api-experimental diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index 2381e3efa..ee3b52394 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-bridge-experimental diff --git a/redline/build-time-compiler/pom.xml b/redline/build-time-compiler/pom.xml index 50ee68428..2e185ea1c 100644 --- a/redline/build-time-compiler/pom.xml +++ b/redline/build-time-compiler/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-build-time-compiler-experimental diff --git a/redline/compiler/pom.xml b/redline/compiler/pom.xml index 0106e935a..1ccd7e28b 100644 --- a/redline/compiler/pom.xml +++ b/redline/compiler/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-compiler-experimental diff --git a/redline/it/pom.xml b/redline/it/pom.xml index 42e7fb2ee..6070dbeea 100644 --- a/redline/it/pom.xml +++ b/redline/it/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-it-experimental diff --git a/redline/pom.xml b/redline/pom.xml index 772ef4d93..fb02218b2 100644 --- a/redline/pom.xml +++ b/redline/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-parent-experimental diff --git a/redline/runner-jffi-tests/pom.xml b/redline/runner-jffi-tests/pom.xml index e84f52570..eb043dce0 100644 --- a/redline/runner-jffi-tests/pom.xml +++ b/redline/runner-jffi-tests/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-runner-jffi-tests-experimental diff --git a/redline/runner-jffi/pom.xml b/redline/runner-jffi/pom.xml index 17917a57c..a21530fe4 100644 --- a/redline/runner-jffi/pom.xml +++ b/redline/runner-jffi/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-runner-jffi-experimental diff --git a/redline/runner-tests/pom.xml b/redline/runner-tests/pom.xml index 95a6b06be..1f44ac017 100644 --- a/redline/runner-tests/pom.xml +++ b/redline/runner-tests/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-runner-tests-experimental diff --git a/redline/runner/pom.xml b/redline/runner/pom.xml index 420368929..54ebae02f 100644 --- a/redline/runner/pom.xml +++ b/redline/runner/pom.xml @@ -5,7 +5,7 @@ run.endive redline-parent-experimental - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml redline-runner-experimental diff --git a/runtime-tests/pom.xml b/runtime-tests/pom.xml index ebe2e10c3..3ca1950aa 100644 --- a/runtime-tests/pom.xml +++ b/runtime-tests/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml runtime-tests diff --git a/runtime/pom.xml b/runtime/pom.xml index bd36b5ccb..c0878ba70 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml runtime diff --git a/runtime/src/main/java/run/endive/runtime/FlatByteArrayMemory.java b/runtime/src/main/java/run/endive/runtime/FlatByteArrayMemory.java new file mode 100644 index 000000000..71f056655 --- /dev/null +++ b/runtime/src/main/java/run/endive/runtime/FlatByteArrayMemory.java @@ -0,0 +1,345 @@ +package run.endive.runtime; + +import static java.lang.Math.min; +import static run.endive.runtime.ConstantEvaluators.computeConstantValue; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; +import java.util.Arrays; +import run.endive.wasm.UninstantiableException; +import run.endive.wasm.WasmEngineException; +import run.endive.wasm.types.ActiveDataSegment; +import run.endive.wasm.types.DataSegment; +import run.endive.wasm.types.MemoryLimits; +import run.endive.wasm.types.PassiveDataSegment; + +/** + * Linear memory kept in one contiguous byte array, for memories that are not shared. + * + *

Every load and store is a single bounds-checked access to that array: there is no page to + * look up and no access that can straddle two pages. Growing the memory copies it into a larger + * array, which is why shared memories, whose other threads could still be reading the old array, + * are rejected: use {@link ByteArrayMemory} for those. + * + *

The memory can grow to {@link Memory#RUNTIME_MAX_PAGES} pages, the largest size a single + * Java array can hold. + */ +public final class FlatByteArrayMemory implements Memory { + // Same note as ByteArrayMemory: catching RuntimeException keeps the accessors short enough to + // inline, and the JVM's own array bounds check is the wasm bounds check. + + private static final VarHandle SHORT_ARR_HANDLE = + MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle INT_ARR_HANDLE = + MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle FLOAT_ARR_HANDLE = + MethodHandles.byteArrayViewVarHandle(float[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle LONG_ARR_HANDLE = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle DOUBLE_ARR_HANDLE = + MethodHandles.byteArrayViewVarHandle(double[].class, ByteOrder.LITTLE_ENDIAN); + + private final MemoryLimits limits; + private DataSegment[] dataSegments; + private byte[] buffer; + + public FlatByteArrayMemory(MemoryLimits limits) { + if (limits.shared()) { + throw new IllegalArgumentException( + "FlatByteArrayMemory cannot back a shared memory; use ByteArrayMemory"); + } + this.limits = limits; + this.buffer = new byte[PAGE_SIZE * min(limits.initialPages(), maximumPages())]; + } + + @Override + public int pages() { + return buffer.length / PAGE_SIZE; + } + + @Override + public int grow(int size) { + int prevPages = pages(); + int numPages = prevPages + size; + if (numPages > maximumPages() || numPages < prevPages) { + return -1; + } + if (numPages != prevPages) { + buffer = Arrays.copyOf(buffer, numPages * PAGE_SIZE); + } + return prevPages; + } + + @Override + public int initialPages() { + return limits.initialPages(); + } + + @Override + public int maximumPages() { + return min(limits.maximumPages(), RUNTIME_MAX_PAGES); + } + + @Override + public boolean shared() { + return false; + } + + @Override + @SuppressWarnings("removal") + public Object lock(int address) { + return this; + } + + @Override + @SuppressWarnings("removal") + public int waitOn(int address, int expected, long timeout) { + throw new WasmEngineException("Attempt to wait on a non-shared memory, not supported."); + } + + @Override + @SuppressWarnings("removal") + public int waitOn(int address, long expected, long timeout) { + throw new WasmEngineException("Attempt to wait on a non-shared memory, not supported."); + } + + @Override + @SuppressWarnings("removal") + public int notify(int address, int maxThreads) { + return 0; + } + + @Override + public void initialize(Instance instance, DataSegment[] dataSegments) { + initialize(instance, dataSegments, 0); + } + + @Override + public void initialize(Instance instance, DataSegment[] dataSegments, int memoryIndex) { + this.dataSegments = dataSegments; + if (dataSegments == null) { + return; + } + for (var s : dataSegments) { + if (s instanceof ActiveDataSegment) { + var segment = (ActiveDataSegment) s; + if (segment.index() != memoryIndex) { + continue; + } + var data = segment.bytes(); + var offset = (int) computeConstantValue(instance, segment.offsetInstructions())[0]; + if (outOfBounds(offset, data.length, buffer.length)) { + throw new UninstantiableException(boundsMessage(offset, data.length)); + } + write(offset, data, 0, data.length); + } else if (!(s instanceof PassiveDataSegment)) { + throw new WasmEngineException("Data segment should be active or passive: " + s); + } + } + } + + @Override + public void initPassiveSegment(int segmentId, int dest, int offset, int size) { + write(dest, dataSegments[segmentId].bytes(), offset, size); + } + + @Override + public void drop(int segment) { + dataSegments[segment] = PassiveDataSegment.EMPTY; + } + + private static boolean outOfBounds(int addr, int size, int limit) { + return addr < 0 + || size < 0 + || addr > limit + || (size > 0 && (long) addr + (long) size > (long) limit); + } + + private String boundsMessage(int addr, int size) { + return "out of bounds memory access: attempted to access address: " + + addr + + " but limit is: " + + buffer.length + + " and size: " + + size; + } + + private RuntimeException outOfBoundsException(RuntimeException e, int addr, int size) { + if (e instanceof IndexOutOfBoundsException + || e instanceof IllegalArgumentException + || e instanceof NegativeArraySizeException) { + return new WasmRuntimeException(boundsMessage(addr, size)); + } + return e; + } + + @Override + public void write(int addr, byte[] data, int offset, int size) { + if (outOfBounds(offset, size, data.length)) { + throw new WasmRuntimeException( + "out of bounds memory access: attempted to read " + + size + + " bytes at " + + offset + + " from an array of " + + data.length); + } + if (outOfBounds(addr, size, buffer.length)) { + throw new WasmRuntimeException(boundsMessage(addr, size)); + } + System.arraycopy(data, offset, buffer, addr, size); + } + + @Override + public byte read(int addr) { + try { + return buffer[addr]; + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 1); + } + } + + @Override + public byte[] readBytes(int addr, int len) { + if (outOfBounds(addr, len, buffer.length)) { + throw new WasmRuntimeException(boundsMessage(addr, len)); + } + return Arrays.copyOfRange(buffer, addr, addr + len); + } + + @Override + public void writeI32(int addr, int data) { + try { + INT_ARR_HANDLE.set(buffer, addr, data); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 4); + } + } + + @Override + public int readInt(int addr) { + try { + return (int) INT_ARR_HANDLE.get(buffer, addr); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 4); + } + } + + @Override + public void writeLong(int addr, long data) { + try { + LONG_ARR_HANDLE.set(buffer, addr, data); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 8); + } + } + + @Override + public long readLong(int addr) { + try { + return (long) LONG_ARR_HANDLE.get(buffer, addr); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 8); + } + } + + @Override + public void writeShort(int addr, short data) { + try { + SHORT_ARR_HANDLE.set(buffer, addr, data); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 2); + } + } + + @Override + public short readShort(int addr) { + try { + return (short) SHORT_ARR_HANDLE.get(buffer, addr); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 2); + } + } + + @Override + public long readU16(int addr) { + return readShort(addr) & 0xFFFF; + } + + @Override + public void writeByte(int addr, byte data) { + try { + buffer[addr] = data; + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 1); + } + } + + @Override + public void writeF32(int addr, float data) { + try { + FLOAT_ARR_HANDLE.set(buffer, addr, data); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 4); + } + } + + @Override + public long readF32(int addr) { + return readInt(addr); + } + + @Override + public float readFloat(int addr) { + try { + return (float) FLOAT_ARR_HANDLE.get(buffer, addr); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 4); + } + } + + @Override + public void writeF64(int addr, double data) { + try { + DOUBLE_ARR_HANDLE.set(buffer, addr, data); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 8); + } + } + + @Override + public double readDouble(int addr) { + try { + return (double) DOUBLE_ARR_HANDLE.get(buffer, addr); + } catch (RuntimeException e) { + throw outOfBoundsException(e, addr, 8); + } + } + + @Override + public long readF64(int addr) { + return readLong(addr); + } + + @Override + public void zero() { + Arrays.fill(buffer, (byte) 0); + } + + @Override + public void fill(byte value, int fromIndex, int toIndex) { + if (outOfBounds(fromIndex, toIndex - fromIndex, buffer.length)) { + throw new WasmRuntimeException(boundsMessage(fromIndex, toIndex - fromIndex)); + } + Arrays.fill(buffer, fromIndex, toIndex, value); + } + + @Override + public void copy(int dest, int src, int size) { + if (outOfBounds(src, size, buffer.length) || outOfBounds(dest, size, buffer.length)) { + throw new WasmRuntimeException(boundsMessage(Math.max(src, dest), size)); + } + System.arraycopy(buffer, src, buffer, dest, size); + } +} diff --git a/runtime/src/main/java/run/endive/runtime/Instance.java b/runtime/src/main/java/run/endive/runtime/Instance.java index 2a582a3f2..45f73b7ac 100644 --- a/runtime/src/main/java/run/endive/runtime/Instance.java +++ b/runtime/src/main/java/run/endive/runtime/Instance.java @@ -62,6 +62,8 @@ public class Instance implements AutoCloseable { private final DataSegment[] dataSegments; private final Global[] globalInitializers; private final GlobalInstance[] globals; + // imported globals first, then the module's own: the index space of global.get and global.set + private final GlobalInstance[] globalSpace; private final FunctionType[] types; private final int[] functionTypes; private final ImportValues imports; @@ -120,6 +122,10 @@ static final class TailCallPending { this.types = types.clone(); this.functionTypes = functionTypes.clone(); this.imports = imports; + this.globalSpace = new GlobalInstance[imports.globalCount() + globalInitializers.length]; + for (int i = 0; i < imports.globalCount(); i++) { + globalSpace[i] = imports.global(i).instance(); + } this.machine = machineFactory.apply(this); this.tables = new TableInstance[tables.length]; this.elements = elements.clone(); @@ -178,6 +184,7 @@ public Instance initialize(boolean start) { .build(); } globals[i].setInstance(this); + globalSpace[imports.globalCount() + i] = globals[i]; if (g.valueType().isReference()) { globals[i].setRefValue(result.ref()); } @@ -387,14 +394,10 @@ public void dropDataSegment(int idx) { } public GlobalInstance global(int idx) { - if (idx < imports.globalCount()) { - return imports.global(idx).instance(); - } - var i = idx - imports.globalCount(); - if (i < 0 || i >= globals.length) { + if (idx < 0 || idx >= globalSpace.length) { throw new InvalidException("unknown global " + idx); } - return globals[idx - imports.globalCount()]; + return globalSpace[idx]; } public FunctionType type(int idx) { diff --git a/runtime/src/test/java/run/endive/runtime/MemoryTest.java b/runtime/src/test/java/run/endive/runtime/MemoryTest.java index 2bf58e464..484d42ee4 100644 --- a/runtime/src/test/java/run/endive/runtime/MemoryTest.java +++ b/runtime/src/test/java/run/endive/runtime/MemoryTest.java @@ -24,7 +24,10 @@ private static Stream memoryImplementations() { (Supplier) () -> new ByteArrayMemory(new MemoryLimits(2, 2))), Arguments.of( "ByteBufferMemory", - (Supplier) () -> new ByteBufferMemory(new MemoryLimits(2, 2)))); + (Supplier) () -> new ByteBufferMemory(new MemoryLimits(2, 2))), + Arguments.of( + "FlatByteArrayMemory", + (Supplier) () -> new FlatByteArrayMemory(new MemoryLimits(2, 2)))); } private static Stream growableMemoryImplementations() { diff --git a/simd/pom.xml b/simd/pom.xml index f63818d90..17d08b1d1 100644 --- a/simd/pom.xml +++ b/simd/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml simd diff --git a/test-gen-lib/pom.xml b/test-gen-lib/pom.xml index ea1f10813..21490de51 100644 --- a/test-gen-lib/pom.xml +++ b/test-gen-lib/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml test-gen-lib diff --git a/test-gen-plugin/pom.xml b/test-gen-plugin/pom.xml index f4269fa02..89254ff00 100644 --- a/test-gen-plugin/pom.xml +++ b/test-gen-plugin/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml test-gen-plugin diff --git a/wabt/pom.xml b/wabt/pom.xml index ddd1f18d6..cc42971c6 100644 --- a/wabt/pom.xml +++ b/wabt/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 wabt jar diff --git a/wasi-test-gen-plugin/pom.xml b/wasi-test-gen-plugin/pom.xml index b9236f5b1..582dc841b 100644 --- a/wasi-test-gen-plugin/pom.xml +++ b/wasi-test-gen-plugin/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml wasi-test-gen-plugin diff --git a/wasi-tests/pom.xml b/wasi-tests/pom.xml index ed3814a8e..6737413b0 100644 --- a/wasi-tests/pom.xml +++ b/wasi-tests/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml wasi-tests diff --git a/wasi/pom.xml b/wasi/pom.xml index 04942e9b9..afac1597e 100644 --- a/wasi/pom.xml +++ b/wasi/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml wasi diff --git a/wasm-corpus/pom.xml b/wasm-corpus/pom.xml index fa1250757..023fd9f8f 100644 --- a/wasm-corpus/pom.xml +++ b/wasm-corpus/pom.xml @@ -4,7 +4,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 wasm-corpus Endive - WASM Corpus diff --git a/wasm-tools/pom.xml b/wasm-tools/pom.xml index 553588fdd..b8fc11dab 100644 --- a/wasm-tools/pom.xml +++ b/wasm-tools/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 wasm-tools jar diff --git a/wasm/pom.xml b/wasm/pom.xml index b674ec5df..88f996886 100644 --- a/wasm/pom.xml +++ b/wasm/pom.xml @@ -5,7 +5,7 @@ run.endive endive - 999-SNAPSHOT + 1.1.0-sculk.3 ../pom.xml wasm diff --git a/wasm/src/main/java/run/endive/wasm/WasmModule.java b/wasm/src/main/java/run/endive/wasm/WasmModule.java index ed732b320..60bacabbc 100644 --- a/wasm/src/main/java/run/endive/wasm/WasmModule.java +++ b/wasm/src/main/java/run/endive/wasm/WasmModule.java @@ -126,6 +126,30 @@ public String digest() { return digest; } + /** + * Returns a copy of this module with the given code section in place of its own. Every other + * section, the custom sections and the digest are shared with this module. + */ + public WasmModule withCodeSection(CodeSection replacement) { + return new WasmModule( + typeSection, + importSection, + functionSection, + tableSection, + memorySection, + globalSection, + exportSection, + startSection, + elementSection, + replacement, + dataSection, + dataCountSection, + tagSection, + customSections, + ignoredSections, + digest); + } + public List customSections() { return new ArrayList<>(customSections.values()); }