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
3 changes: 2 additions & 1 deletion docs/docs/experimental/redline.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ modes, so check [Feature support](#feature-support) before adopting it.

Redline compiles your Wasm module to native machine code using [Cranelift](https://cranelift.dev/),
instead of to JVM bytecode. Compilation happens at build time for every supported platform, and the
right one is selected at runtime.
right one is selected at runtime. Nothing is compiled at runtime: the runners only load the
precompiled code, and Cranelift is not one of their dependencies.

It is a substitute for the [Build Time Compiler](../execution/build-time-compiler.md) only, not for
the interpreter or the [Runtime Compiler](../execution/runtime-compiler.md), and it is enabled on
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
${project.version}. Bump to an immutable released tag before cutting a
release, so the published jars can be rebuilt from source.
-->
<cranelift-bridge.version>999.0.0-SNAPSHOT</cranelift-bridge.version>
<cranelift-bridge.version>999.0.0-redline-202</cranelift-bridge.version>
<commons-io.version>2.22.0</commons-io.version>
<commons-lang.version>3.21.0</commons-lang.version>
<jackson-annotations.version>2.22</jackson-annotations.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private ImportFactory(NativeMachineFactoryProvider 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) {
public static ImportFactory forNativeCode(NativeCode nativeCode) {
if (nativeCode == null) {
return forBytecode();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package run.endive.redline.experimental.api;

import java.util.Objects;

/** Function bodies and trampolines of one module, compiled ahead of time for one target. */
public final class NativeCode {

/** Offset of a function body that was not compiled. */
public static final int NOT_COMPILED = -1;

private final String triple;
private final byte[] image;
private final int[] bodyOffsets;
private final int[] entryTrampolineOffsets;
private final int[] importTrampolineOffsets;
// where the runner writes each import's host stub address, in native byte order
private final int[] importStubSlotOffsets;

public NativeCode(
String triple,
byte[] image,
int[] bodyOffsets,
int[] entryTrampolineOffsets,
int[] importTrampolineOffsets,
int[] importStubSlotOffsets) {
this.triple = Objects.requireNonNull(triple, "triple");
this.image = Objects.requireNonNull(image, "image");
this.bodyOffsets = bodyOffsets.clone();
this.entryTrampolineOffsets = entryTrampolineOffsets.clone();
this.importTrampolineOffsets = importTrampolineOffsets.clone();
this.importStubSlotOffsets = importStubSlotOffsets.clone();

if (this.bodyOffsets.length != this.entryTrampolineOffsets.length) {
throw new IllegalArgumentException(
this.bodyOffsets.length
+ " function bodies but "
+ this.entryTrampolineOffsets.length
+ " entry trampolines");
}
for (int i = 0; i < this.bodyOffsets.length; i++) {
boolean compiled = this.bodyOffsets[i] != NOT_COMPILED;
if (compiled != (this.entryTrampolineOffsets[i] != NOT_COMPILED)) {
throw new IllegalArgumentException(
"Function body " + i + " and its entry trampoline disagree");
}
if (compiled) {
checkOffset("function body " + i, this.bodyOffsets[i]);
checkOffset("entry trampoline " + i, this.entryTrampolineOffsets[i]);
}
}
if (this.importTrampolineOffsets.length != this.importStubSlotOffsets.length) {
throw new IllegalArgumentException(
this.importTrampolineOffsets.length
+ " import trampolines but "
+ this.importStubSlotOffsets.length
+ " stub slots");
}
for (int i = 0; i < this.importTrampolineOffsets.length; i++) {
checkOffset("import trampoline " + i, this.importTrampolineOffsets[i]);
checkOffset("import stub slot " + i, this.importStubSlotOffsets[i], 8);
}
}

private void checkOffset(String what, int offset) {
checkOffset(what, offset, 1);
}

private void checkOffset(String what, int offset, int size) {
if (offset < 0 || offset > image.length - size) {
throw new IllegalArgumentException(
what + " at " + offset + " is outside the " + image.length + " byte image");
}
}

public String triple() {
return triple;
}

/** The code image. Shared, not copied: callers must not modify it. */
public byte[] image() {
return image;
}

public int functionBodyCount() {
return bodyOffsets.length;
}

public boolean isCompiled(int bodyIndex) {
return bodyOffsets[bodyIndex] != NOT_COMPILED;
}

public int bodyOffset(int bodyIndex) {
return bodyOffsets[bodyIndex];
}

public int entryTrampolineOffset(int bodyIndex) {
return entryTrampolineOffsets[bodyIndex];
}

public int importCount() {
return importTrampolineOffsets.length;
}

public int importTrampolineOffset(int importIndex) {
return importTrampolineOffsets[importIndex];
}

public int importStubSlotOffset(int importIndex) {
return importStubSlotOffsets[importIndex];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,56 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.nio.ByteBuffer;

/**
* Serializes/deserializes pre-compiled native code (byte[][]).
* Serializes/deserializes pre-compiled {@link NativeCode}.
*
* <p>Format:
* <pre>
* [4 bytes: magic "CL4J"]
* [4 bytes: version (1)]
* [4 bytes: function count]
* For each function:
* [4 bytes: code length, 0 for null/uncompiled]
* [N bytes: native code]
* [4 bytes: version (2)]
* [modified UTF-8: target triple]
* [4 bytes: image length]
* [N bytes: image]
* [4 bytes: function body count]
* For each function body:
* [4 bytes: body offset, -1 for uncompiled]
* [4 bytes: entry trampoline offset, -1 for uncompiled]
* [4 bytes: imported function count]
* For each imported function:
* [4 bytes: import trampoline offset]
* [4 bytes: offset of the slot for its host stub address]
* </pre>
*/
public final class NativeCodeSerializer {

private static final int MAGIC = 0x434C344A; // "CL4J"
private static final int VERSION = 1;
private static final int VERSION = 2;

private NativeCodeSerializer() {}

public static void serialize(byte[][] code, OutputStream out) throws IOException {
public static void serialize(NativeCode code, OutputStream out) throws IOException {
DataOutputStream dos = new DataOutputStream(out);
dos.writeInt(MAGIC);
dos.writeInt(VERSION);
dos.writeInt(code.length);
for (byte[] func : code) {
if (func != null) {
dos.writeInt(func.length);
dos.write(func);
} else {
dos.writeInt(0);
}
dos.writeUTF(code.triple());
dos.writeInt(code.image().length);
dos.write(code.image());
dos.writeInt(code.functionBodyCount());
for (int i = 0; i < code.functionBodyCount(); i++) {
dos.writeInt(code.bodyOffset(i));
dos.writeInt(code.entryTrampolineOffset(i));
}
dos.writeInt(code.importCount());
for (int i = 0; i < code.importCount(); i++) {
dos.writeInt(code.importTrampolineOffset(i));
dos.writeInt(code.importStubSlotOffset(i));
}
dos.flush();
}

public static byte[][] deserialize(InputStream in) throws IOException {
public static NativeCode deserialize(InputStream in) throws IOException {
DataInputStream dis = new DataInputStream(in);
int magic = dis.readInt();
if (magic != MAGIC) {
Expand All @@ -55,39 +65,67 @@ public static byte[][] deserialize(InputStream in) throws IOException {
if (version != VERSION) {
throw new IOException("Unsupported native code version: " + version);
}
String triple = dis.readUTF();
byte[] image = readBytes(dis, readCount(dis, "image length"), "image");
int bodyCount = readCount(dis, "function body count");
// Read before allocating, so a corrupt count fails on the truncated file
int[] offsets = readInts(dis, bodyCount * 2L, "function body offsets");
int[] bodyOffsets = new int[bodyCount];
int[] entryTrampolineOffsets = new int[bodyCount];
for (int i = 0; i < bodyCount; i++) {
bodyOffsets[i] = offsets[i * 2];
entryTrampolineOffsets[i] = offsets[i * 2 + 1];
}
int importCount = readCount(dis, "imported function count");
int[] importOffsets = readInts(dis, importCount * 2L, "import offsets");
int[] importTrampolineOffsets = new int[importCount];
int[] importStubSlotOffsets = new int[importCount];
for (int i = 0; i < importCount; i++) {
importTrampolineOffsets[i] = importOffsets[i * 2];
importStubSlotOffsets[i] = importOffsets[i * 2 + 1];
}
try {
return new NativeCode(
triple,
image,
bodyOffsets,
entryTrampolineOffsets,
importTrampolineOffsets,
importStubSlotOffsets);
} catch (IllegalArgumentException e) {
throw new IOException("Invalid native code file: " + e.getMessage(), e);
}
}

private static int readCount(DataInputStream dis, String what) throws IOException {
int count = dis.readInt();
if (count < 0) {
throw new IOException("Invalid native code file: negative function count " + count);
throw new IOException("Invalid native code file: negative " + what + " " + count);
}
// Collected rather than pre-allocated: a corrupt count would otherwise
// reserve up to 2^31 array slots before any read could reveal the file is
// truncated, turning a bad file into an OutOfMemoryError.
List<byte[]> code = new ArrayList<>(Math.min(count, 1024));
for (int i = 0; i < count; i++) {
int len = dis.readInt();
if (len < 0) {
throw new IOException(
"Invalid native code file: negative code length "
+ len
+ " for function "
+ i);
}
if (len == 0) {
code.add(null);
continue;
}
byte[] func = dis.readNBytes(len);
if (func.length != len) {
throw new IOException(
"Truncated native code for function "
+ i
+ ": expected "
+ len
+ " bytes, got "
+ func.length);
}
code.add(func);
return count;
}

private static byte[] readBytes(DataInputStream dis, int len, String what) throws IOException {
byte[] bytes = dis.readNBytes(len);
if (bytes.length != len) {
throw new IOException(
"Truncated native code "
+ what
+ ": expected "
+ len
+ " bytes, got "
+ bytes.length);
}
return bytes;
}

private static int[] readInts(DataInputStream dis, long count, String what) throws IOException {
if (count > Integer.MAX_VALUE / 4) {
throw new IOException("Invalid native code file: too many " + what);
}
return code.toArray(new byte[0][]);
byte[] bytes = readBytes(dis, (int) count * 4, what);
int[] ints = new int[(int) count];
ByteBuffer.wrap(bytes).asIntBuffer().get(ints);
return ints;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

public interface NativeMachineFactoryProvider {

Instance.Builder builder(WasmModule module, byte[][] precompiledCode);
Instance.Builder builder(WasmModule module, NativeCode precompiledCode);

Memory createMemory(MemoryLimits limits);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
* ------ ----- ---------------- ------------------------------------------
* Total: 248 bytes used, 256 allocated (CTX_SIZE)
* </pre>
*
* <p>Compiled code calls the host function pointers here with the platform ABI.
*/
public final class CtxBuffer {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package run.endive.redline.experimental.api.internal;

import run.endive.redline.experimental.api.NativeCode;
import run.endive.wasm.WasmEngineException;
import run.endive.wasm.WasmModule;
import run.endive.wasm.types.ExternalType;

/** Checks native code was compiled for this host and module before a runner links it. */
public final class NativeCodeCheck {

private NativeCodeCheck() {}

public static NativeCode check(NativeCode code, WasmModule module) {
if (code == null) {
throw new WasmEngineException(
"No precompiled code provided. Precompile the module with the"
+ " endive-compiler-maven-plugin and a Redline target.");
}
String host = RedlineTarget.detectHost().map(RedlineTarget::triple).orElse(null);
if (!code.triple().equals(host)) {
throw new WasmEngineException(
"Native code compiled for "
+ code.triple()
+ " cannot run on "
+ (host == null ? "this unsupported platform" : host));
}
int bodies = module.codeSection().functionBodyCount();
if (code.functionBodyCount() != bodies) {
throw new WasmEngineException(
"Native code has "
+ code.functionBodyCount()
+ " function bodies but the module has "
+ bodies);
}
long imports =
module.importSection().stream()
.filter(i -> i.importType() == ExternalType.FUNCTION)
.count();
if (code.importCount() != imports) {
throw new WasmEngineException(
"Native code has "
+ code.importCount()
+ " imported functions but the module has "
+ imports);
}
return code;
}
}
Loading
Loading