diff --git a/.github/workflows/redline.yaml b/.github/workflows/redline.yaml deleted file mode 100644 index fda288f84..000000000 --- a/.github/workflows/redline.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: Redline CI - -on: - push: - branches: [ main ] - pull_request: - -jobs: - redline: - name: Redline (Java 25, ${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - name: Checkout sources - uses: actions/checkout@v7 - - name: Checkout testsuite - uses: actions/checkout@v7 - with: - repository: WebAssembly/testsuite - path: testsuite - ref: 88e97b0f742f4c3ee01fea683da130f344dd7b02 - - name: Set up Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '25' - cache: maven - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - name: Build cranelift_bridge.wasm - working-directory: redline/wasm-build - run: make all - - name: Build and test redline - run: ./mvnw -B clean install -Predline - env: - MAVEN_OPTS: "-ea" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 84c647e4f..d5b8eac23 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -37,32 +37,55 @@ jobs: gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-wasip1 - - id: install-secret-key name: Install gpg secret key run: | cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import gpg --list-secret-keys --keyid-format LONG - - name: Build cranelift_bridge.wasm - working-directory: redline/wasm-build - run: make all - - name: Compile - run: ./mvnw --batch-mode -Dquickly -Predline + run: ./mvnw --batch-mode -Dquickly - name: Setup Git run: | git config user.name "Endive BOT" git config user.email "endive@bytecodealliance.org" + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + # Jars must be built against an immutable wasm tag: the development + # snapshot is mutable, so a release built against it stops being + # reproducible the moment that tag moves. + # + # This retags the digest already pinned in wkg.lock rather than rebuilding + # from Rust, so the released wasm is byte-identical to the one CI tested + # and no toolchain is needed here. + - name: Publish the bridge wasm under the release version + run: | + set -euo pipefail + DIGEST=$(grep -oE 'sha256:[0-9a-f]{64}' redline/wkg.lock | head -1 || true) + if [ -z "$DIGEST" ]; then + echo "::error::No digest found in redline/wkg.lock" + exit 1 + fi + echo "Retagging $DIGEST as $VERSION" + echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin + oras tag "ghcr.io/bytecodealliance/endive-cranelift-bridge@${DIGEST}" "$VERSION" + env: + VERSION: ${{ github.event.inputs.release-version }} + + # Runs before "Set the version" so the property and lock changes are + # swept into the release commit by its "git add ." below. + - name: Pin the build to the released wasm + run: | + ./mvnw versions:set-property -Dproperty=cranelift-bridge.version \ + -DnewVersion=${{ github.event.inputs.release-version }} -DgenerateBackupPoms=false + ./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update + - name: Set the version run: | - ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} -Predline + ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.release-version }} git add . git commit -m "Release version update ${{ github.event.inputs.release-version }}" git push @@ -71,11 +94,25 @@ jobs: env: GITHUB_TOKEN: ${{secrets.GH_TOKEN}} + # Last line of defence: if the steps above were skipped or edited away, the + # jars would be built from a mutable tag and could not be rebuilt later. + - name: Verify the bridge wasm tag is immutable + run: | + set -euo pipefail + v=$(./mvnw help:evaluate -Dexpression=cranelift-bridge.version -q -DforceStdout) + echo "cranelift-bridge.version = $v" + case "$v" in + *SNAPSHOT*) + echo "::error::Refusing to release jars built against the mutable wasm tag '$v'" + exit 1 + ;; + esac + - name: Release to Maven Central run: | # -Dquickly is needed to locally publish wasm-corpus - ./mvnw --batch-mode -Dquickly -Predline - ./mvnw --batch-mode clean deploy -Drelease -Predline -DskipTests=true -X + ./mvnw --batch-mode -Dquickly + ./mvnw --batch-mode clean deploy -Drelease -DskipTests=true -X env: MAVEN_USERNAME: ${{ secrets.SONATYPE_USERNAME }} MAVEN_CENTRAL_TOKEN: ${{ secrets.SONATYPE_PASSWORD }} @@ -83,7 +120,10 @@ jobs: - name: Back to Snapshot run: | - ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT -Predline + ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion=999-SNAPSHOT + ./mvnw versions:set-property -Dproperty=cranelift-bridge.version \ + -DnewVersion=999.0.0-SNAPSHOT -DgenerateBackupPoms=false + ./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update git add . git commit -m "Snapshot version update" git push diff --git a/.github/workflows/wasm-publish.yaml b/.github/workflows/wasm-publish.yaml new file mode 100644 index 000000000..d101f71ac --- /dev/null +++ b/.github/workflows/wasm-publish.yaml @@ -0,0 +1,96 @@ +name: Publish cranelift_bridge.wasm + +on: + push: + branches: [main] + paths: ['redline/wasm-build/**'] + workflow_dispatch: + inputs: + version: + description: >- + Semver tag to publish, e.g. 1.2.0. Use an immutable release tag when + preparing a release; leave the default to refresh the development + snapshot. Must be valid semver: wkg.lock rejects tags like "latest". + required: true + default: 999.0.0-SNAPSHOT + +# Pushes to main refresh the development snapshot; a manual run can publish any +# semver tag. Must match cranelift-bridge.version in the root pom.xml for the +# build to actually consume what was published. +env: + WASM_VERSION: ${{ inputs.version || '999.0.0-SNAPSHOT' }} + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Checkout sources + uses: actions/checkout@v7 + + # Catches "latest", "1.2" and similar before anything is pushed: an + # unparseable tag only fails later, when a consumer writes wkg.lock. + - name: Validate version is semver + run: | + if ! echo "$WASM_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then + echo "::error::'$WASM_VERSION' is not valid semver; wkg.lock would reject it" + exit 1 + fi + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + + - name: Build cranelift_bridge.wasm + working-directory: redline/wasm-build + run: make all + + - name: Install ORAS + uses: oras-project/setup-oras@v1 + + - name: Login to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin + + # Pushed from inside redline/ so the org.opencontainers.image.title + # annotation is a bare filename. A path like "redline/cranelift_bridge.wasm" + # makes the OCI client try to write into a directory that does not exist + # on pull. + # + # The source annotation links the package to this repository, which is what + # lets GITHUB_TOKEN write to it from Actions and makes it show up under the + # repo's packages. + - name: Push to GHCR + working-directory: redline + run: | + oras push \ + --annotation "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \ + ghcr.io/bytecodealliance/endive-cranelift-bridge:${WASM_VERSION} \ + cranelift_bridge.wasm:application/wasm + + - name: Next steps + run: | + { + echo "Published \`$WASM_VERSION\`." + echo + echo "Publishing does not update the lock file. Until it is refreshed the" + echo "build keeps resolving the previously pinned digest, and re-pushing an" + echo "already-locked tag makes every build fail with a digest mismatch." + echo + echo "To consume it:" + echo + echo '```bash' + echo "# 1. point the build at this tag" + echo "./mvnw versions:set-property -Dproperty=cranelift-bridge.version \\" + echo " -DnewVersion=$WASM_VERSION -DgenerateBackupPoms=false" + echo + echo "# 2. re-pin the digest" + echo "./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update" + echo + echo "# 3. commit both" + echo "git commit -am 'Use cranelift_bridge.wasm $WASM_VERSION'" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5464aa124..5d579a655 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,36 @@ Basic steps: note: if you're working using a *corporate proxy* (or anything like this), you might need to pass the usual `-Dhttps.proxyHost=...` and `-Dhttps.proxyPort=...` in order to properly instruct Maven about this (this can be required for example for `test-gen-plugin` since it downloads the testsuite). +### Redline and the Cranelift bridge + +The experimental redline native compiler needs `cranelift_bridge.wasm`, a Rust crate compiled to `wasm32-wasip1`. **You do not need a Rust toolchain to build Endive.** The [inlay](https://github.com/roastedroot/inlay) Maven plugin downloads a prebuilt copy from GHCR during `generate-sources`, pinned by digest in `redline/wkg.lock`, so a fresh clone builds with a plain `mvn clean install`. + +To work on the Rust side you do need Rust with the `wasm32-wasip1` target: + +* `make -C redline/wasm-build all` builds `redline/cranelift_bridge.wasm` (gitignored) +* inlay skips the download whenever that file already exists, so your local build picks it up + +That skip has a sharp edge: a **stale** `redline/cranelift_bridge.wasm` left over from an earlier `make all` silently shadows the pinned artifact, and you end up testing against a different bridge than CI. Delete the file to go back to the published one. + +Publishing is handled by `.github/workflows/wasm-publish.yaml`. Pushes to `main` touching `redline/wasm-build/**` refresh the development snapshot; a manual dispatch can publish any semver tag. The tag must be valid semver — the `wkg.lock` format rejects `latest` — and the workflow validates that before pushing anything. + +Which tag the build consumes is the `cranelift-bridge.version` property in the root `pom.xml`. Publishing does **not** update it, and does **not** refresh the lock file: until both are updated the build keeps resolving the previously pinned digest, and re-pushing an already-locked tag makes every build fail with a digest mismatch rather than silently drifting. + +To adopt a published wasm (the workflow prints these in its job summary): + +```bash +# 1. point the build at the tag that was published +./mvnw versions:set-property -Dproperty=cranelift-bridge.version \ + -DnewVersion= -DgenerateBackupPoms=false + +# 2. re-pin the digest +./mvnw generate-sources -pl :redline-bridge-experimental -Dinlay.update + +# 3. commit pom.xml and redline/wkg.lock together +``` + +**Releases handle this automatically.** `release.yaml` retags the digest currently pinned in `wkg.lock` as the release version, points the property at it, re-pins the lock, and commits both alongside the version bump — so every release has a matching immutable wasm artifact, byte-identical to the one CI tested. It retags rather than rebuilding, so the release needs no Rust toolchain. Afterwards it restores the snapshot property, and a guard refuses to deploy if the property still resolves to a `SNAPSHOT` tag. + ### Proposals implementation Our priority is to focus on implementing [proposals](https://github.com/WebAssembly/proposals) that are in the most advanced stages of development. While we wholeheartedly encourage and support explorations, we’ll be dedicating less time to early-stage proposals until we have more comprehensive support for those that are stabilized. diff --git a/bom/pom.xml b/bom/pom.xml index af3f71462..afe9217ed 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -78,6 +78,11 @@ redline-bridge-experimental ${project.version} + + run.endive + redline-build-time-compiler-experimental + ${project.version} + run.endive redline-compiler-experimental diff --git a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java index 9e4245caa..a97b23a8b 100644 --- a/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java +++ b/build-time-compiler-cli/src/main/java/run/endive/experimental/compiler/cli/Cli.java @@ -71,6 +71,14 @@ public String[] getVersion() { "The indexes of functions that should be interpreted, separated by commas") Set interpretedFunctions; + @CommandLine.Option( + order = 7, + names = "--module-interface", + description = + "Fully qualified class name for which to generate _ModuleExports and" + + " _ModuleImports wrappers") + String moduleInterface; + @Override public void run() { var config = @@ -82,6 +90,7 @@ public void run() { .withTargetWasmFolder(targetWasmFolder) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) + .withModuleInterface(moduleInterface) .build(); var generator = new Generator(config); @@ -90,13 +99,15 @@ public void run() { var interpretedFunctions = generator.generateResources(); generator.generateMetaWasm(interpretedFunctions); generator.generateSources(); + if (moduleInterface != null && !moduleInterface.isEmpty()) { + generator.generateModuleInterface(moduleInterface); + } } catch (IOException e) { throw new CommandLine.PicocliException("Failed to execute the command", e); } } public static void main(String[] args) { - int exitCode = new CommandLine(new Cli()).execute(args); - System.exit(exitCode); + System.exit(new CommandLine(new Cli()).execute(args)); } } diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java index 1aba2b9b9..5d6a7c484 100644 --- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Config.java @@ -1,6 +1,7 @@ package run.endive.build.time.compiler; import java.nio.file.Path; +import java.util.List; import java.util.Set; import java.util.StringJoiner; import run.endive.compiler.InterpreterFallback; @@ -46,6 +47,11 @@ public final class Config { */ private final String moduleInterface; + /** + * target triples for redline native compilation (empty = no native compilation) + */ + private final List redlineTargets; + private Config( Path wasmFile, String name, @@ -54,7 +60,8 @@ private Config( Path targetWasmFolder, InterpreterFallback interpreterFallback, Set interpretedFunctions, - String moduleInterface) { + String moduleInterface, + List redlineTargets) { this.wasmFile = wasmFile; this.name = name; this.targetClassFolder = targetClassFolder; @@ -63,6 +70,7 @@ private Config( this.interpreterFallback = interpreterFallback; this.interpretedFunctions = interpretedFunctions; this.moduleInterface = moduleInterface; + this.redlineTargets = redlineTargets; } public Path wasmFile() { @@ -97,6 +105,14 @@ public String moduleInterface() { return moduleInterface; } + public List redlineTargets() { + return redlineTargets; + } + + public boolean hasRedlineTargets() { + return redlineTargets != null && !redlineTargets.isEmpty(); + } + public static Builder builder() { return new Builder(); } @@ -126,6 +142,7 @@ public static final class Builder { private InterpreterFallback interpreterFallback = InterpreterFallback.FAIL; private Set interpretedFunctions; private String moduleInterface; + private List redlineTargets = List.of(); private Builder() {} @@ -169,6 +186,11 @@ public Builder withModuleInterface(String moduleInterface) { return this; } + public Builder withRedlineTargets(List redlineTargets) { + this.redlineTargets = redlineTargets; + return this; + } + public Config build() { return new Config( wasmFile, @@ -178,7 +200,8 @@ public Config build() { targetWasmFolder, interpreterFallback, interpretedFunctions, - moduleInterface); + moduleInterface, + redlineTargets); } } } diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java new file mode 100644 index 000000000..fa24c8d3d --- /dev/null +++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/GeneratorMain.java @@ -0,0 +1,40 @@ +package run.endive.build.time.compiler; + +import java.io.IOException; +import java.nio.file.Path; +import run.endive.compiler.InterpreterFallback; + +public final class GeneratorMain { + + private GeneratorMain() {} + + public static void main(String[] args) throws IOException { + if (args.length < 5) { + throw new IllegalArgumentException( + "Usage: GeneratorMain " + + " " + + " [interpreterFallback] [moduleInterface]"); + } + var configBuilder = + Config.builder() + .withWasmFile(Path.of(args[0])) + .withName(args[1]) + .withTargetClassFolder(Path.of(args[2])) + .withTargetSourceFolder(Path.of(args[3])) + .withTargetWasmFolder(Path.of(args[4])); + if (args.length > 5 && !args[5].isEmpty()) { + configBuilder.withInterpreterFallback(InterpreterFallback.valueOf(args[5])); + } + if (args.length > 6 && !args[6].isEmpty()) { + configBuilder.withModuleInterface(args[6]); + } + var config = configBuilder.build(); + var generator = new Generator(config); + var interpreted = generator.generateResources(); + generator.generateMetaWasm(interpreted); + generator.generateSources(); + if (config.moduleInterface() != null && !config.moduleInterface().isEmpty()) { + generator.generateModuleInterface(config.moduleInterface()); + } + } +} diff --git a/compiler-maven-plugin/pom.xml b/compiler-maven-plugin/pom.xml index 223b8df00..a7ea02a6a 100644 --- a/compiler-maven-plugin/pom.xml +++ b/compiler-maven-plugin/pom.xml @@ -22,6 +22,10 @@ run.endive compiler + + run.endive + redline-build-time-compiler-experimental + org.apache.maven maven-core diff --git a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java index 5866a32ed..cf332d091 100644 --- a/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/run/endive/build/time/maven/EndiveCompilerGenMojo.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.List; import java.util.Set; import java.util.TreeSet; import org.apache.maven.model.Resource; @@ -14,6 +15,7 @@ import run.endive.build.time.compiler.Config; import run.endive.build.time.compiler.Generator; import run.endive.compiler.InterpreterFallback; +import run.endive.redline.experimental.build.RedlineGenerator; /** * This plugin generates an invokable library from the compiled Wasm @@ -77,6 +79,20 @@ public class EndiveCompilerGenMojo extends AbstractMojo { @Parameter(required = false) String moduleInterface; + /** + * Enable Redline native compilation (experimental) for all supported + * platforms (x86_64 and aarch64 on Linux, macOS, and Windows). + */ + @Parameter(required = false, defaultValue = "false") + boolean redlineExperimental; + + /** + * Target triples for Redline native compilation. Overrides {@code redlineExperimental} + * for fine-grained control over which platforms to cross-compile for. + */ + @Parameter(required = false) + List redlineTargetsExperimental; + /** * The current Maven project. */ @@ -87,7 +103,7 @@ public class EndiveCompilerGenMojo extends AbstractMojo { public void execute() throws MojoExecutionException { getLog().info("Compiling classes for " + name + " from " + wasmFile); - var config = + var configBuilder = Config.builder() .withWasmFile(wasmFile.toPath()) .withName(name) @@ -96,8 +112,13 @@ public void execute() throws MojoExecutionException { .withTargetWasmFolder(targetWasmFolder.toPath()) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) - .withModuleInterface(moduleInterface) - .build(); + .withModuleInterface(moduleInterface); + if (redlineTargetsExperimental != null && !redlineTargetsExperimental.isEmpty()) { + configBuilder.withRedlineTargets(redlineTargetsExperimental); + } else if (redlineExperimental) { + configBuilder.withRedlineTargets(RedlineGenerator.allTargets()); + } + var config = configBuilder.build(); var generator = new Generator(config); @@ -106,6 +127,13 @@ public void execute() throws MojoExecutionException { generator.generateMetaWasm(finalInterpretedFunctions); generator.generateSources(); + if (config.hasRedlineTargets()) { + getLog().info("Redline native compilation for targets: " + config.redlineTargets()); + var redlineGenerator = new RedlineGenerator(config); + redlineGenerator.generateNativeCode(); + redlineGenerator.extendGeneratedSources(); + } + if (moduleInterface != null && !moduleInterface.isEmpty()) { generator.generateModuleInterface(moduleInterface); } diff --git a/pom.xml b/pom.xml index 58f1441f0..466fac6ab 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,15 @@ 31.0.0 9.10.1 + + 999.0.0-SNAPSHOT 2.22.0 3.20.0 2.22 @@ -93,6 +102,7 @@ 3.10.1 0.11.0 3.6.3 + 0.0.3 3.9.0 3.1.0 3.2.0 @@ -268,6 +278,11 @@ redline-bridge-experimental ${project.version} + + run.endive + redline-build-time-compiler-experimental + ${project.version} + run.endive redline-compiler-experimental @@ -409,6 +424,11 @@ spotless-maven-plugin ${spotless-maven-plugin.version} + + io.roastedroot + inlay-maven-plugin + ${inlay-maven-plugin.version} + org.apache.maven.plugins maven-antrun-plugin @@ -1030,13 +1050,6 @@ - - redline - - redline - - - default-all-modules @@ -1062,6 +1075,7 @@ jmh log machine-tests + redline runtime runtime-tests test-gen-lib diff --git a/redline/api/pom.xml b/redline/api/pom.xml index 05068e3b2..3b0717413 100644 --- a/redline/api/pom.xml +++ b/redline/api/pom.xml @@ -22,5 +22,15 @@ run.endive wasm + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java index f4cf3cb87..3267aa619 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java @@ -5,6 +5,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** * Serializes/deserializes pre-compiled native code (byte[][]). @@ -54,22 +56,38 @@ public static byte[][] deserialize(InputStream in) throws IOException { throw new IOException("Unsupported native code version: " + version); } int count = dis.readInt(); - byte[][] code = new byte[count][]; + if (count < 0) { + throw new IOException("Invalid native code file: negative function count " + 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 code = new ArrayList<>(Math.min(count, 1024)); for (int i = 0; i < count; i++) { int len = dis.readInt(); - if (len > 0) { - code[i] = dis.readNBytes(len); - if (code[i].length != len) { - throw new IOException( - "Truncated native code for function " - + i - + ": expected " - + len - + " bytes, got " - + code[i].length); - } + 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 code; + return code.toArray(new byte[0][]); } } 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 2ff6f2b27..c3d85ae34 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 @@ -22,14 +22,24 @@ public interface NativeMachineFactoryProvider { static Optional discover() { NativeMachineFactoryProvider best = null; - var loader = ServiceLoader.load(NativeMachineFactoryProvider.class); - for (var provider : loader) { + var it = ServiceLoader.load(NativeMachineFactoryProvider.class).iterator(); + while (it.hasNext()) { + NativeMachineFactoryProvider provider; try { - if (best == null || provider.priority() > best.priority()) { - best = provider; - } - } catch (ServiceConfigurationError e) { - // Provider can't load on this JDK (e.g., Panama on JDK < 25) — skip + provider = it.next(); + } catch (ServiceConfigurationError | LinkageError e) { + // This provider cannot be loaded on this JDK — the Panama runner is + // compiled for 25, so instantiating it on an older JDK fails here. + // Skip it and let a lower-priority provider win. + // + // The catch must wrap next(): ServiceLoader reports these failures + // from the iterator, not from anything we do with the provider, so a + // for-each loop would let them escape. next() has already advanced + // past the failed provider, so this cannot spin. + continue; + } + if (best == null || provider.priority() > best.priority()) { + best = provider; } } return Optional.ofNullable(best); diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java index 933d2feef..42f0f5f54 100644 --- a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/RedlineTarget.java @@ -27,6 +27,15 @@ public String resourceSuffix() { return resourceSuffix; } + public static Optional fromTriple(String triple) { + for (RedlineTarget target : values()) { + if (target.triple.equals(triple)) { + return Optional.of(target); + } + } + return Optional.empty(); + } + public static Optional detectHost() { String osName = System.getProperty("endive.redline.os.name", System.getProperty("os.name", "")) @@ -36,6 +45,15 @@ public static Optional detectHost() { .toLowerCase(Locale.ROOT); boolean isAarch64 = arch.equals("aarch64") || arch.equals("arm64"); + boolean isX8664 = arch.equals("x86_64") || arch.equals("amd64") || arch.equals("x64"); + + // An unrecognised architecture must yield empty rather than defaulting to + // x86_64: the caller uses this to pick a native code blob, and handing + // x86_64 machine code to, say, riscv64 crashes the JVM instead of falling + // back to the build-time compiler. + if (!isAarch64 && !isX8664) { + return Optional.empty(); + } if (osName.contains("linux")) { return Optional.of(isAarch64 ? LINUX_AARCH64 : LINUX_X86_64); diff --git a/redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java b/redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java new file mode 100644 index 000000000..6ad85bd90 --- /dev/null +++ b/redline/api/src/test/java/run/endive/redline/experimental/api/internal/RedlineTargetTest.java @@ -0,0 +1,66 @@ +package run.endive.redline.experimental.api.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class RedlineTargetTest { + + private static final String OS_NAME = "endive.redline.os.name"; + private static final String OS_ARCH = "endive.redline.os.arch"; + + @AfterEach + public void clearOverrides() { + System.clearProperty(OS_NAME); + System.clearProperty(OS_ARCH); + } + + private static Optional detect(String osName, String arch) { + System.setProperty(OS_NAME, osName); + System.setProperty(OS_ARCH, arch); + return RedlineTarget.detectHost(); + } + + @Test + public void detectsSupportedPlatforms() { + assertEquals(Optional.of(RedlineTarget.LINUX_X86_64), detect("Linux", "amd64")); + assertEquals(Optional.of(RedlineTarget.LINUX_X86_64), detect("Linux", "x86_64")); + assertEquals(Optional.of(RedlineTarget.LINUX_AARCH64), detect("Linux", "aarch64")); + assertEquals(Optional.of(RedlineTarget.MACOS_AARCH64), detect("Mac OS X", "arm64")); + assertEquals(Optional.of(RedlineTarget.MACOS_X86_64), detect("Mac OS X", "x86_64")); + assertEquals(Optional.of(RedlineTarget.WINDOWS_X86_64), detect("Windows 11", "amd64")); + assertEquals(Optional.of(RedlineTarget.WINDOWS_AARCH64), detect("Windows 11", "aarch64")); + } + + /** + * An unrecognised architecture must not be reported as x86_64. Callers use the + * result to select a native code blob, so guessing wrong hands machine code for + * the wrong ISA to the CPU and crashes the JVM, instead of falling back to the + * build-time compiler. + */ + @Test + public void unknownArchitectureIsNotMistakenForX8664() { + for (String arch : new String[] {"riscv64", "ppc64le", "s390x", "arm", "mips64", ""}) { + assertTrue( + detect("Linux", arch).isEmpty(), + "Linux/" + arch + " must not resolve to an x86_64 target"); + } + } + + @Test + public void unknownOperatingSystemIsUnsupported() { + assertTrue(detect("FreeBSD", "amd64").isEmpty()); + assertTrue(detect("SunOS", "amd64").isEmpty()); + } + + @Test + public void everyTargetRoundTripsThroughItsTriple() { + for (RedlineTarget target : RedlineTarget.values()) { + assertEquals(Optional.of(target), RedlineTarget.fromTriple(target.triple())); + } + assertTrue(RedlineTarget.fromTriple("not-a-real-triple").isEmpty()); + } +} diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml index 412153b93..2381e3efa 100644 --- a/redline/bridge/pom.xml +++ b/redline/bridge/pom.xml @@ -29,21 +29,95 @@ + - run.endive - endive-compiler-maven-plugin + io.roastedroot + inlay-maven-plugin - redline-bridge + fetch-cranelift-bridge - compile + fetch - run.endive.redline.experimental.bridge.internal.Cranelift - ${project.basedir}/../cranelift_bridge.wasm - WARN - run.endive.redline.experimental.bridge.internal.CraneliftBridge + + + ghcr.io/bytecodealliance/endive-cranelift-bridge:${cranelift-bridge.version} + ${project.basedir}/../cranelift_bridge.wasm + + + ${project.basedir}/../wkg.lock + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-generated-sources + + add-source + + generate-sources + + + ${project.build.directory}/generated-sources/endive-compiler + + + + + add-generated-resources + + add-resource + + generate-resources + + + + ${project.build.directory}/generated-resources/endive-compiler + + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + run.endive + build-time-compiler + ${project.version} + + + + + redline-bridge-compile + + java + + generate-sources + + run.endive.build.time.compiler.GeneratorMain + + ${project.basedir}/../cranelift_bridge.wasm + run.endive.redline.experimental.bridge.internal.Cranelift + ${project.build.directory}/generated-resources/endive-compiler + ${project.build.directory}/generated-sources/endive-compiler + ${project.build.directory}/generated-resources/endive-compiler + WARN + run.endive.redline.experimental.bridge.internal.CraneliftBridge + + true + false diff --git a/redline/build-time-compiler/pom.xml b/redline/build-time-compiler/pom.xml new file mode 100644 index 000000000..50ee68428 --- /dev/null +++ b/redline/build-time-compiler/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-build-time-compiler-experimental + jar + Endive - Redline Build Time Compiler + Build-time native code generation via Cranelift + + + + com.github.javaparser + javaparser-core + + + run.endive + build-time-compiler + + + run.endive + redline-api-experimental + + + run.endive + redline-compiler-experimental + + + run.endive + wasm + + + 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 new file mode 100644 index 000000000..4e6357fd4 --- /dev/null +++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java @@ -0,0 +1,405 @@ +package run.endive.redline.experimental.build; + +import static com.github.javaparser.StaticJavaParser.parseClassOrInterfaceType; +import static com.github.javaparser.StaticJavaParser.parseType; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.BinaryExpr; +import com.github.javaparser.ast.expr.ClassExpr; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.NullLiteralExpr; +import com.github.javaparser.ast.expr.StringLiteralExpr; +import com.github.javaparser.ast.expr.VariableDeclarationExpr; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.CatchClause; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.IfStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.TryStmt; +import com.github.javaparser.ast.type.VarType; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import run.endive.build.time.compiler.Config; +import run.endive.redline.experimental.api.NativeCodeSerializer; +import run.endive.redline.experimental.api.internal.RedlineTarget; +import run.endive.redline.experimental.compiler.internal.NativeCompiler; +import run.endive.wasm.Parser; + +public final class RedlineGenerator { + + private final Config config; + + public static List allTargets() { + return Arrays.stream(RedlineTarget.values()) + .map(RedlineTarget::triple) + .collect(Collectors.toList()); + } + + public RedlineGenerator(Config config) { + this.config = config; + } + + public void generateNativeCode() throws IOException { + if (!config.hasRedlineTargets()) { + return; + } + var module = Parser.parse(config.wasmFile()); + var packagePath = config.getPackageName().replace('.', '/'); + var baseName = config.getBaseName(); + var resourceDir = config.targetClassFolder().resolve(packagePath); + Files.createDirectories(resourceDir); + + for (String triple : config.redlineTargets()) { + var target = + RedlineTarget.fromTriple(triple) + .orElseThrow( + () -> + new IllegalArgumentException( + "Unknown target triple: " + triple)); + byte[][] compiledCode = NativeCompiler.compileAll(triple, module); + var nativeFile = + resourceDir.resolve(baseName + "." + target.resourceSuffix() + ".native"); + + try (var out = new FileOutputStream(nativeFile.toFile())) { + NativeCodeSerializer.serialize(compiledCode, out); + } + } + } + + public void extendGeneratedSources() throws IOException { + if (!config.hasRedlineTargets()) { + return; + } + var packagePath = config.getPackageName().replace('.', '/'); + var baseName = config.getBaseName(); + var sourceFile = + config.targetSourceFolder().resolve(packagePath).resolve(baseName + ".java"); + + var cu = StaticJavaParser.parse(sourceFile); + var type = cu.getClassByName(baseName).orElseThrow(); + + cu.addImport("run.endive.redline.experimental.api.NativeCodeSerializer"); + cu.addImport("run.endive.redline.experimental.api.NativeMachineFactoryProvider"); + cu.addImport("run.endive.redline.experimental.api.internal.RedlineTarget"); + cu.addImport("java.io.InputStream"); + cu.addImport("java.io.IOException"); + cu.addImport("java.util.Optional"); + cu.addImport("run.endive.runtime.Instance"); + + generateNativeCodeHolderInnerClass(type, baseName); + generateLoadNativeCodeMethod(type); + generateNativeProviderMethod(type); + generateBuilderMethod(type, baseName); + generateSafeBuilderMethod(type, baseName); + + Files.writeString(sourceFile, cu.toString()); + } + + private static void generateNativeCodeHolderInnerClass( + ClassOrInterfaceDeclaration type, String moduleName) { + // Generates: + // + // private static class NativeCodeHolder { + // static final byte[][] CODE; + // static { + // byte[][] loaded = null; + // var host = RedlineTarget.detectHost().orElse(null); + // if (host != null) { + // String resource = "." + host.resourceSuffix() + ".native"; + // try (InputStream in = + // .class.getResourceAsStream(resource)) { + // if (in != null) { + // loaded = NativeCodeSerializer.deserialize(in); + // } + // } catch (IOException e) { + // loaded = null; + // } + // } + // CODE = loaded; + // } + // } + // + // + // Loading into a local keeps CODE definitely-assigned-once (a static final + // cannot be assigned in both the try and the catch), and lets a missing or + // unreadable blob leave CODE null so builder() falls back to the build-time + // compiled bytecode instead of the class being permanently unusable. + var holderClass = + new ClassOrInterfaceDeclaration( + NodeList.nodeList( + new Modifier(Modifier.Keyword.PRIVATE), + new Modifier(Modifier.Keyword.STATIC)), + false, + "NativeCodeHolder"); + type.addMember(holderClass); + + holderClass.addField( + parseType("byte[][]"), "CODE", Modifier.Keyword.STATIC, Modifier.Keyword.FINAL); + + // byte[][] loaded = null; + var loadedVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + parseType("byte[][]"), "loaded", new NullLiteralExpr()))); + + // var host = RedlineTarget.detectHost().orElse(null); + var detectHost = + new MethodCallExpr( + new MethodCallExpr(new NameExpr("RedlineTarget"), "detectHost"), + "orElse", + new NodeList<>(new NullLiteralExpr())); + var hostVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator(new VarType(), "host", detectHost))); + + // String resource = "." + host.resourceSuffix() + ".native"; + var resourceName = + new BinaryExpr( + new BinaryExpr( + new StringLiteralExpr(moduleName + "."), + new MethodCallExpr(new NameExpr("host"), "resourceSuffix"), + BinaryExpr.Operator.PLUS), + new StringLiteralExpr(".native"), + BinaryExpr.Operator.PLUS); + var resourceVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + parseClassOrInterfaceType("String"), + "resource", + resourceName))); + + // try (InputStream in = .class.getResourceAsStream(resource)) + var getResource = + new MethodCallExpr( + new ClassExpr(parseType(moduleName)), + "getResourceAsStream", + new NodeList<>(new NameExpr("resource"))); + var streamResource = + new VariableDeclarationExpr( + new VariableDeclarator(parseType("InputStream"), "in", getResource)); + + // if (in != null) { loaded = NativeCodeSerializer.deserialize(in); } + var assignLoaded = + new ExpressionStmt( + new AssignExpr( + new NameExpr("loaded"), + new MethodCallExpr( + new NameExpr("NativeCodeSerializer"), + "deserialize", + new NodeList<>(new NameExpr("in"))), + AssignExpr.Operator.ASSIGN)); + var ifStreamPresent = + new IfStmt() + .setCondition( + new BinaryExpr( + new NameExpr("in"), + new NullLiteralExpr(), + BinaryExpr.Operator.NOT_EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(assignLoaded))); + + // catch (IOException e) { loaded = null; } + var resetLoaded = + new ExpressionStmt( + new AssignExpr( + new NameExpr("loaded"), + new NullLiteralExpr(), + AssignExpr.Operator.ASSIGN)); + var catchIoException = + new CatchClause() + .setParameter(new Parameter(parseClassOrInterfaceType("IOException"), "e")) + .setBody(new BlockStmt(new NodeList<>(resetLoaded))); + + var tryLoad = + new TryStmt() + .setResources(new NodeList<>(streamResource)) + .setTryBlock(new BlockStmt(new NodeList<>(ifStreamPresent))) + .setCatchClauses(new NodeList<>(catchIoException)); + + var loadFromResource = + new IfStmt() + .setCondition( + new BinaryExpr( + new NameExpr("host"), + new NullLiteralExpr(), + BinaryExpr.Operator.NOT_EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(resourceVar, tryLoad))); + + // CODE = loaded; + var assignCode = + new ExpressionStmt( + new AssignExpr( + new NameExpr("CODE"), + new NameExpr("loaded"), + AssignExpr.Operator.ASSIGN)); + + var initBody = holderClass.addStaticInitializer(); + initBody.addStatement(loadedVar); + initBody.addStatement(hostVar); + initBody.addStatement(loadFromResource); + initBody.addStatement(assignCode); + } + + private static void generateLoadNativeCodeMethod(ClassOrInterfaceDeclaration type) { + // Generates: + // + // public static byte[][] loadNativeCode() { + // return NativeCodeHolder.CODE; + // } + // + var method = + type.addMethod("loadNativeCode", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType(parseType("byte[][]")); + method.createBody() + .addStatement( + new ReturnStmt( + new FieldAccessExpr(new NameExpr("NativeCodeHolder"), "CODE"))); + } + + private static void generateNativeProviderMethod(ClassOrInterfaceDeclaration type) { + // Generates: + // + // public static Optional<NativeMachineFactoryProvider> nativeProvider() { + // if (loadNativeCode() == null) { + // return Optional.empty(); + // } + // return NativeMachineFactoryProvider.discover(); + // } + // + // + // Non-empty exactly when builder() will take the native path, so callers can + // tell which backend they got. Modules with imported memories or tables need + // this: the native machines reject a memory that was not created by their own + // factory, so imports must be built with the returned provider. + var method = + type.addMethod("nativeProvider", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType( + parseClassOrInterfaceType( + "Optional")); + + var returnEmpty = new ReturnStmt(new MethodCallExpr(new NameExpr("Optional"), "empty")); + var ifNoNativeCode = + new IfStmt() + .setCondition( + new BinaryExpr( + new MethodCallExpr("loadNativeCode"), + new NullLiteralExpr(), + BinaryExpr.Operator.EQUALS)) + .setThenStmt(new BlockStmt(new NodeList<>(returnEmpty))); + + var body = method.createBody(); + body.addStatement(ifNoNativeCode); + body.addStatement( + new ReturnStmt( + new MethodCallExpr( + new NameExpr("NativeMachineFactoryProvider"), "discover"))); + } + + private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) { + // Generates: + // + // public static Instance.Builder builder() { + // var module = load(); + // var provider = nativeProvider(); + // if (provider.isPresent()) { + // return provider.get().builder(module, loadNativeCode()); + // } + // return Instance.builder(module).withMachineFactory(::create); + // } + // + // + // The native path is selected through nativeProvider() so that callers + // checking it see exactly the decision this method makes. Falling back means + // the build-time compiled bytecode, not the interpreter. + var method = + type.addMethod("builder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType(parseClassOrInterfaceType("Instance.Builder")); + + // var module = load(); + var moduleVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + new VarType(), "module", new MethodCallExpr("load")))); + + // var provider = nativeProvider(); + var providerVar = + new ExpressionStmt( + new VariableDeclarationExpr( + new VariableDeclarator( + new VarType(), + "provider", + new MethodCallExpr("nativeProvider")))); + + // return provider.get().builder(module, loadNativeCode()); + var returnNative = + new ReturnStmt( + new MethodCallExpr( + new MethodCallExpr(new NameExpr("provider"), "get"), + "builder", + new NodeList<>( + new NameExpr("module"), + new MethodCallExpr("loadNativeCode")))); + + var ifProviderPresent = + new IfStmt() + .setCondition(new MethodCallExpr(new NameExpr("provider"), "isPresent")) + .setThenStmt(new BlockStmt(new NodeList<>(returnNative))); + + var body = method.createBody(); + body.addStatement(moduleVar); + body.addStatement(providerVar); + body.addStatement(ifProviderPresent); + body.addStatement(new ReturnStmt(compiledBuilder(new NameExpr("module"), moduleName))); + } + + private static void generateSafeBuilderMethod( + ClassOrInterfaceDeclaration type, String moduleName) { + // Generates: + // + // public static Instance.Builder safeBuilder() { + // return Instance.builder(load()).withMachineFactory(::create); + // } + // + var method = + type.addMethod("safeBuilder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC) + .setType(parseClassOrInterfaceType("Instance.Builder")); + + method.createBody() + .addStatement( + new ReturnStmt(compiledBuilder(new MethodCallExpr("load"), moduleName))); + } + + /** + * {@code Instance.builder().withMachineFactory(::create)} + * + *

{@code create} returns the machine the build-time compiler emitted as JVM + * bytecode, so this is the compiled path, not the interpreter. The interpreter is + * only reached per-function, for functions too large to fit a JVM method. + */ + private static MethodCallExpr compiledBuilder( + com.github.javaparser.ast.expr.Expression module, String moduleName) { + return new MethodCallExpr( + new MethodCallExpr(new NameExpr("Instance"), "builder", new NodeList<>(module)), + "withMachineFactory", + new NodeList<>( + new MethodReferenceExpr() + .setScope(new NameExpr(moduleName)) + .setIdentifier("create"))); + } +} diff --git a/redline/it/pom.xml b/redline/it/pom.xml new file mode 100644 index 000000000..42e7fb2ee --- /dev/null +++ b/redline/it/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-it-experimental + jar + Endive - Redline IT + Integration tests for the Redline native compiler + + + true + + + + + run.endive + endive-compiler-maven-plugin + + + run.endive + redline-runner-jffi-experimental + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + run.endive:endive-compiler-maven-plugin + run.endive:redline-runner-jffi-experimental + run.endive:redline-runner-experimental + + + + + org.apache.maven.plugins + maven-invoker-plugin + + ${project.build.directory}/it + true + src/it/settings.xml + verify + true + ${skipTests} + true + invoker.properties + + + + integration-tests + + install + run + + + + + + + + + + java25 + + [25,) + + + + run.endive + redline-runner-experimental + + + + + diff --git a/redline/it/src/it/redline-e2e-panama/invoker.properties b/redline/it/src/it/redline-e2e-panama/invoker.properties new file mode 100644 index 000000000..d89167bdd --- /dev/null +++ b/redline/it/src/it/redline-e2e-panama/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals=test +invoker.java.version=25+ diff --git a/redline/it/src/it/redline-e2e-panama/pom.xml b/redline/it/src/it/redline-e2e-panama/pom.xml new file mode 100644 index 000000000..65632de54 --- /dev/null +++ b/redline/it/src/it/redline-e2e-panama/pom.xml @@ -0,0 +1,65 @@ + + + + 4.0.0 + run.endive + redline-e2e-panama-it + 0.0-SNAPSHOT + jar + + + + + run.endive + redline-runner-experimental + @project.version@ + + + + org.junit.jupiter + junit-jupiter-api + @junit.version@ + test + + + org.junit.jupiter + junit-jupiter-engine + @junit.version@ + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @maven-compiler-plugin.version@ + + 25 + + + + run.endive + endive-compiler-maven-plugin + @project.version@ + + + compile-i32 + + compile + + + endive.test.AddModule + src/test/resources/add.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 new file mode 100644 index 000000000..7a9074a6b --- /dev/null +++ b/redline/it/src/it/redline-e2e-panama/src/test/java/endive/test/RedlinePanamaE2eTest.java @@ -0,0 +1,79 @@ +package endive.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +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; + +class RedlinePanamaE2eTest { + + @Test + public void panamaProviderIsSelected() { + var provider = NativeMachineFactoryProvider.discover(); + assertTrue(provider.isPresent(), "Should discover a native provider"); + assertEquals(100, provider.get().priority(), "Panama should win with priority 100"); + } + + /** + * Without this, every other test here still passes when the native path silently + * never engages, because the fallback is the build-time compiled bytecode and + * produces identical results. + */ + @Test + public void builderActuallyUsesTheNativePath() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms"); + var provider = AddModule.nativeProvider(); + assertTrue( + provider.isPresent(), + "builder() must take the native path, not fall back to compiled bytecode"); + assertEquals(100, provider.get().priority(), "and it must be the Panama runner"); + } + + @Test + public void nativeBuilderProducesCorrectResults() { + try (var instance = AddModule.builder().build()) { + var add = instance.export("add"); + assertArrayEquals(new long[] {3}, add.apply(1, 2)); + assertArrayEquals(new long[] {0}, add.apply(0, 0)); + assertEquals( + -1, + (int) add.apply(0, -1)[0], + "i32 add(0, -1) should be -1 when narrowed to int"); + } + } + + @Test + public void nativeCodeIsAvailable() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms, so no native code was" + + " cross-compiled for it"); + assertNotNull( + AddModule.loadNativeCode(), "Native code should be available on this platform"); + } + + @Test + public void bothBuildersProduceSameResults() { + try (var nativeInstance = AddModule.builder().build(); + var safeInstance = AddModule.safeBuilder().build()) { + var nativeAdd = nativeInstance.export("add"); + var safeAdd = safeInstance.export("add"); + + for (int a = -10; a <= 10; a++) { + for (int b = -10; b <= 10; b++) { + assertEquals( + (int) safeAdd.apply(a, b)[0], + (int) nativeAdd.apply(a, b)[0], + "add(" + a + ", " + b + ") should match"); + } + } + } + } +} diff --git a/redline/it/src/it/redline-e2e-panama/src/test/resources/add.wat.wasm b/redline/it/src/it/redline-e2e-panama/src/test/resources/add.wat.wasm new file mode 100644 index 000000000..ad1f2f7a3 Binary files /dev/null and b/redline/it/src/it/redline-e2e-panama/src/test/resources/add.wat.wasm differ diff --git a/redline/it/src/it/redline-e2e/invoker.properties b/redline/it/src/it/redline-e2e/invoker.properties new file mode 100644 index 000000000..84099bc5a --- /dev/null +++ b/redline/it/src/it/redline-e2e/invoker.properties @@ -0,0 +1 @@ +invoker.goals=test diff --git a/redline/it/src/it/redline-e2e/pom.xml b/redline/it/src/it/redline-e2e/pom.xml new file mode 100644 index 000000000..628f2c420 --- /dev/null +++ b/redline/it/src/it/redline-e2e/pom.xml @@ -0,0 +1,65 @@ + + + + 4.0.0 + run.endive + redline-e2e-it + 0.0-SNAPSHOT + jar + + + + + run.endive + redline-runner-jffi-experimental + @project.version@ + + + + org.junit.jupiter + junit-jupiter-api + @junit.version@ + test + + + org.junit.jupiter + junit-jupiter-engine + @junit.version@ + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @maven-compiler-plugin.version@ + + 11 + + + + run.endive + endive-compiler-maven-plugin + @project.version@ + + + compile-i32 + + compile + + + endive.test.AddModule + src/test/resources/add.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 new file mode 100644 index 000000000..31dff4161 --- /dev/null +++ b/redline/it/src/it/redline-e2e/src/test/java/endive/test/RedlineE2eTest.java @@ -0,0 +1,92 @@ +package endive.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +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; + +class RedlineE2eTest { + + @Test + public void jffiProviderIsSelected() { + var provider = NativeMachineFactoryProvider.discover(); + assertTrue(provider.isPresent(), "Should discover a native provider"); + assertEquals(50, provider.get().priority(), "JFFI should be selected with priority 50"); + } + + /** + * Without this, every other test here still passes when the native path silently + * never engages, because the fallback is the build-time compiled bytecode and + * produces identical results. + */ + @Test + public void builderActuallyUsesTheNativePath() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms"); + var provider = AddModule.nativeProvider(); + assertTrue( + provider.isPresent(), + "builder() must take the native path, not fall back to compiled bytecode"); + assertEquals(50, provider.get().priority(), "and it must be the JFFI runner"); + } + + @Test + public void nativeBuilderProducesCorrectResults() { + try (var instance = AddModule.builder().build()) { + var add = instance.export("add"); + assertArrayEquals(new long[] {3}, add.apply(1, 2)); + assertArrayEquals(new long[] {0}, add.apply(0, 0)); + assertEquals( + -1, + (int) add.apply(0, -1)[0], + "i32 add(0, -1) should be -1 when narrowed to int"); + } + } + + @Test + public void safeBuilderProducesCorrectResults() { + try (var instance = AddModule.safeBuilder().build()) { + var add = instance.export("add"); + assertArrayEquals(new long[] {3}, add.apply(1, 2)); + assertArrayEquals(new long[] {0}, add.apply(0, 0)); + assertEquals( + -1, + (int) add.apply(0, -1)[0], + "i32 add(0, -1) should be -1 when narrowed to int"); + } + } + + @Test + public void nativeCodeIsAvailable() { + assumeTrue( + RedlineTarget.detectHost().isPresent(), + "Host is not one of the Redline target platforms, so no native code was" + + " cross-compiled for it"); + assertNotNull( + AddModule.loadNativeCode(), "Native code should be available on this platform"); + } + + @Test + public void bothBuildersProduceSameResults() { + try (var nativeInstance = AddModule.builder().build(); + var safeInstance = AddModule.safeBuilder().build()) { + var nativeAdd = nativeInstance.export("add"); + var safeAdd = safeInstance.export("add"); + + for (int a = -10; a <= 10; a++) { + for (int b = -10; b <= 10; b++) { + assertEquals( + (int) safeAdd.apply(a, b)[0], + (int) nativeAdd.apply(a, b)[0], + "add(" + a + ", " + b + ") should match"); + } + } + } + } +} diff --git a/redline/it/src/it/redline-e2e/src/test/resources/add.wat.wasm b/redline/it/src/it/redline-e2e/src/test/resources/add.wat.wasm new file mode 100644 index 000000000..ad1f2f7a3 Binary files /dev/null and b/redline/it/src/it/redline-e2e/src/test/resources/add.wat.wasm differ diff --git a/redline/it/src/it/settings.xml b/redline/it/src/it/settings.xml new file mode 100644 index 000000000..2d90068bb --- /dev/null +++ b/redline/it/src/it/settings.xml @@ -0,0 +1,35 @@ + + + + + it-repo + + true + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + diff --git a/redline/pom.xml b/redline/pom.xml index 13f29061f..772ef4d93 100644 --- a/redline/pom.xml +++ b/redline/pom.xml @@ -13,16 +13,34 @@ Endive - Redline Redline native compiler for WebAssembly - + api bridge + build-time-compiler compiler runner-jffi - runner-jffi-tests + + + default-redline-test-modules + + + !release + + + + it + runner-jffi-tests + + + java25 @@ -30,6 +48,19 @@ runner + + + + + + java25-test-modules + + [25,) + + !release + + + runner-tests diff --git a/redline/wkg.lock b/redline/wkg.lock new file mode 100644 index 000000000..bd266a4ab --- /dev/null +++ b/redline/wkg.lock @@ -0,0 +1,12 @@ +# This file is automatically generated. +# It is not intended for manual editing. +version = 1 + +[[packages]] +name = "bytecodealliance:endive-cranelift-bridge" +registry = "ghcr.io" + +[[packages.versions]] +requirement = "=999.0.0-SNAPSHOT" +version = "999.0.0-SNAPSHOT" +digest = "sha256:2436e889dae98422313f495632557068dda466e8170aa718a3e70d7fee1d652f"