Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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, for whichever backend
* is running. Obtained from the generated module's {@code imports()}.
*/
public final class ImportFactory {

private final NativeMachineFactoryProvider provider;

private ImportFactory(NativeMachineFactoryProvider provider) {
this.provider = provider;
}

/**
* Picks the backend for a module holding this native code, or none. Called by the
* generated {@code imports()}.
*/
public static ImportFactory forNativeCode(byte[][] nativeCode) {
if (nativeCode == null) {
return forBytecode();
}
return NativeMachineFactoryProvider.discover()
.map(ImportFactory::forNative)
.orElseGet(ImportFactory::forBytecode);
}

/** 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -18,6 +21,8 @@ public interface NativeMachineFactoryProvider {

TableInstance createImportTable(Table table, int initValue);

GlobalInstance createImportGlobal(Value value, MutabilityType mutability);

int priority();

static Optional<NativeMachineFactoryProvider> discover() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 aModuleWithoutNativeCodeGetsTheBytecodeTypes() {
assertFalse(ImportFactory.forNativeCode(null).isNative());
}

@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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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());
Expand Down Expand Up @@ -310,6 +312,24 @@ private static void generateNativeProviderMethod(ClassOrInterfaceDeclaration typ
new NameExpr("NativeMachineFactoryProvider"), "discover")));
}

private static void generateImportsMethod(ClassOrInterfaceDeclaration type) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this generated code in favor of, maybe, another method or an instance method on ImportFactory?

// Generates:
// <code>
// public static ImportFactory imports() {
// return ImportFactory.forNativeCode(loadNativeCode());
// }
// </code>
type.addMethod("imports", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
.setType(parseClassOrInterfaceType("ImportFactory"))
.createBody()
.addStatement(
new ReturnStmt(
new MethodCallExpr(
new NameExpr("ImportFactory"),
"forNativeCode",
new NodeList<>(new MethodCallExpr("loadNativeCode")))));
}

private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) {
// Generates:
// <code>
Expand Down
11 changes: 11 additions & 0 deletions redline/it/src/it/redline-e2e-panama/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@
<redlineExperimental>true</redlineExperimental>
</configuration>
</execution>
<execution>
<id>compile-imports</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<name>endive.test.ImportsModule</name>
<wasmFile>src/test/resources/imports.wat.wasm</wasmFile>
<redlineExperimental>true</redlineExperimental>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@

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;

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 {

Expand Down Expand Up @@ -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(
Expand Down
Binary file not shown.
11 changes: 11 additions & 0 deletions redline/it/src/it/redline-e2e/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@
<redlineExperimental>true</redlineExperimental>
</configuration>
</execution>
<execution>
<id>compile-imports</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<name>endive.test.ImportsModule</name>
<wasmFile>src/test/resources/imports.wat.wasm</wasmFile>
<redlineExperimental>true</redlineExperimental>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Expand Down
Loading
Loading