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
Expand Up @@ -73,6 +73,12 @@ private CtxBuffer() {}
public static final int TRAP_UNALIGNED_ATOMIC = 12;
public static final int TRAP_INTERRUPTED = 13;

/**
* A host function threw. The throwable itself is held by the runner; this only
* marks the context so compiled code unwinds instead of running on.
*/
public static final int TRAP_HOST_EXCEPTION = 14;

public static final int TABLE_SIZE_OFFSET = 0;
public static final int TABLE_MAX_OFFSET = 4;
public static final int TABLE_ENTRIES_OFFSET = 8;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,14 @@ ValType resolveGlobalType(int globalIdx) {
return module.globalSection().getGlobal(moduleGlobalIdx).valueType();
}

/**
* Widens a value for the argument buffer a host import reads. i32 is
* sign-extended so a host handed -1 sees -1, matching the interpreter, rather
* than 4294967295.
*/
int widenToI64(int valId, ValType type) {
if (type.equals(ValType.I32)) {
return bridge.exports().emitUextendI64(valId);
}
return valId;
}

int narrowFromI64(int valId, ValType type) {
if (type.equals(ValType.I32)) {
return bridge.exports().emitIreduceI32(valId);
return bridge.exports().emitSextendI64(valId);
}
return valId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,24 @@ static void fillTrapBlock(EmitContext ctx, int trapBlock, int trapCode) {
ctx.emitReturnForFuncType();
}

/**
* A callee that traps records its code and returns like any other call, so the
* caller has to look for it. Without this check the caller runs on to
* completion after the trap: its stores land and its host imports fire.
*/
static void emitTrapCheck(EmitContext ctx) {
var b = ctx.bridge.exports();
int zero = b.emitIconst32(0);
int trapCode = b.emitLoadI32(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAP_CODE);
int trapped = b.emitIcmp(1, trapCode, b.emitIconst32(0));
int propagateBlock = b.createBlock();
int continueBlock = b.createBlock();
b.emitBrif(trapped, propagateBlock, continueBlock);
b.switchToBlock(propagateBlock);
ctx.emitReturnForFuncType();
b.switchToBlock(continueBlock);
}

// --- Extensions ---

static void emitI32Extend8S(EmitContext ctx) {
Expand Down Expand Up @@ -768,6 +786,7 @@ static void emitCall(EmitContext ctx, AnnotatedInstruction ins) {
}

int rawResult = ctx.bridge.exports().emitCallIndirect(sigRef, funcPtr);
emitTrapCheck(ctx);

if (calleeMultiReturn) {
// Read return values from argsBuffer
Expand Down Expand Up @@ -887,6 +906,7 @@ static void emitCallIndirect(EmitContext ctx, AnnotatedInstruction ins) {
}

int rawResult = b.emitCallIndirect(sigRef, funcPtr);
emitTrapCheck(ctx);

// 9. Handle results
if (calleeMultiReturn) {
Expand Down
3 changes: 1 addition & 2 deletions redline/runner-jffi-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,7 @@
SpecV1ImportsTest.test118, SpecV1ImportsTest.test119, SpecV1ImportsTest.test120,
SpecV1ImportsTest.test123, SpecV1ImportsTest.test124, SpecV1ImportsTest.test125,
SpecV1ImportsTest.test127, SpecV1ImportsTest.test128, SpecV1ImportsTest.test129,
SpecV1LinkingTest.test129, SpecV1LinkingTest.test130, SpecV1LinkingTest.test131,
SpecV1StartTest.test18,
SpecV1LinkingTest.test130, SpecV1LinkingTest.test131,
SpecV1FuncTest.test85,
SpecV1ThreadsImportsTest.test64, SpecV1ThreadsImportsTest.test65,
SpecV1ThreadsImportsTest.test66,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package run.endive.redline.experimental.runner.jffi.internal;

import static org.junit.jupiter.api.Assertions.assertEquals;

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.HostFunction;
import run.endive.runtime.ImportValues;
import run.endive.runtime.Instance;
import run.endive.wasm.Parser;
import run.endive.wasm.types.FunctionType;
import run.endive.wasm.types.ValType;
import run.endive.wasm.types.Value;

/**
* Values crossing the host boundary are marshalled by hand in each runner, so each
* conversion is a place they can be mangled. The spec suite does not reach these:
* it drives modules that are self-contained rather than calling back into Java.
*/
public class HostImportRoundTripTest {

@Test
public void floatResultKeepsItsBitPattern() {
try (var instance = buildInstance()) {
assertEquals(
1.5f,
Value.longToFloat(instance.export("callRetF32").apply()[0]),
"a float result must be reinterpreted, not converted numerically");
}
}

@Test
public void doubleResultKeepsItsBitPattern() {
try (var instance = buildInstance()) {
assertEquals(
2.5d,
Value.longToDouble(instance.export("callRetF64").apply()[0]),
"a double result must be reinterpreted, not converted numerically");
}
}

@Test
public void negativeI32ArgumentArrivesSignExtended() {
try (var instance = buildInstance()) {
assertEquals(
1,
(int) instance.export("callTakeI32").apply()[0],
"the host must be handed -1, not 4294967295");
}
}

@Test
public void multiValueResultKeepsEveryValue() {
try (var instance = buildInstance()) {
assertEquals(
30,
(int) instance.export("callRetPairSum").apply()[0],
"both results of a multi-value host import must arrive");
}
}

private static Instance buildInstance() {
var module =
Parser.parse(
CorpusResources.getResource("compiled/host-import-roundtrip.wat.wasm"));

var imports =
ImportValues.builder()
.addFunction(
new HostFunction(
"host",
"retF32",
FunctionType.of(
java.util.List.of(),
java.util.List.of(ValType.F32)),
(inst, args) -> new long[] {Value.floatToLong(1.5f)}),
new HostFunction(
"host",
"retF64",
FunctionType.of(
java.util.List.of(),
java.util.List.of(ValType.F64)),
(inst, args) -> new long[] {Value.doubleToLong(2.5d)}),
new HostFunction(
"host",
"takeI32",
FunctionType.of(
java.util.List.of(ValType.I32),
java.util.List.of(ValType.I32)),
// Reports on the raw long it was handed rather
// than echoing it: an echo would be truncated
// back to -1 on the way out and hide a
// zero-extended argument.
(inst, args) -> new long[] {args[0] == -1L ? 1 : 0}),
new HostFunction(
"host",
"retPair",
FunctionType.of(
java.util.List.of(),
java.util.List.of(ValType.I32, ValType.I32)),
(inst, args) -> new long[] {10, 20}))
.build();

return JffiNativeMachineFactory.builder(module)
.withImportValues(imports)
.withCompilerFunction(
m ->
NativeCompiler.compileAll(
RedlineTarget.detectHost().orElseThrow().triple(), m))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package run.endive.redline.experimental.runner.jffi.internal;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

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.HostFunction;
import run.endive.runtime.ImportValues;
import run.endive.wasm.Parser;
import run.endive.wasm.types.FunctionType;

/**
* An exception from a host function has to abandon the module the same way a trap
* does, otherwise the module keeps running on state the host has already rejected.
*/
public class HostThrowPropagationTest {

private static final class Boom extends RuntimeException {
Boom() {
super("boom");
}
}

@Test
public void moduleStopsWhenAHostFunctionThrows() {
var module =
Parser.parse(
CorpusResources.getResource(
"compiled/host-throw-stops-execution.wat.wasm"));

var imports =
ImportValues.builder()
.addFunction(
new HostFunction(
"host",
"boom",
FunctionType.of(java.util.List.of(), java.util.List.of()),
(inst, args) -> {
throw new Boom();
}))
.build();

try (var instance =
JffiNativeMachineFactory.builder(module)
.withImportValues(imports)
.withCompilerFunction(
m ->
NativeCompiler.compileAll(
RedlineTarget.detectHost().orElseThrow().triple(),
m))
.build()) {
assertThrows(Boom.class, () -> instance.export("callBoom").apply());
assertEquals(
0,
instance.memory().readInt(0),
"the store after the throwing host call must never run");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package run.endive.redline.experimental.runner.jffi.internal;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import org.junit.jupiter.api.AfterEach;
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.HostFunction;
import run.endive.runtime.ImportValues;
import run.endive.wasm.Parser;
import run.endive.wasm.types.FunctionType;

/**
* The watchdog raises the interrupt flag from another thread, so it can land after
* the call it was meant to stop has passed its last check. The flag must not then
* sit in the context and stop a later call that nobody interrupted.
*/
public class InterruptFlagTest {

@AfterEach
public void clearInterruptStatus() {
// Keeps a failure from leaking an interrupt into the rest of the suite.
Thread.interrupted();
}

@Test
public void aFlagRaisedMidCallDoesNotStopTheNextCall() {
var module =
Parser.parse(CorpusResources.getResource("compiled/interrupt-midcall.wat.wasm"));

var machineRef = new JffiNativeMachine[1];
var imports =
ImportValues.builder()
.addFunction(
new HostFunction(
"host",
"raiseFlag",
FunctionType.of(java.util.List.of(), java.util.List.of()),
(inst, args) -> {
machineRef[0].requestInterrupt();
return null;
}))
.build();

try (var instance =
JffiNativeMachineFactory.builder(module)
.withImportValues(imports)
.withCompilerFunction(
m ->
NativeCompiler.compileAll(
RedlineTarget.detectHost().orElseThrow().triple(),
m))
.build()) {
machineRef[0] = (JffiNativeMachine) instance.getMachine();

// Returns normally: the entry check ran before the flag was raised.
instance.export("callHost").apply();

assertEquals(
42,
(int) instance.export("answer").apply()[0],
"a flag left over from the previous call must not stop this one");
assertFalse(
Thread.currentThread().isInterrupted(),
"no interrupt happened, so the caller must not be left interrupted");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package run.endive.redline.experimental.runner.jffi.internal;

import static org.junit.jupiter.api.Assertions.assertEquals;

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.wasm.Parser;
import run.endive.wasm.WasmModule;

/**
* Everything a machine releases on close is an off-heap free, so closing twice
* has to be a no-op rather than a double free, and a memory the instance only
* borrowed has to survive it.
*/
public class LifecycleTest {

@Test
public void closingTwiceIsSafe() {
var instance = build(parse());
instance.close();
instance.close();
}

@Test
public void aMemoryTheModuleDefinesStillWorksBeforeClose() {
try (var instance = build(parse())) {
instance.memory().writeI32(0, 0x5A5A5A5A);
assertEquals(0x5A5A5A5A, instance.memory().readInt(0));
}
}

private static WasmModule parse() {
return Parser.parse(CorpusResources.getResource("compiled/trap-stops-execution.wat.wasm"));
}

private static run.endive.runtime.Instance build(WasmModule module) {
return JffiNativeMachineFactory.builder(module)
.withCompilerFunction(
m ->
NativeCompiler.compileAll(
RedlineTarget.detectHost().orElseThrow().triple(), m))
.build();
}
}
Loading
Loading