diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/ImportFactory.java b/redline/api/src/main/java/run/endive/redline/experimental/api/ImportFactory.java
new file mode 100644
index 000000000..34fa05aec
--- /dev/null
+++ b/redline/api/src/main/java/run/endive/redline/experimental/api/ImportFactory.java
@@ -0,0 +1,73 @@
+package run.endive.redline.experimental.api;
+
+import java.util.Objects;
+import run.endive.runtime.ByteBufferMemory;
+import run.endive.runtime.GlobalInstance;
+import run.endive.runtime.Memory;
+import run.endive.runtime.TableInstance;
+import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
+import run.endive.wasm.types.Table;
+import run.endive.wasm.types.Value;
+
+/**
+ * Creates the memories, tables and globals a module imports.
+ *
+ *
Natively compiled code reaches an import through a raw address, so it can only
+ * use one the running backend built, while the bytecode path wants the ordinary
+ * runtime types. Which of those applies is decided by the platform the code lands
+ * on, not by anything the caller can see, so the choice is made here instead of at
+ * every import site:
+ *
+ *
+ * var f = MyModule.imports();
+ * var memory = f.memory(new MemoryLimits(1, 2));
+ *
+ *
+ * The same source then runs on a platform redline supports and on one it does
+ * not, with no branch of its own.
+ */
+public final class ImportFactory {
+
+ private final NativeMachineFactoryProvider provider;
+
+ private ImportFactory(NativeMachineFactoryProvider provider) {
+ this.provider = provider;
+ }
+
+ /** Builds imports the given native backend can use. */
+ public static ImportFactory forNative(NativeMachineFactoryProvider provider) {
+ return new ImportFactory(Objects.requireNonNull(provider, "provider"));
+ }
+
+ /** Builds the ordinary runtime imports, for when there is no native backend. */
+ public static ImportFactory forBytecode() {
+ return new ImportFactory(null);
+ }
+
+ /** Whether these imports are being built for natively compiled code. */
+ public boolean isNative() {
+ return provider != null;
+ }
+
+ public Memory memory(MemoryLimits limits) {
+ if (provider != null) {
+ return provider.createMemory(limits);
+ }
+ return new ByteBufferMemory(limits);
+ }
+
+ public TableInstance table(Table table, int initValue) {
+ if (provider != null) {
+ return provider.createImportTable(table, initValue);
+ }
+ return new TableInstance(table, initValue);
+ }
+
+ public GlobalInstance global(Value value, MutabilityType mutability) {
+ if (provider != null) {
+ return provider.createImportGlobal(value, mutability);
+ }
+ return GlobalInstance.builder().value(value).mutabilityType(mutability).build();
+ }
+}
diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java
index c3d85ae34..31add1e34 100644
--- a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java
+++ b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeMachineFactoryProvider.java
@@ -3,12 +3,15 @@
import java.util.Optional;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
+import run.endive.runtime.GlobalInstance;
import run.endive.runtime.Instance;
import run.endive.runtime.Memory;
import run.endive.runtime.TableInstance;
import run.endive.wasm.WasmModule;
import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
import run.endive.wasm.types.Table;
+import run.endive.wasm.types.Value;
public interface NativeMachineFactoryProvider {
@@ -18,6 +21,8 @@ public interface NativeMachineFactoryProvider {
TableInstance createImportTable(Table table, int initValue);
+ GlobalInstance createImportGlobal(Value value, MutabilityType mutability);
+
int priority();
static Optional discover() {
diff --git a/redline/api/src/test/java/run/endive/redline/experimental/api/ImportFactoryTest.java b/redline/api/src/test/java/run/endive/redline/experimental/api/ImportFactoryTest.java
new file mode 100644
index 000000000..f440c2887
--- /dev/null
+++ b/redline/api/src/test/java/run/endive/redline/experimental/api/ImportFactoryTest.java
@@ -0,0 +1,53 @@
+package run.endive.redline.experimental.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import org.junit.jupiter.api.Test;
+import run.endive.runtime.ByteBufferMemory;
+import run.endive.runtime.TableInstance;
+import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
+import run.endive.wasm.types.Table;
+import run.endive.wasm.types.TableLimits;
+import run.endive.wasm.types.ValType;
+import run.endive.wasm.types.Value;
+
+/**
+ * With no native provider the factory has to hand back the ordinary runtime types,
+ * which is what a platform redline does not compile for ends up running.
+ */
+public class ImportFactoryTest {
+
+ private static final ImportFactory BYTECODE = ImportFactory.forBytecode();
+
+ @Test
+ public void withoutAProviderItBuildsTheBytecodeTypes() {
+ assertFalse(BYTECODE.isNative());
+ assertInstanceOf(ByteBufferMemory.class, BYTECODE.memory(new MemoryLimits(1, 2)));
+ assertInstanceOf(
+ TableInstance.class,
+ BYTECODE.table(
+ new Table(ValType.FuncRef, new TableLimits(1, 1)), Value.REF_NULL_VALUE));
+ }
+
+ @Test
+ public void aBytecodeGlobalKeepsItsValueAndMutability() {
+ var global = BYTECODE.global(Value.i32(7), MutabilityType.Var);
+
+ assertEquals(7, global.getValue());
+ assertEquals(MutabilityType.Var, global.getMutabilityType());
+ assertEquals(ValType.I32, global.getType());
+ }
+
+ @Test
+ public void aBytecodeTableStartsOnItsInitialiser() {
+ var table =
+ BYTECODE.table(
+ new Table(ValType.FuncRef, new TableLimits(2, 2)), Value.REF_NULL_VALUE);
+
+ assertEquals(2, table.size());
+ assertEquals(Value.REF_NULL_VALUE, table.ref(0));
+ }
+}
diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java
index 4e6357fd4..f8da772f4 100644
--- a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java
+++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java
@@ -92,6 +92,7 @@ public void extendGeneratedSources() throws IOException {
var type = cu.getClassByName(baseName).orElseThrow();
cu.addImport("run.endive.redline.experimental.api.NativeCodeSerializer");
+ cu.addImport("run.endive.redline.experimental.api.ImportFactory");
cu.addImport("run.endive.redline.experimental.api.NativeMachineFactoryProvider");
cu.addImport("run.endive.redline.experimental.api.internal.RedlineTarget");
cu.addImport("java.io.InputStream");
@@ -103,6 +104,7 @@ public void extendGeneratedSources() throws IOException {
generateLoadNativeCodeMethod(type);
generateNativeProviderMethod(type);
generateBuilderMethod(type, baseName);
+ generateImportsMethod(type);
generateSafeBuilderMethod(type, baseName);
Files.writeString(sourceFile, cu.toString());
@@ -310,6 +312,58 @@ private static void generateNativeProviderMethod(ClassOrInterfaceDeclaration typ
new NameExpr("NativeMachineFactoryProvider"), "discover")));
}
+ private static void generateImportsMethod(ClassOrInterfaceDeclaration type) {
+ // Generates:
+ //
+ // public static ImportFactory imports() {
+ // var provider = nativeProvider();
+ // if (provider.isPresent()) {
+ // return ImportFactory.forNative(provider.get());
+ // }
+ // return ImportFactory.forBytecode();
+ // }
+ //
+ //
+ // Compiled code reaches an imported memory, table or global through a raw
+ // address, so it can only use ones the running backend built, while the
+ // bytecode path wants the ordinary runtime types. Going through this factory
+ // lets the same calling code do both, which is what makes one jar work on a
+ // platform redline compiles for and on one it does not.
+ //
+ // The shape mirrors builder() above, so both generated methods decide the
+ // same way.
+ var method =
+ type.addMethod("imports", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
+ .setType(parseClassOrInterfaceType("ImportFactory"));
+
+ var providerVar =
+ new ExpressionStmt(
+ new VariableDeclarationExpr(
+ new VariableDeclarator(
+ new VarType(),
+ "provider",
+ new MethodCallExpr("nativeProvider"))));
+
+ var returnNative =
+ new ReturnStmt(
+ new MethodCallExpr(
+ new NameExpr("ImportFactory"),
+ "forNative",
+ new NodeList<>(
+ new MethodCallExpr(new NameExpr("provider"), "get"))));
+
+ var ifProviderPresent =
+ new IfStmt()
+ .setCondition(new MethodCallExpr(new NameExpr("provider"), "isPresent"))
+ .setThenStmt(new BlockStmt(new NodeList<>(returnNative)));
+
+ var body = method.createBody();
+ body.addStatement(providerVar);
+ body.addStatement(ifProviderPresent);
+ body.addStatement(
+ new ReturnStmt(new MethodCallExpr(new NameExpr("ImportFactory"), "forBytecode")));
+ }
+
private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) {
// Generates:
//
diff --git a/redline/it/src/it/redline-e2e-panama/pom.xml b/redline/it/src/it/redline-e2e-panama/pom.xml
index 65632de54..3304c290c 100644
--- a/redline/it/src/it/redline-e2e-panama/pom.xml
+++ b/redline/it/src/it/redline-e2e-panama/pom.xml
@@ -57,6 +57,17 @@
true
+
+ compile-imports
+
+ compile
+
+
+ endive.test.ImportsModule
+ src/test/resources/imports.wat.wasm
+ true
+
+
diff --git a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java
index 7a9074a6b..0edc35cbd 100644
--- a/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java
+++ b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java
@@ -2,6 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -9,6 +10,16 @@
import org.junit.jupiter.api.Test;
import run.endive.redline.experimental.api.NativeMachineFactoryProvider;
import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.runtime.ImportGlobal;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportTable;
+import run.endive.runtime.ImportValues;
+import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
+import run.endive.wasm.types.Table;
+import run.endive.wasm.types.TableLimits;
+import run.endive.wasm.types.ValType;
+import run.endive.wasm.types.Value;
class RedlinePanamaE2eTest {
@@ -49,6 +60,59 @@ public void nativeBuilderProducesCorrectResults() {
}
}
+ /**
+ * The whole point of the factory: build the imports, hand them to the module, and
+ * read back through the very same objects what the module wrote to them. This is
+ * the shape the documentation shows, and it has to hold whether this platform got
+ * native code or fell back to the build-time compiled bytecode.
+ */
+ @Test
+ public void theModuleSharesTheImportsItWasGiven() {
+ var imports = ImportsModule.imports();
+
+ var memory = imports.memory(new MemoryLimits(1, 1));
+ var table =
+ imports.table(
+ new Table(ValType.FuncRef, new TableLimits(2, 2)), Value.REF_NULL_VALUE);
+ var counter = imports.global(Value.i32(10), MutabilityType.Var);
+
+ var importValues =
+ ImportValues.builder()
+ .addMemory(new ImportMemory("env", "memory", memory))
+ .addTable(new ImportTable("env", "table", table))
+ .addGlobal(new ImportGlobal("env", "counter", counter))
+ .build();
+
+ try (var instance = ImportsModule.builder().withImportValues(importValues).build()) {
+ instance.export("run").apply();
+
+ assertEquals(11, counter.getValue(), "the caller's global must carry the increment");
+ assertEquals(23130, memory.readInt(0), "the caller's memory must carry the write");
+ assertNotEquals(
+ Value.REF_NULL_VALUE,
+ table.ref(0),
+ "the caller's table must carry the stored funcref");
+
+ // and the other direction: what the caller writes, the module reads
+ memory.writeI32(16, 21);
+ instance.export("doubleAt").apply(16);
+ assertEquals(
+ 42, memory.readInt(20), "the module must read what the caller wrote to memory");
+ }
+ }
+
+ /**
+ * imports() must agree with builder() about which backend is in play. Getting this
+ * wrong is silent: the module still runs, and only the caller's view goes stale.
+ */
+ @Test
+ public void importsFactoryMatchesTheBackendInUse() {
+ assertEquals(
+ AddModule.nativeProvider().isPresent(),
+ AddModule.imports().isNative(),
+ "imports() must build for the same backend builder() runs on");
+ }
+
@Test
public void nativeCodeIsAvailable() {
assumeTrue(
diff --git a/redline/it/src/it/redline-e2e-panama/src/test/resources/imports.wat.wasm b/redline/it/src/it/redline-e2e-panama/src/test/resources/imports.wat.wasm
new file mode 100644
index 000000000..aa63c7f05
Binary files /dev/null and b/redline/it/src/it/redline-e2e-panama/src/test/resources/imports.wat.wasm differ
diff --git a/redline/it/src/it/redline-e2e/pom.xml b/redline/it/src/it/redline-e2e/pom.xml
index 628f2c420..b6b1a7a47 100644
--- a/redline/it/src/it/redline-e2e/pom.xml
+++ b/redline/it/src/it/redline-e2e/pom.xml
@@ -57,6 +57,17 @@
true
+
+ compile-imports
+
+ compile
+
+
+ endive.test.ImportsModule
+ src/test/resources/imports.wat.wasm
+ true
+
+
diff --git a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java
index 31dff4161..735848570 100644
--- a/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java
+++ b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java
@@ -2,6 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -9,6 +10,16 @@
import org.junit.jupiter.api.Test;
import run.endive.redline.experimental.api.NativeMachineFactoryProvider;
import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.runtime.ImportGlobal;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportTable;
+import run.endive.runtime.ImportValues;
+import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
+import run.endive.wasm.types.Table;
+import run.endive.wasm.types.TableLimits;
+import run.endive.wasm.types.ValType;
+import run.endive.wasm.types.Value;
class RedlineE2eTest {
@@ -62,6 +73,59 @@ public void safeBuilderProducesCorrectResults() {
}
}
+ /**
+ * The whole point of the factory: build the imports, hand them to the module, and
+ * read back through the very same objects what the module wrote to them. This is
+ * the shape the documentation shows, and it has to hold whether this platform got
+ * native code or fell back to the build-time compiled bytecode.
+ */
+ @Test
+ public void theModuleSharesTheImportsItWasGiven() {
+ var imports = ImportsModule.imports();
+
+ var memory = imports.memory(new MemoryLimits(1, 1));
+ var table =
+ imports.table(
+ new Table(ValType.FuncRef, new TableLimits(2, 2)), Value.REF_NULL_VALUE);
+ var counter = imports.global(Value.i32(10), MutabilityType.Var);
+
+ var importValues =
+ ImportValues.builder()
+ .addMemory(new ImportMemory("env", "memory", memory))
+ .addTable(new ImportTable("env", "table", table))
+ .addGlobal(new ImportGlobal("env", "counter", counter))
+ .build();
+
+ try (var instance = ImportsModule.builder().withImportValues(importValues).build()) {
+ instance.export("run").apply();
+
+ assertEquals(11, counter.getValue(), "the caller's global must carry the increment");
+ assertEquals(23130, memory.readInt(0), "the caller's memory must carry the write");
+ assertNotEquals(
+ Value.REF_NULL_VALUE,
+ table.ref(0),
+ "the caller's table must carry the stored funcref");
+
+ // and the other direction: what the caller writes, the module reads
+ memory.writeI32(16, 21);
+ instance.export("doubleAt").apply(16);
+ assertEquals(
+ 42, memory.readInt(20), "the module must read what the caller wrote to memory");
+ }
+ }
+
+ /**
+ * imports() must agree with builder() about which backend is in play. Getting this
+ * wrong is silent: the module still runs, and only the caller's view goes stale.
+ */
+ @Test
+ public void importsFactoryMatchesTheBackendInUse() {
+ assertEquals(
+ AddModule.nativeProvider().isPresent(),
+ AddModule.imports().isNative(),
+ "imports() must build for the same backend builder() runs on");
+ }
+
@Test
public void nativeCodeIsAvailable() {
assumeTrue(
diff --git a/redline/it/src/it/redline-e2e/src/test/resources/imports.wat.wasm b/redline/it/src/it/redline-e2e/src/test/resources/imports.wat.wasm
new file mode 100644
index 000000000..aa63c7f05
Binary files /dev/null and b/redline/it/src/it/redline-e2e/src/test/resources/imports.wat.wasm differ
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/ImportedMutablesTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/ImportedMutablesTest.java
new file mode 100644
index 000000000..12babcb4b
--- /dev/null
+++ b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/ImportedMutablesTest.java
@@ -0,0 +1,151 @@
+package run.endive.redline.experimental.runner.jffi.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
+import run.endive.runtime.GlobalInstance;
+import run.endive.runtime.ImportGlobal;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportTable;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.Memory;
+import run.endive.runtime.TableInstance;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
+import run.endive.wasm.types.Table;
+import run.endive.wasm.types.TableLimits;
+import run.endive.wasm.types.ValType;
+import run.endive.wasm.types.Value;
+
+/**
+ * An import belongs to the caller, so what the module writes to it has to be
+ * visible through the caller's own object. Copying the value in instead leaves the
+ * two to drift, which makes the same jar answer differently depending on whether
+ * it got native code on this platform.
+ */
+public class ImportedMutablesTest {
+
+ private static final int WRITTEN = 23130;
+
+ @Test
+ public void theCallerSeesWhatTheModuleWroteToItsGlobal() {
+ var counter = nativeGlobal();
+ try (var instance = build(nativeMemory(), nativeTable(), counter)) {
+ instance.export("run").apply();
+ assertEquals(
+ 11L,
+ counter.getValue(),
+ "the caller's own global must carry what the module wrote");
+ }
+ }
+
+ @Test
+ public void theCallerSeesWhatTheModuleWroteToItsMemory() {
+ var memory = nativeMemory();
+ try (var instance = build(memory, nativeTable(), nativeGlobal())) {
+ instance.export("run").apply();
+ assertEquals(
+ WRITTEN, memory.readInt(0), "the caller's own memory must carry the write");
+ }
+ }
+
+ @Test
+ public void theModuleReadsWhatTheCallerWroteToItsMemory() {
+ var memory = nativeMemory();
+ try (var instance = build(memory, nativeTable(), nativeGlobal())) {
+ memory.writeI32(16, 21);
+ instance.export("doubleAt").apply(16);
+ assertEquals(
+ 42,
+ memory.readInt(20),
+ "the module must read back what the caller put in its memory");
+ }
+ }
+
+ @Test
+ public void theCallerSeesWhatTheModuleWroteToItsTable() {
+ var table = nativeTable();
+ try (var instance = build(nativeMemory(), table, nativeGlobal())) {
+ instance.export("run").apply();
+ assertNotEquals(
+ Value.REF_NULL_VALUE,
+ table.ref(0),
+ "the caller's own table must carry the funcref the module stored");
+ }
+ }
+
+ @Test
+ public void matchesTheInterpreter() {
+ var counter =
+ GlobalInstance.builder()
+ .value(Value.i32(10))
+ .mutabilityType(MutabilityType.Var)
+ .build();
+ var reference =
+ run.endive.runtime.Instance.builder(module())
+ .withImportValues(
+ importsFor(
+ new run.endive.runtime.ByteBufferMemory(
+ new MemoryLimits(1, 1)),
+ new TableInstance(table(), Value.REF_NULL_VALUE),
+ counter))
+ .build();
+ reference.export("run").apply();
+
+ var native_ = nativeGlobal();
+ try (var instance = build(nativeMemory(), nativeTable(), native_)) {
+ instance.export("run").apply();
+ assertEquals(
+ counter.getValue(),
+ native_.getValue(),
+ "redline must leave the caller's global where the interpreter does");
+ }
+ }
+
+ private static Memory nativeMemory() {
+ return JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 1));
+ }
+
+ private static TableInstance nativeTable() {
+ return JffiNativeMachineFactory.createImportTable(table(), Value.REF_NULL_VALUE);
+ }
+
+ private static GlobalInstance nativeGlobal() {
+ return JffiNativeMachineFactory.createImportGlobal(10L, ValType.I32, MutabilityType.Var);
+ }
+
+ private static Table table() {
+ return new Table(ValType.FuncRef, new TableLimits(2, 2));
+ }
+
+ private static WasmModule module() {
+ return Parser.parse(CorpusResources.getResource("compiled/imported-mutables.wat.wasm"));
+ }
+
+ private static ImportValues importsFor(
+ Memory memory, TableInstance table, GlobalInstance counter) {
+ return ImportValues.builder()
+ .addMemory(new ImportMemory("env", "memory", memory))
+ .addTable(new ImportTable("env", "table", table))
+ .addGlobal(new ImportGlobal("env", "counter", counter))
+ .build();
+ }
+
+ private static run.endive.runtime.Instance build(
+ Memory memory, TableInstance table, GlobalInstance counter) {
+ return JffiNativeMachineFactory.builder(module())
+ .withImportValues(importsFor(memory, table, counter))
+ .withCompilerFunction(
+ m ->
+ NativeCompiler.compileAll(
+ RedlineTarget.detectHost().orElseThrow().triple(), m))
+ .build();
+ }
+}
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiMachineFactoryProvider.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiMachineFactoryProvider.java
index 17640d46c..d28d4ecbf 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiMachineFactoryProvider.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiMachineFactoryProvider.java
@@ -1,12 +1,15 @@
package run.endive.redline.experimental.runner.jffi;
import run.endive.redline.experimental.api.NativeMachineFactoryProvider;
+import run.endive.runtime.GlobalInstance;
import run.endive.runtime.Instance;
import run.endive.runtime.Memory;
import run.endive.runtime.TableInstance;
import run.endive.wasm.WasmModule;
import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
import run.endive.wasm.types.Table;
+import run.endive.wasm.types.Value;
public final class JffiMachineFactoryProvider implements NativeMachineFactoryProvider {
@@ -27,6 +30,11 @@ public TableInstance createImportTable(Table table, int initValue) {
return JffiNativeMachineFactory.createImportTable(table, initValue);
}
+ @Override
+ public GlobalInstance createImportGlobal(Value value, MutabilityType mutability) {
+ return JffiNativeMachineFactory.createImportGlobal(value.raw(), value.type(), mutability);
+ }
+
@Override
public int priority() {
return 50;
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java
index c64febb42..afc67eb02 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java
@@ -79,8 +79,7 @@ public GlobalInstance createGlobal(
public static GlobalInstance createImportGlobal(
long value, ValType type, MutabilityType mutability) {
- long addr = MEM.allocateMemory(8, true);
- return new JffiNativeGlobalInstance(addr, 0, value, type, mutability);
+ return JffiNativeGlobalInstance.standalone(value, type, mutability);
}
public static Memory createMemory(MemoryLimits limits) {
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java
index d7eeb6c6c..59f0e2591 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java
@@ -14,8 +14,18 @@ public final class JffiNativeGlobalInstance extends GlobalInstance {
private static final MemoryIO MEM = MemoryIO.getInstance();
- private final long bufferAddress;
- private final long offset;
+ // Not final: an imported global is built before the instance that will use it
+ // exists, so it starts out on its own buffer and is moved onto the machine's
+ // globals buffer by rebind() once there is one. See JffiNativeMachine.
+ private long bufferAddress;
+ private long offset;
+
+ /**
+ * True while this global sits on a buffer of its own rather than inside some
+ * machine's globals buffer. Only such a global may be moved: one a module
+ * exports is already where that module's compiled code reads it.
+ */
+ private boolean standalone;
public JffiNativeGlobalInstance(
long bufferAddress,
@@ -30,6 +40,36 @@ public JffiNativeGlobalInstance(
MEM.putLong(bufferAddress + offset, initialValue);
}
+ /**
+ * A global for the host to pass in as an import, on storage of its own until the
+ * instance that receives it adopts it.
+ */
+ public static JffiNativeGlobalInstance standalone(
+ long initialValue, ValType valType, MutabilityType mutabilityType) {
+ var global =
+ new JffiNativeGlobalInstance(
+ MEM.allocateMemory(8, true), 0, initialValue, valType, mutabilityType);
+ global.standalone = true;
+ return global;
+ }
+
+ boolean isStandalone() {
+ return standalone;
+ }
+
+ /**
+ * Moves this global onto the buffer compiled code reads, carrying its current
+ * value across. Both sides then share the same eight bytes, so neither can go
+ * stale while the other writes.
+ */
+ void rebind(long target, int index) {
+ long current = getValue();
+ this.bufferAddress = target;
+ this.offset = (long) index * 8;
+ this.standalone = false;
+ MEM.putLong(target + offset, current);
+ }
+
@Override
public long getValue() {
return MEM.getLong(bufferAddress + offset);
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
index 1d5b84e0d..4adfb8d2b 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
@@ -783,6 +783,20 @@ private long memoryGrowHandler(long ctxAddr) {
// --- Globals initialization ---
+ /**
+ * Compiled code reaches an import through a raw address, so it can only use one
+ * this backend built. The message points at the module's own factory rather than
+ * a backend-specific one, because that is what works on every platform.
+ */
+ private static String foreignImportMessage(String kind, Object actual) {
+ return "this module is running natively compiled code, which can only use an imported "
+ + kind
+ + " created by the same backend, but got "
+ + actual.getClass().getName()
+ + ". Create it through the generated module's imports() factory, which picks"
+ + " the right one whether or not native code is available on this platform.";
+ }
+
private void initializeImportGlobals() {
if (importGlobalsInitialized || globalCount == 0) {
return;
@@ -800,7 +814,21 @@ private void initializeImportGlobals() {
.count();
for (int i = 0; i < importGlobalCount; i++) {
- MEM.putLong(globalsBufferAddr + (long) i * 8, instance.global(i).getValue());
+ var global = instance.global(i);
+ if (!(global instanceof JffiNativeGlobalInstance)) {
+ throw new WasmEngineException(foreignImportMessage("global", global));
+ }
+ var nativeGlobal = (JffiNativeGlobalInstance) global;
+ if (nativeGlobal.isStandalone()) {
+ // Passed in by the host: adopt it, so what this module writes stays
+ // visible through the caller's own object.
+ nativeGlobal.rebind(globalsBufferAddr, i);
+ } else {
+ // Exported by another module, and already sitting where that
+ // module's compiled code reads it. Its storage cannot move, so this
+ // module starts from its current value.
+ MEM.putLong(globalsBufferAddr + (long) i * 8, nativeGlobal.getValue());
+ }
}
}
@@ -835,14 +863,7 @@ private void initializeNativeTables() {
// the instance. An imported one belongs to whoever created it.
owned[i] = i >= importedTableCount;
} else {
- // Imported table not created by our factory — wrap it
- var tableDef = new run.endive.wasm.types.Table(table.elementType(), table.limits());
- var nt = new JffiNativeTable(tableDef, run.endive.wasm.types.Value.REF_NULL_VALUE);
- for (int j = 0; j < table.size(); j++) {
- nt.setRef(j, table.ref(j), instance);
- }
- nativeTables[i] = nt;
- owned[i] = true;
+ throw new WasmEngineException(foreignImportMessage("table", table));
}
MEM.putLong(tablePtrsArrayAddr + (long) i * 8, nativeTables[i].nativeBufferAddress());
@@ -1043,11 +1064,7 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
cachedMemBase = ((JffiNativeMemory) mem).nativeAddress();
MEM.putLong(ctxBufferAddr + CtxBuffer.MEM_BASE_ADDR, cachedMemBase);
} else if (mem != null) {
- throw new WasmEngineException(
- "JffiNativeMachine requires JffiNativeMemory but got "
- + mem.getClass().getName()
- + ". Use JffiNativeMachineFactory.createMemory() for all"
- + " memories, including imports.");
+ throw new WasmEngineException(foreignImportMessage("memory", mem));
} else {
cachedMemBase = 0L;
}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/ImportedMutablesTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/ImportedMutablesTest.java
new file mode 100644
index 000000000..d155499d6
--- /dev/null
+++ b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/ImportedMutablesTest.java
@@ -0,0 +1,151 @@
+package run.endive.redline.experimental.runner.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+import run.endive.corpus.CorpusResources;
+import run.endive.redline.experimental.api.internal.RedlineTarget;
+import run.endive.redline.experimental.compiler.internal.NativeCompiler;
+import run.endive.redline.experimental.runner.NativeMachineFactory;
+import run.endive.runtime.GlobalInstance;
+import run.endive.runtime.ImportGlobal;
+import run.endive.runtime.ImportMemory;
+import run.endive.runtime.ImportTable;
+import run.endive.runtime.ImportValues;
+import run.endive.runtime.Memory;
+import run.endive.runtime.TableInstance;
+import run.endive.wasm.Parser;
+import run.endive.wasm.WasmModule;
+import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
+import run.endive.wasm.types.Table;
+import run.endive.wasm.types.TableLimits;
+import run.endive.wasm.types.ValType;
+import run.endive.wasm.types.Value;
+
+/**
+ * An import belongs to the caller, so what the module writes to it has to be
+ * visible through the caller's own object. Copying the value in instead leaves the
+ * two to drift, which makes the same jar answer differently depending on whether
+ * it got native code on this platform.
+ */
+public class ImportedMutablesTest {
+
+ private static final int WRITTEN = 23130;
+
+ @Test
+ public void theCallerSeesWhatTheModuleWroteToItsGlobal() {
+ var counter = nativeGlobal();
+ try (var instance = build(nativeMemory(), nativeTable(), counter)) {
+ instance.export("run").apply();
+ assertEquals(
+ 11L,
+ counter.getValue(),
+ "the caller's own global must carry what the module wrote");
+ }
+ }
+
+ @Test
+ public void theCallerSeesWhatTheModuleWroteToItsMemory() {
+ var memory = nativeMemory();
+ try (var instance = build(memory, nativeTable(), nativeGlobal())) {
+ instance.export("run").apply();
+ assertEquals(
+ WRITTEN, memory.readInt(0), "the caller's own memory must carry the write");
+ }
+ }
+
+ @Test
+ public void theModuleReadsWhatTheCallerWroteToItsMemory() {
+ var memory = nativeMemory();
+ try (var instance = build(memory, nativeTable(), nativeGlobal())) {
+ memory.writeI32(16, 21);
+ instance.export("doubleAt").apply(16);
+ assertEquals(
+ 42,
+ memory.readInt(20),
+ "the module must read back what the caller put in its memory");
+ }
+ }
+
+ @Test
+ public void theCallerSeesWhatTheModuleWroteToItsTable() {
+ var table = nativeTable();
+ try (var instance = build(nativeMemory(), table, nativeGlobal())) {
+ instance.export("run").apply();
+ assertNotEquals(
+ Value.REF_NULL_VALUE,
+ table.ref(0),
+ "the caller's own table must carry the funcref the module stored");
+ }
+ }
+
+ @Test
+ public void matchesTheInterpreter() {
+ var counter =
+ GlobalInstance.builder()
+ .value(Value.i32(10))
+ .mutabilityType(MutabilityType.Var)
+ .build();
+ var reference =
+ run.endive.runtime.Instance.builder(module())
+ .withImportValues(
+ importsFor(
+ new run.endive.runtime.ByteBufferMemory(
+ new MemoryLimits(1, 1)),
+ new TableInstance(table(), Value.REF_NULL_VALUE),
+ counter))
+ .build();
+ reference.export("run").apply();
+
+ var native_ = nativeGlobal();
+ try (var instance = build(nativeMemory(), nativeTable(), native_)) {
+ instance.export("run").apply();
+ assertEquals(
+ counter.getValue(),
+ native_.getValue(),
+ "redline must leave the caller's global where the interpreter does");
+ }
+ }
+
+ private static Memory nativeMemory() {
+ return NativeMachineFactory.createMemory(new MemoryLimits(1, 1));
+ }
+
+ private static TableInstance nativeTable() {
+ return NativeMachineFactory.createImportTable(table(), Value.REF_NULL_VALUE);
+ }
+
+ private static GlobalInstance nativeGlobal() {
+ return NativeMachineFactory.createImportGlobal(10L, ValType.I32, MutabilityType.Var);
+ }
+
+ private static Table table() {
+ return new Table(ValType.FuncRef, new TableLimits(2, 2));
+ }
+
+ private static WasmModule module() {
+ return Parser.parse(CorpusResources.getResource("compiled/imported-mutables.wat.wasm"));
+ }
+
+ private static ImportValues importsFor(
+ Memory memory, TableInstance table, GlobalInstance counter) {
+ return ImportValues.builder()
+ .addMemory(new ImportMemory("env", "memory", memory))
+ .addTable(new ImportTable("env", "table", table))
+ .addGlobal(new ImportGlobal("env", "counter", counter))
+ .build();
+ }
+
+ private static run.endive.runtime.Instance build(
+ Memory memory, TableInstance table, GlobalInstance counter) {
+ return NativeMachineFactory.builder(module())
+ .withImportValues(importsFor(memory, table, counter))
+ .withCompilerFunction(
+ m ->
+ NativeCompiler.compileAll(
+ RedlineTarget.detectHost().orElseThrow().triple(), m))
+ .build();
+ }
+}
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java
index 8c67a82a6..bc6af6140 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java
@@ -78,8 +78,7 @@ public GlobalInstance createGlobal(
public static GlobalInstance createImportGlobal(
long value, ValType type, MutabilityType mutability) {
- var buf = Arena.ofAuto().allocate(8, 8);
- return new NativeGlobalInstance(buf, 0, value, type, mutability);
+ return NativeGlobalInstance.standalone(value, type, mutability);
}
public static Memory createMemory(MemoryLimits limits) {
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/PanamaMachineFactoryProvider.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/PanamaMachineFactoryProvider.java
index 0ed78e10c..2a775904d 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/PanamaMachineFactoryProvider.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/PanamaMachineFactoryProvider.java
@@ -1,12 +1,15 @@
package run.endive.redline.experimental.runner;
import run.endive.redline.experimental.api.NativeMachineFactoryProvider;
+import run.endive.runtime.GlobalInstance;
import run.endive.runtime.Instance;
import run.endive.runtime.Memory;
import run.endive.runtime.TableInstance;
import run.endive.wasm.WasmModule;
import run.endive.wasm.types.MemoryLimits;
+import run.endive.wasm.types.MutabilityType;
import run.endive.wasm.types.Table;
+import run.endive.wasm.types.Value;
public final class PanamaMachineFactoryProvider implements NativeMachineFactoryProvider {
@@ -27,6 +30,11 @@ public TableInstance createImportTable(Table table, int initValue) {
return NativeMachineFactory.createImportTable(table, initValue);
}
+ @Override
+ public GlobalInstance createImportGlobal(Value value, MutabilityType mutability) {
+ return NativeMachineFactory.createImportGlobal(value.raw(), value.type(), mutability);
+ }
+
@Override
public int priority() {
return 100;
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java
index 3910d5048..d9f3ef916 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java
@@ -1,5 +1,6 @@
package run.endive.redline.experimental.runner.internal;
+import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import run.endive.runtime.GlobalInstance;
@@ -13,8 +14,18 @@
*/
public final class NativeGlobalInstance extends GlobalInstance {
- private final MemorySegment buffer;
- private final long offset;
+ // Not final: an imported global is built before the instance that will use it
+ // exists, so it starts out on its own buffer and is moved onto the machine's
+ // globals buffer by rebind() once there is one. See NativeMachine.
+ private MemorySegment buffer;
+ private long offset;
+
+ /**
+ * True while this global sits on a buffer of its own rather than inside some
+ * machine's globals buffer. Only such a global may be moved: one a module
+ * exports is already where that module's compiled code reads it.
+ */
+ private boolean standalone;
public NativeGlobalInstance(
MemorySegment buffer,
@@ -29,6 +40,36 @@ public NativeGlobalInstance(
buffer.set(ValueLayout.JAVA_LONG, offset, initialValue);
}
+ /**
+ * A global for the host to pass in as an import, on storage of its own until the
+ * instance that receives it adopts it.
+ */
+ public static NativeGlobalInstance standalone(
+ long initialValue, ValType valType, MutabilityType mutabilityType) {
+ var global =
+ new NativeGlobalInstance(
+ Arena.ofAuto().allocate(8, 8), 0, initialValue, valType, mutabilityType);
+ global.standalone = true;
+ return global;
+ }
+
+ boolean isStandalone() {
+ return standalone;
+ }
+
+ /**
+ * Moves this global onto the buffer compiled code reads, carrying its current
+ * value across. Both sides then share the same eight bytes, so neither can go
+ * stale while the other writes.
+ */
+ void rebind(MemorySegment target, int index) {
+ long current = getValue();
+ this.buffer = target;
+ this.offset = (long) index * 8;
+ this.standalone = false;
+ target.set(ValueLayout.JAVA_LONG, offset, current);
+ }
+
@Override
public long getValue() {
return buffer.get(ValueLayout.JAVA_LONG, offset);
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
index 4578b2f1d..b9da6235b 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
@@ -839,13 +839,27 @@ private long memoryGrowHandler(long ctxAddr) {
// --- Globals initialization ---
+ /**
+ * Compiled code reaches an import through a raw address, so it can only use one
+ * this backend built. The message points at the module's own factory rather than
+ * a backend-specific one, because that is what works on every platform.
+ */
+ private static String foreignImportMessage(String kind, Object actual) {
+ return "this module is running natively compiled code, which can only use an imported "
+ + kind
+ + " created by the same backend, but got "
+ + actual.getClass().getName()
+ + ". Create it through the generated module's imports() factory, which picks"
+ + " the right one whether or not native code is available on this platform.";
+ }
+
/**
* Lazily replace module-defined GlobalInstance objects with NativeGlobalInstance
* backed by the off-heap globalsBuffer. Called once on first native call,
* after Instance.initialize() has created the original GlobalInstance objects.
*
- * For imported globals, we copy their current value into the buffer (read-only
- * from native code's perspective — imported mutable globals are rare).
+ * An imported global is moved onto the same buffer rather than copied into it,
+ * so that what the module writes stays visible through the caller's own object.
*/
private void initializeImportGlobals() {
if (importGlobalsInitialized || globalCount == 0) {
@@ -854,7 +868,6 @@ private void initializeImportGlobals() {
importGlobalsInitialized = true;
// Module-defined globals are already NativeGlobalInstance (created by globalFactory).
- // Only need to copy imported global values into the shared buffer.
int importGlobalCount =
(int)
instance.module().importSection().stream()
@@ -866,7 +879,20 @@ private void initializeImportGlobals() {
.count();
for (int i = 0; i < importGlobalCount; i++) {
- globalsBuffer.set(ValueLayout.JAVA_LONG, (long) i * 8, instance.global(i).getValue());
+ var global = instance.global(i);
+ if (!(global instanceof NativeGlobalInstance nativeGlobal)) {
+ throw new WasmEngineException(foreignImportMessage("global", global));
+ }
+ if (nativeGlobal.isStandalone()) {
+ // Passed in by the host: adopt it, so what this module writes stays
+ // visible through the caller's own object.
+ nativeGlobal.rebind(globalsBuffer, i);
+ } else {
+ // Exported by another module, and already sitting where that
+ // module's compiled code reads it. Its storage cannot move, so this
+ // module starts from its current value.
+ globalsBuffer.set(ValueLayout.JAVA_LONG, (long) i * 8, nativeGlobal.getValue());
+ }
}
}
@@ -897,15 +923,7 @@ private void initializeNativeTables() {
nt.resolvePendingRefs(instance);
nativeTables[i] = nt;
} else {
- // Imported table not created by our factory — wrap it
- var tableDef = new run.endive.wasm.types.Table(table.elementType(), table.limits());
- var nt =
- new NativeTable(
- tableDef, run.endive.wasm.types.Value.REF_NULL_VALUE, arena);
- for (int j = 0; j < table.size(); j++) {
- nt.setRef(j, table.ref(j), instance);
- }
- nativeTables[i] = nt;
+ throw new WasmEngineException(foreignImportMessage("table", table));
}
tablePtrsArray.set(
@@ -1049,11 +1067,7 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
CtxBuffer.MEM_BASE_ADDR,
cachedMemBase.address());
} else if (mem != null) {
- throw new WasmEngineException(
- "NativeMachine requires NativeMemory but got "
- + mem.getClass().getName()
- + ". Use NativeMachineFactory.createMemory() for all"
- + " memories, including imports.");
+ throw new WasmEngineException(foreignImportMessage("memory", mem));
} else {
cachedMemBase = MemorySegment.NULL;
}
diff --git a/wasm-corpus/src/main/resources/compiled/imported-mutables.wat.wasm b/wasm-corpus/src/main/resources/compiled/imported-mutables.wat.wasm
new file mode 100644
index 000000000..aa63c7f05
Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/imported-mutables.wat.wasm differ
diff --git a/wasm-corpus/src/main/resources/wat/imported-mutables.wat b/wasm-corpus/src/main/resources/wat/imported-mutables.wat
new file mode 100644
index 000000000..6f30274fa
--- /dev/null
+++ b/wasm-corpus/src/main/resources/wat/imported-mutables.wat
@@ -0,0 +1,25 @@
+;; Mutates every kind of import it was given, so the caller can check that its
+;; own objects observe the change. A backend that copies an import instead of
+;; sharing it comes apart here: wasm sees the new value and the host does not.
+(module
+ (import "env" "memory" (memory 1))
+ (import "env" "table" (table 2 funcref))
+ (import "env" "counter" (global $counter (mut i32)))
+
+ (elem declare func $answer)
+
+ (func $answer (result i32)
+ (i32.const 42))
+
+ (func (export "run")
+ (global.set $counter (i32.add (global.get $counter) (i32.const 1)))
+ (i32.store (i32.const 0) (i32.const 23130))
+ (table.set (i32.const 0) (ref.func $answer)))
+
+ ;; reads what the host left at addr and writes twice that to addr+4, so a
+ ;; round trip through the imported memory is observable from both ends
+ (func (export "doubleAt") (param $addr i32)
+ (i32.store
+ (i32.add (local.get $addr) (i32.const 4))
+ (i32.mul (i32.load (local.get $addr)) (i32.const 2))))
+)