Skip to content
Open
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.
*
* <p>Natively compiled code reaches an import through a raw address, so it can only

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.

This comment is ways too long and it brings history with it which is useless for the user.
Should be very short and just tell the minimum important info.

* 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:
*
* <pre>
* var f = MyModule.imports();
* var memory = f.memory(new MemoryLimits(1, 2));
* </pre>
*
* <p>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();
}
}
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,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));
}
}
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,58 @@ 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() {
// var provider = nativeProvider();
// if (provider.isPresent()) {
// return ImportFactory.forNative(provider.get());
// }
// return ImportFactory.forBytecode();
// }
// </code>
//
// 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:
// <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