From fe0fbccfa8df645ecd129d8b4d4424b16d781cc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:17:08 -0400 Subject: [PATCH 01/85] chore(deps): bump the maven-minor-patch group with 22 updates (#7) Bumps the maven-minor-patch group with 22 updates: | Package | From | To | | --- | --- | --- | | [com.dylibso.chicory:runtime](https://github.com/dylibso/chicory) | `1.7.3` | `1.7.5` | | [com.dylibso.chicory:wasm](https://github.com/dylibso/chicory) | `1.7.3` | `1.7.5` | | [com.dylibso.chicory:wasi](https://github.com/dylibso/chicory) | `1.7.3` | `1.7.5` | | [com.dylibso.chicory:compiler](https://github.com/dylibso/chicory) | `1.7.3` | `1.7.5` | | [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) | `2.15.2` | `2.22.0` | | [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) | `3.12.1` | `3.15.0` | | [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.2.5` | `3.5.6` | | [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) | `3.2.5` | `3.5.6` | | [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) | `3.3.0` | `3.5.0` | | [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) | `3.3.0` | `3.4.0` | | [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) | `3.6.3` | `3.12.0` | | [org.codehaus.mojo:flatten-maven-plugin](https://github.com/mojohaus/flatten-maven-plugin) | `1.6.0` | `1.7.3` | | [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) | `3.1.0` | `3.2.8` | | [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) | `3.1.0` | `3.6.3` | | [org.apache.maven.plugins:maven-shade-plugin](https://github.com/apache/maven-shade-plugin) | `3.5.1` | `3.6.2` | | [info.picocli:picocli](https://github.com/remkop/picocli) | `4.7.5` | `4.7.7` | | [info.picocli:picocli-codegen](https://github.com/remkop/picocli) | `4.7.5` | `4.7.7` | | [org.apache.maven:maven-plugin-api](https://github.com/apache/maven) | `3.8.1` | `3.9.16` | | [org.apache.maven.plugin-tools:maven-plugin-annotations](https://github.com/apache/maven-plugin-tools) | `3.6.4` | `3.15.2` | | org.apache.maven:maven-core | `3.8.1` | `3.9.16` | | org.apache.maven.plugin-testing:maven-plugin-testing-harness | `3.3.0` | `3.5.1` | | [org.apache.maven.plugins:maven-invoker-plugin](https://github.com/apache/maven-invoker-plugin) | `3.2.2` | `3.10.1` | Updates `com.dylibso.chicory:runtime` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.dylibso.chicory:wasm` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.dylibso.chicory:wasi` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.dylibso.chicory:compiler` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.dylibso.chicory:wasm` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.dylibso.chicory:wasi` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.dylibso.chicory:compiler` from 1.7.3 to 1.7.5 - [Release notes](https://github.com/dylibso/chicory/releases) - [Commits](https://github.com/dylibso/chicory/compare/1.7.3...1.7.5) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.15.2 to 2.22.0 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.12.1 to 3.15.0 - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.12.1...maven-compiler-plugin-3.15.0) Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.2.5 to 3.5.6 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.5.6) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.2.5 to 3.5.6 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.5.6) Updates `org.apache.maven.plugins:maven-jar-plugin` from 3.3.0 to 3.5.0 - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.3.0...maven-jar-plugin-3.5.0) Updates `org.apache.maven.plugins:maven-source-plugin` from 3.3.0 to 3.4.0 - [Release notes](https://github.com/apache/maven-source-plugin/releases) - [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.0...maven-source-plugin-3.4.0) Updates `org.apache.maven.plugins:maven-javadoc-plugin` from 3.6.3 to 3.12.0 - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.6.3...maven-javadoc-plugin-3.12.0) Updates `org.codehaus.mojo:flatten-maven-plugin` from 1.6.0 to 1.7.3 - [Release notes](https://github.com/mojohaus/flatten-maven-plugin/releases) - [Commits](https://github.com/mojohaus/flatten-maven-plugin/compare/1.6.0...1.7.3) Updates `org.apache.maven.plugins:maven-gpg-plugin` from 3.1.0 to 3.2.8 - [Release notes](https://github.com/apache/maven-gpg-plugin/releases) - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.1.0...maven-gpg-plugin-3.2.8) Updates `org.codehaus.mojo:exec-maven-plugin` from 3.1.0 to 3.6.3 - [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases) - [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/exec-maven-plugin-3.1.0...3.6.3) Updates `org.apache.maven.plugins:maven-shade-plugin` from 3.5.1 to 3.6.2 - [Release notes](https://github.com/apache/maven-shade-plugin/releases) - [Commits](https://github.com/apache/maven-shade-plugin/compare/maven-shade-plugin-3.5.1...maven-shade-plugin-3.6.2) Updates `info.picocli:picocli` from 4.7.5 to 4.7.7 - [Release notes](https://github.com/remkop/picocli/releases) - [Changelog](https://github.com/remkop/picocli/blob/main/RELEASE-NOTES.md) - [Commits](https://github.com/remkop/picocli/compare/v4.7.5...v4.7.7) Updates `info.picocli:picocli-codegen` from 4.7.5 to 4.7.7 - [Release notes](https://github.com/remkop/picocli/releases) - [Changelog](https://github.com/remkop/picocli/blob/main/RELEASE-NOTES.md) - [Commits](https://github.com/remkop/picocli/compare/v4.7.5...v4.7.7) Updates `org.apache.maven:maven-plugin-api` from 3.8.1 to 3.9.16 - [Release notes](https://github.com/apache/maven/releases) - [Commits](https://github.com/apache/maven/compare/maven-3.8.1...maven-3.9.16) Updates `org.apache.maven.plugin-tools:maven-plugin-annotations` from 3.6.4 to 3.15.2 - [Release notes](https://github.com/apache/maven-plugin-tools/releases) - [Commits](https://github.com/apache/maven-plugin-tools/compare/maven-plugin-tools-3.6.4...maven-plugin-tools-3.15.2) Updates `org.apache.maven:maven-core` from 3.8.1 to 3.9.16 Updates `org.apache.maven.plugin-testing:maven-plugin-testing-harness` from 3.3.0 to 3.5.1 Updates `info.picocli:picocli-codegen` from 4.7.5 to 4.7.7 - [Release notes](https://github.com/remkop/picocli/releases) - [Changelog](https://github.com/remkop/picocli/blob/main/RELEASE-NOTES.md) - [Commits](https://github.com/remkop/picocli/compare/v4.7.5...v4.7.7) Updates `org.apache.maven.plugins:maven-invoker-plugin` from 3.2.2 to 3.10.1 - [Release notes](https://github.com/apache/maven-invoker-plugin/releases) - [Commits](https://github.com/apache/maven-invoker-plugin/compare/maven-invoker-plugin-3.2.2...maven-invoker-plugin-3.10.1) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.2.5 to 3.5.6 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.5.6) --- updated-dependencies: - dependency-name: com.dylibso.chicory:runtime dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.dylibso.chicory:wasm dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.dylibso.chicory:wasi dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.dylibso.chicory:compiler dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.dylibso.chicory:wasm dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.dylibso.chicory:wasi dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.dylibso.chicory:compiler dependency-version: 1.7.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.22.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.15.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.6 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-source-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-version: 3.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.codehaus.mojo:flatten-maven-plugin dependency-version: 1.7.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-version: 3.2.8 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.codehaus.mojo:exec-maven-plugin dependency-version: 3.6.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-shade-plugin dependency-version: 3.6.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: info.picocli:picocli dependency-version: 4.7.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: info.picocli:picocli-codegen dependency-version: 4.7.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.apache.maven:maven-plugin-api dependency-version: 3.9.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugin-tools:maven-plugin-annotations dependency-version: 3.15.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven:maven-core dependency-version: 3.9.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugin-testing:maven-plugin-testing-harness dependency-version: 3.5.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: info.picocli:picocli-codegen dependency-version: 4.7.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-invoker-plugin dependency-version: 3.10.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.6 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 18 +++++++++--------- webassembly4j-benchmarks/pom.xml | 4 ++-- webassembly4j-bindgen/pom.xml | 14 +++++++------- webassembly4j-component-builder/pom.xml | 10 +++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index 7008c50..d4fbcb4 100644 --- a/pom.xml +++ b/pom.xml @@ -65,19 +65,19 @@ 5.10.4 46.0.1-1.1.7 2.4.4-1.0.2 - 1.7.3 + 1.7.5 24.2.1 1.37 - 2.15.2 + 2.22.0 3.2.5 - 3.12.1 - 3.2.5 - 3.3.0 - 3.3.0 - 3.6.3 - 1.6.0 + 3.15.0 + 3.5.6 + 3.5.0 + 3.4.0 + 3.12.0 + 1.7.3 @@ -304,7 +304,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.1.0 + 3.2.8 sign-artifacts diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 3c2f970..a9080d7 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -154,7 +154,7 @@ org.codehaus.mojo exec-maven-plugin - 3.1.0 + 3.6.3 default-cli @@ -227,7 +227,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.2 package diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 7e6f116..a1aa11c 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -23,16 +23,16 @@ 11 - 3.8.1 - 3.8.1 - 3.6.4 + 3.9.16 + 3.9.16 + 3.15.2 2.2.1 1.13.0 - 4.7.5 + 4.7.7 @@ -89,7 +89,7 @@ org.apache.maven.plugin-testing maven-plugin-testing-harness - 3.3.0 + 3.5.1 test @@ -157,7 +157,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.2 package @@ -191,7 +191,7 @@ org.apache.maven.plugins maven-invoker-plugin - 3.2.2 + 3.10.1 true ${project.build.directory}/it diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 342b2e1..bbb8f2a 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -23,13 +23,13 @@ 17 - 3.8.1 - 3.8.1 - 3.6.4 + 3.9.16 + 3.9.16 + 3.15.2 2.2.1 - 4.7.5 + 4.7.7 1.13.0 @@ -149,7 +149,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.2 package From 2a8838002fb6d439a6e4d7d729d0e9d81dcbc130 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 23:17:11 -0400 Subject: [PATCH 02/85] ci(deps): bump the actions group with 5 updates (#6) Bumps the actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `4` | `7` | | [actions/setup-java](https://github.com/actions/setup-java) | `4` | `5` | | [actions/cache](https://github.com/actions/cache) | `4` | `6` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4` | `7` | | [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `2` | `3` | Updates `actions/checkout` from 4 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v7) Updates `actions/setup-java` from 4 to 5 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v4...v5) Updates `actions/cache` from 4 to 6 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v6) Updates `actions/upload-artifact` from 4 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) Updates `softprops/action-gh-release` from 2 to 3 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/setup-java dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/release.yml | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5562e27..79e8fd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,15 +44,15 @@ jobs: java: '21' runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' - name: Cache Maven - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: m2-core-${{ matrix.os }}-${{ matrix.java }}-${{ hashFiles('**/pom.xml') }} @@ -78,7 +78,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-results-${{ matrix.os }}-java${{ matrix.java }} path: '**/target/surefire-reports/' @@ -89,15 +89,15 @@ jobs: name: Code Quality runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: '23' distribution: 'temurin' - name: Cache Maven - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: m2-quality-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab2d6b6..63cf0af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,15 +31,15 @@ jobs: java: '21' runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' - name: Cache Maven - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: m2-release-${{ matrix.os }}-${{ matrix.java }}-${{ hashFiles('**/pom.xml') }} @@ -66,15 +66,15 @@ jobs: name: Code Quality runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: '23' distribution: 'temurin' - name: Cache Maven - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: m2-quality-${{ hashFiles('**/pom.xml') }} @@ -99,9 +99,9 @@ jobs: contents: write packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' @@ -112,7 +112,7 @@ jobs: gpg-passphrase: GPG_PASSPHRASE - name: Cache Maven - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.m2/repository key: m2-publish-${{ hashFiles('**/pom.xml') }} @@ -186,7 +186,7 @@ jobs: done - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: generate_release_notes: true files: | From d7757222b74884eda21cb9143f675980f9203dfe Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 27 Jun 2026 23:18:38 -0400 Subject: [PATCH 03/85] ci(dependabot): ignore major bumps for spring-boot, junit, graalvm These majors require dedicated migrations (Java baseline / breaking APIs) and currently fail CI; ignore them so Dependabot stops reopening failing PRs. --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index da94dbc..93918ea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -37,3 +37,12 @@ updates: update-types: - minor - patch + # Major bumps of these need dedicated migrations (newer Java baseline / breaking + # APIs), so don't open PRs for them until tackled deliberately. + ignore: + - dependency-name: "org.springframework.boot*" + update-types: ["version-update:semver-major"] + - dependency-name: "org.junit*" + update-types: ["version-update:semver-major"] + - dependency-name: "org.graalvm*" + update-types: ["version-update:semver-major"] From 93c866141cb1d683336a45fbfe7b3881b47210ec Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 29 Jun 2026 22:46:51 -0400 Subject: [PATCH 04/85] feat!: rename chicory provider to endive and bump to 2.0.0 Chicory has been donated to the Bytecode Alliance and renamed to Endive. Rename the chicory4j-provider module to endive4j-provider, the provider/engine id "chicory" to "endive", and ChicoryConfig/ChicoryConfigs to EndiveConfig/EndiveConfigs. Switch the dependency from com.dylibso.chicory to run.endive 1.0.0 (packages com.dylibso.chicory.* -> run.endive.*). BREAKING CHANGE: the provider/engine id "chicory" is now "endive" and the ChicoryConfig/ChicoryConfigs facades are now EndiveConfig/EndiveConfigs. --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 18 ++--- CHANGELOG.md | 8 +++ README.md | 6 +- .../provider/chicory/ChicoryConfigs.java | 42 ------------ ...tegmentum.webassembly4j.spi.EngineProvider | 1 - .../provider/chicory/ChicoryConfigsTest.java | 38 ----------- .../pom.xml | 16 ++--- .../provider/endive}/CompilerSupport.java | 22 +++--- .../provider/endive/EndiveConfigs.java | 42 ++++++++++++ .../provider/endive/EndiveEngineAdapter.java | 40 +++++------ .../endive/EndiveEngineCapabilities.java | 4 +- .../provider/endive/EndiveEngineInfo.java | 8 +-- .../endive/EndiveFunctionAdapter.java | 52 +++++++------- .../provider/endive/EndiveGcExtension.java | 50 +++++++------- .../provider/endive/EndiveGlobalAdapter.java | 26 +++---- .../endive/EndiveInstanceAdapter.java | 28 ++++---- .../provider/endive/EndiveMemoryAdapter.java | 12 ++-- .../provider/endive/EndiveModuleAdapter.java | 68 +++++++++---------- .../provider/endive/EndiveProvider.java | 16 ++--- .../provider/endive/EndiveTableAdapter.java | 8 +-- .../provider/endive/config/EndiveConfig.java | 16 ++--- ...tegmentum.webassembly4j.spi.EngineProvider | 1 + .../provider/endive/EndiveConfigsTest.java | 38 +++++++++++ .../endive/EndiveGcExtensionTest.java | 8 +-- .../endive/EndiveIntegrationTest.java | 34 +++++----- .../provider/endive/EndiveProviderTest.java | 20 +++--- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 22 +++--- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- .../api/config/EngineConfig.java | 2 +- webassembly4j-benchmarks/pom.xml | 8 +-- .../benchmarks/BenchmarkReportGenerator.java | 4 +- .../benchmarks/ChartReportGenerator.java | 4 +- .../benchmarks/ConcurrentLoadBenchmark.java | 2 +- .../benchmarks/EndToEndBenchmark.java | 2 +- .../benchmarks/EngineVariant.java | 10 +-- .../FunctionInvocationBenchmark.java | 2 +- .../benchmarks/HostFunctionBenchmark.java | 2 +- .../benchmarks/InstantiationBenchmark.java | 2 +- .../benchmarks/MemoryOperationsBenchmark.java | 2 +- .../benchmarks/ModuleLoadBenchmark.java | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 4 +- .../runtime/FastPathBenchmarkTest.java | 2 +- .../runtime/WasmRuntimeTest.java | 6 +- webassembly4j-spi/pom.xml | 2 +- .../internal/DefaultProviderSelectorTest.java | 8 +-- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- .../testing/WasmTestExtension.java | 2 +- 56 files changed, 371 insertions(+), 363 deletions(-) delete mode 100644 chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigs.java delete mode 100644 chicory4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider delete mode 100644 chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigsTest.java rename {chicory4j-provider => endive4j-provider}/pom.xml (74%) rename {chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive}/CompilerSupport.java (51%) create mode 100644 endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigs.java rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java (58%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineCapabilities.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java (87%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineInfo.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineInfo.java (72%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryFunctionAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveFunctionAdapter.java (79%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtension.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtension.java (84%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGlobalAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGlobalAdapter.java (70%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryInstanceAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveInstanceAdapter.java (71%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryMemoryAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveMemoryAdapter.java (72%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryModuleAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveModuleAdapter.java (83%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProvider.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProvider.java (76%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryTableAdapter.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveTableAdapter.java (70%) rename chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/config/ChicoryConfig.java => endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/config/EndiveConfig.java (71%) create mode 100644 endive4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider create mode 100644 endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigsTest.java rename chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtensionTest.java => endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtensionTest.java (96%) rename chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryIntegrationTest.java => endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveIntegrationTest.java (86%) rename chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProviderTest.java => endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java (79%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79e8fd9..a31ee88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: shell: bash run: | ./mvnw test -B -q \ - -pl webassembly4j-api,webassembly4j-spi,chicory4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-bindgen \ + -pl webassembly4j-api,webassembly4j-spi,endive4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-bindgen \ -am - name: Build and test all modules (Java 17+) @@ -73,7 +73,7 @@ jobs: shell: bash run: | ./mvnw test -B -q \ - -pl webassembly4j-api,webassembly4j-spi,chicory4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen \ + -pl webassembly4j-api,webassembly4j-spi,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen \ -am - name: Upload test results diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63cf0af..fef2abe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: shell: bash run: | ./mvnw test -B -q \ - -pl webassembly4j-api,webassembly4j-spi,chicory4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-bindgen \ + -pl webassembly4j-api,webassembly4j-spi,endive4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-bindgen \ -am - name: Run tests (Java 17+ — all modules) @@ -58,7 +58,7 @@ jobs: shell: bash run: | ./mvnw test -B -q \ - -pl webassembly4j-api,webassembly4j-spi,chicory4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen \ + -pl webassembly4j-api,webassembly4j-spi,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen \ -am # Static analysis quality gate @@ -128,7 +128,7 @@ jobs: - name: Install modules locally run: | - DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,chicory4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" + DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" ./mvnw install -B -q \ -DskipTests -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dgpg.skip=true -DskipNativeTests \ $DEPLOY_MODULES -am @@ -139,7 +139,7 @@ jobs: MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | - DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,chicory4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" + DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" ./mvnw deploy -B -P release \ -DskipTests -Dmaven.test.skip=true -DskipNativeTests \ $DEPLOY_MODULES @@ -148,7 +148,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,chicory4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" + DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" mkdir -p ~/.m2 cat > ~/.m2/settings-github.xml <<'SETTINGS' @@ -170,14 +170,14 @@ jobs: - name: Package artifacts run: | - DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,chicory4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" + DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" ./mvnw package -B -P release \ -DskipTests -Dmaven.test.skip=true -DskipNativeTests \ $DEPLOY_MODULES - name: Create checksums run: | - for dir in webassembly4j-api/target webassembly4j-spi/target webassembly4j-runtime/target webassembly4j-testing/target webassembly4j-pool/target webassembly4j-spring/target webassembly4j-bindgen/target wasmtime4j-provider/target wamr4j-provider/target wasm3-provider/target chicory4j-provider/target graalwasm4j-provider/target; do + for dir in webassembly4j-api/target webassembly4j-spi/target webassembly4j-runtime/target webassembly4j-testing/target webassembly4j-pool/target webassembly4j-spring/target webassembly4j-bindgen/target wasmtime4j-provider/target wamr4j-provider/target wasm3-provider/target endive4j-provider/target graalwasm4j-provider/target; do if [ -d "$dir" ]; then for jar in "$dir"/*.jar; do [ -f "$jar" ] && sha256sum "$jar" > "$jar.sha256" @@ -210,7 +210,7 @@ jobs: wamr4j-provider/target/*.jar.sha256 wasm3-provider/target/*.jar wasm3-provider/target/*.jar.sha256 - chicory4j-provider/target/*.jar - chicory4j-provider/target/*.jar.sha256 + endive4j-provider/target/*.jar + endive4j-provider/target/*.jar.sha256 graalwasm4j-provider/target/*.jar graalwasm4j-provider/target/*.jar.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c40f3c..8cf8b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.0.0 + +### Breaking Changes + +- **Chicory provider renamed to Endive**: Chicory has been donated to the Bytecode Alliance and renamed [Endive](https://endive.run/). The provider module `chicory4j-provider` is now `endive4j-provider`, the provider/engine ID `"chicory"` is now `"endive"`, and the config facade classes (`ChicoryConfig`, `ChicoryConfigs`) are now `EndiveConfig`/`EndiveConfigs`. The underlying dependency moved from `com.dylibso.chicory` to `run.endive` (1.0.0). Update any `.provider("chicory")`/`.engine("chicory")` calls to `"endive"` and any `ChicoryConfig` references to `EndiveConfig`. + + The major version bump reflects this breaking change to the public provider ID and configuration classes; all other modules are source- and behavior-compatible with 1.5.0. + ## 1.4.0 ### New Features diff --git a/README.md b/README.md index f7c37ff..ed010ec 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A unified Java API for executing WebAssembly across multiple runtimes. -Write your code once against a stable API and swap runtimes without changing application code. WebAssembly4J supports [Wasmtime](https://wasmtime.dev/), [WAMR](https://bytecodealliance.github.io/wamr.dev/), [GraalWasm](https://www.graalvm.org/latest/reference-manual/wasm/), and [Chicory](https://github.com/nicholasgasior/chicory) out of the box. +Write your code once against a stable API and swap runtimes without changing application code. WebAssembly4J supports [Wasmtime](https://wasmtime.dev/), [WAMR](https://bytecodealliance.github.io/wamr.dev/), [GraalWasm](https://www.graalvm.org/latest/reference-manual/wasm/), and [Endive](https://endive.run/) out of the box. ## Quick Start @@ -116,13 +116,13 @@ try (WasmInstance inst = compiled.instantiate()) { | `wasmtime4j-provider` | Wasmtime (via wasmtime4j) | 11+ | 200 | | `graalwasm4j-provider` | GraalWasm (Polyglot API) | 17+ | 150 | | `wamr4j-provider` | WAMR (via wamr4j) | 17+ | 100 | -| `chicory4j-provider` | Chicory (pure Java) | 11+ | 50 | +| `endive4j-provider` | Endive (pure Java) | 11+ | 50 | Providers are discovered automatically via `ServiceLoader`. When multiple providers are on the classpath, the one with the highest priority is selected. To select a specific provider, use the builder: ```java Engine engine = WebAssembly.builder() - .provider("chicory") // "wasmtime", "wamr", "graalwasm", or "chicory" + .provider("endive") // "wasmtime", "wamr", "graalwasm", or "endive" .build(); ``` diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigs.java b/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigs.java deleted file mode 100644 index 1cb7100..0000000 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigs.java +++ /dev/null @@ -1,42 +0,0 @@ -package ai.tegmentum.webassembly4j.provider.chicory; - -import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; -import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfigBuilder; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; - -import java.util.function.Consumer; - -/** - * Convenience entry point for building a {@link WebAssemblyConfig} pre-wired for the - * Chicory pure-Java engine. - * - *

Examples

- *
{@code
- * // Just Chicory tunings:
- * WebAssemblyConfig cfg = ChicoryConfigs.of(b -> b
- *         .executionMode(ChicoryConfig.ExecutionMode.COMPILE));
- *
- * // Default interpreter + cross-engine settings:
- * WebAssemblyConfig cfg = ChicoryConfigs.builder(b -> {})
- *         .debug(true)
- *         .build();
- * }
- */ -public final class ChicoryConfigs { - - public static final String ENGINE_ID = "chicory"; - - private ChicoryConfigs() {} - - public static WebAssemblyConfig of(Consumer configurator) { - return builder(configurator).build(); - } - - public static WebAssemblyConfigBuilder builder(Consumer configurator) { - ChicoryConfig.Builder cb = ChicoryConfig.builder(); - configurator.accept(cb); - return WebAssemblyConfig.builder() - .engine(ENGINE_ID) - .engineConfig(cb.build()); - } -} diff --git a/chicory4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider b/chicory4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider deleted file mode 100644 index 080a37a..0000000 --- a/chicory4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider +++ /dev/null @@ -1 +0,0 @@ -ai.tegmentum.webassembly4j.provider.chicory.ChicoryProvider diff --git a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigsTest.java b/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigsTest.java deleted file mode 100644 index f6e7b77..0000000 --- a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryConfigsTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package ai.tegmentum.webassembly4j.provider.chicory; - -import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class ChicoryConfigsTest { - - @Test - void ofWiresEngineAndEngineConfig() { - WebAssemblyConfig cfg = ChicoryConfigs.of(b -> b - .executionMode(ChicoryConfig.ExecutionMode.COMPILE)); - - assertEquals("chicory", cfg.engineId().orElse(null)); - assertTrue(cfg.engineConfig().isPresent()); - ChicoryConfig cc = (ChicoryConfig) cfg.engineConfig().get(); - assertEquals(ChicoryConfig.ExecutionMode.COMPILE, cc.executionMode()); - } - - @Test - void builderHandsOffToCommonSettings() { - WebAssemblyConfig cfg = ChicoryConfigs.builder(b -> {}) - .debug(true) - .build(); - - assertEquals("chicory", cfg.engineId().orElse(null)); - assertTrue(cfg.commonConfig().debug().orElse(false)); - ChicoryConfig cc = (ChicoryConfig) cfg.engineConfig().get(); - assertEquals(ChicoryConfig.ExecutionMode.INTERPRET, cc.executionMode()); - } - - @Test - void engineIdConstantMatchesProviderDescriptor() { - assertEquals(new ChicoryProvider().descriptor().engineId(), ChicoryConfigs.ENGINE_ID); - } -} diff --git a/chicory4j-provider/pom.xml b/endive4j-provider/pom.xml similarity index 74% rename from chicory4j-provider/pom.xml rename to endive4j-provider/pom.xml index f410892..3a87c83 100644 --- a/chicory4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,14 +8,14 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 - chicory4j-provider + endive4j-provider jar - WebAssembly4J Chicory Provider - Chicory pure-Java engine provider for WebAssembly4J + WebAssembly4J Endive Provider + Endive pure-Java engine provider for WebAssembly4J @@ -23,19 +23,19 @@ webassembly4j-spi - com.dylibso.chicory + run.endive runtime - com.dylibso.chicory + run.endive wasm - com.dylibso.chicory + run.endive wasi - com.dylibso.chicory + run.endive compiler true diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/CompilerSupport.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/CompilerSupport.java similarity index 51% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/CompilerSupport.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/CompilerSupport.java index 7c38dc1..9d2fe64 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/CompilerSupport.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/CompilerSupport.java @@ -1,29 +1,29 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; -import com.dylibso.chicory.wasm.WasmModule; +import run.endive.wasm.WasmModule; import java.util.function.Function; /** - * Provides runtime compilation support via the optional {@code chicory-compiler} dependency. + * Provides runtime compilation support via the optional {@code endive-compiler} dependency. *

- * Uses reflection to avoid a hard dependency on {@code com.dylibso.chicory:compiler}, - * which is only needed when {@link ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig.ExecutionMode#COMPILE} + * Uses reflection to avoid a hard dependency on {@code run.endive:compiler}, + * which is only needed when {@link ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig.ExecutionMode#COMPILE} * is selected. */ final class CompilerSupport { - private static final String COMPILER_CLASS = "com.dylibso.chicory.compiler.MachineFactoryCompiler"; + private static final String COMPILER_CLASS = "run.endive.compiler.MachineFactoryCompiler"; private CompilerSupport() {} - static Function createCompilingBuilderFactory() { + static Function createCompilingBuilderFactory() { try { Class.forName(COMPILER_CLASS); } catch (ClassNotFoundException e) { throw new UnsupportedFeatureException( - "Runtime compilation requires com.dylibso.chicory:compiler on the classpath. " + + "Runtime compilation requires run.endive:compiler on the classpath. " + "Add the dependency or use ExecutionMode.INTERPRET."); } return CompilerBridge::createBuilder; @@ -34,10 +34,10 @@ static Function create * when actually needed (after the class presence check). */ private static final class CompilerBridge { - static com.dylibso.chicory.runtime.Instance.Builder createBuilder(WasmModule module) { - return com.dylibso.chicory.runtime.Instance.builder(module) + static run.endive.runtime.Instance.Builder createBuilder(WasmModule module) { + return run.endive.runtime.Instance.builder(module) .withMachineFactory( - com.dylibso.chicory.compiler.MachineFactoryCompiler::compile); + run.endive.compiler.MachineFactoryCompiler::compile); } } } diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigs.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigs.java new file mode 100644 index 0000000..ac684a2 --- /dev/null +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigs.java @@ -0,0 +1,42 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; +import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfigBuilder; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; + +import java.util.function.Consumer; + +/** + * Convenience entry point for building a {@link WebAssemblyConfig} pre-wired for the + * Endive pure-Java engine. + * + *

Examples

+ *
{@code
+ * // Just Endive tunings:
+ * WebAssemblyConfig cfg = EndiveConfigs.of(b -> b
+ *         .executionMode(EndiveConfig.ExecutionMode.COMPILE));
+ *
+ * // Default interpreter + cross-engine settings:
+ * WebAssemblyConfig cfg = EndiveConfigs.builder(b -> {})
+ *         .debug(true)
+ *         .build();
+ * }
+ */ +public final class EndiveConfigs { + + public static final String ENGINE_ID = "endive"; + + private EndiveConfigs() {} + + public static WebAssemblyConfig of(Consumer configurator) { + return builder(configurator).build(); + } + + public static WebAssemblyConfigBuilder builder(Consumer configurator) { + EndiveConfig.Builder cb = EndiveConfig.builder(); + configurator.accept(cb); + return WebAssemblyConfig.builder() + .engine(ENGINE_ID) + .engineConfig(cb.build()); + } +} diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java similarity index 58% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java index ef2a320..07232ca 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java @@ -1,4 +1,4 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Component; import ai.tegmentum.webassembly4j.api.Engine; @@ -8,68 +8,68 @@ import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; import ai.tegmentum.webassembly4j.api.exception.ValidationException; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; -import com.dylibso.chicory.wasm.Parser; -import com.dylibso.chicory.wasm.WasmModule; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; +import run.endive.wasm.Parser; +import run.endive.wasm.WasmModule; import java.util.Optional; import java.util.function.Function; -final class ChicoryEngineAdapter implements Engine { +final class EndiveEngineAdapter implements Engine { private final WebAssemblyConfig config; - private final ChicoryConfig.ExecutionMode executionMode; + private final EndiveConfig.ExecutionMode executionMode; - private ChicoryEngineAdapter(WebAssemblyConfig config, ChicoryConfig.ExecutionMode executionMode) { + private EndiveEngineAdapter(WebAssemblyConfig config, EndiveConfig.ExecutionMode executionMode) { this.config = config; this.executionMode = executionMode; } - static ChicoryEngineAdapter create(WebAssemblyConfig config) { - ChicoryConfig.ExecutionMode mode = ChicoryConfig.ExecutionMode.INTERPRET; + static EndiveEngineAdapter create(WebAssemblyConfig config) { + EndiveConfig.ExecutionMode mode = EndiveConfig.ExecutionMode.INTERPRET; if (config != null) { Optional ec = config.engineConfig(); - if (ec.isPresent() && ec.get() instanceof ChicoryConfig) { - mode = ((ChicoryConfig) ec.get()).executionMode(); + if (ec.isPresent() && ec.get() instanceof EndiveConfig) { + mode = ((EndiveConfig) ec.get()).executionMode(); } } - return new ChicoryEngineAdapter(config, mode); + return new EndiveEngineAdapter(config, mode); } @Override public EngineInfo info() { - return new ChicoryEngineInfo(); + return new EndiveEngineInfo(); } @Override public EngineCapabilities capabilities() { - return new ChicoryEngineCapabilities(); + return new EndiveEngineCapabilities(); } @Override public Module loadModule(byte[] bytes) { try { WasmModule wasmModule = Parser.parse(bytes); - Function builderFactory = + Function builderFactory = createBuilderFactory(wasmModule); - return new ChicoryModuleAdapter(wasmModule, builderFactory); + return new EndiveModuleAdapter(wasmModule, builderFactory); } catch (Exception e) { throw new ValidationException("Failed to load WebAssembly module", e); } } - private Function createBuilderFactory( + private Function createBuilderFactory( WasmModule wasmModule) { - if (executionMode == ChicoryConfig.ExecutionMode.COMPILE) { + if (executionMode == EndiveConfig.ExecutionMode.COMPILE) { return CompilerSupport.createCompilingBuilderFactory(); } - return m -> com.dylibso.chicory.runtime.Instance.builder(m); + return m -> run.endive.runtime.Instance.builder(m); } @Override public Component loadComponent(byte[] bytes) { throw new UnsupportedFeatureException( - "Component model is not supported by Chicory"); + "Component model is not supported by Endive"); } @Override diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineCapabilities.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java similarity index 87% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineCapabilities.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java index 97976f0..df1c9ed 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineCapabilities.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java @@ -1,8 +1,8 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.EngineCapabilities; -final class ChicoryEngineCapabilities implements EngineCapabilities { +final class EndiveEngineCapabilities implements EngineCapabilities { @Override public boolean supportsCoreModules() { diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineInfo.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineInfo.java similarity index 72% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineInfo.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineInfo.java index cb2194b..0e519fa 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryEngineInfo.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineInfo.java @@ -1,17 +1,17 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.EngineInfo; -final class ChicoryEngineInfo implements EngineInfo { +final class EndiveEngineInfo implements EngineInfo { @Override public String engineId() { - return "chicory"; + return "endive"; } @Override public String providerId() { - return "chicory"; + return "endive"; } @Override diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryFunctionAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveFunctionAdapter.java similarity index 79% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryFunctionAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveFunctionAdapter.java index 9f79d42..69ea7d5 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryFunctionAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveFunctionAdapter.java @@ -1,16 +1,16 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Function; import ai.tegmentum.webassembly4j.api.ValueType; import ai.tegmentum.webassembly4j.api.exception.ExecutionException; import ai.tegmentum.webassembly4j.api.exception.TrapException; -import com.dylibso.chicory.runtime.ExportFunction; -import com.dylibso.chicory.wasm.types.FunctionType; -import com.dylibso.chicory.wasm.types.ValType; +import run.endive.runtime.ExportFunction; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.ValType; import java.util.List; -final class ChicoryFunctionAdapter implements Function { +final class EndiveFunctionAdapter implements Function { private static final long[] EMPTY_LONGS = new long[0]; @@ -24,7 +24,7 @@ private enum FastPath { V_V, V_I, I_I, II_I, I_V, J_J, II_V, I_J, D_D, V_J, GENE private final long[] argScratch; private final FastPath fastPath; - ChicoryFunctionAdapter(ExportFunction nativeFunction, FunctionType functionType) { + EndiveFunctionAdapter(ExportFunction nativeFunction, FunctionType functionType) { this.nativeFunction = nativeFunction; this.functionType = functionType; this.cachedParamTypes = functionType.params(); @@ -116,14 +116,14 @@ public Object invoke(Object... args) { argScratch[0] = ((Number) args[0]).longValue(); return nativeFunction.apply(argScratch)[0]; case D_D: - argScratch[0] = com.dylibso.chicory.wasm.types.Value.doubleToLong( + argScratch[0] = run.endive.wasm.types.Value.doubleToLong( ((Number) args[0]).doubleValue()); - return com.dylibso.chicory.wasm.types.Value.longToDouble( + return run.endive.wasm.types.Value.longToDouble( nativeFunction.apply(argScratch)[0]); default: return invokeGeneric(args); } - } catch (com.dylibso.chicory.runtime.TrapException e) { + } catch (run.endive.runtime.TrapException e) { throw new TrapException(e.getMessage(), e); } catch (Exception e) { if (e instanceof ExecutionException) throw (ExecutionException) e; @@ -164,9 +164,9 @@ private long convertToLong(Object value, ValType type) { } else if (ValType.I64.equals(type)) { return num.longValue(); } else if (ValType.F32.equals(type)) { - return com.dylibso.chicory.wasm.types.Value.floatToLong(num.floatValue()); + return run.endive.wasm.types.Value.floatToLong(num.floatValue()); } else if (ValType.F64.equals(type)) { - return com.dylibso.chicory.wasm.types.Value.doubleToLong(num.doubleValue()); + return run.endive.wasm.types.Value.doubleToLong(num.doubleValue()); } else { throw new ExecutionException("Unsupported parameter type: " + type); } @@ -178,30 +178,30 @@ private Object extractValue(long raw, ValType type) { } else if (ValType.I64.equals(type)) { return raw; } else if (ValType.F32.equals(type)) { - return com.dylibso.chicory.wasm.types.Value.longToFloat(raw); + return run.endive.wasm.types.Value.longToFloat(raw); } else if (ValType.F64.equals(type)) { - return com.dylibso.chicory.wasm.types.Value.longToDouble(raw); + return run.endive.wasm.types.Value.longToDouble(raw); } else { return raw; } } - private static ValueType[] convertTypes(List chicoryTypes) { - ValueType[] types = new ValueType[chicoryTypes.size()]; - for (int i = 0; i < chicoryTypes.size(); i++) { - types[i] = convertType(chicoryTypes.get(i)); + private static ValueType[] convertTypes(List endiveTypes) { + ValueType[] types = new ValueType[endiveTypes.size()]; + for (int i = 0; i < endiveTypes.size(); i++) { + types[i] = convertType(endiveTypes.get(i)); } return types; } - private static ValueType convertType(ValType chicoryType) { - if (chicoryType .equals(ValType.I32)) return ValueType.I32; - if (chicoryType .equals(ValType.I64)) return ValueType.I64; - if (chicoryType .equals(ValType.F32)) return ValueType.F32; - if (chicoryType .equals(ValType.F64)) return ValueType.F64; - if (chicoryType .equals(ValType.V128)) return ValueType.V128; - if (chicoryType .equals(ValType.FuncRef)) return ValueType.FUNCREF; - if (chicoryType .equals(ValType.ExternRef)) return ValueType.EXTERNREF; - throw new IllegalArgumentException("Unknown type: " + chicoryType); + private static ValueType convertType(ValType endiveType) { + if (endiveType .equals(ValType.I32)) return ValueType.I32; + if (endiveType .equals(ValType.I64)) return ValueType.I64; + if (endiveType .equals(ValType.F32)) return ValueType.F32; + if (endiveType .equals(ValType.F64)) return ValueType.F64; + if (endiveType .equals(ValType.V128)) return ValueType.V128; + if (endiveType .equals(ValType.FuncRef)) return ValueType.FUNCREF; + if (endiveType .equals(ValType.ExternRef)) return ValueType.EXTERNREF; + throw new IllegalArgumentException("Unknown type: " + endiveType); } } diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtension.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtension.java similarity index 84% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtension.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtension.java index 980ae01..27cdce4 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtension.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtension.java @@ -1,4 +1,4 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.gc.GcArrayInstance; import ai.tegmentum.webassembly4j.api.gc.GcArrayType; @@ -12,23 +12,23 @@ import ai.tegmentum.webassembly4j.api.gc.GcStructInstance; import ai.tegmentum.webassembly4j.api.gc.GcStructType; import ai.tegmentum.webassembly4j.api.gc.GcValue; -import com.dylibso.chicory.runtime.WasmArray; -import com.dylibso.chicory.runtime.WasmI31Ref; -import com.dylibso.chicory.runtime.WasmStruct; -import com.dylibso.chicory.wasm.types.Value; +import run.endive.runtime.WasmArray; +import run.endive.runtime.WasmI31Ref; +import run.endive.runtime.WasmStruct; +import run.endive.wasm.types.Value; import java.util.List; import java.util.concurrent.atomic.AtomicLong; /** - * Chicory implementation of the GC extension. + * Endive implementation of the GC extension. * - *

Chicory GC objects are plain Java objects allocated on the JVM heap. - * Values are encoded as {@code long} using Chicory's {@link Value} helpers. + *

Endive GC objects are plain Java objects allocated on the JVM heap. + * Values are encoded as {@code long} using Endive's {@link Value} helpers. * A synthetic type index starting from 0x10000 is used for programmatically * created types to avoid collision with module type sections. */ -final class ChicoryGcExtension implements GcExtension { +final class EndiveGcExtension implements GcExtension { private static final int SYNTHETIC_TYPE_BASE = 0x10000; @@ -48,7 +48,7 @@ public GcStructInstance createStruct(GcStructType type, GcValue... values) { int typeIdx = (int) nextTypeIndex.getAndIncrement(); WasmStruct nativeStruct = new WasmStruct(typeIdx, fields); allocationCount.incrementAndGet(); - return new ChicoryGcStructInstance(nativeStruct, type); + return new EndiveGcStructInstance(nativeStruct, type); } @Override @@ -66,7 +66,7 @@ public GcArrayInstance createArray(GcArrayType type, GcValue... elements) { int typeIdx = (int) nextTypeIndex.getAndIncrement(); WasmArray nativeArray = new WasmArray(typeIdx, encoded); allocationCount.incrementAndGet(); - return new ChicoryGcArrayInstance(nativeArray, type); + return new EndiveGcArrayInstance(nativeArray, type); } @Override @@ -83,14 +83,14 @@ public GcArrayInstance createArray(GcArrayType type, int length) { int typeIdx = (int) nextTypeIndex.getAndIncrement(); WasmArray nativeArray = new WasmArray(typeIdx, encoded); allocationCount.incrementAndGet(); - return new ChicoryGcArrayInstance(nativeArray, type); + return new EndiveGcArrayInstance(nativeArray, type); } @Override public GcI31Instance createI31(int value) { WasmI31Ref nativeRef = new WasmI31Ref(value); allocationCount.incrementAndGet(); - return new ChicoryGcI31Instance(nativeRef); + return new EndiveGcI31Instance(nativeRef); } @Override @@ -162,12 +162,12 @@ private static GcValue decodeValue(long raw, GcFieldType fieldType) { // --- Inner adapter classes --- - private static final class ChicoryGcStructInstance implements GcStructInstance { + private static final class EndiveGcStructInstance implements GcStructInstance { private final WasmStruct nativeStruct; private final GcStructType structType; - ChicoryGcStructInstance(WasmStruct nativeStruct, GcStructType structType) { + EndiveGcStructInstance(WasmStruct nativeStruct, GcStructType structType) { this.nativeStruct = nativeStruct; this.structType = structType; } @@ -214,19 +214,19 @@ public boolean isNull() { @Override public boolean refEquals(GcObject other) { - if (!(other instanceof ChicoryGcStructInstance)) { + if (!(other instanceof EndiveGcStructInstance)) { return false; } - return this.nativeStruct == ((ChicoryGcStructInstance) other).nativeStruct; + return this.nativeStruct == ((EndiveGcStructInstance) other).nativeStruct; } } - private static final class ChicoryGcArrayInstance implements GcArrayInstance { + private static final class EndiveGcArrayInstance implements GcArrayInstance { private final WasmArray nativeArray; private final GcArrayType arrayType; - ChicoryGcArrayInstance(WasmArray nativeArray, GcArrayType arrayType) { + EndiveGcArrayInstance(WasmArray nativeArray, GcArrayType arrayType) { this.nativeArray = nativeArray; this.arrayType = arrayType; } @@ -271,18 +271,18 @@ public boolean isNull() { @Override public boolean refEquals(GcObject other) { - if (!(other instanceof ChicoryGcArrayInstance)) { + if (!(other instanceof EndiveGcArrayInstance)) { return false; } - return this.nativeArray == ((ChicoryGcArrayInstance) other).nativeArray; + return this.nativeArray == ((EndiveGcArrayInstance) other).nativeArray; } } - private static final class ChicoryGcI31Instance implements GcI31Instance { + private static final class EndiveGcI31Instance implements GcI31Instance { private final WasmI31Ref nativeRef; - ChicoryGcI31Instance(WasmI31Ref nativeRef) { + EndiveGcI31Instance(WasmI31Ref nativeRef) { this.nativeRef = nativeRef; } @@ -313,10 +313,10 @@ public boolean isNull() { @Override public boolean refEquals(GcObject other) { - if (!(other instanceof ChicoryGcI31Instance)) { + if (!(other instanceof EndiveGcI31Instance)) { return false; } - return this.nativeRef.value() == ((ChicoryGcI31Instance) other).nativeRef.value(); + return this.nativeRef.value() == ((EndiveGcI31Instance) other).nativeRef.value(); } } } diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGlobalAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGlobalAdapter.java similarity index 70% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGlobalAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGlobalAdapter.java index 18992b5..c26f92b 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGlobalAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGlobalAdapter.java @@ -1,19 +1,19 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Global; import ai.tegmentum.webassembly4j.api.ValueType; -import com.dylibso.chicory.runtime.GlobalInstance; -import com.dylibso.chicory.wasm.types.MutabilityType; -import com.dylibso.chicory.wasm.types.ValType; -import com.dylibso.chicory.wasm.types.Value; +import run.endive.runtime.GlobalInstance; +import run.endive.wasm.types.MutabilityType; +import run.endive.wasm.types.ValType; +import run.endive.wasm.types.Value; import java.util.Optional; -final class ChicoryGlobalAdapter implements Global { +final class EndiveGlobalAdapter implements Global { private final GlobalInstance nativeGlobal; - ChicoryGlobalAdapter(GlobalInstance nativeGlobal) { + EndiveGlobalAdapter(GlobalInstance nativeGlobal) { this.nativeGlobal = nativeGlobal; } @@ -70,11 +70,11 @@ public Optional unwrap(Class nativeType) { return Optional.empty(); } - private static ValueType convertType(ValType chicoryType) { - if (chicoryType .equals(ValType.I32)) return ValueType.I32; - if (chicoryType .equals(ValType.I64)) return ValueType.I64; - if (chicoryType .equals(ValType.F32)) return ValueType.F32; - if (chicoryType .equals(ValType.F64)) return ValueType.F64; - throw new IllegalArgumentException("Unknown type: " + chicoryType); + private static ValueType convertType(ValType endiveType) { + if (endiveType .equals(ValType.I32)) return ValueType.I32; + if (endiveType .equals(ValType.I64)) return ValueType.I64; + if (endiveType .equals(ValType.F32)) return ValueType.F32; + if (endiveType .equals(ValType.F64)) return ValueType.F64; + throw new IllegalArgumentException("Unknown type: " + endiveType); } } diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryInstanceAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveInstanceAdapter.java similarity index 71% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryInstanceAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveInstanceAdapter.java index c5819ce..1f38a23 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryInstanceAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveInstanceAdapter.java @@ -1,4 +1,4 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Function; import ai.tegmentum.webassembly4j.api.Global; @@ -11,13 +11,13 @@ import java.util.Map; import java.util.Optional; -final class ChicoryInstanceAdapter implements Instance { +final class EndiveInstanceAdapter implements Instance { - private final com.dylibso.chicory.runtime.Instance nativeInstance; + private final run.endive.runtime.Instance nativeInstance; private final Map> functionCache = new HashMap<>(); private final Map> memoryCache = new HashMap<>(); - ChicoryInstanceAdapter(com.dylibso.chicory.runtime.Instance nativeInstance) { + EndiveInstanceAdapter(run.endive.runtime.Instance nativeInstance) { this.nativeInstance = nativeInstance; } @@ -29,11 +29,11 @@ public Optional function(String name) { } Optional result; try { - com.dylibso.chicory.runtime.ExportFunction exportFunc = + run.endive.runtime.ExportFunction exportFunc = nativeInstance.exports().function(name); - com.dylibso.chicory.wasm.types.FunctionType funcType = + run.endive.wasm.types.FunctionType funcType = nativeInstance.exportType(name); - result = Optional.of(new ChicoryFunctionAdapter(exportFunc, funcType)); + result = Optional.of(new EndiveFunctionAdapter(exportFunc, funcType)); } catch (Exception e) { result = Optional.empty(); } @@ -49,9 +49,9 @@ public Optional memory(String name) { } Optional result; try { - com.dylibso.chicory.runtime.Memory nativeMemory = + run.endive.runtime.Memory nativeMemory = nativeInstance.exports().memory(name); - result = Optional.of(new ChicoryMemoryAdapter(nativeMemory)); + result = Optional.of(new EndiveMemoryAdapter(nativeMemory)); } catch (Exception e) { result = Optional.empty(); } @@ -62,9 +62,9 @@ public Optional memory(String name) { @Override public Optional table(String name) { try { - com.dylibso.chicory.runtime.TableInstance tableInstance = + run.endive.runtime.TableInstance tableInstance = nativeInstance.exports().table(name); - return Optional.of(new ChicoryTableAdapter(tableInstance)); + return Optional.of(new EndiveTableAdapter(tableInstance)); } catch (Exception e) { return Optional.empty(); } @@ -73,9 +73,9 @@ public Optional
table(String name) { @Override public Optional global(String name) { try { - com.dylibso.chicory.runtime.GlobalInstance globalInstance = + run.endive.runtime.GlobalInstance globalInstance = nativeInstance.exports().global(name); - return Optional.of(new ChicoryGlobalAdapter(globalInstance)); + return Optional.of(new EndiveGlobalAdapter(globalInstance)); } catch (Exception e) { return Optional.empty(); } @@ -85,7 +85,7 @@ public Optional global(String name) { @SuppressWarnings("unchecked") public Optional extension(Class extensionType) { if (extensionType == GcExtension.class) { - return Optional.of((T) new ChicoryGcExtension()); + return Optional.of((T) new EndiveGcExtension()); } return Optional.empty(); } diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryMemoryAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveMemoryAdapter.java similarity index 72% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryMemoryAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveMemoryAdapter.java index 7dd075e..477030e 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryMemoryAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveMemoryAdapter.java @@ -1,21 +1,21 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Memory; import java.nio.ByteBuffer; import java.util.Optional; -final class ChicoryMemoryAdapter implements Memory { +final class EndiveMemoryAdapter implements Memory { - private final com.dylibso.chicory.runtime.Memory nativeMemory; + private final run.endive.runtime.Memory nativeMemory; - ChicoryMemoryAdapter(com.dylibso.chicory.runtime.Memory nativeMemory) { + EndiveMemoryAdapter(run.endive.runtime.Memory nativeMemory) { this.nativeMemory = nativeMemory; } @Override public long byteSize() { - return (long) nativeMemory.pages() * com.dylibso.chicory.runtime.Memory.PAGE_SIZE; + return (long) nativeMemory.pages() * run.endive.runtime.Memory.PAGE_SIZE; } @Override @@ -36,7 +36,7 @@ public long grow(long pages) { @Override public ByteBuffer asByteBuffer() { throw new UnsupportedOperationException( - "Chicory does not expose memory as a direct ByteBuffer"); + "Endive does not expose memory as a direct ByteBuffer"); } @Override diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryModuleAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveModuleAdapter.java similarity index 83% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryModuleAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveModuleAdapter.java index 2053352..d001f20 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryModuleAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveModuleAdapter.java @@ -1,4 +1,4 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.ExportDescriptor; import ai.tegmentum.webassembly4j.api.HostFunctionDefinition; @@ -10,19 +10,19 @@ import ai.tegmentum.webassembly4j.api.WasiContext; import ai.tegmentum.webassembly4j.api.exception.InstantiationException; import ai.tegmentum.webassembly4j.api.exception.LinkingException; -import com.dylibso.chicory.runtime.HostFunction; -import com.dylibso.chicory.runtime.ImportFunction; -import com.dylibso.chicory.runtime.ImportValues; -import com.dylibso.chicory.wasi.WasiOptions; -import com.dylibso.chicory.wasi.WasiPreview1; -import com.dylibso.chicory.wasm.WasmModule; -import com.dylibso.chicory.wasm.types.Export; -import com.dylibso.chicory.wasm.types.ExternalType; -import com.dylibso.chicory.wasm.types.FunctionImport; -import com.dylibso.chicory.wasm.types.FunctionType; -import com.dylibso.chicory.wasm.types.Import; -import com.dylibso.chicory.wasm.types.ValType; -import com.dylibso.chicory.wasm.types.Value; +import run.endive.runtime.HostFunction; +import run.endive.runtime.ImportFunction; +import run.endive.runtime.ImportValues; +import run.endive.wasi.WasiOptions; +import run.endive.wasi.WasiPreview1; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.Export; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.FunctionImport; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.Import; +import run.endive.wasm.types.ValType; +import run.endive.wasm.types.Value; import java.nio.file.Paths; import java.util.ArrayList; @@ -31,13 +31,13 @@ import java.util.List; import java.util.Map; -final class ChicoryModuleAdapter implements Module { +final class EndiveModuleAdapter implements Module { private final WasmModule wasmModule; - private final java.util.function.Function builderFactory; + private final java.util.function.Function builderFactory; - ChicoryModuleAdapter(WasmModule wasmModule, - java.util.function.Function builderFactory) { + EndiveModuleAdapter(WasmModule wasmModule, + java.util.function.Function builderFactory) { this.wasmModule = wasmModule; this.builderFactory = builderFactory; } @@ -45,9 +45,9 @@ final class ChicoryModuleAdapter implements Module { @Override public Instance instantiate() { try { - com.dylibso.chicory.runtime.Instance nativeInstance = + run.endive.runtime.Instance nativeInstance = builderFactory.apply(wasmModule).build(); - return new ChicoryInstanceAdapter(nativeInstance); + return new EndiveInstanceAdapter(nativeInstance); } catch (Exception e) { throw new InstantiationException( "Failed to instantiate WebAssembly module", e); @@ -90,8 +90,8 @@ public Instance instantiate(LinkingContext linkingContext) { List hostFunctions = linkingContext.hostFunctions(); for (HostFunctionDefinition def : hostFunctions) { - List paramTypes = convertToChicoryTypes(def.parameterTypes()); - List returnTypes = convertToChicoryTypes(def.resultTypes()); + List paramTypes = convertToEndiveTypes(def.parameterTypes()); + List returnTypes = convertToEndiveTypes(def.resultTypes()); HostFunction hostFunc = new HostFunction( def.moduleName(), def.functionName(), @@ -115,25 +115,25 @@ public Instance instantiate(LinkingContext linkingContext) { importBuilder.withFunctions(functions); ImportValues imports = importBuilder.build(); - com.dylibso.chicory.runtime.Instance nativeInstance = + run.endive.runtime.Instance nativeInstance = builderFactory.apply(wasmModule) .withImportValues(imports) .build(); - return new ChicoryInstanceAdapter(nativeInstance); + return new EndiveInstanceAdapter(nativeInstance); } catch (Exception e) { throw new LinkingException("Failed to instantiate with linking context", e); } } - private static List convertToChicoryTypes(ValueType[] types) { + private static List convertToEndiveTypes(ValueType[] types) { List result = new ArrayList<>(types.length); for (ValueType type : types) { - result.add(convertToChicoryType(type)); + result.add(convertToEndiveType(type)); } return result; } - private static ValType convertToChicoryType(ValueType type) { + private static ValType convertToEndiveType(ValueType type) { switch (type) { case I32: return ValType.I32; case I64: return ValType.I64; @@ -232,8 +232,8 @@ private ExportDescriptor convertFunctionExport(Export export) { FunctionType funcType = wasmModule.functionSection() .getFunctionType(localIndex, wasmModule.typeSection()); return ExportDescriptor.function(export.name(), - convertChicoryTypes(funcType.params()), - convertChicoryTypes(funcType.returns())); + convertEndiveTypes(funcType.params()), + convertEndiveTypes(funcType.returns())); } return ExportDescriptor.function(export.name(), new ValueType[0], new ValueType[0]); @@ -248,8 +248,8 @@ private ImportDescriptor convertImport(Import imp) { if (typeIdx < wasmModule.typeSection().typeCount()) { FunctionType funcType = wasmModule.typeSection().getType(typeIdx); return ImportDescriptor.function(imp.module(), imp.name(), - convertChicoryTypes(funcType.params()), - convertChicoryTypes(funcType.returns())); + convertEndiveTypes(funcType.params()), + convertEndiveTypes(funcType.returns())); } return ImportDescriptor.function(imp.module(), imp.name(), new ValueType[0], new ValueType[0]); @@ -264,15 +264,15 @@ private ImportDescriptor convertImport(Import imp) { } } - private static ValueType[] convertChicoryTypes(List types) { + private static ValueType[] convertEndiveTypes(List types) { ValueType[] result = new ValueType[types.size()]; for (int i = 0; i < types.size(); i++) { - result[i] = convertChicoryType(types.get(i)); + result[i] = convertEndiveType(types.get(i)); } return result; } - private static ValueType convertChicoryType(ValType type) { + private static ValueType convertEndiveType(ValType type) { if (type .equals(ValType.I32)) return ValueType.I32; if (type .equals(ValType.I64)) return ValueType.I64; if (type .equals(ValType.F32)) return ValueType.F32; diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProvider.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProvider.java similarity index 76% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProvider.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProvider.java index 8178220..eb9939b 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProvider.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProvider.java @@ -1,9 +1,9 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Engine; import ai.tegmentum.webassembly4j.api.config.EngineConfig; import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; import ai.tegmentum.webassembly4j.spi.EngineProvider; import ai.tegmentum.webassembly4j.spi.ProviderAvailability; import ai.tegmentum.webassembly4j.spi.ProviderDescriptor; @@ -13,10 +13,10 @@ import java.util.Optional; import java.util.Set; -public final class ChicoryProvider implements EngineProvider { +public final class EndiveProvider implements EngineProvider { - private static final String ENGINE_ID = "chicory"; - private static final String PROVIDER_ID = "chicory"; + private static final String ENGINE_ID = "endive"; + private static final String PROVIDER_ID = "endive"; private static final String VERSION = "1.0.0-SNAPSHOT"; private static final int MIN_JAVA = 11; private static final int PRIORITY = 50; @@ -42,17 +42,17 @@ public ProviderDescriptor descriptor() { public ProviderAvailability availability() { return new ProviderAvailability() { @Override public boolean available() { return true; } - @Override public String message() { return "Chicory pure-Java runtime always available"; } + @Override public String message() { return "Endive pure-Java runtime always available"; } }; } @Override public Optional> configType() { - return Optional.of(ChicoryConfig.class); + return Optional.of(EndiveConfig.class); } @Override public Engine create(WebAssemblyConfig config) { - return ChicoryEngineAdapter.create(config); + return EndiveEngineAdapter.create(config); } } diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryTableAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveTableAdapter.java similarity index 70% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryTableAdapter.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveTableAdapter.java index cd0542f..5fbf670 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryTableAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveTableAdapter.java @@ -1,15 +1,15 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Table; -import com.dylibso.chicory.runtime.TableInstance; +import run.endive.runtime.TableInstance; import java.util.Optional; -final class ChicoryTableAdapter implements Table { +final class EndiveTableAdapter implements Table { private final TableInstance nativeTable; - ChicoryTableAdapter(TableInstance nativeTable) { + EndiveTableAdapter(TableInstance nativeTable) { this.nativeTable = nativeTable; } diff --git a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/config/ChicoryConfig.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/config/EndiveConfig.java similarity index 71% rename from chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/config/ChicoryConfig.java rename to endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/config/EndiveConfig.java index a834ffc..ae257e3 100644 --- a/chicory4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/chicory/config/ChicoryConfig.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/config/EndiveConfig.java @@ -1,20 +1,20 @@ -package ai.tegmentum.webassembly4j.provider.chicory.config; +package ai.tegmentum.webassembly4j.provider.endive.config; import ai.tegmentum.webassembly4j.api.config.EngineConfig; import java.util.Optional; /** - * Configuration for the Chicory WebAssembly engine. + * Configuration for the Endive WebAssembly engine. * *

Controls the execution mode used when instantiating modules: *

    *
  • {@link ExecutionMode#INTERPRET} (default) — pure interpretation, no extra dependencies
  • *
  • {@link ExecutionMode#COMPILE} — runtime AOT compilation to JVM bytecode via ASM, - * requires {@code com.dylibso.chicory:compiler} on the classpath
  • + * requires {@code run.endive:compiler} on the classpath *
*/ -public final class ChicoryConfig implements EngineConfig { +public final class EndiveConfig implements EngineConfig { /** * Execution mode for WebAssembly modules. @@ -22,13 +22,13 @@ public final class ChicoryConfig implements EngineConfig { public enum ExecutionMode { /** Pure interpreter — no extra dependencies required. */ INTERPRET, - /** Runtime compilation to JVM bytecode — requires chicory-compiler dependency. */ + /** Runtime compilation to JVM bytecode — requires endive-compiler dependency. */ COMPILE } private final ExecutionMode executionMode; - private ChicoryConfig(Builder builder) { + private EndiveConfig(Builder builder) { this.executionMode = builder.executionMode; } @@ -50,8 +50,8 @@ public Builder executionMode(ExecutionMode executionMode) { return this; } - public ChicoryConfig build() { - return new ChicoryConfig(this); + public EndiveConfig build() { + return new EndiveConfig(this); } } } diff --git a/endive4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider b/endive4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider new file mode 100644 index 0000000..5918e2e --- /dev/null +++ b/endive4j-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider @@ -0,0 +1 @@ +ai.tegmentum.webassembly4j.provider.endive.EndiveProvider diff --git a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigsTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigsTest.java new file mode 100644 index 0000000..fa8377f --- /dev/null +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveConfigsTest.java @@ -0,0 +1,38 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EndiveConfigsTest { + + @Test + void ofWiresEngineAndEngineConfig() { + WebAssemblyConfig cfg = EndiveConfigs.of(b -> b + .executionMode(EndiveConfig.ExecutionMode.COMPILE)); + + assertEquals("endive", cfg.engineId().orElse(null)); + assertTrue(cfg.engineConfig().isPresent()); + EndiveConfig cc = (EndiveConfig) cfg.engineConfig().get(); + assertEquals(EndiveConfig.ExecutionMode.COMPILE, cc.executionMode()); + } + + @Test + void builderHandsOffToCommonSettings() { + WebAssemblyConfig cfg = EndiveConfigs.builder(b -> {}) + .debug(true) + .build(); + + assertEquals("endive", cfg.engineId().orElse(null)); + assertTrue(cfg.commonConfig().debug().orElse(false)); + EndiveConfig cc = (EndiveConfig) cfg.engineConfig().get(); + assertEquals(EndiveConfig.ExecutionMode.INTERPRET, cc.executionMode()); + } + + @Test + void engineIdConstantMatchesProviderDescriptor() { + assertEquals(new EndiveProvider().descriptor().engineId(), EndiveConfigs.ENGINE_ID); + } +} diff --git a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtensionTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtensionTest.java similarity index 96% rename from chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtensionTest.java rename to endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtensionTest.java index c904892..ecd37eb 100644 --- a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryGcExtensionTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveGcExtensionTest.java @@ -1,4 +1,4 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.gc.GcArrayInstance; import ai.tegmentum.webassembly4j.api.gc.GcArrayType; @@ -17,13 +17,13 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -class ChicoryGcExtensionTest { +class EndiveGcExtensionTest { - private ChicoryGcExtension gc; + private EndiveGcExtension gc; @BeforeEach void setUp() { - gc = new ChicoryGcExtension(); + gc = new EndiveGcExtension(); } @Test diff --git a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryIntegrationTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveIntegrationTest.java similarity index 86% rename from chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryIntegrationTest.java rename to endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveIntegrationTest.java index a0d130a..522066d 100644 --- a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryIntegrationTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveIntegrationTest.java @@ -1,4 +1,4 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; import ai.tegmentum.webassembly4j.api.Engine; @@ -9,12 +9,12 @@ import ai.tegmentum.webassembly4j.api.ValueType; import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; import ai.tegmentum.webassembly4j.api.exception.WebAssemblyException; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -class ChicoryIntegrationTest { +class EndiveIntegrationTest { // (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add) private static final byte[] ADD_MODULE = new byte[] { @@ -59,7 +59,7 @@ class ChicoryIntegrationTest { @Test void addFunction() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(ADD_MODULE)) { Instance instance = module.instantiate(); Function add = instance.function("add").orElseThrow(); @@ -70,7 +70,7 @@ void addFunction() { @Test void voidFunction() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(VOID_MODULE)) { Instance instance = module.instantiate(); Function noop = instance.function("noop").orElseThrow(); @@ -81,14 +81,14 @@ void voidFunction() { @Test void invalidModuleThrows() { - try (Engine engine = ChicoryEngineAdapter.create(null)) { + try (Engine engine = EndiveEngineAdapter.create(null)) { assertThrows(WebAssemblyException.class, () -> engine.loadModule(INVALID_MODULE)); } } @Test void nonexistentFunctionReturnsEmpty() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(ADD_MODULE)) { Instance instance = module.instantiate(); assertFalse(instance.function("nonexistent").isPresent()); @@ -97,7 +97,7 @@ void nonexistentFunctionReturnsEmpty() { @Test void memoryReadWrite() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(MEMORY_MODULE)) { Instance instance = module.instantiate(); Function store = instance.function("store").orElseThrow(); @@ -111,7 +111,7 @@ void memoryReadWrite() { @Test void memoryDirectAccess() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(MEMORY_MODULE)) { Instance instance = module.instantiate(); Memory memory = instance.memory("memory").orElseThrow(); @@ -125,7 +125,7 @@ void memoryDirectAccess() { @Test void memoryGrow() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(MEMORY_MODULE)) { Instance instance = module.instantiate(); Memory memory = instance.memory("memory").orElseThrow(); @@ -140,7 +140,7 @@ void memoryGrow() { @Test void hostFunctionLinking() { - try (Engine engine = ChicoryEngineAdapter.create(null); + try (Engine engine = EndiveEngineAdapter.create(null); Module module = engine.loadModule(IMPORT_MODULE)) { DefaultLinkingContext ctx = DefaultLinkingContext.builder() .addHostFunction("env", "add_offset", @@ -158,12 +158,12 @@ void hostFunctionLinking() { @Test void runtimeCompilationMode() { WebAssemblyConfig config = WebAssemblyConfig.builder() - .engineConfig(ChicoryConfig.builder() - .executionMode(ChicoryConfig.ExecutionMode.COMPILE) + .engineConfig(EndiveConfig.builder() + .executionMode(EndiveConfig.ExecutionMode.COMPILE) .build()) .build(); - try (Engine engine = ChicoryEngineAdapter.create(config); + try (Engine engine = EndiveEngineAdapter.create(config); Module module = engine.loadModule(ADD_MODULE)) { Instance instance = module.instantiate(); Function add = instance.function("add").orElseThrow(); @@ -174,12 +174,12 @@ void runtimeCompilationMode() { @Test void compilationModeWithHostFunctions() { WebAssemblyConfig config = WebAssemblyConfig.builder() - .engineConfig(ChicoryConfig.builder() - .executionMode(ChicoryConfig.ExecutionMode.COMPILE) + .engineConfig(EndiveConfig.builder() + .executionMode(EndiveConfig.ExecutionMode.COMPILE) .build()) .build(); - try (Engine engine = ChicoryEngineAdapter.create(config); + try (Engine engine = EndiveEngineAdapter.create(config); Module module = engine.loadModule(IMPORT_MODULE)) { DefaultLinkingContext ctx = DefaultLinkingContext.builder() .addHostFunction("env", "add_offset", diff --git a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProviderTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java similarity index 79% rename from chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProviderTest.java rename to endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java index 1ec0ea9..8b6acdc 100644 --- a/chicory4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/chicory/ChicoryProviderTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java @@ -1,26 +1,26 @@ -package ai.tegmentum.webassembly4j.provider.chicory; +package ai.tegmentum.webassembly4j.provider.endive; import ai.tegmentum.webassembly4j.api.Engine; import ai.tegmentum.webassembly4j.api.EngineCapabilities; import ai.tegmentum.webassembly4j.api.EngineInfo; import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; import ai.tegmentum.webassembly4j.spi.ProviderAvailability; import ai.tegmentum.webassembly4j.spi.ProviderDescriptor; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -class ChicoryProviderTest { +class EndiveProviderTest { - private final ChicoryProvider provider = new ChicoryProvider(); + private final EndiveProvider provider = new EndiveProvider(); @Test void descriptorIsCorrect() { ProviderDescriptor desc = provider.descriptor(); - assertEquals("chicory", desc.engineId()); - assertEquals("chicory", desc.providerId()); + assertEquals("endive", desc.engineId()); + assertEquals("endive", desc.providerId()); assertEquals(11, desc.minimumJavaVersion()); assertEquals(50, desc.priority()); assertTrue(desc.tags().contains("pure-java")); @@ -44,8 +44,8 @@ void createEngine() { try (Engine engine = provider.create(null)) { assertNotNull(engine); EngineInfo info = engine.info(); - assertEquals("chicory", info.engineId()); - assertEquals("chicory", info.providerId()); + assertEquals("endive", info.engineId()); + assertEquals("endive", info.providerId()); assertEquals(11, info.minimumJavaVersion()); } } @@ -74,7 +74,7 @@ void loadComponentThrowsUnsupported() { } @Test - void configTypeAdvertisesChicoryConfig() { - assertEquals(ChicoryConfig.class, provider.configType().orElse(null)); + void configTypeAdvertisesEndiveConfig() { + assertEquals(EndiveConfig.class, provider.configType().orElse(null)); } } diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index ae756ac..f9e1a00 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index d4fbcb4..195c726 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 pom WebAssembly4J Parent @@ -43,7 +43,7 @@ wasmtime4j-provider wamr4j-provider wasm3-provider - chicory4j-provider + endive4j-provider graalwasm4j-provider webassembly4j-runtime webassembly4j-testing @@ -65,7 +65,7 @@ 5.10.4 46.0.1-1.1.7 2.4.4-1.0.2 - 1.7.5 + 1.0.0 24.2.1 1.37 2.22.0 @@ -123,24 +123,24 @@ ${wamr4j.version} - com.dylibso.chicory + run.endive runtime - ${chicory.version} + ${endive.version} - com.dylibso.chicory + run.endive wasm - ${chicory.version} + ${endive.version} - com.dylibso.chicory + run.endive wasi - ${chicory.version} + ${endive.version} - com.dylibso.chicory + run.endive compiler - ${chicory.version} + ${endive.version} org.graalvm.polyglot diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 50c46c5..d86a457 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 8522d4f..a6e6f92 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index ca160ba..3eaeba5 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index c771269..5e3b8b1 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-api diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/config/EngineConfig.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/config/EngineConfig.java index 55b7d66..3b11329 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/config/EngineConfig.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/config/EngineConfig.java @@ -6,7 +6,7 @@ *

The {@code webassembly4j-api} module is intentionally free of provider-specific * types, so this interface carries no methods. Each provider artifact ships its own * concrete implementation that exposes the tunables for that engine — for example - * {@code WasmtimeConfig}, {@code WamrConfig}, or {@code ChicoryConfig}. Obtain one + * {@code WasmtimeConfig}, {@code WamrConfig}, or {@code EndiveConfig}. Obtain one * from the provider's builder and hand it to * {@link WebAssemblyConfigBuilder#engineConfig(EngineConfig)}:

* diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index a9080d7..afb0421 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-benchmarks @@ -68,7 +68,7 @@
ai.tegmentum.webassembly4j - chicory4j-provider + endive4j-provider ${project.version} @@ -103,9 +103,9 @@ runtime - + - com.dylibso.chicory + run.endive compiler diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/BenchmarkReportGenerator.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/BenchmarkReportGenerator.java index f798696..785030b 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/BenchmarkReportGenerator.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/BenchmarkReportGenerator.java @@ -12,7 +12,7 @@ public final class BenchmarkReportGenerator { private static final String[] VARIANT_ORDER = { "WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", - "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY" + "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE" }; private static final Map VARIANT_LABELS = new LinkedHashMap<>(); @@ -24,7 +24,7 @@ public final class BenchmarkReportGenerator { VARIANT_LABELS.put("WAMR_LLVM_JIT_JNI", "WAMR LLVM JNI"); VARIANT_LABELS.put("WAMR_LLVM_JIT_PANAMA", "WAMR LLVM Panama"); VARIANT_LABELS.put("GRAALWASM", "GraalWasm"); - VARIANT_LABELS.put("CHICORY", "Chicory"); + VARIANT_LABELS.put("ENDIVE", "Endive"); } public static void main(String[] args) throws IOException { diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ChartReportGenerator.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ChartReportGenerator.java index 5d35ebb..f830e87 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ChartReportGenerator.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ChartReportGenerator.java @@ -29,7 +29,7 @@ public final class ChartReportGenerator { private static final String[] VARIANT_ORDER = { "WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", - "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY" + "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE" }; private static final Map VARIANT_LABELS = new LinkedHashMap<>(); @@ -41,7 +41,7 @@ public final class ChartReportGenerator { VARIANT_LABELS.put("WAMR_LLVM_JIT_JNI", "WAMR LLVM JNI"); VARIANT_LABELS.put("WAMR_LLVM_JIT_PANAMA", "WAMR LLVM Panama"); VARIANT_LABELS.put("GRAALWASM", "GraalWasm"); - VARIANT_LABELS.put("CHICORY", "Chicory"); + VARIANT_LABELS.put("ENDIVE", "Endive"); } private ChartReportGenerator() {} diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ConcurrentLoadBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ConcurrentLoadBenchmark.java index c903080..a61d55d 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ConcurrentLoadBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ConcurrentLoadBenchmark.java @@ -29,7 +29,7 @@ @Measurement(iterations = 5) public class ConcurrentLoadBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; private Engine engine; diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EndToEndBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EndToEndBenchmark.java index a51cc3e..254d621 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EndToEndBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EndToEndBenchmark.java @@ -16,7 +16,7 @@ @Measurement(iterations = 5) public class EndToEndBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; private EngineVariant engineVariant; diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EngineVariant.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EngineVariant.java index 0e96d06..696cf7d 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EngineVariant.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/EngineVariant.java @@ -1,7 +1,7 @@ package ai.tegmentum.webassembly4j.benchmarks; import ai.tegmentum.webassembly4j.api.config.EngineConfig; -import ai.tegmentum.webassembly4j.provider.chicory.config.ChicoryConfig; +import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; import ai.tegmentum.webassembly4j.provider.wamr.config.WamrConfig; import ai.tegmentum.webassembly4j.provider.wamr.config.WamrRunningMode; @@ -13,7 +13,7 @@ *
    *
  • {@code STANDARD} - native runtimes (Wasmtime, WAMR) that don't benefit from GraalVM JVMCI
  • *
  • {@code GRAALVM} - GraalWasm, which requires GraalVM CE/EE for JIT compilation
  • - *
  • {@code ANY} - pure-Java runtimes (Chicory) that benefit from comparison on both JDKs
  • + *
  • {@code ANY} - pure-Java runtimes (Endive) that benefit from comparison on both JDKs
  • *
*/ public enum EngineVariant { @@ -28,9 +28,9 @@ public enum EngineVariant { WAMR_LLVM_JIT_PANAMA("wamr", "wamr4j.runtime", "panama", () -> WamrConfig.builder().runningMode(WamrRunningMode.LLVM_JIT).build(), JdkAffinity.STANDARD), GRAALWASM("graalwasm", null, null, null, JdkAffinity.GRAALVM), - CHICORY("chicory", null, null, null, JdkAffinity.ANY), - CHICORY_COMPILED("chicory", null, null, - () -> ChicoryConfig.builder().executionMode(ChicoryConfig.ExecutionMode.COMPILE).build(), + ENDIVE("endive", null, null, null, JdkAffinity.ANY), + ENDIVE_COMPILED("endive", null, null, + () -> EndiveConfig.builder().executionMode(EndiveConfig.ExecutionMode.COMPILE).build(), JdkAffinity.ANY); public enum JdkAffinity { diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/FunctionInvocationBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/FunctionInvocationBenchmark.java index d664a55..3e87fc7 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/FunctionInvocationBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/FunctionInvocationBenchmark.java @@ -16,7 +16,7 @@ @Measurement(iterations = 5) public class FunctionInvocationBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; private Engine engine; diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/HostFunctionBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/HostFunctionBenchmark.java index 5279589..525702d 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/HostFunctionBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/HostFunctionBenchmark.java @@ -18,7 +18,7 @@ @Measurement(iterations = 5) public class HostFunctionBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; private Engine engine; diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/InstantiationBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/InstantiationBenchmark.java index bc2c162..22d101b 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/InstantiationBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/InstantiationBenchmark.java @@ -17,7 +17,7 @@ @Measurement(iterations = 5) public class InstantiationBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; private Engine engine; diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/MemoryOperationsBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/MemoryOperationsBenchmark.java index bf21d97..24e277e 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/MemoryOperationsBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/MemoryOperationsBenchmark.java @@ -17,7 +17,7 @@ @Measurement(iterations = 5) public class MemoryOperationsBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; @Param({"64", "1024", "4096", "65536"}) diff --git a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ModuleLoadBenchmark.java b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ModuleLoadBenchmark.java index d600ac7..5f3aebb 100644 --- a/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ModuleLoadBenchmark.java +++ b/webassembly4j-benchmarks/src/main/java/ai/tegmentum/webassembly4j/benchmarks/ModuleLoadBenchmark.java @@ -14,7 +14,7 @@ @Measurement(iterations = 5) public class ModuleLoadBenchmark { - @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "CHICORY", "CHICORY_COMPILED"}) + @Param({"WASMTIME_JNI", "WASMTIME_PANAMA", "WAMR_JNI", "WAMR_PANAMA", "WAMR_LLVM_JIT_JNI", "WAMR_LLVM_JIT_PANAMA", "GRAALWASM", "ENDIVE", "ENDIVE_COMPILED"}) private String variant; private Engine engine; diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index a1aa11c..b051e8e 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index bbb8f2a..45a0ded 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index d5032c2..4cf4bf4 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 87bfa1d..43f7aef 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-runtime @@ -36,7 +36,7 @@ ai.tegmentum.webassembly4j - chicory4j-provider + endive4j-provider ${project.version} test diff --git a/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/FastPathBenchmarkTest.java b/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/FastPathBenchmarkTest.java index c405ab7..e62f8d9 100644 --- a/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/FastPathBenchmarkTest.java +++ b/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/FastPathBenchmarkTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; /** - * Benchmarks for function signatures that hit vs miss the Chicory FastPath. + * Benchmarks for function signatures that hit vs miss the Endive FastPath. */ class FastPathBenchmarkTest { diff --git a/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/WasmRuntimeTest.java b/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/WasmRuntimeTest.java index b9d3fbe..11caa65 100644 --- a/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/WasmRuntimeTest.java +++ b/webassembly4j-runtime/src/test/java/ai/tegmentum/webassembly4j/runtime/WasmRuntimeTest.java @@ -195,7 +195,7 @@ void loadWithHostFunctions() { @Test void builderWithEngineSelection() { int result = WasmRuntime.builder() - .engine("chicory") + .engine("endive") .call(ADD_MODULE, "add", Integer.class, 3, 4); assertEquals(7, result); } @@ -203,7 +203,7 @@ void builderWithEngineSelection() { @Test void builderLoadInterface() { try (Calculator calc = WasmRuntime.builder() - .engine("chicory") + .engine("endive") .load(Calculator.class, ADD_MODULE)) { assertEquals(7, calc.add(3, 4)); } catch (Exception e) { @@ -226,7 +226,7 @@ void builderWithHostObjects() { @Test void builderCompile() { try (WasmModule module = WasmRuntime.builder() - .engine("chicory") + .engine("endive") .compile(ADD_MODULE)) { assertEquals(7, module.call("add", Integer.class, 3, 4)); } diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index bb207d0..35a6a3c 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-spi diff --git a/webassembly4j-spi/src/test/java/ai/tegmentum/webassembly4j/spi/internal/DefaultProviderSelectorTest.java b/webassembly4j-spi/src/test/java/ai/tegmentum/webassembly4j/spi/internal/DefaultProviderSelectorTest.java index 4f62649..bdc3d33 100644 --- a/webassembly4j-spi/src/test/java/ai/tegmentum/webassembly4j/spi/internal/DefaultProviderSelectorTest.java +++ b/webassembly4j-spi/src/test/java/ai/tegmentum/webassembly4j/spi/internal/DefaultProviderSelectorTest.java @@ -69,13 +69,13 @@ void explainsWhyEachProviderWasRejected() { @Test void filtersByEngineId() { EngineProvider wasmtime = stubProvider("wasmtime", "wasmtime-ffm", 200, 11, true); - EngineProvider chicory = stubProvider("chicory", "chicory", 200, 11, true); - ProviderContext ctx = context("chicory", null); + EngineProvider endive = stubProvider("endive", "endive", 200, 11, true); + ProviderContext ctx = context("endive", null); - ProviderSelectionResult result = selector.select(Arrays.asList(wasmtime, chicory), ctx); + ProviderSelectionResult result = selector.select(Arrays.asList(wasmtime, endive), ctx); assertTrue(result.selectedProvider().isPresent()); - assertEquals("chicory", result.selectedProvider().get().descriptor().providerId()); + assertEquals("endive", result.selectedProvider().get().descriptor().providerId()); } @Test diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index 488a6b8..334b02c 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 9e4fa1f..d12691a 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 1.5.0 + 2.0.0 webassembly4j-testing diff --git a/webassembly4j-testing/src/main/java/ai/tegmentum/webassembly4j/testing/WasmTestExtension.java b/webassembly4j-testing/src/main/java/ai/tegmentum/webassembly4j/testing/WasmTestExtension.java index 349b5fc..79c85bd 100644 --- a/webassembly4j-testing/src/main/java/ai/tegmentum/webassembly4j/testing/WasmTestExtension.java +++ b/webassembly4j-testing/src/main/java/ai/tegmentum/webassembly4j/testing/WasmTestExtension.java @@ -46,7 +46,7 @@ public Stream provideTestTemplateInvocationContex if (engines.isEmpty()) { throw new IllegalStateException( "No WebAssembly engines found on the classpath. " - + "Add a provider dependency (e.g., wasmtime4j-provider, chicory4j-provider)."); + + "Add a provider dependency (e.g., wasmtime4j-provider, endive4j-provider)."); } return engines.stream().map(factory -> createInvocationContext(factory, context)); } From 0863e1f9e5b891d1dab406c9122a15869d6a7315 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 30 Jun 2026 05:42:18 -0400 Subject: [PATCH 05/85] feat(wasmtime-provider): forward WasiContext + ComponentConfig into component instantiation Previously WasmtimeComponentAdapter.instantiate enabled WASI Preview 2 with an empty context (dropping the caller's WasiContext) and ignored ComponentConfig entirely, so a component could be granted no preopens/env and no resource limits via the high-level API. - Translate WasiContext -> WasiPreview2Config (preopens [host==guest], env, stdio inherit); network denied by default (deny-by-default; not yet expressible via WasiContext). - Apply ComponentConfig limits: StoreLimits (memory/table/instances) at store creation + fuel/epoch when the engine has them enabled. - Create the store per instantiation (so limits apply) and close stores with the component; loadComponent no longer pre-creates a shared store. - Override instantiate(ComponentConfig) and instantiate(LinkingContext, ComponentConfig). Unit-tested (WasmtimeComponentAdapterWasiTest): env/stdio/preopens forwarded, empty context is deny-all + network-denied. Follow-ons: granular guest-path / per-dir permissions on WasiContext, and an end-to-end WASI-using-component test. --- .../wasmtime/WasmtimeComponentAdapter.java | 132 +++++++++++++++--- .../wasmtime/WasmtimeEngineAdapter.java | 5 +- .../WasmtimeComponentAdapterWasiTest.java | 65 +++++++++ 3 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 7911445..6c77516 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -3,11 +3,17 @@ import ai.tegmentum.wasmtime4j.WasmRuntime; import ai.tegmentum.wasmtime4j.component.ComponentEngine; import ai.tegmentum.wasmtime4j.component.ComponentLinker; +import ai.tegmentum.wasmtime4j.wasi.DirPerms; +import ai.tegmentum.wasmtime4j.wasi.FilePerms; +import ai.tegmentum.wasmtime4j.wasi.WasiPreview2Config; import ai.tegmentum.webassembly4j.api.ComponentInstance; import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.WasiContext; +import ai.tegmentum.webassembly4j.api.config.ComponentConfig; import ai.tegmentum.webassembly4j.api.exception.InstantiationException; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -19,50 +25,57 @@ final class WasmtimeComponentAdapter implements ai.tegmentum.webassembly4j.api.C private final ai.tegmentum.wasmtime4j.Engine engine; private final ComponentEngine componentEngine; private final ai.tegmentum.wasmtime4j.component.Component nativeComponent; - private final ai.tegmentum.wasmtime4j.Store store; + // Stores are created per instantiation (so ComponentConfig limits can be applied) and + // closed together with the component. + private final List stores = new ArrayList<>(); WasmtimeComponentAdapter(WasmRuntime runtime, ai.tegmentum.wasmtime4j.Engine engine, ComponentEngine componentEngine, - ai.tegmentum.wasmtime4j.component.Component nativeComponent, - ai.tegmentum.wasmtime4j.Store store) { + ai.tegmentum.wasmtime4j.component.Component nativeComponent) { this.runtime = runtime; this.engine = engine; this.componentEngine = componentEngine; this.nativeComponent = nativeComponent; - this.store = store; } @Override public ComponentInstance instantiate() { - try { - ComponentLinker linker = ComponentLinker.create(engine); - linker.enableWasiPreview2(); - ai.tegmentum.wasmtime4j.component.ComponentInstance nativeInstance = - linker.instantiate(store, nativeComponent); - return new WasmtimeComponentInstanceAdapter(nativeInstance); - } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { - throw new InstantiationException( - "Failed to instantiate WebAssembly component", e); - } + return doInstantiate(null, null); } @Override public ComponentInstance instantiate(LinkingContext linkingContext) { - if (linkingContext == null) { - return instantiate(); - } + return doInstantiate(linkingContext, null); + } - if (!linkingContext.hostFunctions().isEmpty()) { + @Override + public ComponentInstance instantiate(ComponentConfig config) { + return doInstantiate(null, config); + } + + @Override + public ComponentInstance instantiate(LinkingContext linkingContext, ComponentConfig config) { + return doInstantiate(linkingContext, config); + } + + private ComponentInstance doInstantiate(LinkingContext linkingContext, ComponentConfig config) { + if (linkingContext != null && !linkingContext.hostFunctions().isEmpty()) { throw new UnsupportedFeatureException( "Core host functions are not supported for component instantiation. " + "Use unwrap() to access the native ComponentLinker for WIT-level imports."); } - try { + ai.tegmentum.wasmtime4j.Store store = newStore(config); ComponentLinker linker = ComponentLinker.create(engine); - if (linkingContext.wasiContext() != null) { + WasiContext wasi = linkingContext != null ? linkingContext.wasiContext() : null; + if (wasi != null) { + // Forward the capability policy (preopens / env / stdio) into the component's + // WASI Preview 2 context. Network is denied unless explicitly granted (not yet + // expressible through WasiContext), matching deny-by-default. + linker.enableWasiPreview2(toWasiPreview2Config(wasi)); + } else { linker.enableWasiPreview2(); } @@ -71,10 +84,78 @@ public ComponentInstance instantiate(LinkingContext linkingContext) { return new WasmtimeComponentInstanceAdapter(nativeInstance); } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { throw new InstantiationException( - "Failed to instantiate WebAssembly component with linking context", e); + "Failed to instantiate WebAssembly component", e); } } + /** Create a store, applying ComponentConfig resource limits (memory/table/instances + fuel/epoch). */ + private ai.tegmentum.wasmtime4j.Store newStore(ComponentConfig config) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + ai.tegmentum.wasmtime4j.Store store; + if (config != null && hasStoreLimits(config)) { + ai.tegmentum.wasmtime4j.config.StoreLimits.Builder lim = + ai.tegmentum.wasmtime4j.config.StoreLimits.builder(); + config.maxMemoryBytes().ifPresent(lim::memorySize); + config.maxTableElements().ifPresent(lim::tableElements); + config.maxInstances().ifPresent(lim::instances); + config.maxTables().ifPresent(lim::tables); + config.maxMemories().ifPresent(lim::memories); + lim.trapOnGrowFailure(config.trapOnGrowFailure()); + store = runtime.createStore(engine, lim.build()); + } else { + store = engine.createStore(); + } + if (config != null) { + // Fuel/epoch are only settable when the engine was built with them enabled; otherwise + // skip rather than trap (the engine config governs whether they're available). + if (config.fuelLimit().isPresent() && engine.isFuelEnabled()) { + store.setFuel(config.fuelLimit().getAsLong()); + } + if (config.epochDeadline().isPresent() && engine.isEpochInterruptionEnabled()) { + store.setEpochDeadline(config.epochDeadline().getAsLong()); + } + } + stores.add(store); + return store; + } + + private static boolean hasStoreLimits(ComponentConfig c) { + return c.maxMemoryBytes().isPresent() || c.maxTableElements().isPresent() + || c.maxInstances().isPresent() || c.maxTables().isPresent() + || c.maxMemories().isPresent(); + } + + /** + * Translate the provider-agnostic WasiContext into wasmtime4j's WasiPreview2Config. + * Package-private for unit testing the mapping. + */ + static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { + WasiPreview2Config.Builder b = WasiPreview2Config.builder(); + if (wasi.args() != null && !wasi.args().isEmpty()) { + b.args(wasi.args().toArray(new String[0])); + } + if (wasi.env() != null && !wasi.env().isEmpty()) { + b.env(wasi.env()); + } + if (wasi.inheritStdin()) { + b.inheritStdin(); + } + if (wasi.inheritStdout()) { + b.inheritStdout(); + } + if (wasi.inheritStderr()) { + b.inheritStderr(); + } + // Preopens: host path == guest path (a granular guest-remap / per-dir-perms form is a + // follow-on WasiContext API extension). Granting a dir grants full access within it. + if (wasi.preopenDirs() != null) { + for (String dir : wasi.preopenDirs()) { + b.preopenDir(Paths.get(dir), dir, DirPerms.all(), FilePerms.all()); + } + } + return b.build(); + } + @Override public List exportedInterfaces() { try { @@ -125,12 +206,17 @@ public byte[] serialize() { @Override public void close() { + for (ai.tegmentum.wasmtime4j.Store s : stores) { + try { + s.close(); + } catch (RuntimeException ignored) { + // best effort + } + } nativeComponent.close(); - store.close(); try { componentEngine.close(); } catch (java.io.IOException e) { - // ComponentEngine.close() is a Closeable; wrap if it ever throws throw new ai.tegmentum.webassembly4j.api.exception.WebAssemblyException( "Failed to close component engine", e); } diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java index 22f7ee1..0fc8e94 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java @@ -190,9 +190,10 @@ public Component loadComponent(byte[] bytes) { ComponentEngine componentEngine = runtime.createComponentEngine(); ai.tegmentum.wasmtime4j.component.Component nativeComponent = componentEngine.compileComponent(bytes); - ai.tegmentum.wasmtime4j.Store store = engine.createStore(); + // The store is created per-instantiation (in the adapter) so ComponentConfig + // resource limits can be applied; see WasmtimeComponentAdapter. return new WasmtimeComponentAdapter( - runtime, engine, componentEngine, nativeComponent, store); + runtime, engine, componentEngine, nativeComponent); } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { throw new ai.tegmentum.webassembly4j.api.exception.WebAssemblyException( "Failed to load WebAssembly component", e); diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java new file mode 100644 index 0000000..a36c140 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java @@ -0,0 +1,65 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.wasmtime4j.wasi.WasiPreview2Config; +import ai.tegmentum.webassembly4j.api.DefaultWasiContext; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Fix 1 unit test: a WasiContext attached to component instantiation must be forwarded into the + * component's WasiPreview2Config (preopens / env / stdio), not dropped. Pure mapping test — no + * native library required (WasiPreview2Config is a config holder). + */ +class WasmtimeComponentAdapterWasiTest { + + @Test + @DisplayName("WasiContext (env/stdio/preopens) is forwarded into the component WASI config") + void forwardsWasiContext() { + DefaultWasiContext wasi = DefaultWasiContext.builder() + .env("SVLN_FIX1", "ok") + .env("LOG_LEVEL", "INFO") + .inheritStdout(true) + .preopenDir("/fiji-jdk") + .preopenDir("/cp") + .build(); + + WasiPreview2Config cfg = WasmtimeComponentAdapter.toWasiPreview2Config(wasi); + + // env forwarded (was silently dropped before Fix 1). + assertEquals("ok", cfg.getEnv().get("SVLN_FIX1")); + assertEquals("INFO", cfg.getEnv().get("LOG_LEVEL")); + + // stdio inheritance forwarded. + assertTrue(cfg.isInheritStdout(), "inheritStdout should be forwarded"); + assertFalse(cfg.isInheritStderr(), "inheritStderr was not set"); + + // preopens forwarded (host == guest for now). + Set guests = new HashSet<>(); + for (WasiPreview2Config.PreopenDir d : cfg.getPreopenDirs()) { + guests.add(d.getGuestPath()); + } + assertEquals(2, cfg.getPreopenDirs().size()); + assertTrue(guests.contains("/fiji-jdk")); + assertTrue(guests.contains("/cp")); + + // Capability-minimal default: network denied unless explicitly granted. + assertFalse(cfg.isAllowNetwork(), "network must default to denied"); + } + + @Test + @DisplayName("Empty WasiContext yields an empty, network-denied WASI config") + void emptyContextIsDenyAll() { + WasiPreview2Config cfg = + WasmtimeComponentAdapter.toWasiPreview2Config(DefaultWasiContext.builder().build()); + assertTrue(cfg.getEnv().isEmpty()); + assertTrue(cfg.getPreopenDirs().isEmpty()); + assertFalse(cfg.isAllowNetwork()); + assertFalse(cfg.isInheritStdout()); + } +} From 50a74df503800389637c14b315e36e11992b9a2c Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 30 Jun 2026 06:26:58 -0400 Subject: [PATCH 06/85] feat(wasmtime-provider): marshal natural Java arg types in ComponentInstance.invoke ComponentInstance.invoke's javadoc promises a natural Java<->WIT type mapping and return values already arrived as natural Java, but arguments were passed straight to the native layer, which requires WitValue instances ("Argument 0 must be a WitValue instance, got java.lang.String"). WasmtimeComponentInstanceAdapter.invoke now marshals each Java arg via toWitValue: Boolean->bool, Byte/Short/Integer/Long->s8/s16/s32/s64, Float/Double->f32/f64, Character->char, String->string, byte[]->list, non-empty homogeneous List->list (recursive). A WitValue passes through unchanged (escape hatch for option/result/ record/variant/enum/flags and exact unsigned/narrow integer widths); null and empty lists raise a clear UnsupportedFeatureException naming the arg index + function instead of a cryptic native error. Unit-tested (WasmtimeComponentInstanceMarshalTest, 4 cases). --- .../WasmtimeComponentInstanceAdapter.java | 99 ++++++++++++++++++- .../WasmtimeComponentInstanceMarshalTest.java | 79 +++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceMarshalTest.java diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java index 88e02ad..41fc33b 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java @@ -1,11 +1,24 @@ package ai.tegmentum.webassembly4j.provider.wasmtime; +import ai.tegmentum.wasmtime4j.wit.WitBool; +import ai.tegmentum.wasmtime4j.wit.WitChar; +import ai.tegmentum.wasmtime4j.wit.WitFloat32; +import ai.tegmentum.wasmtime4j.wit.WitFloat64; +import ai.tegmentum.wasmtime4j.wit.WitList; +import ai.tegmentum.wasmtime4j.wit.WitS16; +import ai.tegmentum.wasmtime4j.wit.WitS32; +import ai.tegmentum.wasmtime4j.wit.WitS64; +import ai.tegmentum.wasmtime4j.wit.WitS8; +import ai.tegmentum.wasmtime4j.wit.WitString; +import ai.tegmentum.wasmtime4j.wit.WitU8; +import ai.tegmentum.wasmtime4j.wit.WitValue; import ai.tegmentum.webassembly4j.api.ComponentInstance; import ai.tegmentum.webassembly4j.api.Function; import ai.tegmentum.webassembly4j.api.Global; import ai.tegmentum.webassembly4j.api.Memory; import ai.tegmentum.webassembly4j.api.Table; import ai.tegmentum.webassembly4j.api.exception.ExecutionException; +import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; import java.util.ArrayList; import java.util.Collections; @@ -24,14 +37,98 @@ final class WasmtimeComponentInstanceAdapter implements ComponentInstance { @Override public Object invoke(String functionName, Object... args) { + Object[] witArgs = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + try { + witArgs[i] = toWitValue(args[i]); + } catch (UnsupportedFeatureException e) { + throw new UnsupportedFeatureException( + "Argument " + i + " of '" + functionName + "': " + e.getMessage(), e); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Argument " + i + " of '" + functionName + "' is not a valid WIT value", e); + } + } try { - return nativeInstance.invoke(functionName, args); + return nativeInstance.invoke(functionName, witArgs); } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { throw new ExecutionException( "Failed to invoke component function: " + functionName, e); } } + /** + * Marshal a natural Java argument into a {@link WitValue}, fulfilling the + * {@link ComponentInstance#invoke} contract. Values already of type {@code WitValue} pass + * through unchanged — the escape hatch for WIT types whose target type can't be inferred from a + * bare Java value (option/result/record/variant/enum/flags, and exact integer widths beyond the + * defaults below). + * + *

Mapping: Boolean→bool, Byte→s8, Short→s16, Integer→s32, Long→s64, Float→f32, Double→f64, + * Character→char, String→string, {@code byte[]}→list<u8>, non-empty homogeneous List→list + * (elements marshalled recursively). Integer/Long default to the signed widths; pass an explicit + * {@code WitValue} (e.g. {@code WitU64.of(..)}) for unsigned or narrower-than-natural widths. + */ + static WitValue toWitValue(Object arg) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + if (arg instanceof WitValue) { + return (WitValue) arg; + } + if (arg instanceof Boolean) { + return WitBool.of((Boolean) arg); + } + if (arg instanceof Byte) { + return WitS8.of((Byte) arg); + } + if (arg instanceof Short) { + return WitS16.of((Short) arg); + } + if (arg instanceof Integer) { + return WitS32.of((Integer) arg); + } + if (arg instanceof Long) { + return WitS64.of((Long) arg); + } + if (arg instanceof Float) { + return WitFloat32.of((Float) arg); + } + if (arg instanceof Double) { + return WitFloat64.of((Double) arg); + } + if (arg instanceof Character) { + return WitChar.of((Character) arg); + } + if (arg instanceof String) { + return WitString.of((String) arg); + } + if (arg instanceof byte[]) { + byte[] bytes = (byte[]) arg; + List elems = new ArrayList<>(bytes.length); + for (byte b : bytes) { + elems.add(WitU8.of(b)); + } + return WitList.of(elems); + } + if (arg instanceof List) { + List list = (List) arg; + if (list.isEmpty()) { + throw new UnsupportedFeatureException( + "cannot infer element type of an empty list; pass a typed " + + "WitList.empty(elementType)"); + } + List elems = new ArrayList<>(list.size()); + for (Object e : list) { + elems.add(toWitValue(e)); + } + return WitList.of(elems); + } + throw new UnsupportedFeatureException( + "cannot marshal Java type " + + (arg == null ? "null" : arg.getClass().getName()) + + " to a WIT value; build a WitValue explicitly " + + "(option/result/record/variant/enum/flags and null require an explicit type)"); + } + @Override public boolean hasFunction(String name) { return nativeInstance.hasFunction(name); diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceMarshalTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceMarshalTest.java new file mode 100644 index 0000000..69e53da --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceMarshalTest.java @@ -0,0 +1,79 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import ai.tegmentum.wasmtime4j.wit.WitBool; +import ai.tegmentum.wasmtime4j.wit.WitFloat32; +import ai.tegmentum.wasmtime4j.wit.WitFloat64; +import ai.tegmentum.wasmtime4j.wit.WitList; +import ai.tegmentum.wasmtime4j.wit.WitS16; +import ai.tegmentum.wasmtime4j.wit.WitS32; +import ai.tegmentum.wasmtime4j.wit.WitS64; +import ai.tegmentum.wasmtime4j.wit.WitS8; +import ai.tegmentum.wasmtime4j.wit.WitString; +import ai.tegmentum.wasmtime4j.wit.WitValue; +import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Fix 10 unit test: the high-level {@code ComponentInstance.invoke} must marshal natural Java + * argument types into {@link WitValue} (its javadoc contract), not require callers to pre-build + * WitValue. Pure mapping test — no native library required. + */ +class WasmtimeComponentInstanceMarshalTest { + + @Test + @DisplayName("scalars + string map to their natural WIT value types") + void scalarsAndStrings() throws Exception { + assertEquals(Boolean.TRUE, + WasmtimeComponentInstanceAdapter.toWitValue(true) instanceof WitBool); + assertEquals("hi", ((WitString) WasmtimeComponentInstanceAdapter.toWitValue("hi")).toJava()); + + // Integer -> s32, Long -> s64 (signed defaults). + assertEquals(WitS32.class, WasmtimeComponentInstanceAdapter.toWitValue(42).getClass()); + assertEquals(WitS64.class, WasmtimeComponentInstanceAdapter.toWitValue(42L).getClass()); + assertEquals(WitS8.class, WasmtimeComponentInstanceAdapter.toWitValue((byte) 1).getClass()); + assertEquals(WitS16.class, + WasmtimeComponentInstanceAdapter.toWitValue((short) 1).getClass()); + assertEquals(WitFloat32.class, + WasmtimeComponentInstanceAdapter.toWitValue(1.5f).getClass()); + assertEquals(WitFloat64.class, + WasmtimeComponentInstanceAdapter.toWitValue(1.5d).getClass()); + } + + @Test + @DisplayName("WitValue arguments pass through unchanged (escape hatch)") + void witValuePassthrough() throws Exception { + WitValue prebuilt = WitString.of("already-wit"); + assertSame(prebuilt, WasmtimeComponentInstanceAdapter.toWitValue(prebuilt)); + } + + @Test + @DisplayName("byte[] and homogeneous lists become list values") + void listsAndBytes() throws Exception { + WitValue bytes = WasmtimeComponentInstanceAdapter.toWitValue(new byte[] {1, 2, 3}); + assertEquals(WitList.class, bytes.getClass()); + + WitValue strings = WasmtimeComponentInstanceAdapter.toWitValue(List.of("a", "b")); + assertEquals(WitList.class, strings.getClass()); + } + + @Test + @DisplayName("unmarshallable / ambiguous types fail with guidance, not a cryptic native error") + void unsupportedTypesAreRejected() { + // Composite types whose WIT type can't be inferred from a bare Java value. + assertThrows(UnsupportedFeatureException.class, + () -> WasmtimeComponentInstanceAdapter.toWitValue(Map.of("k", "v"))); + // null (could be option none — needs an explicit type). + assertThrows(UnsupportedFeatureException.class, + () -> WasmtimeComponentInstanceAdapter.toWitValue((Object) null)); + // empty list — element type unknowable. + assertThrows(UnsupportedFeatureException.class, + () -> WasmtimeComponentInstanceAdapter.toWitValue(List.of())); + } +} From 1c80e37072a33a62bf0932289f3b5c51218299f5 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 30 Jun 2026 07:20:12 -0400 Subject: [PATCH 07/85] fix(wasmtime-provider): give metered component stores a fuel budget With the shared component engine now fuel-metered (wasmtime4j Fix 11), a store created on a fuel-enabled engine defaults to 0 fuel and the component traps on its first instruction. newStore now sets the store's fuel whenever the engine meters fuel: the ComponentConfig.fuelLimit cap when present, else MAX_VALUE (unlimited). This is the value the native layer reads as the per-instance compute cap. --- .../wasmtime/WasmtimeComponentAdapter.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 6c77516..9af6e12 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -105,15 +105,19 @@ private ai.tegmentum.wasmtime4j.Store newStore(ComponentConfig config) } else { store = engine.createStore(); } - if (config != null) { - // Fuel/epoch are only settable when the engine was built with them enabled; otherwise - // skip rather than trap (the engine config governs whether they're available). - if (config.fuelLimit().isPresent() && engine.isFuelEnabled()) { - store.setFuel(config.fuelLimit().getAsLong()); - } - if (config.epochDeadline().isPresent() && engine.isEpochInterruptionEnabled()) { - store.setEpochDeadline(config.epochDeadline().getAsLong()); - } + // Fuel: when the engine meters fuel, the store's remaining fuel is what the component + // provider carries down as the per-instance compute cap. Set the requested cap, or + // MAX_VALUE ("unlimited") when none is given — otherwise a metered store defaults to 0 fuel + // and the component would trap on its first instruction. + if (engine.isFuelEnabled()) { + long fuel = (config != null && config.fuelLimit().isPresent()) + ? config.fuelLimit().getAsLong() + : Long.MAX_VALUE; + store.setFuel(fuel); + } + if (config != null && config.epochDeadline().isPresent() + && engine.isEpochInterruptionEnabled()) { + store.setEpochDeadline(config.epochDeadline().getAsLong()); } stores.add(store); return store; From a8bf60e642e0411ab2cb4c303397a80a4b38f0da Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 30 Jun 2026 09:23:17 -0400 Subject: [PATCH 08/85] feat(wasmtime-provider): forward memory + epoch caps to component instantiation doInstantiate now passes ComponentConfig.maxMemoryBytes and epochDeadline to the native instantiation via ComponentLinker.setComponentResourceLimits (-1 = unlimited). The component runs on wasmtime4j's shared component engine (metered for fuel + epoch), so these apply regardless of this engine's own config. Completes ComponentConfig resource-limit forwarding (fuel + memory + epoch). --- .../provider/wasmtime/WasmtimeComponentAdapter.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 9af6e12..7259551 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -69,6 +69,18 @@ private ComponentInstance doInstantiate(LinkingContext linkingContext, Component ai.tegmentum.wasmtime4j.Store store = newStore(config); ComponentLinker linker = ComponentLinker.create(engine); + // Memory cap + epoch deadline can't be read back from the store (unlike fuel), so carry + // them to the instantiation explicitly. The component runs on wasmtime4j's shared + // component engine (metered for fuel + epoch), so these apply regardless of this + // engine's own config. -1 = unlimited. + if (config != null) { + long maxMemory = config.maxMemoryBytes().isPresent() + ? config.maxMemoryBytes().getAsLong() : -1L; + long epoch = config.epochDeadline().isPresent() + ? config.epochDeadline().getAsLong() : -1L; + linker.setComponentResourceLimits(maxMemory, epoch); + } + WasiContext wasi = linkingContext != null ? linkingContext.wasiContext() : null; if (wasi != null) { // Forward the capability policy (preopens / env / stdio) into the component's From e1ecbf7557b10e89c9fd953d0a43a944c8b70a3c Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 30 Jun 2026 12:49:13 -0400 Subject: [PATCH 09/85] feat(wasi): per-directory read-only preopens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WasiContext gains readOnlyPreopenDirs() and DefaultWasiContext.builder().preopenDir(dir, writable); toWasiPreview2Config maps a read-only grant to DirPerms.readOnly()/ FilePerms.readOnly() instead of full perms. The native layer already honored per-dir permission bits, so a read-only preopen now actually denies guest writes (was a Java-only gap). preopenDir(dir) still defaults to read-write — existing callers are unchanged. --- .../wasmtime/WasmtimeComponentAdapter.java | 14 +++++++--- .../webassembly4j/api/DefaultWasiContext.java | 26 +++++++++++++++++-- .../webassembly4j/api/WasiContext.java | 9 +++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 7259551..1754712 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -162,11 +162,19 @@ static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { if (wasi.inheritStderr()) { b.inheritStderr(); } - // Preopens: host path == guest path (a granular guest-remap / per-dir-perms form is a - // follow-on WasiContext API extension). Granting a dir grants full access within it. + // Preopens: host path == guest path. A dir listed in readOnlyPreopenDirs is granted read + // permissions only (the guest cannot create/write/delete within it); all others are + // read-write. (Guest-path remap remains a follow-on.) if (wasi.preopenDirs() != null) { + java.util.Set readOnly = wasi.readOnlyPreopenDirs() == null + ? java.util.Collections.emptySet() + : new java.util.HashSet<>(wasi.readOnlyPreopenDirs()); for (String dir : wasi.preopenDirs()) { - b.preopenDir(Paths.get(dir), dir, DirPerms.all(), FilePerms.all()); + if (readOnly.contains(dir)) { + b.preopenDir(Paths.get(dir), dir, DirPerms.readOnly(), FilePerms.readOnly()); + } else { + b.preopenDir(Paths.get(dir), dir, DirPerms.all(), FilePerms.all()); + } } } return b.build(); diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java index ff0e13b..79b9de8 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java @@ -14,16 +14,20 @@ public final class DefaultWasiContext implements WasiContext { private final boolean inheritStdout; private final boolean inheritStderr; private final List preopenDirs; + private final List readOnlyPreopenDirs; private DefaultWasiContext(List args, Map env, boolean inheritStdin, boolean inheritStdout, - boolean inheritStderr, List preopenDirs) { + boolean inheritStderr, List preopenDirs, + List readOnlyPreopenDirs) { this.args = Collections.unmodifiableList(new ArrayList<>(args)); this.env = Collections.unmodifiableMap(new LinkedHashMap<>(env)); this.inheritStdin = inheritStdin; this.inheritStdout = inheritStdout; this.inheritStderr = inheritStderr; this.preopenDirs = Collections.unmodifiableList(new ArrayList<>(preopenDirs)); + this.readOnlyPreopenDirs = + Collections.unmodifiableList(new ArrayList<>(readOnlyPreopenDirs)); } @Override @@ -56,6 +60,11 @@ public List preopenDirs() { return preopenDirs; } + @Override + public List readOnlyPreopenDirs() { + return readOnlyPreopenDirs; + } + public static Builder builder() { return new Builder(); } @@ -68,6 +77,7 @@ public static final class Builder { private boolean inheritStdout; private boolean inheritStderr; private final List preopenDirs = new ArrayList<>(); + private final List readOnlyPreopenDirs = new ArrayList<>(); private Builder() { } @@ -110,14 +120,26 @@ public Builder inheritStdio(boolean inherit) { return this; } + /** Grant a read-write preopen (the default). */ public Builder preopenDir(String dir) { + return preopenDir(dir, true); + } + + /** + * Grant a preopen with an explicit access mode. {@code writable=false} preopens the + * directory read-only — the guest can read but not create/write/delete within it. + */ + public Builder preopenDir(String dir, boolean writable) { this.preopenDirs.add(dir); + if (!writable) { + this.readOnlyPreopenDirs.add(dir); + } return this; } public DefaultWasiContext build() { return new DefaultWasiContext(args, env, inheritStdin, inheritStdout, - inheritStderr, preopenDirs); + inheritStderr, preopenDirs, readOnlyPreopenDirs); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java index 138bb12..5932b80 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java @@ -29,4 +29,13 @@ default boolean inheritStderr() { default List preopenDirs() { return Collections.emptyList(); } + + /** + * The subset of {@link #preopenDirs()} that is granted read-only. A directory listed here is + * preopened with read permissions only — the guest cannot create, write, or delete within it. + * Directories not listed are read-write (the default), preserving prior behaviour. + */ + default List readOnlyPreopenDirs() { + return Collections.emptyList(); + } } From 4f822a8be0e13642674b40ee654a32bb71a836c2 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 30 Jun 2026 13:45:49 -0400 Subject: [PATCH 10/85] feat(wasi): host->guest path remap for preopens WasiContext.preopenGuestPaths() + DefaultWasiContext.preopenDir(hostDir, guestPath, writable); toWasiPreview2Config forwards the guest path (native preopen already accepted one). A preopen can now expose a host dir at a different guest path without leaking the host layout. Default stays host==guest. --- .../wasmtime/WasmtimeComponentAdapter.java | 8 ++++-- .../webassembly4j/api/DefaultWasiContext.java | 27 +++++++++++++++++-- .../webassembly4j/api/WasiContext.java | 10 +++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 1754712..581bebd 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -169,11 +169,15 @@ static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { java.util.Set readOnly = wasi.readOnlyPreopenDirs() == null ? java.util.Collections.emptySet() : new java.util.HashSet<>(wasi.readOnlyPreopenDirs()); + java.util.Map guestPaths = wasi.preopenGuestPaths() == null + ? java.util.Collections.emptyMap() + : wasi.preopenGuestPaths(); for (String dir : wasi.preopenDirs()) { + String guest = guestPaths.getOrDefault(dir, dir); // default: guest path == host path if (readOnly.contains(dir)) { - b.preopenDir(Paths.get(dir), dir, DirPerms.readOnly(), FilePerms.readOnly()); + b.preopenDir(Paths.get(dir), guest, DirPerms.readOnly(), FilePerms.readOnly()); } else { - b.preopenDir(Paths.get(dir), dir, DirPerms.all(), FilePerms.all()); + b.preopenDir(Paths.get(dir), guest, DirPerms.all(), FilePerms.all()); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java index 79b9de8..569daa0 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java @@ -15,11 +15,13 @@ public final class DefaultWasiContext implements WasiContext { private final boolean inheritStderr; private final List preopenDirs; private final List readOnlyPreopenDirs; + private final Map preopenGuestPaths; private DefaultWasiContext(List args, Map env, boolean inheritStdin, boolean inheritStdout, boolean inheritStderr, List preopenDirs, - List readOnlyPreopenDirs) { + List readOnlyPreopenDirs, + Map preopenGuestPaths) { this.args = Collections.unmodifiableList(new ArrayList<>(args)); this.env = Collections.unmodifiableMap(new LinkedHashMap<>(env)); this.inheritStdin = inheritStdin; @@ -28,6 +30,8 @@ private DefaultWasiContext(List args, Map env, this.preopenDirs = Collections.unmodifiableList(new ArrayList<>(preopenDirs)); this.readOnlyPreopenDirs = Collections.unmodifiableList(new ArrayList<>(readOnlyPreopenDirs)); + this.preopenGuestPaths = + Collections.unmodifiableMap(new LinkedHashMap<>(preopenGuestPaths)); } @Override @@ -65,6 +69,11 @@ public List readOnlyPreopenDirs() { return readOnlyPreopenDirs; } + @Override + public Map preopenGuestPaths() { + return preopenGuestPaths; + } + public static Builder builder() { return new Builder(); } @@ -78,6 +87,7 @@ public static final class Builder { private boolean inheritStderr; private final List preopenDirs = new ArrayList<>(); private final List readOnlyPreopenDirs = new ArrayList<>(); + private final Map preopenGuestPaths = new LinkedHashMap<>(); private Builder() { } @@ -137,9 +147,22 @@ public Builder preopenDir(String dir, boolean writable) { return this; } + /** + * Grant a preopen of host directory {@code hostDir} that the guest sees at {@code guestPath} + * (host layout not leaked). {@code writable=false} preopens it read-only. + */ + public Builder preopenDir(String hostDir, String guestPath, boolean writable) { + this.preopenDirs.add(hostDir); + this.preopenGuestPaths.put(hostDir, guestPath); + if (!writable) { + this.readOnlyPreopenDirs.add(hostDir); + } + return this; + } + public DefaultWasiContext build() { return new DefaultWasiContext(args, env, inheritStdin, inheritStdout, - inheritStderr, preopenDirs, readOnlyPreopenDirs); + inheritStderr, preopenDirs, readOnlyPreopenDirs, preopenGuestPaths); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java index 5932b80..fae0ca6 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java @@ -38,4 +38,14 @@ default List preopenDirs() { default List readOnlyPreopenDirs() { return Collections.emptyList(); } + + /** + * Optional host→guest path remaps for preopens. For a host directory in {@link #preopenDirs()}, + * the value here is the path the guest sees it at; absent means the guest sees the host path + * unchanged (host == guest). Lets a policy expose, say, host {@code /var/data/app} as guest + * {@code /data} without leaking the host layout. + */ + default Map preopenGuestPaths() { + return Collections.emptyMap(); + } } From daef46348f8049a972bdef73ecce1b422edc4021 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 5 Jul 2026 11:08:24 -0400 Subject: [PATCH 11/85] fix(wasmtime-provider): route component instantiation through WASI-config path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The no-arg enableWasiPreview2() leaves JniComponentLinker.wasiConfig null, so instantiate() falls back to nativeInstantiateWithLinker — currently a stub that returns a phantom instance ID without registering the instance in the component engine's HashMap. The subsequent invoke then fails with "Instance ID N not found in engine". Always pass a WasiPreview2Config (an empty default when the caller provides no WasiContext) so wasiConfig gets set and instantiate() routes through nativeInstantiateComponentWithWasi, which actually registers the instance. Deny-by-default: an empty config grants no preopens, no env, no network. --- .../wasmtime/WasmtimeComponentAdapter.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 581bebd..22acaba 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -82,14 +82,16 @@ private ComponentInstance doInstantiate(LinkingContext linkingContext, Component } WasiContext wasi = linkingContext != null ? linkingContext.wasiContext() : null; - if (wasi != null) { - // Forward the capability policy (preopens / env / stdio) into the component's - // WASI Preview 2 context. Network is denied unless explicitly granted (not yet - // expressible through WasiContext), matching deny-by-default. - linker.enableWasiPreview2(toWasiPreview2Config(wasi)); - } else { - linker.enableWasiPreview2(); - } + // Always route through the (config) overload — the no-arg variant leaves the + // linker's stored wasiConfig null, and JniComponentLinker.instantiate falls back to + // the stub linker path (Fix 12 tracking) which returns a phantom instance ID and the + // subsequent invoke fails with "Instance ID N not found in engine". A default config + // (no preopens, no env, deny-network) yields a WASI context that resolves the guest's + // imports without granting anything. + WasiPreview2Config p2 = (wasi != null) + ? toWasiPreview2Config(wasi) + : WasiPreview2Config.builder().build(); + linker.enableWasiPreview2(p2); ai.tegmentum.wasmtime4j.component.ComponentInstance nativeInstance = linker.instantiate(store, nativeComponent); From bc3ca9c8fcb6a1ae36da6aeef561f9b13a22f33f Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 5 Jul 2026 11:28:43 -0400 Subject: [PATCH 12/85] feat(api): expose ComponentInstance.invokeWit for typed round-trip The api-level invoke() returns natural Java shapes (Map for records, List for tuples/lists, Optional for options, etc.) via WitValue.toJava() in the wasmtime provider. This is asymmetric with the argument side (records/variants must be built as WitValue) and drops WIT-specific typing on the way back (u64 width, enum discriminant vs case name, option's inner type). Add invokeWit as an optional typed variant that returns the provider's WitValue tree instead. Default delegates to invoke and only round-trips cleanly for providers that already return WitValue there. Wasmtime provider overrides to call the new wasmtime4j ComponentInstance.invokeWit directly, skipping the toJava() unwrap on the return path. Shared arg marshalling extracted into marshalArgs so invoke and invokeWit can't drift in what they accept. --- .../WasmtimeComponentInstanceAdapter.java | 30 ++++++++++++++----- .../webassembly4j/api/ComponentInstance.java | 27 +++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java index 41fc33b..9d23231 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java @@ -37,7 +37,28 @@ final class WasmtimeComponentInstanceAdapter implements ComponentInstance { @Override public Object invoke(String functionName, Object... args) { - Object[] witArgs = new Object[args.length]; + final Object[] witArgs = marshalArgs(functionName, args); + try { + return nativeInstance.invoke(functionName, witArgs); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Failed to invoke component function: " + functionName, e); + } + } + + @Override + public Object invokeWit(String functionName, Object... args) { + final Object[] witArgs = marshalArgs(functionName, args); + try { + return nativeInstance.invokeWit(functionName, witArgs); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Failed to invoke component function: " + functionName, e); + } + } + + private static Object[] marshalArgs(String functionName, Object[] args) { + final Object[] witArgs = new Object[args.length]; for (int i = 0; i < args.length; i++) { try { witArgs[i] = toWitValue(args[i]); @@ -49,12 +70,7 @@ public Object invoke(String functionName, Object... args) { "Argument " + i + " of '" + functionName + "' is not a valid WIT value", e); } } - try { - return nativeInstance.invoke(functionName, witArgs); - } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { - throw new ExecutionException( - "Failed to invoke component function: " + functionName, e); - } + return witArgs; } /** diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java index a754cf0..69b811d 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java @@ -42,6 +42,33 @@ public interface ComponentInstance extends Instance { */ Object invoke(String functionName, Object... args); + /** + * Typed variant of {@link #invoke} that returns the raw WIT value tree from the + * provider instead of unwrapping it to a natural Java shape via + * {@code WitValue.toJava()}. Symmetric with the input side, which already + * requires typed values for records/variants/options/results. + * + *

Callers doing round-trip WIT marshalling (typed in, typed out) should prefer + * this over {@link #invoke} — it saves an extra pass through {@code toJava()} + * and preserves precise WIT typing that the Java-shape unwrap loses (e.g. u64 + * width, enum discriminants, option's inner type). + * + *

The return type is {@code Object} rather than a stronger provider-specific + * WIT type to keep this interface provider-neutral; the concrete type is the + * provider's WIT value tree (for wasmtime, {@code ai.tegmentum.wasmtime4j.wit.WitValue}). + * + *

Default implementation delegates to {@link #invoke}. Providers with a + * typed native invoke path should override. + * + * @param functionName the exported function name + * @param args the function arguments + * @return the provider's WIT value, or {@code null} for void functions + * @throws ai.tegmentum.webassembly4j.api.exception.ExecutionException if invocation fails + */ + default Object invokeWit(String functionName, Object... args) { + return invoke(functionName, args); + } + /** * Returns whether this instance exports a function with the given name. */ From f4321b1b3b1d1ef2c2313a44e1745202876c3491 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 5 Jul 2026 11:32:46 -0400 Subject: [PATCH 13/85] feat(api): register WIT-typed host imports via LinkingContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Components with WIT imports beyond WASI previously required consumers to unwrap the api Component down to the native wasmtime4j ComponentLinker and call defineFunction directly — defeating the api abstraction and coupling call sites to wasmtime4j-jni. Adds: - WitHostFunction (Object[] args -> Object[] results) - WitHostFunctionDefinition (witPath + fn) - LinkingContext.witHostFunctions() - DefaultLinkingContext.Builder.addWitHostFunction(witPath, fn) Wasmtime provider wraps each WitHostFunction in a native ComponentHostFunction and calls ComponentLinker.defineFunction(witPath, impl) before instantiation. The Object[] elements are the provider's native WIT value type — for wasmtime that is ComponentVal — since a cross-provider WIT value abstraction is out of scope here. Host functions written against a specific provider stay portable-within-provider; cross-provider portability at this layer waits for a future WitValue-style abstraction. --- .../wasmtime/WasmtimeComponentAdapter.java | 23 ++++++++ .../api/DefaultLinkingContext.java | 28 +++++++++- .../webassembly4j/api/LinkingContext.java | 9 ++++ .../webassembly4j/api/WitHostFunction.java | 34 ++++++++++++ .../api/WitHostFunctionDefinition.java | 52 +++++++++++++++++++ 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunction.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunctionDefinition.java diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 22acaba..2422009 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -2,13 +2,16 @@ import ai.tegmentum.wasmtime4j.WasmRuntime; import ai.tegmentum.wasmtime4j.component.ComponentEngine; +import ai.tegmentum.wasmtime4j.component.ComponentHostFunction; import ai.tegmentum.wasmtime4j.component.ComponentLinker; +import ai.tegmentum.wasmtime4j.component.ComponentVal; import ai.tegmentum.wasmtime4j.wasi.DirPerms; import ai.tegmentum.wasmtime4j.wasi.FilePerms; import ai.tegmentum.wasmtime4j.wasi.WasiPreview2Config; import ai.tegmentum.webassembly4j.api.ComponentInstance; import ai.tegmentum.webassembly4j.api.LinkingContext; import ai.tegmentum.webassembly4j.api.WasiContext; +import ai.tegmentum.webassembly4j.api.WitHostFunctionDefinition; import ai.tegmentum.webassembly4j.api.config.ComponentConfig; import ai.tegmentum.webassembly4j.api.exception.InstantiationException; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; @@ -93,6 +96,26 @@ private ComponentInstance doInstantiate(LinkingContext linkingContext, Component : WasiPreview2Config.builder().build(); linker.enableWasiPreview2(p2); + // Register any WIT-typed host imports declared on the linking context. Each is + // wrapped in a native ComponentHostFunction that just delegates — the caller + // (whose api-level function takes/returns Object[]) already speaks the provider's + // native value type (ComponentVal here); a cross-provider WIT value abstraction + // is intentionally out of scope for this version. + if (linkingContext != null) { + for (WitHostFunctionDefinition def : linkingContext.witHostFunctions()) { + final WitHostFunctionDefinition d = def; + ComponentHostFunction impl = args -> { + Object[] argsArr = new Object[args.size()]; + for (int i = 0; i < args.size(); i++) argsArr[i] = args.get(i); + Object[] results = d.function().execute(argsArr); + java.util.List out = new java.util.ArrayList<>(results.length); + for (Object r : results) out.add((ComponentVal) r); + return out; + }; + linker.defineFunction(def.witPath(), impl); + } + } + ai.tegmentum.wasmtime4j.component.ComponentInstance nativeInstance = linker.instantiate(store, nativeComponent); return new WasmtimeComponentInstanceAdapter(nativeInstance); diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java index deb2a98..2a7e493 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java @@ -11,12 +11,15 @@ public final class DefaultLinkingContext implements LinkingContext { private final WasiContext wasiContext; private final Map imports; private final List hostFunctions; + private final List witHostFunctions; private DefaultLinkingContext(WasiContext wasiContext, Map imports, - List hostFunctions) { + List hostFunctions, + List witHostFunctions) { this.wasiContext = wasiContext; this.imports = Collections.unmodifiableMap(new LinkedHashMap<>(imports)); this.hostFunctions = Collections.unmodifiableList(new ArrayList<>(hostFunctions)); + this.witHostFunctions = Collections.unmodifiableList(new ArrayList<>(witHostFunctions)); } @Override @@ -29,6 +32,11 @@ public List hostFunctions() { return hostFunctions; } + @Override + public List witHostFunctions() { + return witHostFunctions; + } + public Map imports() { return imports; } @@ -42,6 +50,7 @@ public static final class Builder { private WasiContext wasiContext; private final Map imports = new LinkedHashMap<>(); private final List hostFunctions = new ArrayList<>(); + private final List witHostFunctions = new ArrayList<>(); private Builder() { } @@ -69,8 +78,23 @@ public Builder addHostFunction(HostFunctionDefinition definition) { return this; } + /** + * Register a WIT-typed host function under {@code witPath} — the standard WIT + * import path form ({@code "package:name/interface#function"} or {@code + * "#function"} for a root import). See {@link WitHostFunctionDefinition}. + */ + public Builder addWitHostFunction(String witPath, WitHostFunction function) { + this.witHostFunctions.add(new WitHostFunctionDefinition(witPath, function)); + return this; + } + + public Builder addWitHostFunction(WitHostFunctionDefinition definition) { + this.witHostFunctions.add(definition); + return this; + } + public DefaultLinkingContext build() { - return new DefaultLinkingContext(wasiContext, imports, hostFunctions); + return new DefaultLinkingContext(wasiContext, imports, hostFunctions, witHostFunctions); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java index 4ba46cd..9776235 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java @@ -9,6 +9,15 @@ default List hostFunctions() { return Collections.emptyList(); } + /** + * WIT-typed host functions to link into a component's imports. Providers may + * ignore these when instantiating a core module. See {@link WitHostFunction} + * for the value-type contract. + */ + default List witHostFunctions() { + return Collections.emptyList(); + } + default WasiContext wasiContext() { return null; } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunction.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunction.java new file mode 100644 index 0000000..e792548 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunction.java @@ -0,0 +1,34 @@ +package ai.tegmentum.webassembly4j.api; + +/** + * Host implementation of a WIT-typed function imported by a component. + * + *

Unlike {@link HostFunction}, whose arguments and results are core-module i32/i64 + * values, WIT host functions exchange richer typed values with the guest — records, + * variants, options, results, lists, etc. + * + *

Concrete value type is provider-specific. The {@code Object[]} entries are + * the values in the provider's native WIT representation: + *

    + *
  • Wasmtime provider: + * {@code ai.tegmentum.wasmtime4j.component.ComponentVal} + * (built via {@code ComponentValFactory.INSTANCE.createXxx(...)}).
  • + *
+ * A cross-provider WIT value abstraction is not yet part of this API; write host + * functions against the provider you plan to use. + * + *

Registered on a {@link LinkingContext} via {@link + * DefaultLinkingContext.Builder#addWitHostFunction(String, WitHostFunction)}. + */ +@FunctionalInterface +public interface WitHostFunction { + + /** + * Invoked when the component calls the imported WIT function. + * + * @param args provider-native WIT value tree, one entry per declared parameter. + * @return provider-native WIT value tree, matching the declared results tuple. + * A function with no return value returns an empty array. + */ + Object[] execute(Object[] args); +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunctionDefinition.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunctionDefinition.java new file mode 100644 index 0000000..00f0ff9 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitHostFunctionDefinition.java @@ -0,0 +1,52 @@ +package ai.tegmentum.webassembly4j.api; + +import java.util.Objects; + +/** + * A WIT-typed host function to be linked into a component's imports. + * + *

Immutable value. Register via {@link + * DefaultLinkingContext.Builder#addWitHostFunction(String, WitHostFunction)} or the + * two-arg overload that takes a pre-built definition. + * + *

The {@code witPath} uses the standard WIT import path form — + * {@code "package:name/interface#function"} for an interface-scoped import (e.g. + * {@code "stardog:webfunction/mapping-dictionary#get"}) or {@code "#function"} for + * a root-level import — matching the format the native component linker expects. + */ +public final class WitHostFunctionDefinition { + + private final String witPath; + private final WitHostFunction function; + + public WitHostFunctionDefinition(final String witPath, final WitHostFunction function) { + this.witPath = Objects.requireNonNull(witPath, "witPath"); + this.function = Objects.requireNonNull(function, "function"); + } + + public String witPath() { + return witPath; + } + + public WitHostFunction function() { + return function; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof WitHostFunctionDefinition)) return false; + final WitHostFunctionDefinition other = (WitHostFunctionDefinition) o; + return witPath.equals(other.witPath) && function.equals(other.function); + } + + @Override + public int hashCode() { + return Objects.hash(witPath, function); + } + + @Override + public String toString() { + return "WitHostFunctionDefinition{witPath='" + witPath + "'}"; + } +} From 0e9cd590f9fa163f78226cb22b346b70c77492c4 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 6 Jul 2026 06:27:49 -0400 Subject: [PATCH 14/85] chore(release): 2.1.0 Bumps wasmtime4j to 46.0.1-1.2.0 and cuts the 2.1.0 release covering end-to-end Component Model invocation on the Wasmtime provider, WIT-typed host imports, WASI preopen enhancements, and per-instance component resource caps. --- CHANGELOG.md | 17 +++++++++++++++++ endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 4 ++-- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 16 files changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf8b32..f048ebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 2.1.0 + +### New Features + +- **Component Model invocation, end-to-end (Wasmtime)**: `ComponentInstance.invoke()` now marshals natural Java argument types across the full composite type surface (record / list / option / variant / result / tuple / enum / flags), and `ComponentInstance.invokeWit` provides a symmetric typed round-trip that returns the raw `WitValue` tree instead of a lossy `toJava()` conversion, preserving `u64` width, enum discriminants, and option inner types. +- **WIT-typed host imports via `LinkingContext`**: register host functions against WIT interface types so component imports resolve to strongly-typed Java lambdas. +- **WASI preopens with per-directory read-only + host→guest path remapping**: preopens can now be marked read-only individually, and the guest-visible path can differ from the host path. +- **Per-instance component resource caps**: memory, epoch, and fuel caps are forwarded through to component instantiation on the Wasmtime provider (metered stores get a real fuel budget instead of running unbounded). + +### Bug Fixes + +- **Wasmtime provider**: component instantiation is now routed through the WASI-config path so `WasiContext` + `ComponentConfig` are honored end-to-end. + +### Dependency Upgrades + +- wasmtime4j: 46.0.1-1.1.7 → 46.0.1-1.2.0 (Component Model end-to-end, per-instance fuel/memory/epoch caps, WIT marshalling fixes, panama artifact fix) + ## 2.0.0 ### Breaking Changes diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 3a87c83..41b9279 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index f9e1a00..86941fa 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 195c726..1c55022 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 pom WebAssembly4J Parent @@ -63,7 +63,7 @@ 5.10.4 - 46.0.1-1.1.7 + 46.0.1-1.2.0 2.4.4-1.0.2 1.0.0 24.2.1 diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index d86a457..eb35630 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index a6e6f92..bac7ec2 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 3eaeba5..a797581 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 5e3b8b1..2e758b3 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index afb0421..6705619 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index b051e8e..feb67d7 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 45a0ded..b71dc7d 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 4cf4bf4..425199a 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 43f7aef..3402150 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 35a6a3c..6b5015e 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index 334b02c..1b6d9b9 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index d12691a..3b20845 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.0.0 + 2.1.0 webassembly4j-testing From 4785127db90681c881f44a84aeabc9ee0972caed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:27:35 -0400 Subject: [PATCH 15/85] chore(deps): bump the maven-minor-patch group across 1 directory with 14 updates (#12) --- updated-dependencies: - dependency-name: run.endive:runtime dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: run.endive:wasm dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: run.endive:wasi dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: run.endive:compiler dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: run.endive:wasm dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: run.endive:wasi dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: run.endive:compiler dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.graalvm.polyglot:polyglot dependency-version: 24.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.graalvm.polyglot:wasm-community dependency-version: 24.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.graalvm.polyglot:wasm-community dependency-version: 24.2.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.22.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.junit.jupiter:junit-jupiter dependency-version: 5.14.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 5.14.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.junit.jupiter:junit-jupiter-params dependency-version: 5.14.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-version: 5.14.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.junit.jupiter:junit-jupiter-params dependency-version: 5.14.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-autoconfigure dependency-version: 3.5.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-actuator-autoconfigure dependency-version: 3.5.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-configuration-processor dependency-version: 3.5.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.16 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-actuator-autoconfigure dependency-version: 3.5.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-configuration-processor dependency-version: 3.5.16 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.16 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 1c55022..f5a280d 100644 --- a/pom.xml +++ b/pom.xml @@ -62,14 +62,14 @@ UTF-8 - 5.10.4 + 5.14.4 46.0.1-1.2.0 2.4.4-1.0.2 - 1.0.0 - 24.2.1 + 1.0.1 + 24.2.2 1.37 - 2.22.0 - 3.2.5 + 2.22.1 + 3.5.16 3.15.0 From e8c0e6919395435ee5f5f3dc67b1516eec8b8419 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 13 Jul 2026 06:56:01 -0400 Subject: [PATCH 16/85] feat(api): network egress allow-list surface + wasmtime adapter enforcement Add a provider-agnostic egress capability so a policy can grant outbound network access to specific endpoints. NetworkEgressRule (literal IP or CIDR + port + tcp/udp) with total, fail-closed matching incl. CIDR containment. WasiContext gains allowNetwork() (default false) + egressRules() (default empty = deny-all); DefaultWasiContext.Builder gains allowNetwork() + allowEgress(). WasmtimeComponentAdapter.toWasiPreview2Config maps a granted context to allowNetwork(true) + allowTcp/allowUdp + a SocketAddrCheck that permits ONLY connect/outgoing-datagram to an allow-listed endpoint and denies every bind/listen use (egress only, no ingress). The native wasmtime4j machinery (allowNetwork/socketAddrCheck) already existed; this is the Java plumbing. Version 2.1.0 -> 2.2.0-SNAPSHOT (needs a Maven Central publish for a real release). --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- .../wasmtime/WasmtimeComponentAdapter.java | 28 ++++ webassembly4j-api/pom.xml | 2 +- .../webassembly4j/api/DefaultWasiContext.java | 35 ++++- .../webassembly4j/api/NetworkEgressRule.java | 120 ++++++++++++++++++ .../webassembly4j/api/WasiContext.java | 19 +++ webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 19 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/NetworkEgressRule.java diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 41b9279..f41f7ae 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 86941fa..0ca2963 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT graalwasm4j-provider diff --git a/pom.xml b/pom.xml index f5a280d..2cdae69 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index eb35630..0210231 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index bac7ec2..2209313 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index a797581..f7e984e 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT wasmtime4j-provider diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 2422009..c2b7891 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -206,6 +206,34 @@ static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { } } } + // Network egress: deny-by-default unless the policy grants it. When granted, allow TCP/UDP but + // gate every connection through a SocketAddrCheck that permits ONLY egress (connect / outgoing + // datagram) to an allow-listed endpoint and denies every bind/listen use — no ingress, ever. + if (wasi.allowNetwork() && wasi.egressRules() != null && !wasi.egressRules().isEmpty()) { + final java.util.List rules = + new ArrayList<>(wasi.egressRules()); + b.allowNetwork(true).allowTcp(true).allowUdp(true); + b.socketAddrCheck((address, use) -> { + final boolean tcp; + switch (use) { + case TCP_CONNECT: + tcp = true; + break; + case UDP_CONNECT: + case UDP_OUTGOING_DATAGRAM: + tcp = false; + break; + default: + return false; // deny all *_BIND — egress only + } + for (ai.tegmentum.webassembly4j.api.NetworkEgressRule r : rules) { + if (r.matches(address, tcp)) { + return true; + } + } + return false; + }); + } return b.build(); } diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 2e758b3..884a421 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-api diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java index 569daa0..b9f9057 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java @@ -16,12 +16,15 @@ public final class DefaultWasiContext implements WasiContext { private final List preopenDirs; private final List readOnlyPreopenDirs; private final Map preopenGuestPaths; + private final boolean allowNetwork; + private final List egressRules; private DefaultWasiContext(List args, Map env, boolean inheritStdin, boolean inheritStdout, boolean inheritStderr, List preopenDirs, List readOnlyPreopenDirs, - Map preopenGuestPaths) { + Map preopenGuestPaths, + boolean allowNetwork, List egressRules) { this.args = Collections.unmodifiableList(new ArrayList<>(args)); this.env = Collections.unmodifiableMap(new LinkedHashMap<>(env)); this.inheritStdin = inheritStdin; @@ -32,6 +35,8 @@ private DefaultWasiContext(List args, Map env, Collections.unmodifiableList(new ArrayList<>(readOnlyPreopenDirs)); this.preopenGuestPaths = Collections.unmodifiableMap(new LinkedHashMap<>(preopenGuestPaths)); + this.allowNetwork = allowNetwork; + this.egressRules = Collections.unmodifiableList(new ArrayList<>(egressRules)); } @Override @@ -74,6 +79,16 @@ public Map preopenGuestPaths() { return preopenGuestPaths; } + @Override + public boolean allowNetwork() { + return allowNetwork; + } + + @Override + public List egressRules() { + return egressRules; + } + public static Builder builder() { return new Builder(); } @@ -88,6 +103,8 @@ public static final class Builder { private final List preopenDirs = new ArrayList<>(); private final List readOnlyPreopenDirs = new ArrayList<>(); private final Map preopenGuestPaths = new LinkedHashMap<>(); + private boolean allowNetwork; + private final List egressRules = new ArrayList<>(); private Builder() { } @@ -160,9 +177,23 @@ public Builder preopenDir(String hostDir, String guestPath, boolean writable) { return this; } + /** Allow (or deny) the guest to open outbound network connections. Default deny. */ + public Builder allowNetwork(boolean allow) { + this.allowNetwork = allow; + return this; + } + + /** Add an egress allow-list rule (implies {@link #allowNetwork(boolean) allowNetwork(true)}). */ + public Builder allowEgress(NetworkEgressRule rule) { + this.egressRules.add(rule); + this.allowNetwork = true; + return this; + } + public DefaultWasiContext build() { return new DefaultWasiContext(args, env, inheritStdin, inheritStdout, - inheritStderr, preopenDirs, readOnlyPreopenDirs, preopenGuestPaths); + inheritStderr, preopenDirs, readOnlyPreopenDirs, preopenGuestPaths, + allowNetwork, egressRules); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/NetworkEgressRule.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/NetworkEgressRule.java new file mode 100644 index 0000000..98be109 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/NetworkEgressRule.java @@ -0,0 +1,120 @@ +package ai.tegmentum.webassembly4j.api; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +/** + * A provider-agnostic network egress allow-list rule: permit outbound connections to a literal IP or + * CIDR range, on a given port (0 = any), over TCP or UDP. Hostnames are resolved to IPs by the policy + * compiler before a rule is built, so a rule always matches on address, not name — keeping DNS out of + * the guest. Matching is total and fail-closed: any address the rule cannot positively match yields + * {@code false}. + * + *

Egress only by construction: this type expresses connect permission; the provider that + * consumes it must deny every bind/listen use regardless of the rules. + */ +public final class NetworkEgressRule { + + private final String cidrOrIp; + private final int prefixLen; // -1 ⇒ exact IP (no CIDR) + private final byte[] network; // resolved network address bytes, or exact IP bytes + private final int port; // 0 ⇒ any + private final boolean tcp; + + private NetworkEgressRule(String cidrOrIp, int prefixLen, byte[] network, int port, boolean tcp) { + this.cidrOrIp = cidrOrIp; + this.prefixLen = prefixLen; + this.network = network; + this.port = port; + this.tcp = tcp; + } + + /** + * Build a rule from a literal IP ({@code 93.184.216.34}) or CIDR ({@code 10.0.0.0/8}), a port + * (0 = any), and a protocol. Throws {@link IllegalArgumentException} if the address/CIDR is + * malformed — a compile-time error the policy layer surfaces as a hard failure (never a permissive + * default). + */ + public static NetworkEgressRule of(String cidrOrIp, int port, boolean tcp) { + Objects.requireNonNull(cidrOrIp, "cidrOrIp"); + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("port out of range: " + port); + } + String addrPart = cidrOrIp; + int prefix = -1; + int slash = cidrOrIp.indexOf('/'); + if (slash >= 0) { + addrPart = cidrOrIp.substring(0, slash); + try { + prefix = Integer.parseInt(cidrOrIp.substring(slash + 1)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("malformed CIDR prefix: " + cidrOrIp, e); + } + } + byte[] bytes; + try { + bytes = InetAddress.getByName(addrPart).getAddress(); // numeric only (no DNS for a literal) + } catch (UnknownHostException e) { + throw new IllegalArgumentException("malformed IP/CIDR address: " + cidrOrIp, e); + } + if (slash >= 0 && (prefix < 0 || prefix > bytes.length * 8)) { + throw new IllegalArgumentException("CIDR prefix out of range for address: " + cidrOrIp); + } + return new NetworkEgressRule(cidrOrIp, prefix, bytes, port, tcp); + } + + /** + * Whether an outbound {@code addr} over the given protocol is permitted by this rule. Proto must + * match, port must match (or the rule is any-port), and the address must fall within the rule's + * IP/CIDR. Returns {@code false} on any mismatch or resolution anomaly. + */ + public boolean matches(InetSocketAddress addr, boolean isTcp) { + if (addr == null || isTcp != tcp) { + return false; + } + if (port != 0 && addr.getPort() != port) { + return false; + } + InetAddress ia = addr.getAddress(); + if (ia == null) { + return false; // unresolved — fail closed + } + byte[] target = ia.getAddress(); + if (target.length != network.length) { + return false; // v4 vs v6 mismatch + } + int bits = prefixLen < 0 ? target.length * 8 : prefixLen; + int fullBytes = bits / 8; + for (int i = 0; i < fullBytes; i++) { + if (target[i] != network[i]) { + return false; + } + } + int remBits = bits % 8; + if (remBits != 0) { + int mask = 0xFF << (8 - remBits) & 0xFF; + if ((target[fullBytes] & mask) != (network[fullBytes] & mask)) { + return false; + } + } + return true; + } + + /** The protocol this rule permits: true = TCP, false = UDP. */ + public boolean tcp() { + return tcp; + } + + /** The port this rule permits, or 0 for any port. */ + public int port() { + return port; + } + + @Override + public String toString() { + return "NetworkEgressRule{" + cidrOrIp + ":" + (port == 0 ? "*" : port) + + "/" + (tcp ? "tcp" : "udp") + "}"; + } +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java index fae0ca6..e8df6e1 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java @@ -48,4 +48,23 @@ default List readOnlyPreopenDirs() { default Map preopenGuestPaths() { return Collections.emptyMap(); } + + /** + * Whether the guest may open outbound network connections. Default {@code false} (deny-by-default); + * only true when a policy grants egress. Even when true, {@link #egressRules()} is the allow-list + * that gates which endpoints are reachable — an empty rule set with {@code allowNetwork()==true} + * denies everything. + */ + default boolean allowNetwork() { + return false; + } + + /** + * The network egress allow-list: outbound connections are permitted only to an endpoint matching one + * of these rules. Empty ⇒ deny-all. The provider must additionally deny every bind/listen use, so + * these rules can only ever grant egress, never ingress. + */ + default List egressRules() { + return Collections.emptyList(); + } } diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 6705619..d0fca00 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index feb67d7..64f7038 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index b71dc7d..cdc8dee 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 425199a..8df1689 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 3402150..0d4bd58 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 6b5015e..f725d57 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index 1b6d9b9..e509f71 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 3b20845..84240b1 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.1.0 + 2.2.0-SNAPSHOT webassembly4j-testing From 1b926971056565a46dd4c366cf5a87c89e7774d1 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 15 Jul 2026 13:41:29 -0400 Subject: [PATCH 17/85] chore(deps): bump wasmtime4j to 46.0.1-1.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2cdae69..8217d6d 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.2.0 + 46.0.1-1.3.0 2.4.4-1.0.2 1.0.1 24.2.2 From 669e23d3fd4a02ddea4eee85b5c5309cb6272bda Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 15 Jul 2026 13:44:03 -0400 Subject: [PATCH 18/85] chore(release): 2.2.0 Cuts the 2.2.0 release covering a Wasmtime network egress allow-list (fail-closed CIDR/port/protocol matching, egress-only via SocketAddrCheck), plus a wasmtime4j bump to 46.0.1-1.3.0 and the maven-minor-patch group Dependabot roll-up (endive 1.0.1, GraalVM 24.2.2, JUnit 5.14.4, Jackson 2.22.1, Spring Boot 3.5.16). --- CHANGELOG.md | 15 +++++++++++++++ endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 16 files changed, 30 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f048ebb..312dcf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 2.2.0 + +### New Features + +- **Network egress allow-list (Wasmtime)**: new `NetworkEgressRule` (literal IP or CIDR + port + tcp/udp) with total, fail-closed matching including CIDR containment. `WasiContext` gains `allowNetwork()` and `egressRules()`; `DefaultWasiContext.Builder` gains `allowNetwork()` and `allowEgress()`. The Wasmtime component adapter maps a granted context to `allowNetwork(true)` + `allowTcp`/`allowUdp` and installs a `SocketAddrCheck` that permits only `connect`/`outgoing-datagram` to allow-listed endpoints while denying every `bind`/`listen` use (egress only, no ingress). + +### Dependency Upgrades + +- wasmtime4j: 46.0.1-1.2.0 → 46.0.1-1.3.0 +- endive: 1.0.0 → 1.0.1 +- GraalVM Polyglot: 24.2.1 → 24.2.2 +- JUnit Jupiter: 5.10.4 → 5.14.4 +- Jackson: 2.22.0 → 2.22.1 +- Spring Boot: 3.2.5 → 3.5.16 + ## 2.1.0 ### New Features diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index f41f7ae..e13fa8e 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 0ca2963..7f222cb 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 8217d6d..e6cc5e4 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 0210231..932f5eb 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 2209313..5db176e 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index f7e984e..680db2b 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 884a421..7e8462f 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index d0fca00..fed5580 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 64f7038..f4bb476 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index cdc8dee..f43d04a 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 8df1689..16d75b1 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 0d4bd58..97d65ea 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index f725d57..4ab1da1 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index e509f71..448d52f 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 84240b1..a22c80d 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0-SNAPSHOT + 2.2.0 webassembly4j-testing From 182932c83697dffed9cf64d3f54793953146e453 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 15 Jul 2026 16:43:22 -0400 Subject: [PATCH 19/85] feat(api): engine-neutral FsAccessObserver + wasmtime fs-denial passthrough Add ai.tegmentum.webassembly4j.api.FsAccessObserver, an engine-neutral, observe-only callback fired when a component's path-based filesystem open is denied on the WASI-preopen path (java.lang types only; no runtime dep). Surface it as an optional WasiContext.fsAccessObserver(). In the wasmtime provider, toWasiPreview2Config now bridges a present observer to wasmtime4j's WasiPreview2Config.fsAccessObserver so a denied component open-at/stat-at surfaces the raw guest path + classified reason to the host. Observe-only: it cannot change the enforcement outcome. Requires a wasmtime4j build carrying the FsAccessObserver hook; against a runtime without it the observer is simply never invoked. --- .../wasmtime/WasmtimeComponentAdapter.java | 10 +++++ .../webassembly4j/api/FsAccessObserver.java | 42 +++++++++++++++++++ .../webassembly4j/api/WasiContext.java | 12 ++++++ 3 files changed, 64 insertions(+) create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FsAccessObserver.java diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index c2b7891..703180e 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -234,6 +234,16 @@ static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { return false; }); } + // Filesystem-denial observability: if the policy supplied an engine-neutral FsAccessObserver, + // bridge it to wasmtime4j's own observer so a denied component open-at/stat-at on the preopen + // path surfaces the raw guest path + classified reason. Observe-only — it cannot change the + // enforcement outcome (wasmtime has already refused the open by the time this fires). + if (wasi.fsAccessObserver() != null && wasi.fsAccessObserver().isPresent()) { + final ai.tegmentum.webassembly4j.api.FsAccessObserver neutral = wasi.fsAccessObserver().get(); + b.fsAccessObserver( + (path, operation, reason, errorCode) -> + neutral.onDenied(path, operation, reason, errorCode)); + } return b.build(); } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FsAccessObserver.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FsAccessObserver.java new file mode 100644 index 0000000..639fe78 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FsAccessObserver.java @@ -0,0 +1,42 @@ +package ai.tegmentum.webassembly4j.api; + +/** + * Engine-neutral, observe-only callback notified when a component's path-based filesystem access is + * DENIED on the capability-confined (WASI preopen) instantiation path. + * + *

This is a pure observability hook. It CANNOT change enforcement: by the time it fires the + * runtime has already performed the real {@code open-at} / {@code stat-at} and produced the real + * error, which flows back to the guest unchanged regardless of what this method does. Its sole + * purpose is to surface the raw guest-supplied path together with the classified failure reason so + * the host can log, meter, or alert on denied filesystem access that would otherwise be invisible on + * the component/preopen path (where the runtime refuses opens internally, with no host callback on + * the enforcement path itself). + * + *

The interface is intentionally engine-agnostic — it names only {@code java.lang} types and does + * not depend on any particular WebAssembly runtime. A provider that supports a native denial hook + * (for example the wasmtime provider, which bridges this to wasmtime4j's own observer) adapts this + * to its runtime; providers without such a hook simply never invoke it. + * + *

Implementations are invoked synchronously on the guest's calling thread while the denied + * operation is being serviced. They should be fast and must not assume they can influence the + * outcome. A provider is expected to isolate the guest from any exception this method throws. + * + * @since 2.3.0 + */ +@FunctionalInterface +public interface FsAccessObserver { + + /** + * Called when a path-based filesystem operation was refused by WASI enforcement. + * + * @param path the raw guest-supplied path the operation targeted (relative to the descriptor it + * was resolved against; never {@code null}) + * @param operation the operation that was denied, e.g. {@code "open-at"} or {@code "stat-at"} + * @param reason the classified failure reason: the kebab-case WASI {@code error-code} name, e.g. + * {@code "not-permitted"}, {@code "no-entry"}, {@code "access"}; {@code "unknown"} if the + * failure could not be classified to an error code + * @param errorCode the numeric discriminant of {@code reason} within the WASI {@code error-code} + * enum, or {@code -1} when unclassified + */ + void onDenied(String path, String operation, String reason, int errorCode); +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java index e8df6e1..f0fac23 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; public interface WasiContext { @@ -67,4 +68,15 @@ default boolean allowNetwork() { default List egressRules() { return Collections.emptyList(); } + + /** + * An optional observe-only hook notified when the guest's path-based filesystem access is denied on + * the capability-confined (WASI preopen) instantiation path. Empty by default (no observation). When + * present, a provider that supports a native denial hook installs it so a denied {@code open-at} / + * {@code stat-at} surfaces the raw guest path and the classified reason to the host — pure + * observability, it never changes enforcement. Providers without such a hook ignore it. + */ + default Optional fsAccessObserver() { + return Optional.empty(); + } } From db878ee855f2cad681f3711dead9c8f75ac48568 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 15 Jul 2026 21:19:44 -0400 Subject: [PATCH 20/85] build(deps): bump wasmtime4j to 46.0.1-1.3.1 for the fs-denial observer The fs-denial passthrough in the wasmtime provider calls WasiPreview2Config.fsAccessObserver, which ships in wasmtime4j 46.0.1-1.3.1 (FsAccessObserver hook). Bump the dependency so the passthrough compiles against the released observer. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6cc5e4..c7705dc 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.3.0 + 46.0.1-1.3.1 2.4.4-1.0.2 1.0.1 24.2.2 From 030935a6dde5903ef1022c664a3277a6bfc073ec Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 15 Jul 2026 21:36:35 -0400 Subject: [PATCH 21/85] chore(release): 2.3.0 Cuts the 2.3.0 release covering the engine-neutral FsAccessObserver (observe-only callback fired when a component's path-based filesystem open is denied on the WASI preopen path, surfacing raw guest path + classified reason) with a wasmtime provider passthrough to wasmtime4j's WasiPreview2Config.fsAccessObserver, plus a wasmtime4j bump to 46.0.1-1.3.1 carrying the underlying hook. --- CHANGELOG.md | 10 ++++++++++ endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 16 files changed, 25 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 312dcf6..f199272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2.3.0 + +### New Features + +- **Engine-neutral filesystem access observer**: new `ai.tegmentum.webassembly4j.api.FsAccessObserver`, an observe-only callback fired when a component's path-based filesystem open is denied on the WASI preopen path. Surfaces the raw guest path and a classified reason (`java.lang` types only, no runtime dependency). Wired as an optional `WasiContext.fsAccessObserver()`. The Wasmtime provider bridges a present observer through to wasmtime4j's `WasiPreview2Config.fsAccessObserver` so denied `open-at`/`stat-at` calls report the raw guest path + reason to the host. Observe-only: the observer cannot change the enforcement outcome. + +### Dependency Upgrades + +- wasmtime4j: 46.0.1-1.3.0 → 46.0.1-1.3.1 + ## 2.2.0 ### New Features diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index e13fa8e..faf6421 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 7f222cb..f1df739 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index c7705dc..87f3a88 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 932f5eb..77c80f6 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 5db176e..b73eb92 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 680db2b..caa2901 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 7e8462f..aa36c21 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index fed5580..85b7445 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index f4bb476..03e4156 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index f43d04a..5b515a3 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 16d75b1..5e390b9 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 97d65ea..86a12b0 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 4ab1da1..6d21098 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index 448d52f..c40af68 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index a22c80d..38bae8e 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.2.0 + 2.3.0 webassembly4j-testing From 38bc671c75686602cd4d1fee1d9b4f6f5aee5e81 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 16 Jul 2026 18:50:20 -0400 Subject: [PATCH 22/85] feat(api): add WasiNnConfig + LinkingContext.Builder.enableWasiNn(WasiNnConfig) Introduces a provider-neutral WasiNnConfig marker in webassembly4j-api and threads it through LinkingContext (default null) so a caller can opt a component into wasi:nn without touching the native ComponentLinker. The wasmtime4j-provider translates it to ai.tegmentum.wasmtime4j.wasi.nn.WasiNnConfig and calls the native ComponentLinker#enableWasiNn(WasiNnConfig) added in wasmtime4j 46.0.1-1.4.0. Providers without wasi:nn surface a WasmException from their native linker which InstantiationException wraps at the api boundary. Bumps wasmtime4j dep 46.0.1-1.3.1 -> 46.0.1-1.4.0 and cuts webassembly4j 2.4.0. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 4 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- .../wasmtime/WasmtimeComponentAdapter.java | 28 +++++ webassembly4j-api/pom.xml | 2 +- .../api/DefaultLinkingContext.java | 29 ++++- .../webassembly4j/api/LinkingContext.java | 11 ++ .../webassembly4j/api/WasiNnConfig.java | 104 ++++++++++++++++++ webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 19 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiNnConfig.java diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index faf6421..52a94b8 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index f1df739..121b1f6 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 87f3a88..6ce6161 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 pom WebAssembly4J Parent @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.3.1 + 46.0.1-1.4.0 2.4.4-1.0.2 1.0.1 24.2.2 diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 77c80f6..53e57f7 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index b73eb92..40d2902 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index caa2901..760960d 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 wasmtime4j-provider diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index 703180e..d4105ee 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -11,6 +11,7 @@ import ai.tegmentum.webassembly4j.api.ComponentInstance; import ai.tegmentum.webassembly4j.api.LinkingContext; import ai.tegmentum.webassembly4j.api.WasiContext; +import ai.tegmentum.webassembly4j.api.WasiNnConfig; import ai.tegmentum.webassembly4j.api.WitHostFunctionDefinition; import ai.tegmentum.webassembly4j.api.config.ComponentConfig; import ai.tegmentum.webassembly4j.api.exception.InstantiationException; @@ -96,6 +97,16 @@ private ComponentInstance doInstantiate(LinkingContext linkingContext, Component : WasiPreview2Config.builder().build(); linker.enableWasiPreview2(p2); + // WASI-NN opt-in: translate the framework-neutral WasiNnConfig into + // wasmtime4j's native WasiNnConfig and register wasi:nn on the linker + // before instantiation. Providers without wasi:nn surface via the + // native linker throwing WasmException; we let that propagate as + // InstantiationException below. + WasiNnConfig nn = linkingContext != null ? linkingContext.wasiNnConfig() : null; + if (nn != null) { + linker.enableWasiNn(toNativeWasiNnConfig(nn)); + } + // Register any WIT-typed host imports declared on the linking context. Each is // wrapped in a native ComponentHostFunction that just delegates — the caller // (whose api-level function takes/returns Object[]) already speaks the provider's @@ -247,6 +258,23 @@ static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { return b.build(); } + /** + * Translate the provider-neutral WasiNnConfig into wasmtime4j's native + * WasiNnConfig. Named-model entries are copied via the native builder + * (which itself clones the byte array). Package-private for unit test. + */ + static ai.tegmentum.wasmtime4j.wasi.nn.WasiNnConfig toNativeWasiNnConfig(WasiNnConfig config) { + if (config == null || config.namedModels().isEmpty()) { + return ai.tegmentum.wasmtime4j.wasi.nn.WasiNnConfig.defaults(); + } + ai.tegmentum.wasmtime4j.wasi.nn.WasiNnConfig.Builder b = + ai.tegmentum.wasmtime4j.wasi.nn.WasiNnConfig.builder(); + for (java.util.Map.Entry e : config.namedModels().entrySet()) { + b.registerModel(e.getKey(), e.getValue()); + } + return b.build(); + } + @Override public List exportedInterfaces() { try { diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index aa36c21..629394f 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-api diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java index 2a7e493..38c4ea6 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java @@ -9,14 +9,17 @@ public final class DefaultLinkingContext implements LinkingContext { private final WasiContext wasiContext; + private final WasiNnConfig wasiNnConfig; private final Map imports; private final List hostFunctions; private final List witHostFunctions; - private DefaultLinkingContext(WasiContext wasiContext, Map imports, + private DefaultLinkingContext(WasiContext wasiContext, WasiNnConfig wasiNnConfig, + Map imports, List hostFunctions, List witHostFunctions) { this.wasiContext = wasiContext; + this.wasiNnConfig = wasiNnConfig; this.imports = Collections.unmodifiableMap(new LinkedHashMap<>(imports)); this.hostFunctions = Collections.unmodifiableList(new ArrayList<>(hostFunctions)); this.witHostFunctions = Collections.unmodifiableList(new ArrayList<>(witHostFunctions)); @@ -27,6 +30,11 @@ public WasiContext wasiContext() { return wasiContext; } + @Override + public WasiNnConfig wasiNnConfig() { + return wasiNnConfig; + } + @Override public List hostFunctions() { return hostFunctions; @@ -48,6 +56,7 @@ public static Builder builder() { public static final class Builder { private WasiContext wasiContext; + private WasiNnConfig wasiNnConfig; private final Map imports = new LinkedHashMap<>(); private final List hostFunctions = new ArrayList<>(); private final List witHostFunctions = new ArrayList<>(); @@ -60,6 +69,21 @@ public Builder wasiContext(WasiContext wasiContext) { return this; } + /** + * Enable WASI-NN on the resulting linking context with the given + * configuration. Providers that support wasi:nn (currently + * wasmtime4j-provider) translate this to their native enablement call + * before component instantiation. Passing {@link WasiNnConfig#defaults()} + * requests the provider's default backend set (ORT/ONNX under the + * current wasmtime4j pin). Passing {@code null} clears the request. + * + * @param config the WASI-NN configuration; {@code null} disables + */ + public Builder enableWasiNn(WasiNnConfig config) { + this.wasiNnConfig = config; + return this; + } + public Builder addImport(String name, Object value) { this.imports.put(name, value); return this; @@ -94,7 +118,8 @@ public Builder addWitHostFunction(WitHostFunctionDefinition definition) { } public DefaultLinkingContext build() { - return new DefaultLinkingContext(wasiContext, imports, hostFunctions, witHostFunctions); + return new DefaultLinkingContext( + wasiContext, wasiNnConfig, imports, hostFunctions, witHostFunctions); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java index 9776235..3d9d25a 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java @@ -21,4 +21,15 @@ default List witHostFunctions() { default WasiContext wasiContext() { return null; } + + /** + * Optional WASI-NN configuration. When non-null, providers that support + * wasi:nn enable the interface on their component linker before + * instantiation; providers that do not surface + * {@link ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException}. + * Defaults to {@code null} (WASI-NN disabled). + */ + default WasiNnConfig wasiNnConfig() { + return null; + } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiNnConfig.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiNnConfig.java new file mode 100644 index 0000000..fe1ec1b --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiNnConfig.java @@ -0,0 +1,104 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.api; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Provider-neutral configuration for enabling WASI-NN on a component + * {@link LinkingContext}. Passed to + * {@link DefaultLinkingContext.Builder#enableWasiNn(WasiNnConfig)}; providers that + * support wasi:nn translate this to their native config and register the interface + * on their linker. Providers without wasi:nn support surface + * {@link ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException} when + * a config is present. + * + *

An empty config (the {@link #defaults()} instance) requests the provider's + * default backend set — typically ORT/ONNX under the current wasmtime4j pin. That + * mirrors the zero-arg {@code enableWasiNn()} on wasmtime4j's native linker. + * + *

Extensibility. Named-model registry entries are additive; a caller + * that supplies models today gets no behaviour change from a provider that has not + * yet wired the registry through (wasmtime4j {@code 46.0.1-1.4.0} silently ignores + * them). Future additions (preferred backend enumeration, per-graph execution + * targets) land as additive setters. + * + *

Thread safety. Instances are immutable after {@link Builder#build()} + * and safe to share across threads. + */ +public final class WasiNnConfig { + + private static final WasiNnConfig DEFAULTS = new WasiNnConfig(Collections.emptyMap()); + + private final Map namedModels; + + private WasiNnConfig(Map namedModels) { + this.namedModels = Collections.unmodifiableMap(new LinkedHashMap<>(namedModels)); + } + + /** + * Returns a shared, empty configuration equivalent to the zero-arg + * {@code enableWasiNn()} on the underlying provider linker. + */ + public static WasiNnConfig defaults() { + return DEFAULTS; + } + + /** Starts a builder for a customised configuration. */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the (read-only) named-model registry entries supplied to this + * config. Providers translate these into their native wasi:nn registry when + * the plumbing is present; entries are silently ignored otherwise. + */ + public Map namedModels() { + return namedModels; + } + + /** Builder for {@link WasiNnConfig}. */ + public static final class Builder { + private final Map namedModels = new LinkedHashMap<>(); + + private Builder() { + } + + /** + * Register a model by name so guests can look it up via + * {@code wasi:nn/graph.load-by-name}. Model bytes are copied at build + * time; the caller's array may be mutated afterwards without affecting + * linker behaviour. + */ + public Builder registerModel(String name, byte[] modelBytes) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("model name cannot be null or empty"); + } + if (modelBytes == null) { + throw new IllegalArgumentException("modelBytes cannot be null"); + } + namedModels.put(name, modelBytes.clone()); + return this; + } + + public WasiNnConfig build() { + return new WasiNnConfig(namedModels); + } + } +} diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 85b7445..132c9de 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 03e4156..3e5baf4 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 5b515a3..6868927 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 5e390b9..c75b951 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 86a12b0..27bc3af 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 6d21098..514e126 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index c40af68..f4c35aa 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 38bae8e..32b1e8d 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.3.0 + 2.4.0 webassembly4j-testing From e7fa53ec6c6c5e64b35acff5b0e6fe5d8ec62a7d Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 16 Jul 2026 20:01:37 -0400 Subject: [PATCH 23/85] feat(wasi-nn): add supportsWasiNn() capability probe Closes a regression where plugins that call DefaultLinkingContext.Builder#enableWasiNn(...) unconditionally against the shipping 46.0.1-1.4.x wasmtime4j native (built without the wasi-nn cargo feature) throw at instantiation time even for guests that import zero wasi:nn interfaces. - Adds a default-false supportsWasiNn() probe on both LinkingContext and Component so callers can gate the enable path on the actual provider capability, not on their own configuration intent. - Overrides supportsWasiNn() on WasmtimeComponentAdapter to delegate (via reflection, so wasmtime4j-jni stays runtime-scoped) to JniComponentLinker.wasiNnAvailable(), which reports the compile-time wasi-nn feature bit of the loaded libwasmtime4j. - Bumps webassembly4j 2.4.0 -> 2.4.1 (patch, additive API) and pins wasmtime4j 46.0.1-1.4.0 -> 46.0.1-1.4.2 to pick up the JNI probe. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 4 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- .../wasmtime/WasmtimeComponentAdapter.java | 38 ++++++++++++++++ .../wasmtime/WasmtimeSupportsWasiNnTest.java | 45 +++++++++++++++++++ webassembly4j-api/pom.xml | 2 +- .../webassembly4j/api/Component.java | 14 ++++++ .../webassembly4j/api/LinkingContext.java | 13 ++++++ .../api/DefaultLinkingContextTest.java | 12 +++++ webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 20 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeSupportsWasiNnTest.java diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 52a94b8..3d93d46 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 121b1f6..d4d7f11 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 6ce6161..a69993d 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 pom WebAssembly4J Parent @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.4.0 + 46.0.1-1.4.2 2.4.4-1.0.2 1.0.1 24.2.2 diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 53e57f7..d1425f2 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 40d2902..6491fac 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 760960d..08dbb8b 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 wasmtime4j-provider diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index d4105ee..e2763e3 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -323,6 +323,44 @@ public byte[] serialize() { } } + /** + * Reports whether the loaded wasmtime4j native library was compiled with the + * {@code wasi-nn} cargo feature. Delegates via reflection to wasmtime4j's + * static probe (which itself queries a compile-time constant baked into the + * native), so the answer is cheap and stable for the process lifetime — no + * exception-catching semantics required. When {@code false}, callers must + * not pass a {@link WasiNnConfig} through + * {@link ai.tegmentum.webassembly4j.api.DefaultLinkingContext.Builder#enableWasiNn} + * — the native enable-nn path would throw "WASI-NN support not compiled in" + * during {@link #instantiate(ai.tegmentum.webassembly4j.api.LinkingContext)}. + * + *

Reflection is used to keep {@code wasmtime4j-jni} at runtime scope in + * this provider's pom, matching pre-2.4.1 layering (Panama backend users + * can substitute the JNI implementation without a compile-time coupling). + */ + @Override + public boolean supportsWasiNn() { + return WasiNnProbeHolder.AVAILABLE; + } + + /** Reflection-lookup holder — computed once, then a constant boolean read. */ + private static final class WasiNnProbeHolder { + private static final boolean AVAILABLE = probeNative(); + + private static boolean probeNative() { + try { + Class cls = Class.forName("ai.tegmentum.wasmtime4j.jni.JniComponentLinker"); + Object result = cls.getMethod("wasiNnAvailable").invoke(null); + return Boolean.TRUE.equals(result); + } catch (ReflectiveOperationException | LinkageError e) { + // JNI class missing (Panama-only classpath) or older wasmtime4j + // without the probe method — treat as no wasi:nn support so + // callers stay on the safe path. + return false; + } + } + } + @Override public void close() { for (ai.tegmentum.wasmtime4j.Store s : stores) { diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeSupportsWasiNnTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeSupportsWasiNnTest.java new file mode 100644 index 0000000..2629311 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeSupportsWasiNnTest.java @@ -0,0 +1,45 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ai.tegmentum.wasmtime4j.jni.JniComponentLinker; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Pins the {@link WasmtimeComponentAdapter#supportsWasiNn()} probe to the truth + * reported by the loaded wasmtime4j native. The provider override delegates to + * {@link JniComponentLinker#wasiNnAvailable()}, so the two must agree — a drift + * would silently return a stale (probably {@code true}) answer and re-open the + * regression this probe exists to close. + * + *

The test intentionally does not assert the boolean is {@code true} or + * {@code false} in absolute terms: whether the native was built with the + * {@code wasi-nn} cargo feature depends on the classifier the reactor + * consumed, and this repo's default build ships without it. + */ +class WasmtimeSupportsWasiNnTest { + + @Test + @DisplayName("provider probe returns the loaded native's wasi:nn truth") + void supportsWasiNnMirrorsNative() { + // The override is a straight delegation — no component / engine state is read + // — so a null-inflated adapter is safe here and lets us exercise the real + // override without paying for a compile+instantiate. + WasmtimeComponentAdapter adapter = + new WasmtimeComponentAdapter(null, null, null, null); + + // Compute the native truth defensively — an older cached wasmtime4j native + // on this workstation's java.library.path may miss the nativeWasiNnAvailable + // symbol; that surfaces as UnsatisfiedLinkError, which the reflection-based + // provider override treats as "no support" (returns false). Either way the + // adapter answer must match the raw JNI answer for the classpath in use. + boolean nativeAnswer; + try { + nativeAnswer = JniComponentLinker.wasiNnAvailable(); + } catch (UnsatisfiedLinkError | ExceptionInInitializerError err) { + nativeAnswer = false; + } + assertEquals(nativeAnswer, adapter.supportsWasiNn()); + } +} diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 629394f..b735add 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-api diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Component.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Component.java index 87bcaa0..3215132 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Component.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Component.java @@ -91,6 +91,20 @@ default Optional extension(Class extensionType) { return Optional.empty(); } + /** + * @return {@code true} if this component's provider ships a native library + * built with wasi:nn support. Callers should probe before configuring + * a {@link LinkingContext} with + * {@link DefaultLinkingContext.Builder#enableWasiNn(WasiNnConfig)}; + * a false return means {@link #instantiate(LinkingContext)} will + * throw at wasi:nn linker-enable time. Default {@code false} so + * providers that don't override see the safe (no-support) answer. + * @since 2.4.1 + */ + default boolean supportsWasiNn() { + return false; + } + @Override void close(); } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java index 3d9d25a..e19219d 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java @@ -32,4 +32,17 @@ default WasiContext wasiContext() { default WasiNnConfig wasiNnConfig() { return null; } + + /** + * @return {@code true} if this linking context's provider ships a native + * library built with wasi:nn support. Callers should probe before + * invoking {@link DefaultLinkingContext.Builder#enableWasiNn(WasiNnConfig)}; + * a false return means enableWasiNn will throw at instantiation time. + * Default {@code false} so providers that don't override see the safe + * (no-support) answer. + * @since 2.4.1 + */ + default boolean supportsWasiNn() { + return false; + } } diff --git a/webassembly4j-api/src/test/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContextTest.java b/webassembly4j-api/src/test/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContextTest.java index d74d49b..0be5d16 100644 --- a/webassembly4j-api/src/test/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContextTest.java +++ b/webassembly4j-api/src/test/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContextTest.java @@ -76,4 +76,16 @@ void hostFunctionsAreUnmodifiable() { () -> ctx.hostFunctions().add(new HostFunctionDefinition( "m", "f", new ValueType[0], new ValueType[0], args -> null))); } + + @Test + void supportsWasiNnDefaultsToFalse() { + // The provider-agnostic default is false — DefaultLinkingContext does not + // know which provider the caller will hand it to, so it can't report the + // (provider-specific) native truth. Providers that support wasi:nn override + // supportsWasiNn on their Component (e.g. WasmtimeComponentAdapter). This + // test pins the safe default so a caller that forgets to probe the actual + // provider gets a false answer and skips enableWasiNn. + LinkingContext ctx = DefaultLinkingContext.builder().build(); + assertFalse(ctx.supportsWasiNn()); + } } diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 132c9de..b5cb1ef 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 3e5baf4..e66a830 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 6868927..5f3010a 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index c75b951..2751256 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 27bc3af..07b16b2 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 514e126..a3ebbb5 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index f4c35aa..cb48eb4 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 32b1e8d..b30eece 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.4.1 webassembly4j-testing From c50fd1c97d9e1bf1b61ac967f73f139352c92e86 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 18 Jul 2026 13:51:49 -0400 Subject: [PATCH 24/85] feat(api): add WitCallableResource surface for resource-method invocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a provider-neutral abstraction for the returned own case: WitCallableResource wraps a component-model resource handle with invokeMethod/invokeMethodWit/close, so JVM plugins driving components via SPARQL SERVICE can address aggregate-state, iterator, and handle-then-method WIT patterns without threading the receiver + instance pair by hand. The wasmtime4j provider implements this by binding a wasmtime4j WitResource to the ComponentInstance it came from and delegating through the new invokeResourceMethodWit + dropResource entry points added to wasmtime4j 46.0.1-1.4.5. Endive/graalwasm/wamr providers inherit the default UnsupportedFeatureException — extending them is a follow-up when their underlying runtimes grow equivalent hooks. Bump wasmtime4j.version to 46.0.1-1.4.5 to pick up the new native + Java surface. --- pom.xml | 2 +- .../wasmtime/WasmtimeCallableResource.java | 107 ++++++++++++++++++ .../WasmtimeComponentInstanceAdapter.java | 12 ++ .../webassembly4j/api/ComponentInstance.java | 26 +++++ .../api/WitCallableResource.java | 73 ++++++++++++ 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResource.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitCallableResource.java diff --git a/pom.xml b/pom.xml index a69993d..a4e4ae8 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.4.2 + 46.0.1-1.4.5 2.4.4-1.0.2 1.0.1 24.2.2 diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResource.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResource.java new file mode 100644 index 0000000..a192461 --- /dev/null +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResource.java @@ -0,0 +1,107 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.wasmtime4j.component.ComponentInstance; +import ai.tegmentum.wasmtime4j.exception.WasmException; +import ai.tegmentum.wasmtime4j.wit.WitResource; +import ai.tegmentum.wasmtime4j.wit.WitValue; +import ai.tegmentum.webassembly4j.api.WitCallableResource; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; + +import java.util.Objects; + +/** + * Provider-neutral callable-resource surface for wasmtime — binds a wasmtime {@link WitResource} + * to the {@link ComponentInstance} it came from, so downstream code can dispatch a method on + * the resource without threading the receiver + instance pair by hand. + * + *

Instantiated via {@link WasmtimeComponentInstanceAdapter#asCallableResource(Object)}. + */ +final class WasmtimeCallableResource implements WitCallableResource { + + private final WitResource resource; + private final ComponentInstance instance; + private volatile boolean closed; + + WasmtimeCallableResource(final WitResource resource, final ComponentInstance instance) { + this.resource = Objects.requireNonNull(resource, "resource"); + this.instance = Objects.requireNonNull(instance, "instance"); + } + + @Override + public String resourceTypeName() { + return resource.getResourceTypeName(); + } + + @Override + public Object invokeMethod(final String methodExportName, final Object... args) { + ensureOpen(); + try { + return instance.invokeResourceMethod(resource, methodExportName, marshalArgs(args)); + } catch (final WasmException e) { + throw new ExecutionException( + "Failed to invoke resource method '" + methodExportName + "'", e); + } + } + + @Override + public Object invokeMethodWit(final String methodExportName, final Object... args) { + ensureOpen(); + try { + final WitValue result = + instance.invokeResourceMethodWit(resource, methodExportName, marshalArgs(args)); + return result; + } catch (final WasmException e) { + throw new ExecutionException( + "Failed to invoke resource method '" + methodExportName + "'", e); + } + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + try { + instance.dropResource(resource); + } catch (final UnsupportedOperationException e) { + // A pure-Java WitResource has no native backing; drop is a no-op — we already + // flipped `closed`, so further invokeMethod calls will throw the usual "closed" + // error rather than silently continuing to talk to a phantom native handle. + } catch (final WasmException e) { + throw new ExecutionException("Failed to drop resource", e); + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Resource has been closed"); + } + } + + /** + * Reuse the adapter's argument marshaller so an {@link Object} arg that isn't already a + * {@code WitValue} is coerced by the same rules as {@link + * WasmtimeComponentInstanceAdapter#invoke(String, Object...)}. + */ + private static Object[] marshalArgs(final Object[] args) { + if (args == null || args.length == 0) { + return new Object[0]; + } + final Object[] witArgs = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + try { + witArgs[i] = WasmtimeComponentInstanceAdapter.toWitValue(args[i]); + } catch (final WasmException e) { + throw new ExecutionException( + "Argument " + i + " is not a valid WIT value", e); + } + } + return witArgs; + } +} diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java index 9d23231..aa5c1f6 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java @@ -5,6 +5,7 @@ import ai.tegmentum.wasmtime4j.wit.WitFloat32; import ai.tegmentum.wasmtime4j.wit.WitFloat64; import ai.tegmentum.wasmtime4j.wit.WitList; +import ai.tegmentum.wasmtime4j.wit.WitResource; import ai.tegmentum.wasmtime4j.wit.WitS16; import ai.tegmentum.wasmtime4j.wit.WitS32; import ai.tegmentum.wasmtime4j.wit.WitS64; @@ -17,6 +18,7 @@ import ai.tegmentum.webassembly4j.api.Global; import ai.tegmentum.webassembly4j.api.Memory; import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.WitCallableResource; import ai.tegmentum.webassembly4j.api.exception.ExecutionException; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; @@ -145,6 +147,16 @@ static WitValue toWitValue(Object arg) + "(option/result/record/variant/enum/flags and null require an explicit type)"); } + @Override + public WitCallableResource asCallableResource(Object resource) { + if (!(resource instanceof WitResource)) { + throw new IllegalArgumentException( + "Wasmtime provider expects a wasmtime4j WitResource; got " + + (resource == null ? "null" : resource.getClass().getName())); + } + return new WasmtimeCallableResource((WitResource) resource, nativeInstance); + } + @Override public boolean hasFunction(String name) { return nativeInstance.hasFunction(name); diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java index 69b811d..4435cbb 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java @@ -69,6 +69,32 @@ default Object invokeWit(String functionName, Object... args) { return invoke(functionName, args); } + /** + * Adopts a resource-shaped value returned from a prior {@link #invokeWit} call and + * returns a {@link WitCallableResource} bound to this instance, so method invocations + * on the resource can be written as {@code res.invokeMethod("m", args...)} rather than + * manually threading the receiver through {@link #invokeWit}. + * + *

The passed value must be the provider's WIT resource type (for wasmtime, + * {@code ai.tegmentum.wasmtime4j.wit.WitResource}). The returned handle owns the + * lifecycle: closing it drops the underlying store-side resource. + * + *

Default implementation throws — providers that host resources through a native + * store (currently only wasmtime4j) override. + * + * @param resource the provider-specific resource value (typically the return of an + * {@link #invokeWit} call whose signature declared {@code own}) + * @return a callable handle bound to this instance + * @throws UnsupportedFeatureException if the provider does not host callable resources + * @throws IllegalArgumentException if {@code resource} is not of the provider's expected + * resource type or has no native backing + * @since 2.4.0 + */ + default WitCallableResource asCallableResource(Object resource) { + throw new UnsupportedFeatureException( + "Provider does not host callable resources; wrap the receiver manually via invokeWit"); + } + /** * Returns whether this instance exports a function with the given name. */ diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitCallableResource.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitCallableResource.java new file mode 100644 index 0000000..78b5cd2 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WitCallableResource.java @@ -0,0 +1,73 @@ +package ai.tegmentum.webassembly4j.api; + +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; + +/** + * A callable handle to a Component Model resource returned from a component invocation. + * + *

Component-model resource methods (a WIT {@code resource X { m: func(...) }}) surface as + * ordinary exports whose first parameter is the resource handle. This interface hides that + * mechanical detail — the receiver is bound; the caller writes {@code res.invokeMethod("m", + * args...)} directly. Symmetric with {@link ComponentInstance#invoke(String, Object...)} on + * the argument-shape side. + * + *

Ownership: the receiver is passed by borrow on every call — the resource is not + * consumed and remains valid for further invocations until {@link #close()} drops it. + * Close the handle exactly once per {@code own} received from the component; leaking it + * leaks a slot in the underlying store's resource table, and dropping it twice is an error. + * + *

Implements {@link AutoCloseable} for try-with-resources ergonomics. + * + * @since 2.4.0 + */ +public interface WitCallableResource extends AutoCloseable { + + /** + * Returns the resource type name (e.g. {@code "wasi:io/streams/input-stream"}), if the + * provider tracks it. Providers may return an empty string if the type name was not + * carried across the FFI boundary. + * + * @return the resource type name, or an empty string if unknown + */ + String resourceTypeName(); + + /** + * Invokes a method on this resource with the receiver prepended as arg 0, unwrapping the + * result via the same Java-shape convention as {@link ComponentInstance#invoke(String, + * Object...)}. + * + * @param methodExportName the method's export path (a top-level + * {@code [method].} or an interface-scoped + * {@code #[method].}) + * @param args the additional args to pass after the receiver + * @return the method result unwrapped to a natural Java shape, or {@code null} for void + * methods + * @throws ExecutionException if invocation fails + */ + Object invokeMethod(String methodExportName, Object... args); + + /** + * Typed variant of {@link #invokeMethod} that returns the provider's WIT value tree + * directly. Symmetric with {@link ComponentInstance#invokeWit(String, Object...)}. + * + * @param methodExportName the method's export path + * @param args the additional args to pass after the receiver + * @return the provider's WIT value, or {@code null} for void methods + * @throws ExecutionException if invocation fails + */ + Object invokeMethodWit(String methodExportName, Object... args); + + /** + * Whether this resource has already been {@link #close() closed}. Further calls to + * {@link #invokeMethod} / {@link #invokeMethodWit} on a closed resource throw. + */ + boolean isClosed(); + + /** + * Drops this resource, releasing it in the underlying store and removing it from the + * provider's resource registry. Idempotent — calling {@code close()} twice is a no-op on + * the second call. + */ + @Override + void close(); +} From 178b2d1ce8d144febebde4a2b75a1fa20fe445dc Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 18 Jul 2026 16:15:19 -0400 Subject: [PATCH 25/85] test(wasmtime-provider): end-to-end WitCallableResource verification via counter component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the deferred half of Follow-up 2: a synthetic Rust component + JUnit smoke test that drives WitCallableResource through the full lifecycle against real wasmtime4j resource-method dispatch, replacing the "honest failure" placeholder with observed behavior. Rust component (src/test/rust/counter_component/): - Exports the interface-scoped resource pattern from the task spec (tegmentum:test-counter/counter-api with a counter resource + ctor + increment + value methods) and additionally writes a lifecycle marker file from its constructor and Drop impl. The marker file is the primary drop-verification signal because Java's System.setErr swap does not redirect the WASI stderr path (writes go straight to FD 2), so a file-based signal is what actually survives the JVM/WASM boundary. Built via `cargo component build --release --target wasm32-wasip2`, wired into `mvn test` through exec-maven-plugin with a `-Dskip.rust.build` escape hatch. Java smoke test (WasmtimeCallableResourceTest): - resourceLifecycleAcrossInterfaceScopedMethods — invokeWit the interface-scoped [constructor]counter, asserts the returned own surfaces as WitOwn, wraps via asCallableResource, runs invokeMethod(increment) x 3 + invokeMethod(value)==3, then verifies close() is idempotent. - invokingAfterCloseThrows — post-close invokeMethod must throw. - closeInvokesGuestSideDrop — verifies the guest's Drop impl actually ran by reading the marker file the guest wrote to a preopened dir (final value = 2 after two increments). Provider adapter (WasmtimeComponentInstanceAdapter#asCallableResource): - Broaden the input type from WitResource to also accept WitOwn / WitBorrow, translating via WitResource.fromHandle. The wasmtime WIT deserializer surfaces `own` / `borrow` as WitOwn / WitBorrow (discriminators 22/23), not WitResource — the prior check rejected the shape actually returned by invokeWit at run time. No change to the provider-neutral WitCallableResource surface itself. --- wasmtime4j-provider/pom.xml | 65 ++++++ .../WasmtimeComponentInstanceAdapter.java | 22 +- .../WasmtimeCallableResourceTest.java | 218 ++++++++++++++++++ .../src/test/resources/counter_component.wasm | Bin 0 -> 78618 bytes .../test/rust/counter_component/.gitignore | 4 + .../test/rust/counter_component/Cargo.toml | 24 ++ .../test/rust/counter_component/src/lib.rs | 91 ++++++++ .../rust/counter_component/wit/counter.wit | 13 ++ 8 files changed, 434 insertions(+), 3 deletions(-) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResourceTest.java create mode 100644 wasmtime4j-provider/src/test/resources/counter_component.wasm create mode 100644 wasmtime4j-provider/src/test/rust/counter_component/.gitignore create mode 100644 wasmtime4j-provider/src/test/rust/counter_component/Cargo.toml create mode 100644 wasmtime4j-provider/src/test/rust/counter_component/src/lib.rs create mode 100644 wasmtime4j-provider/src/test/rust/counter_component/wit/counter.wit diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 08dbb8b..0901d23 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -44,4 +44,69 @@ test + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + build-counter-component + generate-test-resources + + exec + + + cargo + ${project.basedir}/src/test/rust/counter_component + + component + build + --release + --target + wasm32-wasip2 + + ${skip.rust.build} + + + + stage-counter-component + generate-test-resources + + exec + + + cp + + ${project.basedir}/src/test/rust/counter_component/target/wasm32-wasip2/release/counter_component.wasm + ${project.basedir}/src/test/resources/counter_component.wasm + + ${skip.rust.build} + + + + + + + + + + false + diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java index aa5c1f6..1490f24 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java @@ -1,10 +1,12 @@ package ai.tegmentum.webassembly4j.provider.wasmtime; import ai.tegmentum.wasmtime4j.wit.WitBool; +import ai.tegmentum.wasmtime4j.wit.WitBorrow; import ai.tegmentum.wasmtime4j.wit.WitChar; import ai.tegmentum.wasmtime4j.wit.WitFloat32; import ai.tegmentum.wasmtime4j.wit.WitFloat64; import ai.tegmentum.wasmtime4j.wit.WitList; +import ai.tegmentum.wasmtime4j.wit.WitOwn; import ai.tegmentum.wasmtime4j.wit.WitResource; import ai.tegmentum.wasmtime4j.wit.WitS16; import ai.tegmentum.wasmtime4j.wit.WitS32; @@ -149,12 +151,26 @@ static WitValue toWitValue(Object arg) @Override public WitCallableResource asCallableResource(Object resource) { - if (!(resource instanceof WitResource)) { + // The wasmtime WIT deserializer returns owned/borrowed resource handles as WitOwn / + // WitBorrow (discriminators 22/23), NOT WitResource — the latter is a higher-level + // convenience wrapper. The invokeResourceMethodWit / dropResource entry points on + // wasmtime4j's ComponentInstance want WitResource though, so translate here rather + // than making every caller do it (and rather than expanding WitCallableResource's + // input contract, which is intentionally provider-neutral). + final WitResource wr; + if (resource instanceof WitResource) { + wr = (WitResource) resource; + } else if (resource instanceof WitOwn) { + wr = WitResource.fromHandle(((WitOwn) resource).getHandle()); + } else if (resource instanceof WitBorrow) { + wr = WitResource.fromHandle(((WitBorrow) resource).getHandle()); + } else { throw new IllegalArgumentException( - "Wasmtime provider expects a wasmtime4j WitResource; got " + "Wasmtime provider expects a wasmtime4j WitResource / WitOwn / WitBorrow;" + + " got " + (resource == null ? "null" : resource.getClass().getName())); } - return new WasmtimeCallableResource((WitResource) resource, nativeInstance); + return new WasmtimeCallableResource(wr, nativeInstance); } @Override diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResourceTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResourceTest.java new file mode 100644 index 0000000..37c5ae9 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallableResourceTest.java @@ -0,0 +1,218 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.wasmtime4j.wit.WitOwn; +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.DefaultWasiContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.WitCallableResource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.io.TempDir; + +/** + * End-to-end verification for the {@link WitCallableResource} surface — the deferred half of + * Follow-up 2. Exercises the full resource lifecycle through the wasmtime4j provider using a + * synthetic counter component (see {@code src/test/rust/counter_component/}): + * + *

    + *
  • constructor invocation → wasmtime {@link WitOwn} + *
  • {@link ComponentInstance#asCallableResource(Object)} → {@link WitCallableResource} + *
  • method invocation on the callable (state accumulates across calls) + *
  • {@link WitCallableResource#close()} idempotency + post-close rejection + *
  • try-with-resources triggers a drop + *
+ * + *

Drop timing (the exact hazard Follow-up 2 flagged) is verified via a file marker the Rust + * guest writes into a preopened directory from both its constructor and its Drop impl — Java + * cannot capture WASI stderr through a {@code System.setErr} swap because WASI writes bypass the + * Java PrintStream and land on FD 2 directly. + */ +class WasmtimeCallableResourceTest { + + private static final String COUNTER_INTERFACE = + "tegmentum:test-counter/counter-api@0.1.0"; + private static final String CTOR_EXPORT = COUNTER_INTERFACE + "#[constructor]counter"; + private static final String INCREMENT_METHOD = + COUNTER_INTERFACE + "#[method]counter.increment"; + private static final String VALUE_METHOD = COUNTER_INTERFACE + "#[method]counter.value"; + + static boolean runtimeAvailable() { + return new WasmtimeProvider().availability().available(); + } + + private static byte[] loadCounterComponent() throws IOException { + try (InputStream is = + WasmtimeCallableResourceTest.class.getResourceAsStream("/counter_component.wasm")) { + assertNotNull( + is, + "counter_component.wasm not on classpath — run " + + "`cargo component build --release --target wasm32-wasip2` in " + + "src/test/rust/counter_component/ and copy the artifact into " + + "src/test/resources/ (the Maven build does this automatically)."); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = is.read(chunk)) != -1) { + buf.write(chunk, 0, n); + } + return buf.toByteArray(); + } + } + + /** + * A component built with cargo-component (any wit-bindgen ≥ 0.30) pulls in {@code wasi:cli} + + * {@code wasi:io} imports as its ambient environment because {@code eprintln!} requires + * stdout/stderr, and cargo-component's proxy adapter also pulls {@code wasi:filesystem}. The + * linking context therefore always includes a WASI context — an empty one is enough for the + * pure lifecycle tests since we never touch the filesystem or network. Tests that verify Drop + * pass a preopen so the guest can leave a file marker. + */ + private static DefaultLinkingContext wasiLinking() { + return DefaultLinkingContext.builder() + .wasiContext(DefaultWasiContext.builder().build()) + .build(); + } + + private static DefaultLinkingContext wasiLinkingWithLifecycleDir(final Path hostDir) { + return DefaultLinkingContext.builder() + .wasiContext( + DefaultWasiContext.builder() + .inheritStderr(true) + .preopenDir(hostDir.toString(), "/lifecycle", true) + .build()) + .build(); + } + + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("constructor → invokeMethod × 3 → value returns 3 → close is idempotent") + void resourceLifecycleAcrossInterfaceScopedMethods() throws Exception { + byte[] wasm = loadCounterComponent(); + try (Engine engine = WasmtimeEngineAdapter.create(null); + Component component = engine.loadComponent(wasm)) { + ComponentInstance instance = component.instantiate(wasiLinking()); + + Object rawHandle = instance.invokeWit(CTOR_EXPORT); + assertNotNull(rawHandle, "constructor should return an own handle"); + // The wasmtime WIT deserializer surfaces `own` as WitOwn (discriminator 22), + // not WitResource. WasmtimeComponentInstanceAdapter#asCallableResource accepts + // both — testing this here makes that contract explicit. + assertTrue( + rawHandle instanceof WitOwn, + "wasmtime provider should hand back a WitOwn for own; got " + + rawHandle.getClass().getName()); + + try (WitCallableResource counter = instance.asCallableResource(rawHandle)) { + assertFalse(counter.isClosed(), "freshly-wrapped resource must be open"); + + for (int i = 0; i < 3; i++) { + Object result = counter.invokeMethod(INCREMENT_METHOD); + assertEquals( + null, + result, + "increment is void; expected null from invokeMethod, got " + result); + } + + Object value = counter.invokeMethod(VALUE_METHOD); + assertEquals( + 3, + ((Number) value).intValue(), + "value must be 3 after three increments — proves receiver + method " + + "dispatch actually landed on the same resource across calls"); + + counter.close(); + assertTrue(counter.isClosed(), "close() must flip isClosed"); + // Second close is a no-op per the WitCallableResource contract — asserting it + // doesn't blow up covers the try-with-resources double-close path too. + counter.close(); + } + } + } + + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("closed resource: further invokeMethod throws IllegalStateException") + void invokingAfterCloseThrows() throws Exception { + byte[] wasm = loadCounterComponent(); + try (Engine engine = WasmtimeEngineAdapter.create(null); + Component component = engine.loadComponent(wasm)) { + ComponentInstance instance = component.instantiate(wasiLinking()); + + Object rawHandle = instance.invokeWit(CTOR_EXPORT); + WitCallableResource counter = instance.asCallableResource(rawHandle); + counter.close(); + + assertTrue(counter.isClosed()); + // The interface contract: post-close invokeMethod must throw (either explicit + // "resource has been closed" or an execution exception once the native handle is + // gone). Both are honest failure modes — the resource is unusable either way. + assertThrows(RuntimeException.class, () -> counter.invokeMethod(INCREMENT_METHOD)); + assertThrows(RuntimeException.class, () -> counter.invokeMethod(VALUE_METHOD)); + } + } + + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("try-with-resources close observably reaches the guest's Drop impl") + void closeInvokesGuestSideDrop(@TempDir Path lifecycleDir) throws Exception { + byte[] wasm = loadCounterComponent(); + + // The Rust guest writes a lifecycle-marker file to a preopened directory in both the + // constructor and its Drop impl. WASI stderr is also inherited so the JVM's console + // still shows the diagnostic lines, but we don't assert on stderr — Java's + // `System.setErr` only replaces the PrintStream, not the process's file descriptor 2, + // so WASI writes bypass any Java-side redirect. The file marker is the durable signal + // that survives the FD-2 bypass and proves Drop actually landed on the guest — which + // is the exact concern Follow-up 2 called out. + try (Engine engine = WasmtimeEngineAdapter.create(null); + Component component = engine.loadComponent(wasm)) { + ComponentInstance instance = + component.instantiate(wasiLinkingWithLifecycleDir(lifecycleDir)); + + Object rawHandle = instance.invokeWit(CTOR_EXPORT); + try (WitCallableResource counter = instance.asCallableResource(rawHandle)) { + counter.invokeMethod(INCREMENT_METHOD); + counter.invokeMethod(INCREMENT_METHOD); + // Leaving the block triggers close() → dropResource → guest Drop. + } + } + + Path marker = lifecycleDir.resolve("marker.log"); + String directoryContents; + try (Stream entries = Files.list(lifecycleDir)) { + directoryContents = entries.map(Path::toString) + .reduce((a, b) -> a + ", " + b) + .orElse("(empty)"); + } + assertTrue( + Files.exists(marker), + "guest should have written a lifecycle marker at " + marker + + "; directory contents: " + directoryContents); + String contents = Files.readString(marker, StandardCharsets.UTF_8); + // Constructor line proves the WASI preopen wiring works at all; drop line proves the + // close on the callable resource actually reached the guest's Drop. + assertTrue( + contents.contains("constructor=0"), + "marker file should record the constructor call; got:\n" + contents); + assertTrue( + contents.contains("drop=2"), + "marker file should record the Drop call with final value = 2 " + + "(after two increments); got:\n" + contents); + } +} diff --git a/wasmtime4j-provider/src/test/resources/counter_component.wasm b/wasmtime4j-provider/src/test/resources/counter_component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..93e2b025e8c4c05d7980485366def420751156ae GIT binary patch literal 78618 zcmd?S3!G(FUFW;k-ut{yb)O`iq?2yQJ|_)!G?i{1rbA+|ch5i)BQJ;XaxeNJmFnuG zs=BMX9^FX`q$>{)c}N^Vqk^Cy0VNCqDq?1=(M*Vnl5s$B44Od(5fI~RL}2Lq{r%V4 z`|MLy{UGE%yq6|*_G3N%>;L}$*JDQ$ONYiH7d4*c_N8gmJUoB!;KYpwrxNFGj@(|C zE=?Ug;9QhO=e_jM)bh>q`+u+~a@mas=O<@f+& z6NeI-9GgF~TzwKJv5U)|qS#F*ZXrJR;Kb7M`269i#fjzV`ML1}6VnH$_EWt+d2oJd zYJbG2y1vnI4`WnQOGyE=$)eCgAqIMef3v`Tta@pj~Q5HxCvi*)37&+?%24fQDA!Rrtt#@k1XBnHoNKO zuv#yTF50--(vcgNCKsn~oN{NiFLcql1yMXMmI7JQ#twMG&Bsei?l8O%MWAl;O;gL` zQ*%eB7w6{=F%rmsk&7<07A6l)@2b2C&D0uCa{ChSAa<%REl(^TS%TrxD4n`>df8Rj z!s_!ZRIJy}a!C}A5Wlh8?K2uR)^Y{SPdfn#AG8GI((?Z4IhV9AaM6|$JbM`0Z`RLN z`=h+nczWodwT{cGJ+wm)5T7ne9BKq~!PC%3`;>ij&ZrMqbAvv38v5uq&NDgNv^=$V zXnJnq;JAqk0yl8clggowz72K98s}Tx&E;Fq&Q!kGMNeK|*}o38H`UJpgUvM^H=gbG z)g?!p(`e1B#N%)_7NHQcvxUa8YfrKCBw$xb*Q;-#p4>UwoFYUTz;^4t?l;7lRpy0Xn%aXM+woWFJXsm3t@f#ht@9AmO z-ajQ3a(G#q-Q7aJ2T-m{(?3MNPQ+iQwxh%ydP;p_a&l^EsdLNxk%RlkO?@>c4x-lg zAKP@}#QyPNtLcqLmX0;|^G4v-7LLp>Pt?#DOUs+o)A;iI{P@9%#ha$ioj!PQ>L$4U z#$(G<<4aQuN2cZ`r#jPf=Vq6>hp0Sp6OA&gvAKyuq1XAjn_6@8z3 z9Xy5_wSj}7saeGG*5z@-qtM))T%2D588#qj3$o@H$0z6aLrl}Q=dV{(4L4U$)q2hB z)j8If5PcK2@7Va#vAM}}V8scPQT5rDMG)w~65ymL_2`9-T`#e-kjn<|X zZ>hn4{G`!2IDiud`ppwdH;=?ZlrB@H*9k^4x4&AaPp9pn zYjXN{5FRr!UP7iy!PhdG|}FZk4|MLG+ef+#`psTt;xc7MNPxW!eB; zGjVttEBds{cAxuFtjzq8#mTAhxv5)zFuady-*iLg4IA3OIDfcoX``@d_m(wt;Pir}0B~Klx2uaB87Wg7U4nE*dX*7 z#t^$V2t77rw2JG$d!v})WR*&o)>RU4r)0@9u$NKu6l#3;nOMtHw@`AxP#i8>XFQ|i zR6v~4ik`aa$;|1gYn{SgMy*ppdRoI7B|Z{9nGN&Wpknw4Za?E5b?r_)?sn_-q~2~Q z%!(t-W>ibt-FnhZ$6_KS7k9_VIK{lY7wMV&G3;%?k-kJXd7(Qenf?KV|ZpECSE zNz!hVBuSV5XfWnSE%Bp<_+OpdHrAKYQR>>YT8&~+&Hm{ZJL}?FqtW2a=FQHzo{QQs zCY-uwQ7n$rhVE*$cDv?y$dj7midwZAb~=vfv7Of3VfXy!*Af?*0oPDo+`73Vw8Tb3{58TJy!|uPh zKXm`qebW7r`(yW~?h*HC_h;_U#kya9Yx1azqYez#c~3p59e206;}@^+zbIc#W(VmG zyUxW$Yx;R+zpkGv`p$RtUH<2p{G@r4R<6+1d$9@B%2SH_0r!bU0HB?} z{dZRxct7FtP5zQ0CgV{AeG;0ag<2AdAMLPA5kd=Q3K_3RQXDu<`f)r>dx2P51S158$iGjA!}H z9S|R7Q6SrAe^1c&?iC18XA6L3!h`;#y$21jUrSuQke%M_Ao1-K=JE(JaQ!;Jjbv|Dhn4zuRm{@W{P_16E2cKE72~N|R^O$nGgL!xX7ZTw zVk&O(WFc?fI%wv#N< z({LZk)D22-4|HkP$ZC1-x&_8j%iZ!I&5wWYSPILIK`TE38yX$;-Y4t~>dzSiutb8` zgd}yT6Ie&;Bndcvhnbl3dCaiXnz4^)= zcQxrYr6Z{LG4{ zL_@Ka##ArH6hqr`Ow_>$i7NIJ!T*p4A(WJ;R2FN1AkOe8I#!Cp&K|v?1>Q!TpTL=@ z2t;8^VE49Q8jo6@y9?Bh{;ELJSROeDjJ7a_IvkHWuq8iZ{RXgc(H&4QYxrU+{hTov zRtO24_4;2n?zy`6&*BK_8Mt;~tY(to`ZbuImMZ-`XRMKd`G2)fJ!)#IUQKhV5xm@)A?DN5FZW_n}@S}-3%HmEj8gpWA)c;6QE zj_-5j?Qy=6S+{zDN)XQtFmZn3#ECcFwy-EZK!f&OycfK_|kau7%gW*!d&*HOa zbtSStk_s77AzCjBQ|!V;=NoZMGVr$GlxC$*8VE>hsB2N^diVsMS&1Z#n1uWGWv_<6 zc=x<9#w*tqhfp4hTvu2u@6OzJ*PaBw(C(Uc*9$7+BP**iMq-;R>3u{k%IhmyLEX{H zy8Z^IYxed#Hf|QE1l;3o_9JJ~?CHn6Q7b6&e;|&mDDr3r*jRzM0afpLSYv{(#sYWr zUGHaA0zvWb(R_g)OWDuafn7~hj~BVGbWLmDD*<+Hse;()-S0-)cIO1 ziH|3@l_TFhJBa4o$v~8qPEYy0&AYAavcp*d(j}-wkJW^z|slA60jNi$Wq=RN_1NU8h01H zyr}HhKN>Vp;(g2m4mNt}%7}qv%`2lUYmODJm@jFqqDupv{X;ydL6RSr(+%FC zmod0Q^Z|z86W)iDmfvG-lyXYw=2ei(VuB;{rPLlJy%S{`sc8*o&6BC4LKh|eLRQ6w zk?l!(ToPMCDY=$XqLN@bfVt=dBh<~3eFS-mP{yO4j6TC6Qt~XcxzqcsXEVb>?@Rus z9NT-n>+n?CTCr%U_uq=*0g+(Vs~<377%_!sxRq7|yVSLKt$5)TTsG|EPBE>gLXv-K z1cRnhqu#IkP9IBMtD*qh8U=7V`Ls|8TnBne-OB+u(RhLH#+-A=KcY@Sr-!s3XU?Y| z;{2mN-gi*Z>yEsw*Rr39ndT(_eB(OgTaS5ye5ov!ZBWZ!QMiNu@h3EJ>SgifF3QH#iCh5+lT2osmzrr^JswRm-K))yMSj%l188~KU%ekR2p z`S<|q(UNgWeOToop`k%M3JHo|2npRbHtNA-Tl_1QC5VL;!@$IQh0ObbNvfC8$5OgE zeySX>Hl9hGx1Wn84D|>%h~i?DTIuxAZcHC$s7av&r^izv|KuBmv|OQI>~(Y22y#)V zfc*XkRi^jvtIS+h%u&p~q5wiLhw+(j2gnLt-1)QEH7gt!6feUdDIjX6{Th&_CAIO2 zOq-&avDeUc9kq#A2NCMts9ifXwd?yV58-vtq8;gdt7E9C_UtS<+d+RSPZ^xr%_^ zAfGO8ifdgKJ8WFURo z9iX}#6e1g=|E}9MFnt+Aqr|6eLwu3N7WC_R@1Kp2En!fyYpwGXvFjZ|{2NW$w7RejCcT z$%Ce@dLIsL;b9Q15l)$}p+DVWdtklq7zdF9{N(LqCAW2_00e(&ycei&Or8%w4yjEo zVQf#$YnELAX}w=jr#>&46ovs)VFgoP7c3~vW5TWsp7LD~sp(BF8USqrjA$;POytreL81T)*C}_()T6ZILdw(ZB3@hg-7?erJ%LD6l8%^&Q3j#` z<&1E>qCw4PR+Tv6iP}CYm!W$W@T^4qw*r4@GA9Re!$bbFfn0oUre58ej z#zHdLU`Vo#H;rIymrJdAUS{ECYvg51@p$Vhw~3$5RZdZT8y(D#*pnYvDWdVyQ9uGw z15p7(=CF%vB2zro)KOOJ-0nu$x8@~2BS)k@Yo7dsXyRf>Ab4uQPj~sNp?R=!AS7fT zs9N>YYqEVs7Gl-gKG1_i73Oc3ATSn!3i5qH_!qo}^69iu-tKG_5~OnqqZQ(s&i_F} zqA{;c8=*i@CnT83EM;gLT(n;k#P-UUCxM4hk*J@TNZ2eCcKQH*-Dl3urskTQ$CH*a9$qKw#q(m{%PmC z$(kLv>|$O&sw{KS9b8enGQwJr`C4_0c*vGd&AUetje1efSHMRz?_D*NK&tzSjr`HF zqP2$asdp;U2rc-MEcRty0V-?#H ze6&Cd8#yx#aIOX?gHq22x*(?68TSUwijpZKufzlyD-b5t`YkST5|Qn+-%a*r-K;ay zAJcmw)E*EhZITvs`R&JNEUf-Hrsy099Z3JV{E9p-+N_ibr$=MS}2sRR7*$Cs1j3UvG4il zFl=0|qhg%i1A;u{fHvi~LnCFTNm8J7tGr-yPP~9b z(5c$GhqDw^;Oe*N4xmd}EuK}y)jv~6hZdD(@)d%#D<-J6U_`H0SD@h3MmrnJy0e20 zU@()LwaMptMqGpA1acXkZOXaDT@}{`Z`<>^?;8FVF8C3jVv+oqFctytOss~-8S$k} ze*zCu8hH{_2tWf8vnoQp>!8U@U@l~Nge0ivieve)vEelF(Esslb@IQN+q5%w;eScZ zxwN>c{KcD6kEo$n*>z+3IT2kq*6w0NBl1)Z1CT%RDC;9M4OMyc zc^t7wb6~?#pS4Fg%)>{I*drBdPLyCB}Zk=jFRUuzoa=)%!!SEnZNTC~3Ay zKQiS-BJ#^`7ITvcb@`T~Gvg6uHiK&2r$_V63;xkL;Zbw==|=J}^1I)?Jd+>3-pZ`p&Gn}bhMN^`-n#5>-v1{{Gx-mN7vH#M zCjaNpz1ZJ9cIOq>jio>iR7@V_AXW;y!7VxKz~%3IyJ%yMDcC|oGL|-k`=e{2 zR^4z7$=1wl_#l-HBOql)rU`_H|T}XfDDJORt0OXwbrc$(aH8i| zN4=xTYEcsgA7h~1R#q+W^fF?v5l{_h{0)EfHwL+b+y~Q^n?jYB=y;H%-lZs<)_$RWGY@ z?W@|14~tF^u?BVnnQs*n>@@`vEI^_8Re1}mN<(!7Vb{ipvR@vI<&QNM2Hs6+Nvk#F zhH4Q|D{C0UZS0b~dU{&Q8%I%k^`nb9Ykryw0MR_fJ3qm-VZDQ~0^A$5%&Vvhhnhuo=5rqjxNgh!(HhOJH>Xs}(i}ezzlp^7m z;Tor(L8n@ck+n2#rbmoMqWFM1idc-|q7h8e%5PAt(Uz(+vbC8o#_`MNuiwTKb6aQC zI(9%k8ixR6Q*M5fMIz?z?{eKi+uG6UEZJ1?nyF;^NLZlJK4w-%ldmv>UUoAKky3?nXz#a)KIIkT{6Epac6<7In_IQH=?yY71ETYg zo4JDG+$kZmTdn6mECp9gr*5_U%}#r9XZ1zTUS#zDw|986g}&tOvER{Mk;U1L=7ehC zqrk>6E7;+sC3Y0g`(i2-5(9;PwFZjSr&m-l$CQrq3Ob8e@2E79@JHO8#d~NG=Xs`g z$L$Oz>U=ImF&hk;(h?tey=@XwhcU&VGR&j{B_|3aoJk410{MC7vL@WGXT?8|(p1!} zrIfnX0>zl*AfZJoPi)0xl(j4#Bhc+jkkt@Dt=tLCQ`U&+BPnQ~Q6Q(FHIrR@NbK$U z;z)h9xcb8FR-JyA(w$DR;HpL3>(?WV<*$9yyC060X0x%d?usR2rdOl#O|-#6OETRv z0~DPtw3H}h9rUFQk7!5j_s1A>XI2s~$&L(hW5cnz^}te-90r*PTvQ5TFfdW_p@|)& zDoV`I6-~DAGspN(3&4b5iXdO+YR4vv4Z%QbB1l#Pr^?tB)5O2^E-_+G%sMKB-n(gd zsHN|iYL@fofCzQwDFZHf&`{C@8YvcH9VU7OwYauDxl2+FXsmkYCe{#nHbXMSW{Emb zzA*~m6)|549?6TUDq;vZYjo(z!6E7yy{`;MRiAY#DYZA5a!{EdIj^t9>|ePp{|@|& z2TXjLq|MCb7&NxYY~|-3%^h=@ifW9bQKYd&Gw~~54{bw)Z5Z?U_Oa47 zU}&WHWgDDFHhl=$f|1n;vJ8HO!wbg5C__*)rZPq`3A&LXp&K329BjH*h&XXUm2u0{k^lBtRY8W@^4xX8`T04_FGl$4=DHo$>6o??lIokA7{j5gQ#OSWzukJ-Az-sU1@ zOl~RmHXz?3&2G!k{Hlq4pKrXF_Vl<|HNm7Rb4k&Gwg7~1ru3xA4{K2 zgwhw%Te~koH2O|BcN~;TWUoAx0LF+eI$WK;X^?8(c4-yF>Ee$INld|CnYA zf9bJgxSIgpt_9x4x&SdIg#3G^bvYfob3%T98;NlyWVnjQJXix#% z$Y15#qv8gjnVBU^!xYnICLGO`%)R}SGhEA>Vc&~gvD;uP&uXl+u?fnoCLh`S|K7(; zcSHTqNHL7!1=wYIs!d2%z&G1$V+d$KaiGt!Mz1N*!2Om{y(Py0+FTjc;2vhsQ0aLo zY*FXFb{r2^ezW{G&>TRSUX}j0MGt_@AiRYf{)I@&waG;gv%&Oiw^6KUal#9fj)otD z&DKNl5iAQJMqg=QArCf}>p&VM4nXshC|9$d+dQG7JIxbXV?`-HK^Rvi2;odJI3_wFMud|k89w@} zk)09lcdAtWBh?pMLiDE>w^v?_km_|CAsG!SERaZhK=lX(a57=sjH$F3j3y;x^6(7U zHSqJWnxlh646C#&Uc4r|Y?PubB89k0Oq{T_5nU}v@!|npV^3GW6J7aXg|ktp1oV5g z<&P%uZj1#WB*dRmENR~ex^jOBXwiQ)WOq*O{w_7f}KGAkYv@Iq6ZyP&fCq#SNSkuX+sn0-dY ztenlXcL^)G{3PENb8F1u1}`g2YoEH)hpdSwu2XbQFjdkHU`K;kE#TyS zjrsQ$&aZld9KlsGtfnPa?aHu?{h3yezVw(e*+lB9D1y#$uO{a$0>tzkB! z^5(yLmdV1KmuPWTIrwCUT}nI3W5M%P7BdMO9qts4yg^B19>Ru){DC{w8SIFx#D)L+ zS0`N&4}#3!Ra9}&kwLVSkvOdlm&W6^U>hyK`~T^(co4&^!$qWaa=ebyE{<T7-ij(7Mu?=yBiKm*UZD5ecJ}#PRuNKUS-=gKD;Or{W$ zky>S@H7@3wKX3KNhnwhV!#}fKZWRBl?}2Ds@?q^;GhedjzE>R*fGfFb4&{loP}ujs zN5JKOANBrRqftU|ul)0W-FsB`_pUsdC zwm_X_Tud`+SB;Wy)lYNawp|`SAU7T1B7q?=;6w3};cH6;UpZc!pxr{6jGta#o)>W}x-5jNf#^nX9PZv{gc|P*%XNdDcI4f@ z+84=5%Z6FQMzt4~dS{qp_je!s+uwfo7e4(P$6u8-_q}H2)nEAQ zKmFp@ew@bkMK6*{&wuBag*n!KK@Uwn;9Ia1*iXeVI7WcAjWp9-lc-!OK46Dn?=L)C z2g+oRVZMl8wz5!M)mU z*GukceDXgl>DN1uLc9JRf(yA9|9gh-GbQZ{dNBYCLjf3o z)qE$6K4|SpKJO)FL2NO<1msLuJRdz(d8}Nzc*(F@NRErZ|el zKu=sx;>Lm^1@=o!2Lua=Rjo9F#Zub>AZP%cPaJT5Mw$j60ZHL zTO^G=G+=B&*|=D#X79G`u>*(cM2%qJZF!zps?tnA$ZVTZZL z;arN^=jbWljbaV+yj@7H7CW*DUql1*VFvPlijyXEvT|!=wqV^K`$h59 zz+Dp6_qaN!=1N=F0q?E^za`(?dxyqj(#(MGB8g-(Ry*U@B)z|2tVjhU)q5-BdVg&X zjm~kgjZB;I6a(5*P*@zQlHd)*ViLW}#l5$p%o`DNt-J~h2uqP06LGDHKwR_&&bF~B z`=3-bvOg#v=@jFpuzQ~(veP?s8Him z?-JeeTJJ#>)6sz8#i#v?ViDHc(xg_MgCC~9*4PcT&jPEJ)T=!cl$A7`Vq1l_waxNAMH*HMPOaB zE{rYQcpzD)60@kJZ<{}D(aXxM(hRH(aCh!YI4g%sPO)6?ifS@+piNhxqP0fjYhu*i zcxWM0xbWx$^ujlO=Lc3rP5~{wa@|_q~FoCF7wIMk~mK*Tr^lkw1Wx6u|m1Ylya1TimMQDyucH z<2X_DYY59bBWzkCwYUI4bUlQSUhh1bq>{hNd0`a^jB_N)FTB#ieN9@G8%6O(v{5t# zPTpx>DyT@bR)Vftf_}!q$T}Xtnk?8M!Y7~UwjisZSR|q zB9=fV-Z$Y#zKPwjhkxjsppE5&a0(%!iTR*OvZu^^>3JfbqVP>>^r}TEwZc2Gl5mE0 zfI$?}Vd8^X<-veX^tGuXo52XO^w|-2L=9Ht3N2X48%***tAE)tmVpG^0Sw3A%NU44!%@2`_l4M5{9}jJ zGF6a44iW=)bVycc!?(O=6-zUawbN-3wC`iZ&fX}71)-1=*ZGP5I2@LXk6xHnSR@?E zF^Dlaz2F#TapyjVvlH5gt1%W0uZyuNC*--z`Ev4~ z;KqB58&!vAaufC?cOj-)QVALgYhO7+FRxsm^b~@pFpLsiAN$Zr(4}R)`A+LQK=X4} zR0toUo*S?kLBAF6Hv9caz1wwHBusuXsp^SgY#-Wvjpcp@2{ziIPwm8-44_0pxhEJj5-}8_-UrLOxI=7$L{EmqqD+Ef43z< z&DGMMrWTwdiJU*&`!k_lOJQ;~e=E%7aiSR$IpDTqB7fkt4O|!n$a2S|o^=&eXy+y6 zE9*je6Y1h1I2Ia=PEsqvP#Gy^73^|jUZQW=E1Wm-#fG7|+g;AI1Boj85y_-+*Ck~Y zOFLbf#rp&YnY0}|*-`N4-F}F25A3i+3kAJqyAaj}-`~M{;wNwDYdjK$8Qrsn}H_6+m1Fp2_zm$_&f~>%%!SwGi z88eX%axK9f73t2u(f$`bmu^AKEI?}(2iKYiG>fzBBm!QM!B0a5`urN^ulghF-gT*G zLa3=q>7>l{P9XWHyWfpR(s!}ft#C4O$kawLU+A;kXw^UZ;3~$81zYQfg@niQm@QE9 zN6EtT%3EliSN`X|Cgk@PlqB>U1j9`46Z)aV7mBt+*5XGiuhqD%{Lv39dN{Ww-YYF{ zk+Ut5=H4Ou2jRF2h^ggvLLbZR5?jle7uN2^8h=9i8wuc|&PPU<@dte6&QCfSWVRv9h#Y8x6_H(1 z%>7TEUF=CZV69DekXEuX1Z5EmS^AMjf5{YTzgRzzzqX+8NX;aZguF@K2PtE9NRS_R z#;>U>R>Z%q;HZ)MvGu`Ut-$9rHpwqOq1h-uj>>v`RDcBp9O4U|{3?8|WKU$$a1uQr z!Sn8;544)z4Oi21zh*VP7TXpe{^&+SG`p~t-T%Id&{37;*TWUw8R?bZVydK=ZW<<5 zPQ&YDK0qzb^Z<7NnP7LPrm97dBCZI>){3pQr6}dEj@V_|u{dG3>!)n|E=Hx^KoaK{ zC7Rb-ySIl7c5(Wx96S9JPE~v+C(}Qa1WBH}`~(=i){jf_3 z-)NVcM!S6Il)`uFE`3vLx?CfFUv3RgO}v{ux+4n9lhR~IP!fCaNl-kySaM08(mPx| zCdTuXrWU(+X62^$B?*p}v?b8kIM`1CqMe|%P7s0$Nq&q4;M8KLlUWX$f+(+8uo`@+ zB#%Uh0t|Z!K>%e?UmdU$QqFY^e6m4)3{lURD~l7~ALUpwXUPs*gSQyIkb*ErXhOQx z5(%dD_{av^gNr_@zd=l2eb4F&v07dIqJ=jlL|`PW8Hd*QF@YKbrtM|6M`VeLM}9vG zCk+fA+2KD#Zo^$AI#$Pz%uWa^x0$}3t~MpE^1ldfY2|NYQJ)rN$K+WFC`~T9%3e0| z+ZOV(xa@OPXu{jmz%J!j%2L22(--A)Lp5IL`H5WyZJoW~v$J%2zhfa8~x; zA43czfV(+($hZ4Oz%U>pPCxK?fZpK29$Ih37+Dm-f)snD5@3$Kg3|rT77oDlV)++Ng6sZNLg=x#j-YxRw-$7Tz;8Ff= z^P0H$u%E*WUkb7nkHP$!$uPqxAmPA*^Rzz*ro*qNKIGSCbT+ZW45Dz@A8Vq4y~yB5 z^ZTmXSm^48PoZDq9w&juaK^KGoKH+!{Js+9l zZb1nS5O~T9%F%xIYH|yc6U|OZRulXWbUzE?j6;31Dd?$~xSKp=SBzLEhfhI*ZwJxp~ z29X>2HI=D!GMCllaT17?n8OMsy)4DOpOj5u^a=Tz0o-l-h|GRgCeOs{VGhP;x`%5* zIF|D);{*F61zM~LDHGh483#?JSRSL2=%&+aI)AH8zr$IDIDi`~&ykv1T^G*cKyEmn zM2ZkZ*nE;-WrE^6{d|&b3ezr0%@^6>qb%2}%qMByj)?~q8_p-GbUB{{LsjOJY#obi zkOG8&Z&Q}8L^%N$3%E~$a%G7O<%zeqwujg+!#M+#m$N;nk^<$@P64fSdQIo*k$L=r zL5f65_+W@D0&oeyumSd^MV%kRabZV%pc=`s(1wu<<;@or`LjcRcFy$v#5*rK8~Zrw z5e84Rig&BTTbZy+DaQ6pb<+EFm8IY+u8QYPI9yph%5CZWlC|_CPTufSMpA{oQNW_D zOOmbOV=yYev-e?@50ABHT~kRGKr^F+mQ>7>)n2^q*_ya4AhF0ANIvp7A+e*3!l-EG zHy(F00;t=!k?$DTx*`-IS|p1!F^slJ%H$lH_?)Qs3pU!Q_ip{NSEZAnlV(KgJEWiT zd;VY*(=G0bJ)uBr-21#r56Tq}`EH?9W)mIMhsNr|wo+uP;Ef8^q9=Cfc z+x%UeU&uxizMhy57DYt0#V+)198dsvVTXG63Zj8*DqVV)VT*PdR~j=lDm|Fw+i`s= zyOhicwxK6vD|??(TmD#g8mAhsAPZ?t_1j!{4lQia#DSe9$8WsMQQZ1H%9-7ZT)1Tp z-(JjWii0^u2B|1U$W@^ znyN;f9~>zgybahx9+P0Hj@e0{Gj3*j{P@DYTAugbX^^nmVQ5c@%)c1(fgbu*R>(_P z0%w`Wsvu?;`U%FM7cXZYHuNzxTVxU*-d{Yl#f>c0vk%#6{iTktZ*Y{7Dm1;kl~D^H zg2FY)`0Qj6+P2kE*5WaTgds}1z4oU0d`s+j;0On8>i@8*T z%Eb1Zkf&&X<5Fl%@9g9hHOt{r8dcU-<4$gq0wO!0f<;R$eF!nJ?0~Xk{wsoou!_+6 zanlYprN{bF6o|AJPlc5(T`pIb;V`GUg8ye1IFfjx_)PG@~*&>zpMe zy|1Xf0Y126Ao|dOUz-KjWG!1LRS+R4ez)=D!kAIs`R|W#YkQdQT|)K2`#| zNQ--5=Ad2dN~+2EH7!kJF-mao9-mB?4GlI!OHtPj!JyPBDwbNuU&&D+Z+Bi>4d5eu z@SIqe6rRTxt@+Odm)|+eAB=iGuYniq^aBQU?hDibK9MJU6;T>iC&WOb?Szb-Vmj4x zge}g6yGsq(xtH&|+ZrZMw*dY_1yhv+@=vUO@4pEc0c|vo`p&G19arV+_mfQP|8YI^ zX=t>|y?qD<$&&oxg4_f6DgQ)qR}!C{q#r9D+S+TaX!%s}M4=9Ciz8seZ4~Yv^j}nw zYAHfn)(=|+pAO|CD*nz-ZVdaBkyc~I>1tGy5dft<6@DCpD3l&Wbu=7>JTFsnRk({b z{ZeRl!i4^b9z`j9EuD&I7_<{?y&EAvL%M|^GpfW9C`KMfop(D_x{!D#BtoQJAd~V5 z#jJxTLS!~QQ>0nqpS(+ zqlY;GV(qt(K3$CC8?6|7yFpR(^PR`I=#=?;LS3 zT;YrWTJJ|xDg!K=a}aoi6VutmbI8(rt%?LtUT2No5|1vUOhSu@OV5_Mi}m$Snm7uZ z?-WDJ^xLzkN17z|wI<~N!*8KUu8Jlr z(P}{18x3X6sAt)%4i`UGJo$Hak&krjs?_;$707ICz|Jq-7!MUhf~(wf$+T4u78q&a zP2X}0Z$7gYrbl2XoLFH=DXiX4s1>n<{$(lS2yCIba1q+eZtg;its-5BcQm;kcfX+w^AvmaAbOi^^~}LkKmtpI+N%IW{G>R4BSU!lN0FUIPCS zxN@~uSFAc7k9O*coc<+AMqW^P4RZE8@4>p<-M~R>2;oO7u*_PZdxT;#Nw-Hcg^YG? z8R3vdsWhB<29|@C3xQ(^R)1y$tBoPY=wmKg>Pucu_S~6w#v*!(1diclW2gz^&dbrj zWeRJUt|;jD7WK5x5^+J|7(+=z5yX~<_q;x?`SnRbEpI&NGD>%yQ&^L?s$YQEz?z`< z{+C?Q&THLBY#$V@z+^UdXNVdS^lRjO>=1%mX6LwHzhrr^O#Yyl5y`Tdkl@3-#S%q+ zvcZr!_noga444}QVeZ}n#1_1>$J8H$I!vC5dpIp5&j_V6ya3$FLR zVLjUlLp{=@atU1T{lj;fErg##{7|?d|78>@L45|=QJ(euMMfy<{fcjmJ+FYtVU5Z- zkxlq*J*GP}=jK7A#7vRR|In|kDoBlr#7xL06$wcDOo3A4ij0w&-oIB%pqA0c8UVT( z=Hr0^ZqTh~Am2XKi~_1?=XjXIIpKy0wb4lwA)u)N(S&yJ={`eY7+!2}{YDr%M~~P# zPNo<9Apf&K3rnCI1TNqpym`QeZ%M8p28s~xv=~Ur+><5(N=*Fn8De6)VP^yrIwFXF z3@v~80<+>ctJ^pkH_M~WZ`PVVj`KTmD$dl3TQlumK?>e4s)xUS!y2jIvC%@B@o1i8 zd?gDPhN@{o%ff}tV7A^+@rlm^4slI`Boch9AN^KZWj&`7<8Tm(UOwk7t;*G5(=L2* z9XWKwvMQvjo#6^6>IPy^2J%7-2^C%fNAU1kID)&qm=NAwfeGQ^Zfpk+cVatum|;7( z+luY5zNPWII*{GvtC4RWhE->9^Nan@ek&R;bN6*Cg9dT=T~5XA^7W47n)C{GW?z(b zwhh|0herEoPLNcp+`O1`KxBGO{P-6i(NX^@txErfY|sm--=Kc;H1%In)@OehrHlGE zP@nTcvh+0dZ!YU6)KAO$hlL>jr2)%{r8ztKqWqsIqZG76*df3{Y2M>C1c&#MmGg`v zafdBdTw(1RL7f=B-RRsP14Gy2zuSMp^kxG&VNL7_S%{tzGd- zDs)=@f55zT{!%H0i}3^?J&OMnsMsu2;Nm_Q)DEcFuy`|Go!S4A~@jd>~0t2Zn z1oh5TQe7uO$0EPQ(3VTj@W=Et;i-UH^{E}e^dCp^Pfb`8rg9g^QtA7#nA-feSP{)< zwEPEXLyA^S6Tc29Vhe!-QALg2s0=OWDIdI%CSu>jKXB=^j0*tD_xdoaHs>?FjAmmyuf5T# znZ3`qm0%j4()7%Bm#3HFDcci&FewE|7+?lK5Cq|OfN5uD!qO)?{}np9Vm-8!wsK)X zbzGp^)TxqEWV0w_cSomm|zYKlhcMWj6$+niBE1Qm0q{-zs;u)4m+nxn(_1M zKRg`u;={vH_nlrsQtv?70iI|vGb>Kf%n^i#O;Vk7N`?O*6lhAuC%)Eu*5+!cQt4Fd zYu0kKJ1eg2hqRbj%KtMqB#XEymJRLwrn+EqqnvOqAnE-_|3=IJ1Oy($%Dr_S?Rp3b zSHE*Uedc$2bm76??O4|-$j%9xGKcju18190d7nh<3vMfB>m|`4cCNyzz|)RGhoJO6 zpgRHd0}^)e zO@4Lnf9PHlWwIC2rAk@ZoNs*btE7Jg2Crsx=HWLYL4QBvP{3p z9$wA);;3E`K-%4pi$$CIDWpDei!#|Eeppi7hI9?kYk|sb<0CP*FNJFj_{vo;WN&H zxim_*`MFnPC#te3U5&y-XIc{6oW4z31X)@^kgxCoI+OB2GRtA>VU4x_ksA}+C+ZH$ z0rF)D`j$2<>q1*JJ&?`apb(4zX^>k^TZRgWVL)R>qky9VUB>tlXq^S7e;F^A49AqA zyrtUoI6U)D2b|m!@l-rz_!comb`f_Fmzd6CzjV&RR~VF9;tcpx6VAQ@2tT6=Nl`&O zc_uvw?*POPX3&r?`5(lyS-cMUVpK~m26~0^Lhwl{1qAD8D8q!3e*$f8BfPGFPcVQNFc87_!a&6Xt4fS5U%`EE=wKM1SPVDEo{EoHWU#Vbc z%9;r33zX^Ib_y1264SF8hnWdc>sJ&*5?mIpdVs8ig+nE_z~6EgYNKGO9(Er5= zXS4zzcRub=2%wbK)TQS}U4d7j9gSxyQzj*_s@T_tLGlu58yz9DNl*JfU+|-=tj#Z02(dC%S&4^}|TD9}{5FtN79R?%kT{_g9 zRMi5Bl44XIt=buZUYY7xLHoK4@6r2U(~1G7bu7flocbozv06n_ic#&tnwjnB+$Z%7 z6aKB|GpZDOD@}`=G8feAD92bj|5K`3FkB;n_Wue31v6k&0x;9ikV@8wgUNu#7=x`V z)HP69s3!gHGORYEV+_6;=7+MkVMU9wigiv^_LY^gkopv5O+y0_^bXAn_W&qBL6~RZ zDlA&Yp&c2Z88K2j9RE8m%}^q=^%d=!YK5EU@ekiwjM$ zjxv@t)xgQT5W0ag(d{u$H`od)#oId1k7xn0_-_1(XZb=PpUj_-*%(Qe<$ilWI?l923$&V7R_a(|L(12oIM4lm@N`o&NFVhx+A2;pLz_V3p& zwmE;DX6K4WNJ3mZp3VP6UpEV{QvbMLoFVP|VFsbjX)^;hCO)9>a@ZJGmL$cj@Tc-FOgyin7xg#%f-i^-SDKfAs(wx0YWK-Y{e6 zAAigqud-y9xlD-^NQn;Xd#2QFV#+);gKgMVDqP(Lo zedP&jgpW!J5P2%Csm9RVLY6M_HEES)A!RBofqwGbI+%+@1OpsJE*rbDAV33MV!Il{L;e!o4iM z#fx2BxEW{#v!|>kO?GRof7A9st%o_ig)e9YsF~@}gdJ-RL9qr=K&qJ>0A~&sn2jCj zOVCr)lK}t~oQ$;3*qZsRd~$dv9+ZZ;B`o1(F(Pl0qbxi`HH_3z z;lc$5i}vF)QXxuV?{ZIiP@5IE=!7g{6L1@fvR#hu*s?gNYo{b?JmUgPaY&z(EsbuK zIn_lMcJe`!v6DY0({oLC45!_fVud+Kg>AR+AxbmM0)S@#rBJUWgWu|tii6xLBWP?{ zIy|iRx~Brtn4!8w=B_d`vU$IG9D(8Y9I^LdEb|C50)`?qRxQ7%uLI+du@fikd?1tsurjZJ z3tc*d_8l2D2FFgyMGq3v)GVbbYm@Ee1l$qaWVMn}G%;&76gpHG0Wh!YBdsO+jV}6h znz9Iy=MFabZ(|I=t^?9L+jzx1Z5kgzg_sxW$&OW}k9LBn^AdgG zKLT(1uGAPHq81DeFT6?=i_6<6^kYq0*YIk(9##=o)JKJxB9d{*n@PMW1NeL$sNDg) z$rpfqdO_j|Y(e#ofbUVdRkG}DO`!+lVcTWvg$cvuFOX0?Vp$y{@=epx<8v_BQcyY= z&>&w~N%0OPHIP}D`zZFfsm1=dY_o&Tf_5?^+9BI+dFKs~@!>=(z~Rx6k?+KjVD!pr zWp$ydou%3(FxuEimGS8Q1Q{Cx9OFF2Q6mNNRU&Ad5x~Vz$?K}ig=MX_nZ6meL|TT& za;fq#dw*wcWg};wg0=+=DOn(vEUr9K7UIZ&vz4KNbHMkF4FwBqPY6gyO<+-#bSnPw13G0&QOc{+em3=z53aO$&oK^}IIBV}7>Q?1l}C)NN% z=PT6U&|XrhGwa`8Xxme5>WBTUIw{tIu^LT?f0!7`Kr*M$t`R8nNkmmF@%<2o?`?YI zv9!y#+=tu3mDW%(3}qed$5b?T&}5*oUNK_~R8A__M!-($t3Ew<3d`RLsLJx`d zhqPPW{~LLy+=56o^044GX@QwA12-=nbm;6YS?YqO6x@-F@|(V{m&z$m<&;awX5x#TBz<+A z$|(<5a>`RV<#vzt@CD?Qvpp3h5Ne0wa?0&VT~0YWCd?^^ShqqMIoE^(l8bAN%V!5I z@5p(Sz|Y0-C{n<0Ub;D5^1YzUR`I`i_qK};5focvcO~E;yUVsUJ)lo~clvDL(1#wH zMGx|Z#0h-hnPY7|6g01ovdc?m?Zqens1BRu5}vzsM*f@dB5b2;tR*P`jXd&%1a5t} zedLYj9PwGQ*YgfIHpVWvMoH(CrGJbHpJ0?hirS{4IpH;3*zz($ z3%-vnT5z`dEZ=H2dk2&)KRC6L??Kmq#F;#|umQC8W(a3m`Na%BV6|OfHDxo_8oIH| zKy)+g$pe6G9>RwYhg*X1^pbcr(TSHs9)^S9?HCo%)btl`JeRlFhbyH{8;TzIft?^0 zBSv3jb@NW-tE3!wAu{R_)ans=Rr3hA55S9qud3|S&UB4Iv4~`gAp=`6}Q7bP)a9I zz27oibrH?)Z>p^e28rh*V&k?{i&|FOAUA6%H-^`bJrK?8YDMO0(*othJk1emoUTi5|SO+W@RZs?1nWs^H!We`oZqyG}zYN?IchD5vhSH$O(+4Qh zjYJC*Rfty2det&+k`l7uiplZi)^I8{OtjeRr>-HXZ5V-XBw9v6Wex084|K19o4D>G z@1d3gmgY~T2MJisM~$ReeAGw~WwluZv0lIk2MY0$pU?)*+VY?-sUnPBCsp{ZhBV%S zZ`!4ZfD@)D=8HV7dvsWoM@xX8cHCq+fgjv6c@LY zl`46dwVEWv#KTMv2OhSo3J=Rd00f;`yck$@$iut{!R5($Sdv=dVcJ;F!yrIDNC?4% zkHEt`D|vW*T8U4l6%0%QQ?aJ;ab6~s2n*J-Fd>4m@Vc~;F)hvb2+{~baLwJQdG|r4 zKFTikhF^_XN}U%I=UJ`ur6!;D;nl6dP!BZ!;$_(J(3$m?mehhNW}H2V*GBwX*X9

-M52&Ix6AbWAU*jNqV>*xugj zo(krz_}qqNq5w|(=DQg+AoQ>u_f(<>{=5NkWSB#0qg#lZ(%^MqRK!CjvNok4~SbCfgBu`NVy=!%ZR;_p! z3?&qF6<37=#?=PymlGHJ%x$2xYy>lWJeQ4#)(IGIOu%CAelhmoewlz72OF;qEe2%6 zM$o)K2u@gpjX<=p5wW)s@TRvBP{wxQ@L|^VHbP&)D6NE}MSXliu|&p^T(W0i2sqX# zQ{p@`6-?tuyvQ_(EA7Wj1!@{o0R)%|js=&g0N*kd4T}*PVgxf47AC|p6#@iPK{>oR znF=FfLwqL2G$`m|*rDWD{fdbtmtNReHy@z!row8dD0D{qXfIgDX2<@?X2!~uaq zr2Gmf2aED!2P?q;w=QrjS&`p0uNN4F6mVv9p3X*vMxxp-4% z#(h*1P>BU#AlQO5W-U>FdgMXIS;TdQU6q>H0BIWTc6>W25}3m)5{^j;y7v@{q&A{R z@M6&jQY5Uy4T{9BDvAWpQ--`ikthSC=%1i3hWNA>nlAB*#Mzm89xa&XVwx;)0Ra!} zL&KwaamcuV0WelK!VgVXiM-4ic}Px;pcqZ#k*uFBHj%uxD;$uGJL5!fwP^-!lu~pS z6rjy)GreQl654zA4c@XbqW9W1=z*qX&*Un5dj_*Wmu$~AWHDhPrOggQxD+axe zM~4_%$D@TgE62qM*KvgexLK@4Sf6Mhh#9qX$RrE&FdZkebkx|`(!uFnD9(@61A-(i z?tA~{44qZx3*H+TI+&<5bmsL+B<1}mB~CqNm!;XwUaT>9^2Z4TNCU7z(C=j3(8A$S z6HQBtnhybXlmNno!eMPjVF3c5sCoNl160GU4wRHu@pgHI zm25J;F}ZX7%mxG$89fq&%i86k5z5>Xq2lBM5M`BgR27he31^rLRXThzkBx7&Z38a@ zkKDW;`!+rA?S{v_ugayc)s^z1GU^r@2?#V{&`w+rBD+*b#e9)G{s8lDD+O;9VDC@*?0ZlL$ z1`4hj9U0v#zA^^K{9t65?-o53uBtz_NQyZ8%(71s>~{MeaBc7NkrZyD3CTLkWBtqW zXyHT|Nr6CV8!uCWo!T}{4AE#xw#T>Uh0YhJoX{n@%KB(w39bax(aE~rpTe&PL8QiL*mpC6C-u6W6t^585bqtJnmIty z9vNRE94TL*o}rI8K$eYD$eH0n1_#KlaDcSBpT9*mt{4-I3t?J2MGXR`Ap-10Af9b6 zZ~I(aV20L^8JHZcGJ_F%LvC{}36I^9PEgSdSa^EELFyzoejkX3SC%nX?6)bU@C=e|-mhCBg6K8KzEU zj17%eM2{SgHbMaCEk>er*OpEPKM6VE5el6N#F6r(Jp}k#FNwn5&8J>yPj0@YT);AX zjO*lKD4il(`C@yws2$vVTlrFPioD|#JSiKUx%p<8*0}jNZ$d)h=F7qIK9X7O(NUXy z5`IfJeB}9e_{bLNQ`!u$IdWY04LD;O1Mtjud(6=5qy?YlW!PRdDkQ z6iN97H=lBFIWGz)Q!nJ^+Y2Gv|Fc{LwkY`Zqx9U!uZMcab<*0|4Ae2S*!jz?7 zKZ;%f{6sLN`Smu1!uc_QDebDllp%>#40;_?4l!ihS~BI3n!|B%wcf9X+=r6QLHm2)2bOgBOy*8n)f))6>GR?LahhpBt7ufV53~ZZOG#Vn->5S;UkU<&mKIiNlu%=0gy50NltJu>|ty zcq6G^GuMGA;G>Yg0s!(?iO*kwYS18mbHu+V?Ltr9zZbwP5G?$AqaW5D&Xa#HAV&I= z&`YKzdXlYxE(13Rx*!8UQxBBvbY5XQ5R5AL5VGLG_cqMu+v%Q75AuMOezu>O6}xc$ z@fmJoeDxfp&obGV@3w);!|q+bjds3nlY#PZa3uU|BNi&hHXn5E)5mPAZwv{Vh-%8! zTGIex>B~IxTWAK6M+VPv+_Ywde)f{xU+?7eO6`p}`UCdm@ttZPR@n9mfKBk%Ljs*O z@Ps$!@%yZ4qOiaiRpuUb#o`J+4FZN|7v$zAx|9}UCgJ*Rn*YPiPi8H7CO1J=TGl`; z0pTdBNqdVDlp8h&HpBrcLYBC$<&uR{f+jQms00g`$Tfv{;yo8(LrnsekCGVKf&~^m zmi!13@l+9as6RBf93_rYDS{8cR4f9l1q_frG!H(>ax%}{`Rfi^MsO9!VXr?Tm} z{ZqGQOUo0B%S+kBa<)2_-FRdJhnWQEl$kcG#5t4m18`pJ!4|e{yhhtb@{G?(>E?oEFRkhb>}B{EiF#&I&f(Dvc;uHmp3vTT3^!r z<863pdA~KgbZlwY(y_V8U5Ab=Pu;p}ZvJJrYe;uPWq(=5MC&^|HEF6{!$sl~D>iN)Ee#mf%P-xTeNE{ir$o==tXoSR?HCbG$!Cl_9jdp(*5c+ zrp8h9rVHCkv(twUOV!;tF*%FyU~sD1Z{q0u^#1Jd{4$a?J#jEwoSHncxP)$PpRD~& zqwPms&+b$E&346CwpX9g_IS_AH@Do3UZ&sq$*H9!0b_42%O-A|N0B2a?dY-%?CAc* z`NP40W;+i|&jH`jiGxR`va8YBm$ai*clD}kN3Ej2Z(B{iE!(-e>5^g3vZsQHViy#> zW&3FY&2K=IP*Vc-I50JNY!Vwn&yy2}Cnl$tk7e^mrxp(!oWJEc(h||(s1c1j=VJc3 zYdgEo?YXH&T#sXyUhJN{?WLV7TP?RWbuEWq_Z`Vqt#9AzvOQEto2g40?f6OVN>xvr z6lup7x;TE8OZf&&oFwkaae}AFeT@QcOMG74sWfe~k|SxyPj**Pw@b~PG*7EZEpcgs zbg^0m21g@yzde2ycSSExa@V@l(YR~67rB@<+c&zn)oEN8Z=*fu_H1(WSnIeQEq5Sw zb()GVh|{E(;@!q|x7kHWlx|O+6h9eH)is(f?zG%E?H!2+?r4(6Emu$e6aav>p%&w2 zy&b#Vm&M&J>F#!#Jhjy&QjF_Y@QP}+E1Ge9PeN*~q3)9S!M&0D?S7QJ!QGHW_31d` zeEckaVH`2e)LjtQ+)u_Ao;~KC+Pt9S+@;BG#-@XZcDR29?qf2~O}EQE0|dlzjgfzQ z+;m?S;#}mSUauF?(r4V8Yf-{D)167`et|ynS`7HZ{oeg=_yD zZuue**ZJM1w-lZ+EKTWe>S|HT#sBEqY3lB#|CDsx&bm>r9w(PUga>Kcx!-AkEcboq z0U#~?P@P9m13pxDE`kr9S*s=IGv@kc=8==Ac2y&C-<`e)t^xy>#pg%0q~2`CjVGsX zN}@gK)0=LSJFn(8M-6wDwOZTn-cFsX(V zx#C$9H%{)KI*`3QyEMBp%U<5Tw7qj@`<0huuk;>ocEbw}ODFxG?Oh3UTh*DqZ`EpB z^1fTnzF4*-%aZLZ#MuoY2?+}VEUA~Yn?CbXR_J6=nMn{U4c*_9Ow@8box5|oq^8I&R}O(XQ(sW+1=R_^acIFK(I3y40Z)W z!EmrU*wf|f@^=NgI=g~hU0tEBa94L%PskVYhXSF_P%zXL3WdU<)H!b%(mc-QC?iJ(yw-hVMbwJ!sa0B2fPy`eCR1At5^Y z2meo?;G_P3K>mJn24vRHVl)yCT;2VW|>z*FT?MPnsb#n`Dgj(oX^`1@h|W%a<59S@o#d!5r50S<#|W^3;#Fiea<;^-on*u_TPBp zS3YsY=fC*XZ{GK*dt{3(v|!=JKRolSSXmniZ``!^j=K+hzw6cVPk;968^w}Rclm68 zuy@(={?k{li7J<0dDZ^!{@{m?9(?l9mhXPAq0wTsJ1T2JJ-xTx{_|hh!k@e5Hj91U z!lBsyYu$0y~2>db+l*qH3|N*Msb?0S(+;?6-(!2 z?~td7)5KZUpkuWVbWVhA)pl#`yk$MYpw;H9mRf}dsbNO9c!ty=+ATKABF{|GX$uLx zQoTiVTGsZ5J4-q(?N+-yLuj({*=N?dWUH;U-8!ebenw+ujcqjsTUt_QvCAv1Gi@2i z;)QeMd6Hc|OXeh(AUV#p@=ZIMS6c1aTenSF=CI2p6}_@uu54@*YqIywkFIyFwAq(0 zYglPrU(#=}Z?v8>rPMAjw>JqZ`olsg2JS)MZK6HxT-EmdH%R+dTz_xxrw;DxvCI*-$TRHA?X#qciEs2~4{nx%ZD)u*7WX2_VX^(o zq1m2D>gEq7I?K2wxm2`HT=`jXmsBFyEUx_n*$;$?Z_71h?y?oO^!)65_LOz3YWYQ# z&PwMdTV3|k6Dx$v7ME3By0%4@v(L|!7PfL@UZGy(Cl!Y+suh`tWAPLm?t)4o3ig;D#4sdk}UHU*=n)5?TwB)XI+V_ z)LACFg!1wVTQyfB)^c@1y{&<3)*6_ z+xDn()it-H#bZyr^7XZBG2giFf4X8Zk(LfAI3r84_(*6d4_^98%y zZC!44%4tWlaGrRsEqh6AqrKX;O3Yp^-+7C(M)cn-PP{n7;*_L)-}20nQE`=+eNbo+ z%1WTV1;#ExBJ3JzfNia~iIvxQTAZGNmbcpm=FaiGIyZ6aO>O*b9&hveo_6-(K#uEj=C_fp9`@F@ceuUV+uJ?(`1a&Hi@wE6mM&X9 z6d6e=$f7Chl%XZc$Oz&b%6p7^kO)B-JdSt_{`hVsI=S8`k5H~_G?La5J;?|~ZKGo& z$|yn|EnG?}=}a={!A)97sVhE-~aG(Qt3yy!x-QhGhfOx&|OhGg5ox((~k-oChR$r)#_ z>szvHfqwwGz6)c?M4V;@$0!-2FrOlnXe>205AQ8)1r1l++y2+Kg-dV0|9)$>@1>XT{`2RDB58{Gkid*#L277; ziK@9E52aYJ8cN;mPR59cZDzb?*Kv%<2zazNYMWHy@+huE3`KGD5M?08l;mg(K_Gm= zs1lDUQIIhX!cjp|8Oo$4m4F9^6R9*d1S2302W8?)dUqnZE59hO6E(_HqBNjhszf8v zXcCbpLUs=ZFy06yav{Rx{7$2Z-SHjC#4hl`SZs_EPBeXk_mn-cR63QwA~X)#oyd$J zZ%VXMd&7<)f=1qV90`I*?#(P#389RRrN@b2-b5UXOe}>oUs@UUsL9zxGJ;GmI471F z&8r(pAQ}b&B^PNBqS#1m6l^_9Y2VrINsW(=#NxZK35l`U$ORLh>XcHMb~qA`jwrD> zLTIpZEIx+3@K_`X7Df8nD2{?7h#8`kz@>dxJQpco%>=|in^y`^v651hUF6PZ;`F4l zD~)kMCXtSKls$ur5>=w=sOdz)GZIPeRImYwOcLpb9hua)vIltv5@SVOy;>FpL5Prp z$wVsUjVc#Xa$H>z7M)Z`mZ-DQNPJu^Mj}C53XMax7$u{M&4AQ1}U{Nh+0+Mp#rLtCP*3#nh;AuQ*|mSt@RO&7bo`2 zkB-MZeev;ydID!KLh=Znje8NrX<6+fb2mr?V)c4z9a>9co87|+jJYE-G^8ZKjaay; z!+PzY5>(>)1hh_6ffgNsw17wna5J`~?dZyKC^IsGJU=bmgS51xW- zY)`8BuM9Mbng%0fx)X{6VoSO`b^&F8)I7SUe`&90M=Y(fHCT5TOU4v7q=&kfQBNw1 zZO@EtPbajV!D|%3(%f||I2_UPggc-u^3^77704e|urDz+7@jqo5{FS`3><=)MCXw@ z@+{S~Jk0U8-~4*_j%O#n_2#2@T`vFgv(LFu<_44l=$W8Ge<$-@3aA1&0S^6{-pc?~ zN4PTp-GDkk4S;ZM06T#CqcXxJpgt=BRzL%Q$_S?#K>gAf^h|j4Oh9As02yEbP+OyK zs-ym>4UJz9pne+xO@J1F2QURN6)+tz7tjW12lxP;fDj-IpmpFc2eNs9`G5t0(*TPA z%K*y(rvuIaumOMvun0h(n*dqB!~Z+yyFR0eFzd&8;Z9g(^&Bq+kyYY@3R{{Og|DG$ z8K#+j)s!TOmr~@>K$o)BWYIk7^f+5P+03#vrqLo5rJ7-qHMH@hGK0gKihx|1rpm0* z^R{d`DG|*AvW;pU)>Q7QM3U|Bq{qi-9?)NuYF5wZR8vpdoUPH0A~mf_%g9*-)}PbP zFlDMWBv&Vg1^Wn%4etnAH4`@tHyUbYfGiPPGTC^beZHO5yJkxbJDoGgY)QT+0>1-W z06HnjJkpt_u@G5xTi8nSQy|}Yt_tfb@I2V!oM*xMJJ#s?X>5UR^GLaA&CD&(YVb&HiTBye7@823R1M+0m*46EvWQF}9_&_rnzR+zjuSZb^sOcvE70^u?|$)Om!P2$+*LSU~V zXN_!!YBy2}W`WYR$wWFam>AJ_j0mOosqcOY3sur=sgi&$8?xT`n z1l^hOo-akkBvbco^950N{fQkkzeCqTxdp9PiAO?e#s*9;qgmw&bK7pX{&`Md9#iC( zf~g>SDqCy5Han}pA7*DFV2cgIi$_pOF#xuE2@aWd#6}Qb;Q$dQig1@=gE8<543+W8 z@T=J&?pNZy$P8UA{Wo7N{+?YYa6-3qfZNBd5jw-hP?yTa@2hr-3;PuLy8xHOHAbEEh> zOm~V)+56nx>=I#sy@3;&FNoX3x$GRShr5@L}e7)kf~g?UGA1 z`(&2MRWRPkY+Q$c-bH$G~gF^Ay0;N#)b+Hfi_2C528i?=m z-^q2Pvd>^1J@{$3k@QSFYpii0o@q@6?U$l6qPMY^E8yc!!#la2|0rmwa zACmcg0J;72A$iIF5dYBUOzp=<{>DAp!)%LfESZRA1`)|}PCQbqtwg;$#)T53^t|TP zKZ~zD5Dd2aTvST8zz}nK>1LViYY%p{`&4YBd=Q0Q{&pYtC4og!qa-xkba{Z^yO#}! zd+}id!ro8f+d?n+umMg3!R>;K@g!C_m8Xw{3j<@E%Vk;T<{-J8pK)A(@mwd4yMs7N z?_v@cVlo%TF>g1say`t(`+dyL`~A$p`vdr|1O84{!ux~xC%pbHR%V?B#|HV46vyn3 zb1nz-;I?ERX)*opw0m&vvX66fxDBb*@l>bZvgG>&H=p+=(wmMa5uW}m=RI5Kxk~Me z`u54aMmwNx0mB~PsS^qiyMQlJQrHK4kt)JY2n<^R1&O^7@$FZ?1L}9D`i;#)Iku1G zc7!$>2Su!pIlu?JAWE{uYO_0>C8cE_d5O4c0A7-<9OgqKStRbXy&SVjJl`lZ@b{h|)zs&nOZU&gH9Wjlw3Nou~t3P+_0w_G@`auT{)%>=mw$jmyH z7*X@Egx@UVRtn77W@P+^6Va_kUAvlSwMtaU_z3@pz}2|MY6P zeHybxE~oMmB>s>X&8xumWtadEiR6m5mA(U@Rf{pbf+p>$%nr);>`>Hs0`EzzU2d2( z(fr|=+&HPWphsqDEk4rVurj#It7hAnwV^nUR%@G9yjGWkD+ zJnlthrKd@Afq&ZQoh_M zC55IJ3ROm-rnORfx!9zZ501n-%;l?Msn<)*N7ifNq$9`;Dgp(rTJolY(gQ~KmU^>! z9%JZDt=jZyY~m2T)K+OWQTIVFwrdL@WE5ixdby*Z9Msm!okbRw-(N|Qg%xyMS{xxa zLYan;w{m1tNqmgOyW}-sQqC>wDNAs1&JHvF1Qavl+|EIS_}igC=ywIIxUC0AOknsg zUN8>FQrjx3g-~Y`s;Nq}&C?Z;W)n>vuC}pKo0({HBAQHXfy=5)o<%KAM6Ib!$i}M^ zD>Wx}EFGt|aZsC98s?Y^PsMc-ZX@9yP5Y^6wmZv_b_k7p_-|EljS#v%QG+QTrfgLxy_7zrrh`dsGziJ5&_s5h&nP6@`qh zI+L==YDb+(r9PAS_kn1{^;)phiBfJzxw3FM~0pVfg<+v0WYg~lDSze+}alwgC z5h*#SJN0*|jG0DI-HOAXF>Wz89Z(-@1FLYvuM>9OtK`w$%ar zY^KG*8QrsSd8yj{!jtYErxY1Q?K;3_iI4IWS(LydvDb2}LW{P%q<9T$feT6?8w1%m zdke?vb(B1U7?`-w4N@?Dt^&H8#<3P1Ut6Ss3WrT!B(8~@%M;kR-NUhl;!V_vbCRP3 z*MN0eL6T^4GA3tnG;*wRbKbO!eS)*aS!rgnJ~JD{pp|2FhFfw3;bEdVdv01bHtuNR zSk+{FZ9*`@4rwSHdurs&o*;z}Nyb)u|*GzhzWZmF3j@X|pFcOBAK zoyG$O5bL|S}_`9|@TQ1~hqzDh?4c;rGfOW~_^k#U>EtD0=}SGHiOMs)+| zHAr7*bMhh&TklErpnA&oB`4LkFluoeFKBTVF1e)ewY2cHY(?Re@?3*Fr^qBrp2eFK zAh=eZY)6xW2zUR<_H;y`QQmj5U6JupgOQ5)C)))DES0C7Y->HJsakfj9TWsU)t!ai zs19uI5}xLOdTG&lK5va0^wj3!nmQv)fppXx&#>7IJxBI#y5@NvtU5qpMdQ3-={Su|$M=ZFsA&!tj-1E&g-)^BI4|;PvFNU?=aiL6>nSLcDIbS2nfh^w|Fn-y z{HK3>;y>fm#ee20iT|uq75~{EkND3y8S$Tc@>1P)9Kk%MRC|xp_?S}NezH>Cagt$# z@6>#I|Ht6I2TsvP$1lTCNS7Lpje693Rh)yu7(EAomIzPOywt*rUtP|O zD11d>)lh6tW{i7ZVC9vpp|DDAg`@v8-7n!jata9}9hpHAiX@Q4;`t+~NR}lpeIysj zp@3bHBiD?ioIaiwqmIxd(q5MVS>mM7H9FiQAv>nNC-ka{rc$bpo-?UAN_yl?Ntni}(&J`eZyqIt zxaz!OOd_FW+(W!}WGk45v0`l=V*aT^NNO@_iiJ)g*&IQwHjV~Dmcnz_n*2`kRD!MW z7}jo1gL=!?4UR%Y=VZj3lE9TmjS>ZlJx`03widhn%s8_a3t z;TD97p++NL*rah*v&mU41@rL~&Zl(}ou^Eu$ke=PaPx43A$+2#I89HRnK, +} + +impl Guest for Component { + type Counter = CounterState; +} + +impl GuestCounter for CounterState { + fn new() -> Self { + eprintln!("counter_component: constructor called"); + write_marker("constructor", "0"); + CounterState { + count: RefCell::new(0), + } + } + + fn increment(&self) { + let mut c = self.count.borrow_mut(); + *c += 1; + } + + fn value(&self) -> u32 { + *self.count.borrow() + } +} + +impl Drop for CounterState { + fn drop(&mut self) { + let final_value = *self.count.borrow(); + eprintln!( + "counter_component: drop called (final value = {})", + final_value + ); + write_marker("drop", &final_value.to_string()); + } +} + +/// Best-effort write of "=\n" to the lifecycle marker file. Silently +/// swallows errors — Drop can't return them, and the host may not have granted a +/// preopen (e.g. the plain lifecycle test doesn't need file evidence). +fn write_marker(kind: &str, value: &str) { + let path = format!("{}/marker.log", LIFECYCLE_MARKER_DIR); + let line = format!("{}={}\n", kind, value); + let _ = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .and_then(|mut f| { + use std::io::Write; + f.write_all(line.as_bytes()) + }); +} + +bindings::export!(Component with_types_in bindings); diff --git a/wasmtime4j-provider/src/test/rust/counter_component/wit/counter.wit b/wasmtime4j-provider/src/test/rust/counter_component/wit/counter.wit new file mode 100644 index 0000000..bcccc5c --- /dev/null +++ b/wasmtime4j-provider/src/test/rust/counter_component/wit/counter.wit @@ -0,0 +1,13 @@ +package tegmentum:test-counter@0.1.0; + +interface counter-api { + resource counter { + constructor(); + increment: func(); + value: func() -> u32; + } +} + +world counter-world { + export counter-api; +} From a5ec1bcf071eb6e8a1c4f4c61715fd922c7c2ec1 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 18 Jul 2026 16:36:10 -0400 Subject: [PATCH 26/85] feat(provider): BridgingSparqlExtensionDispatch prototype Transitional helper on wasmtime4j-provider that lets JVM plugins dispatch against SPARQL extension guests speaking either the new sparql-extension ABI (tegmentum:webfunction@0.1.0) or the old flat evaluate/aggregate-step/aggregate-finish ABI. Detects the guest's shape once at construction and caches the result. Delete when every guest in ~/git/webfunctions/crates/ exports the sparql-extension world. See webfunction-wit/docs/design/bridging-dispatch.md. --- .../BridgingSparqlExtensionDispatch.java | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java new file mode 100644 index 0000000..6e01211 --- /dev/null +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java @@ -0,0 +1,363 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.wasmtime4j.wit.WitList; +import ai.tegmentum.wasmtime4j.wit.WitString; +import ai.tegmentum.wasmtime4j.wit.WitValue; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.WitCallableResource; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Transitional helper that lets one JVM plugin dispatch against SPARQL extension guests speaking + * either the new sparql-extension ABI ({@code tegmentum:webfunction@0.1.0}) or the old flat-export + * ABI (per-crate world exporting {@code evaluate} / {@code aggregate-step} / + * {@code aggregate-finish} at world root). + * + *

Motivation: the substrate is committed to the new ABI (see + * {@code webfunction-wit/docs/design/custom-function-registry.md}) but 177 of 181 guest crates in + * {@code ~/git/webfunctions/crates/} still export the old shape. Plugins that migrate dispatch + * without a bridging layer trap every {@code wf:call} against an unmigrated guest. This helper + * absorbs the try-new-fall-back-to-old branch so every JVM plugin gets bridging for free, and + * every plugin retires the bridging by deleting one construction site the day the last old-shape + * guest migrates. See {@code webfunction-wit/docs/design/bridging-dispatch.md}. + * + *

Lifecycle: one instance per {@link ComponentInstance}. Capability detection runs at + * construction time — the helper probes the instance's export surface once and caches the result + * so subsequent dispatches skip the fallback path. Every detection logs at {@link Level#FINE} + * so operators can watch the migration curve. + * + *

Thread-safety: dispatch calls are as thread-safe as the underlying + * {@link ComponentInstance}, which in wasmtime4j means single-threaded per instance. This class + * adds no locking of its own. + * + *

Transitional. Delete when every guest in {@code ~/git/webfunctions/crates/} exports the + * sparql-extension world. + */ +public final class BridgingSparqlExtensionDispatch { + + private static final Logger LOG = Logger.getLogger(BridgingSparqlExtensionDispatch.class.getName()); + + /** + * New-shape filter call — the {@code call} function on the {@code extension} interface of + * package {@code tegmentum:webfunction@0.1.0}. Interface-scoped exports use the + * {@code /@#} form on wasmtime4j. + */ + static final String NEW_SHAPE_FILTER_EXPORT = + "tegmentum:webfunction/extension@0.1.0#call"; + + /** + * Old-shape filter call — a bare {@code evaluate} function exported at the guest world's root. + */ + static final String OLD_SHAPE_FILTER_EXPORT = "evaluate"; + + /** + * New-shape aggregate constructor — {@code new-aggregate: func(name: string)} on the + * {@code aggregate} interface, returning an {@code own} handle. + */ + static final String NEW_SHAPE_AGGREGATE_CTOR = + "tegmentum:webfunction/aggregate@0.1.0#new-aggregate"; + + /** + * New-shape aggregate methods. The plugin-side driver calls {@code step} and {@code finish} + * on the {@link WitCallableResource} returned by {@link #newAggregate(String)}; these + * constants name the methods the resource dispatches to when the guest is new-shape. + */ + static final String NEW_SHAPE_AGGREGATE_STEP = + "tegmentum:webfunction/aggregate@0.1.0#[method]aggregate-state.step"; + + static final String NEW_SHAPE_AGGREGATE_FINISH = + "tegmentum:webfunction/aggregate@0.1.0#[method]aggregate-state.finish"; + + /** + * Old-shape aggregate exports — bare {@code aggregate-step} + {@code aggregate-finish} at + * world root. The shim {@link WitCallableResource} returned by + * {@link #newAggregate(String)} routes {@code step} / {@code finish} method calls back to + * these bare exports. + */ + static final String OLD_SHAPE_AGGREGATE_STEP = "aggregate-step"; + static final String OLD_SHAPE_AGGREGATE_FINISH = "aggregate-finish"; + + /** Neutral method names the shim recognizes. See {@link ShimAggregateResource}. */ + static final String SHIM_METHOD_STEP = "step"; + static final String SHIM_METHOD_FINISH = "finish"; + + private final ComponentInstance instance; + private final boolean filterIsNewShape; + private final boolean aggregateIsNewShape; + + /** + * Bind bridging to a specific component instance and detect its ABI shape once. Both + * capability probes run at construction time; the results are cached for the lifetime of + * this dispatcher. + * + * @param instance the underlying component instance; must be non-null and open + */ + public BridgingSparqlExtensionDispatch(final ComponentInstance instance) { + this.instance = Objects.requireNonNull(instance, "instance"); + this.filterIsNewShape = instance.hasFunction(NEW_SHAPE_FILTER_EXPORT); + this.aggregateIsNewShape = instance.hasFunction(NEW_SHAPE_AGGREGATE_CTOR); + if (LOG.isLoggable(Level.FINE)) { + LOG.log( + Level.FINE, + "Bridging dispatch: filter shape = {0}, aggregate shape = {1}", + new Object[] { + filterIsNewShape ? "new (sparql-extension@0.1.0)" : "old (flat evaluate)", + aggregateIsNewShape ? "new (aggregate-state resource)" : "old (bare aggregate-step/finish)" + }); + } + } + + /** + * Returns whether the underlying instance dispatches its filter function calls through the + * new sparql-extension ABI. Exposed for observability and for plugin-side callers whose + * downstream marshalling differs between shapes (new-shape returns a single {@code term}; + * old-shape returns a {@code list}). + */ + public boolean filterIsNewShape() { + return filterIsNewShape; + } + + /** + * Returns whether the underlying instance dispatches its aggregate lifecycle through the + * new sparql-extension ABI. + */ + public boolean aggregateIsNewShape() { + return aggregateIsNewShape; + } + + /** + * Dispatch a filter call against either the new or the old ABI. The choice is made once at + * construction time (see {@link #filterIsNewShape()}); this method reads the cached + * decision and calls the appropriate underlying export. + * + *

New-shape signature: + * {@code call(name: string, args: list) -> result}. The + * {@code functionName} is marshalled as the {@code name} argument; {@code args} become the + * {@code list}. + * + *

Old-shape signature: {@code evaluate(args: list) -> result}. + * The {@code functionName} is discarded — old-shape guests are per-function crates and + * their identity comes from which artifact was loaded. + * + * @param functionName the SPARQL-level function name (e.g. {@code "upper"}) + * @param args the filter arguments as provider {@code WitValue}s + * @return the provider's raw WitValue result (shape differs by ABI; see method contract) + * @throws ExecutionException if the underlying invocation fails + */ + public WitValue callFilter(final String functionName, final List args) { + Objects.requireNonNull(functionName, "functionName"); + Objects.requireNonNull(args, "args"); + if (args.isEmpty()) { + // WitList.of rejects an empty list because element type isn't inferrable from an + // empty varargs — SPARQL filter functions with zero terms are also degenerate at + // the language layer. Callers with a real need (a pre-typed WitList.empty) should + // pass through the bridge(newShapePath, oldShapePath, args) method directly. + throw new IllegalArgumentException( + "callFilter args list must be non-empty; use bridge() directly if you have " + + "a pre-typed WitList"); + } + final WitValue argList = WitList.of(args); + if (filterIsNewShape) { + return (WitValue) instance.invokeWit( + NEW_SHAPE_FILTER_EXPORT, witString(functionName), argList); + } + // Old-shape: bare `evaluate` takes just the arg list. Any old-shape crate that needs + // to distinguish by name is broken by construction — the crate IS the function. + return (WitValue) instance.invokeWit(OLD_SHAPE_FILTER_EXPORT, argList); + } + + /** + * Return a {@link WitCallableResource} that models the aggregate lifecycle. For new-shape + * guests, this is the real resource returned by {@code new-aggregate(name)}. For old-shape + * guests, this is a shim whose {@code step} / {@code finish} method calls dispatch back to + * the guest's flat {@code aggregate-step} / {@code aggregate-finish} exports. + * + *

Either way, the plugin-side driver calls + * {@code resource.invokeMethodWit("step", args)} and + * {@code resource.invokeMethodWit("finish")} — the bridging hides which ABI the guest + * actually speaks. + * + *

Old-shape caveat: the shim discards the {@code mult: u64} row-multiplicity argument + * the flat aggregate-step signature carries (defaults to {@code 1}) because the new-shape + * ABI does not model row multiplicity. Guests that need multiplicity semantics under the + * old shape must migrate to new-shape or bypass the bridging. + * + * @param aggregateName the SPARQL-level aggregate name (e.g. {@code "sum"}) + * @return an open {@link WitCallableResource} ready for {@code step} / {@code finish} calls + * @throws ExecutionException if new-shape ctor invocation fails + */ + public WitCallableResource newAggregate(final String aggregateName) { + Objects.requireNonNull(aggregateName, "aggregateName"); + if (aggregateIsNewShape) { + final Object handle = instance.invokeWit( + NEW_SHAPE_AGGREGATE_CTOR, witString(aggregateName)); + return instance.asCallableResource(handle); + } + return new ShimAggregateResource(instance); + } + + /** + * Adapter over {@link WitString#of(String)}, which declares a checked + * {@link ai.tegmentum.wasmtime4j.exception.ValidationException} for the null-arg case that + * cannot occur here (we {@link Objects#requireNonNull} the string on entry to each dispatch + * method). Converting to the API-level {@link ExecutionException} keeps the public method + * signatures free of provider-specific checked exceptions. + */ + private static WitString witString(final String value) { + try { + return WitString.of(value); + } catch (final ai.tegmentum.wasmtime4j.exception.ValidationException e) { + throw new ExecutionException("Invalid WIT string: " + value, e); + } + } + + /** + * Generic bridging entry point. Try the new-shape export path first; if the underlying + * instance does not export it, fall back to the old-shape path. Callers whose dispatch + * shape isn't covered by {@link #callFilter} or {@link #newAggregate} — the Stardog + * plugin's {@code cardinality-estimate} overlay is the running example — compose against + * this method rather than duplicate the try/fallback pattern. + * + *

Detection is performed lazily on every call (unlike the filter/aggregate probes done + * at construction) because callers may bridge many distinct export paths and pre-probing + * every one would grow the constructor arbitrarily. Callers who bridge the same path + * repeatedly should cache the result themselves. + * + * @param newShapePath the interface-scoped export name to try first + * @param oldShapePath the bare export name to fall back to + * @param args the arguments to pass through + * @return the provider's raw WitValue result + * @throws ExecutionException if both paths are absent or invocation fails + */ + public WitValue bridge( + final String newShapePath, + final String oldShapePath, + final Object... args) { + Objects.requireNonNull(newShapePath, "newShapePath"); + Objects.requireNonNull(oldShapePath, "oldShapePath"); + if (instance.hasFunction(newShapePath)) { + return (WitValue) instance.invokeWit(newShapePath, args); + } + if (instance.hasFunction(oldShapePath)) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log( + Level.FINE, + "Bridging dispatch: {0} not exported; falling back to {1}", + new Object[] {newShapePath, oldShapePath}); + } + return (WitValue) instance.invokeWit(oldShapePath, args); + } + throw new ExecutionException( + "Neither new-shape export '" + newShapePath + + "' nor old-shape export '" + oldShapePath + + "' is present on the component instance"); + } + + /** + * Shim {@link WitCallableResource} that fronts a pair of flat exports + * ({@code aggregate-step} / {@code aggregate-finish}) as if they were methods on a WIT + * resource. The shim owns no native handle — {@link #close()} is a no-op because old-shape + * aggregates keep their accumulator state on the component instance itself, not on a + * per-group resource. + */ + static final class ShimAggregateResource implements WitCallableResource { + + private final ComponentInstance instance; + private volatile boolean closed; + + ShimAggregateResource(final ComponentInstance instance) { + this.instance = Objects.requireNonNull(instance, "instance"); + } + + @Override + public String resourceTypeName() { + // No native resource type — this is a synthesized handle over flat exports. + return ""; + } + + @Override + public Object invokeMethod(final String methodExportName, final Object... args) { + final Object result = invokeMethodWit(methodExportName, args); + if (result instanceof WitValue) { + return ((WitValue) result).toJava(); + } + return result; + } + + @Override + public Object invokeMethodWit(final String methodExportName, final Object... args) { + ensureOpen(); + // Accept both the neutral shim method names ("step" / "finish") and the fully + // qualified new-shape method paths a plugin might pass through unchanged when it + // isn't aware which ABI it's holding. The latter path lets a plugin that thinks + // it's talking to a new-shape resource work against the shim without branching. + if (SHIM_METHOD_STEP.equals(methodExportName) + || NEW_SHAPE_AGGREGATE_STEP.equals(methodExportName)) { + return dispatchStep(args); + } + if (SHIM_METHOD_FINISH.equals(methodExportName) + || NEW_SHAPE_AGGREGATE_FINISH.equals(methodExportName)) { + return dispatchFinish(); + } + throw new ExecutionException( + "Shim aggregate resource does not recognize method '" + methodExportName + + "'; expected 'step' or 'finish'"); + } + + private Object dispatchStep(final Object[] args) { + // Old-shape signature: aggregate-step(args: list, mult: u64) -> result<_, string>. + // Default mult to 1 — new-shape has no multiplicity, and per the memo any guest + // that needs multiplicity must migrate off the bridging. + if (args == null || args.length == 0) { + throw new IllegalArgumentException( + "Shim aggregate step requires at least one argument (the row's terms)"); + } + final Object argList; + if (args.length == 1 && args[0] instanceof WitList) { + argList = args[0]; + } else { + final java.util.List collected = new java.util.ArrayList<>(args.length); + for (final Object a : args) { + if (!(a instanceof WitValue)) { + throw new ExecutionException( + "Shim aggregate step expects WitValue args; got " + + (a == null ? "null" : a.getClass().getName())); + } + collected.add((WitValue) a); + } + argList = WitList.of(collected); + } + return instance.invokeWit( + OLD_SHAPE_AGGREGATE_STEP, + argList, + ai.tegmentum.wasmtime4j.wit.WitU64.of(1L)); + } + + private Object dispatchFinish() { + return instance.invokeWit(OLD_SHAPE_AGGREGATE_FINISH); + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + // Idempotent — old-shape has no resource to drop, but we still flip the flag so + // downstream code that treats the resource as consumed sees the expected state. + closed = true; + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Shim aggregate resource has been closed"); + } + } + } +} From 69450a6c7d70040d2bee3124603e182b6bbeed80 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 18 Jul 2026 16:36:19 -0400 Subject: [PATCH 27/85] test(provider): bridging round-trip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-trip coverage for BridgingSparqlExtensionDispatch. Two fake ComponentInstance implementations — one advertising the new-shape extension@0.1.0#call / aggregate@0.1.0#new-aggregate exports, one advertising only the old flat evaluate / aggregate-step / aggregate-finish exports — dispatch through the same helper API and assert identical caller-visible results. Also covers the shim aggregate resource's step/finish routing, close idempotency, and the generic bridge() fallback used by the Stardog overlay cardinality export. --- .../BridgingSparqlExtensionDispatchTest.java | 448 ++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java new file mode 100644 index 0000000..294c731 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java @@ -0,0 +1,448 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.wasmtime4j.wit.WitList; +import ai.tegmentum.wasmtime4j.wit.WitString; +import ai.tegmentum.wasmtime4j.wit.WitU64; +import ai.tegmentum.wasmtime4j.wit.WitValue; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.Global; +import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.WitCallableResource; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Round-trip verification for {@link BridgingSparqlExtensionDispatch}. Constructs two fake + * {@link ComponentInstance} implementations — one advertising the new sparql-extension exports, + * one advertising only the old flat exports — and asserts the same dispatch surface routes both + * to the ABI-appropriate underlying call with identical caller-visible results. + * + *

Rationale for fakes over real components: the bridging is a pure dispatch selector on top + * of {@link ComponentInstance#invokeWit(String, Object...)} + {@link + * ComponentInstance#hasFunction(String)}. Real component fixtures would additionally test the + * wasmtime4j FFI, which is out of scope here and already covered by + * {@link WasmtimeCallableResourceTest}. The fake records every dispatched call so we can assert + * the exact export path the bridging chose per ABI shape — the fixture approach would only be + * able to assert the observable result, not the path taken to produce it. + */ +class BridgingSparqlExtensionDispatchTest { + + @Test + @DisplayName("callFilter — new-shape guest dispatches through extension@0.1.0#call") + void newShapeFilterCall() throws Exception { + final RecordingComponentInstance instance = RecordingComponentInstance.newShape(); + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + + assertTrue(dispatch.filterIsNewShape(), + "detection at construction should recognize the new-shape filter export"); + + final WitValue result = + dispatch.callFilter("upper", args(WitString.of("hello"))); + + assertEquals(1, instance.calls.size(), + "exactly one underlying invokeWit call per callFilter"); + assertEquals( + BridgingSparqlExtensionDispatch.NEW_SHAPE_FILTER_EXPORT, + instance.calls.get(0).exportName, + "new-shape callFilter must route through the extension@0.1.0#call path"); + // Args to the new-shape ABI: [name: string, args: list] + assertEquals(2, instance.calls.get(0).args.length); + assertTrue(instance.calls.get(0).args[0] instanceof WitString); + assertTrue(instance.calls.get(0).args[1] instanceof WitList); + assertEquals("HELLO", ((WitString) result).getValue(), + "fake returns the uppercased argument to prove the caller can consume the result"); + } + + @Test + @DisplayName("callFilter — old-shape guest falls back to bare evaluate export") + void oldShapeFilterCall() throws Exception { + final RecordingComponentInstance instance = RecordingComponentInstance.oldShape(); + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + + assertFalse(dispatch.filterIsNewShape(), + "old-shape guest should not detect as new-shape"); + + final WitValue result = + dispatch.callFilter("upper", args(WitString.of("hello"))); + + assertEquals(1, instance.calls.size()); + assertEquals( + BridgingSparqlExtensionDispatch.OLD_SHAPE_FILTER_EXPORT, + instance.calls.get(0).exportName, + "old-shape callFilter must fall back to bare evaluate"); + // Args to the old-shape ABI: [args: list]. Function name is discarded. + assertEquals(1, instance.calls.get(0).args.length); + assertTrue(instance.calls.get(0).args[0] instanceof WitList); + assertEquals("HELLO", ((WitString) result).getValue(), + "both ABIs surface the same caller-visible result through the bridging"); + } + + @Test + @DisplayName("callFilter — same helper interface, same result across both ABIs") + void bothAbiSurfacesReturnIdenticalResults() throws Exception { + final RecordingComponentInstance newShape = RecordingComponentInstance.newShape(); + final RecordingComponentInstance oldShape = RecordingComponentInstance.oldShape(); + + final WitValue fromNew = new BridgingSparqlExtensionDispatch(newShape) + .callFilter("upper", args(WitString.of("hello"))); + final WitValue fromOld = new BridgingSparqlExtensionDispatch(oldShape) + .callFilter("upper", args(WitString.of("hello"))); + + assertNotSame(fromNew, fromOld, "distinct instances produce distinct return values"); + assertEquals(((WitString) fromNew).getValue(), ((WitString) fromOld).getValue(), + "the whole point of the bridging: caller sees identical results regardless " + + "of which ABI the guest speaks"); + } + + @Test + @DisplayName("callFilter — empty args rejected with clear diagnostic") + void emptyArgsRejected() { + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(RecordingComponentInstance.newShape()); + final IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> dispatch.callFilter("noop", Collections.emptyList())); + assertTrue(ex.getMessage().contains("non-empty"), + "diagnostic should point at the non-empty precondition; got: " + ex.getMessage()); + } + + @Test + @DisplayName("newAggregate — new-shape guest returns the real WitCallableResource") + void newShapeAggregate() { + final RecordingComponentInstance instance = RecordingComponentInstance.newShape(); + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + + assertTrue(dispatch.aggregateIsNewShape()); + + try (WitCallableResource agg = dispatch.newAggregate("sum")) { + assertNotNull(agg); + assertEquals(1, instance.calls.size(), + "new-aggregate ctor call should be the only invokeWit so far"); + assertEquals( + BridgingSparqlExtensionDispatch.NEW_SHAPE_AGGREGATE_CTOR, + instance.calls.get(0).exportName); + } + } + + @Test + @DisplayName("newAggregate — old-shape guest returns a shim that routes step/finish " + + "to bare aggregate-step/aggregate-finish") + void oldShapeAggregateShim() { + final RecordingComponentInstance instance = RecordingComponentInstance.oldShape(); + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + + assertFalse(dispatch.aggregateIsNewShape()); + + try (WitCallableResource agg = dispatch.newAggregate("sum")) { + // No underlying call yet — the shim is constructed in Java without touching the + // component instance. This mirrors old-shape semantics where accumulator state + // lives on the instance itself, not on a per-group resource. + assertEquals(0, instance.calls.size(), + "shim construction should not touch the underlying instance"); + + agg.invokeMethodWit("step", WitU64.of(42L)); + assertEquals(1, instance.calls.size()); + assertEquals( + BridgingSparqlExtensionDispatch.OLD_SHAPE_AGGREGATE_STEP, + instance.calls.get(0).exportName, + "shim.step must dispatch to bare aggregate-step"); + // Old-shape signature: aggregate-step(args: list, mult: u64). Shim + // supplies mult=1 as documented. + assertEquals(2, instance.calls.get(0).args.length); + assertTrue(instance.calls.get(0).args[1] instanceof WitU64, + "shim must append the multiplicity argument the old-shape signature requires"); + + agg.invokeMethodWit("finish"); + assertEquals(2, instance.calls.size()); + assertEquals( + BridgingSparqlExtensionDispatch.OLD_SHAPE_AGGREGATE_FINISH, + instance.calls.get(1).exportName, + "shim.finish must dispatch to bare aggregate-finish"); + } + } + + @Test + @DisplayName("shim aggregate — close() is idempotent and further calls throw") + void shimAggregateCloseSemantics() { + final RecordingComponentInstance instance = RecordingComponentInstance.oldShape(); + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + final WitCallableResource agg = dispatch.newAggregate("sum"); + + assertFalse(agg.isClosed()); + agg.close(); + assertTrue(agg.isClosed()); + // Second close is a no-op; try-with-resources double-close must not blow up. + agg.close(); + // Post-close invocation should be rejected — same contract as the real WitCallableResource. + assertThrows(IllegalStateException.class, + () -> agg.invokeMethodWit("step", WitU64.of(1L))); + } + + @Test + @DisplayName("bridge() — generic fallback picks the old-shape path when new-shape absent") + void bridgeGenericFallback() { + final RecordingComponentInstance instance = RecordingComponentInstance.oldShape(); + // Register an extra bare export the general bridge should find. + instance.exports.add("cardinality_estimate"); + instance.returnValues.put("cardinality_estimate", WitU64.of(100L)); + + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + final WitValue result = dispatch.bridge( + "stardog:webfunction/planner@0.3.0#cardinality-estimate", + "cardinality_estimate"); + + assertEquals("cardinality_estimate", instance.calls.get(0).exportName, + "bridge() should fall back to the bare path when the interface-scoped one is absent"); + assertNotNull(result); + } + + @Test + @DisplayName("bridge() — throws when neither shape is exported") + void bridgeNeitherShape() { + final RecordingComponentInstance instance = RecordingComponentInstance.oldShape(); + final BridgingSparqlExtensionDispatch dispatch = + new BridgingSparqlExtensionDispatch(instance); + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> dispatch.bridge("no-such-new", "no-such-old")); + assertTrue(ex.getMessage().contains("no-such-new") + && ex.getMessage().contains("no-such-old"), + "diagnostic should name both attempted paths; got: " + ex.getMessage()); + } + + // --- helpers ------------------------------------------------------------------------- + + private static List args(final WitValue... values) throws Exception { + return Arrays.asList(values); + } + + /** + * Fake ComponentInstance that records every {@code invokeWit} call and returns a + * canned per-export result. Two factory presets: + * + *

    + *
  • {@link #newShape()} — advertises the sparql-extension interface exports (filter + + * aggregate) and returns uppercased results plus a stub resource handle. + *
  • {@link #oldShape()} — advertises only bare {@code evaluate} / {@code aggregate-step} / + * {@code aggregate-finish} and returns the same uppercased result on evaluate. + *
+ */ + private static final class RecordingComponentInstance implements ComponentInstance { + + final Set exports; + final List calls = new ArrayList<>(); + final java.util.Map returnValues = new java.util.HashMap<>(); + + private RecordingComponentInstance(final Set exports) { + this.exports = exports; + } + + static RecordingComponentInstance newShape() { + final Set exports = new HashSet<>(); + exports.add(BridgingSparqlExtensionDispatch.NEW_SHAPE_FILTER_EXPORT); + exports.add(BridgingSparqlExtensionDispatch.NEW_SHAPE_AGGREGATE_CTOR); + final RecordingComponentInstance inst = new RecordingComponentInstance(exports); + // "upper" filter behavior: return uppercased first arg. + inst.uppercaseOn(BridgingSparqlExtensionDispatch.NEW_SHAPE_FILTER_EXPORT, /*argIdx*/ 1); + // Aggregate ctor: return a stub resource-shaped Object the shim + // asCallableResource can wrap. We return a marker string here because + // asCallableResource is overridden in this fake to short-circuit. + inst.returnValues.put( + BridgingSparqlExtensionDispatch.NEW_SHAPE_AGGREGATE_CTOR, + "STUB_RESOURCE"); + return inst; + } + + static RecordingComponentInstance oldShape() { + final Set exports = new HashSet<>(); + exports.add(BridgingSparqlExtensionDispatch.OLD_SHAPE_FILTER_EXPORT); + exports.add(BridgingSparqlExtensionDispatch.OLD_SHAPE_AGGREGATE_STEP); + exports.add(BridgingSparqlExtensionDispatch.OLD_SHAPE_AGGREGATE_FINISH); + final RecordingComponentInstance inst = new RecordingComponentInstance(exports); + // Old-shape evaluate takes args as arg[0]. + inst.uppercaseOn(BridgingSparqlExtensionDispatch.OLD_SHAPE_FILTER_EXPORT, /*argIdx*/ 0); + inst.returnValues.put(BridgingSparqlExtensionDispatch.OLD_SHAPE_AGGREGATE_STEP, null); + inst.returnValues.put(BridgingSparqlExtensionDispatch.OLD_SHAPE_AGGREGATE_FINISH, null); + return inst; + } + + /** + * Configure the fake so that calling {@code exportName} returns a WitString containing + * the uppercased value of the first WitString found inside the {@code argIdx}-th arg + * (unwrapping {@link WitList} as needed). + */ + private void uppercaseOn(final String exportName, final int argIdx) { + returnValues.put(exportName, new UppercaseBehavior(argIdx)); + } + + @Override + public Object invoke(final String functionName, final Object... args) { + return invokeWit(functionName, args); + } + + @Override + public Object invokeWit(final String functionName, final Object... args) { + calls.add(new RecordedCall(functionName, args)); + final Object canned = returnValues.get(functionName); + if (canned instanceof UppercaseBehavior) { + return ((UppercaseBehavior) canned).apply(args); + } + return canned; + } + + @Override + public WitCallableResource asCallableResource(final Object resource) { + // In the fake, we don't have real wasmtime resource machinery. Return a passive + // resource handle for the new-shape aggregate ctor test. + return new StubCallableResource((String) resource); + } + + @Override + public boolean hasFunction(final String name) { + return exports.contains(name); + } + + @Override + public List exportedFunctions() { + return new ArrayList<>(exports); + } + + @Override + public List exportedInterfaces() { + return Collections.emptyList(); + } + + @Override + public boolean exportsInterface(final String name) { + return false; + } + + @Override + public Optional function(final String name) { + return Optional.empty(); + } + + @Override + public Optional memory(final String name) { + return Optional.empty(); + } + + @Override + public Optional

table(final String name) { + return Optional.empty(); + } + + @Override + public Optional global(final String name) { + return Optional.empty(); + } + + @Override + public Optional unwrap(final Class nativeType) { + return Optional.empty(); + } + } + + private static final class RecordedCall { + final String exportName; + final Object[] args; + + RecordedCall(final String exportName, final Object[] args) { + this.exportName = exportName; + this.args = args; + } + } + + /** + * Behavior descriptor: unwrap the WitString inside args[argIdx] (unwrapping WitList as + * needed) and return its uppercased value as a fresh WitString. + */ + private static final class UppercaseBehavior { + final int argIdx; + + UppercaseBehavior(final int argIdx) { + this.argIdx = argIdx; + } + + Object apply(final Object[] args) { + final Object slot = args[argIdx]; + final WitString found; + if (slot instanceof WitList) { + final List elems = ((WitList) slot).getElements(); + found = (WitString) elems.get(0); + } else if (slot instanceof WitString) { + found = (WitString) slot; + } else { + throw new IllegalStateException( + "test fake expects WitList or WitString at arg " + argIdx + + "; got " + slot); + } + try { + return WitString.of(found.getValue().toUpperCase()); + } catch (final ai.tegmentum.wasmtime4j.exception.ValidationException e) { + throw new AssertionError("test-fake uppercased string cannot be invalid", e); + } + } + } + + /** + * Minimal WitCallableResource used only to prove the new-shape aggregate path routes + * through {@code asCallableResource} — the real resource machinery is covered by + * {@link WasmtimeCallableResourceTest}. + */ + private static final class StubCallableResource implements WitCallableResource { + private final String tag; + private boolean closed; + + StubCallableResource(final String tag) { + this.tag = tag; + } + + @Override + public String resourceTypeName() { + return tag; + } + + @Override + public Object invokeMethod(final String methodExportName, final Object... args) { + return null; + } + + @Override + public Object invokeMethodWit(final String methodExportName, final Object... args) { + return null; + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + closed = true; + } + } +} From ec2210310361a62e1809475dcba412998245c2de Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 18 Jul 2026 20:04:25 -0400 Subject: [PATCH 28/85] test(bridging): interface-qualified name probe via hasFunction Adds a regression test to `BridgingSparqlExtensionDispatchTest` that drives a real wasmtime component (the counter fixture already used by `WasmtimeCallableResourceTest`) and asserts `ComponentInstance.hasFunction` returns `true` for the interface-qualified export `tegmentum:test-counter/counter-api@0.1.0#[method]counter.value` and for the interface-scoped constructor, plus a negative-control check on a fake qualified name to ensure the fix isn't masking probes with an unconditional `true`. Before wasmtime4j 46.0.1-1.4.6 the probe returned `false` for every interface-qualified name even when `invokeWit` of the same name succeeded, which silently forced `BridgingSparqlExtensionDispatch#filterIsNewShape` to fall back to the old-shape ABI for new-shape guests at construction time. This test would have caught the regression at CI time. Bumps wasmtime4j.version to 46.0.1-1.4.6 to consume the fix. --- pom.xml | 2 +- .../BridgingSparqlExtensionDispatchTest.java | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a4e4ae8..9b9f1e0 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.4.5 + 46.0.1-1.4.6 2.4.4-1.0.2 1.0.1 24.2.2 diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java index 294c731..8cbebf4 100644 --- a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatchTest.java @@ -11,13 +11,20 @@ import ai.tegmentum.wasmtime4j.wit.WitString; import ai.tegmentum.wasmtime4j.wit.WitU64; import ai.tegmentum.wasmtime4j.wit.WitValue; +import ai.tegmentum.webassembly4j.api.Component; import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.DefaultWasiContext; +import ai.tegmentum.webassembly4j.api.Engine; import ai.tegmentum.webassembly4j.api.Function; import ai.tegmentum.webassembly4j.api.Global; import ai.tegmentum.webassembly4j.api.Memory; import ai.tegmentum.webassembly4j.api.Table; import ai.tegmentum.webassembly4j.api.WitCallableResource; import ai.tegmentum.webassembly4j.api.exception.ExecutionException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -27,6 +34,7 @@ import java.util.Set; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; /** * Round-trip verification for {@link BridgingSparqlExtensionDispatch}. Constructs two fake @@ -232,8 +240,85 @@ void bridgeNeitherShape() { "diagnostic should name both attempted paths; got: " + ex.getMessage()); } + /** + * Regression: {@link ComponentInstance#hasFunction(String)} must agree with the invocation + * path on interface-qualified export names ({@code :@#}). + * + *

Before the wasmtime4j fix this probe returned {@code false} for interface-scoped + * exports even when {@code invokeWit} of the same qualified name succeeded — which broke + * {@link BridgingSparqlExtensionDispatch#filterIsNewShape} at construction time (the + * constructor asked {@code hasFunction("tegmentum:webfunction/extension@0.1.0#call")} and + * always mis-detected new-shape guests as old-shape). Rather than reuse the webfunction + * artifact this test drives the counter component fixture that already backs the callable + * resource smoke test — it exports {@link #COUNTER_VALUE_METHOD} solely through the + * {@code counter-api} interface, so a passing probe here proves the qualified-name lookup + * on the underlying wasmtime4j {@code ComponentInstance} is fixed. A negative-control + * check on a fake export name guards against the "always returns true" failure mode. + */ + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("regression: hasFunction agrees with invokeWit on interface-qualified names") + void hasFunctionRecognizesInterfaceQualifiedExport() throws Exception { + final byte[] wasm = loadCounterComponent(); + try (Engine engine = WasmtimeEngineAdapter.create(null); + Component component = engine.loadComponent(wasm)) { + final ComponentInstance instance = component.instantiate(wasiLinking()); + + assertTrue( + instance.hasFunction(COUNTER_VALUE_METHOD), + "hasFunction must recognize the interface-qualified export " + + COUNTER_VALUE_METHOD + + " that the counter guest exports only via counter-api; if this " + + "regressed, BridgingSparqlExtensionDispatch#filterIsNewShape will " + + "silently fall back to old-shape dispatch for new-shape guests"); + assertTrue( + instance.hasFunction(COUNTER_CTOR_EXPORT), + "and it must also see the interface-scoped constructor export"); + assertFalse( + instance.hasFunction( + "tegmentum:test-counter/counter-api@0.1.0#[method]counter.does-not-exist"), + "sanity check: an unknown qualified name still returns false — otherwise the " + + "fix would be masking every probe with an unconditional true"); + } + } + // --- helpers ------------------------------------------------------------------------- + static boolean runtimeAvailable() { + return new WasmtimeProvider().availability().available(); + } + + private static final String COUNTER_INTERFACE = + "tegmentum:test-counter/counter-api@0.1.0"; + private static final String COUNTER_CTOR_EXPORT = + COUNTER_INTERFACE + "#[constructor]counter"; + private static final String COUNTER_VALUE_METHOD = + COUNTER_INTERFACE + "#[method]counter.value"; + + private static DefaultLinkingContext wasiLinking() { + return DefaultLinkingContext.builder() + .wasiContext(DefaultWasiContext.builder().build()) + .build(); + } + + private static byte[] loadCounterComponent() throws IOException { + try (InputStream is = + BridgingSparqlExtensionDispatchTest.class.getResourceAsStream( + "/counter_component.wasm")) { + assertNotNull( + is, + "counter_component.wasm not on classpath — the Maven build copies it into " + + "src/test/resources/ from src/test/rust/counter_component/"); + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + final byte[] chunk = new byte[8192]; + int n; + while ((n = is.read(chunk)) != -1) { + buf.write(chunk, 0, n); + } + return buf.toByteArray(); + } + } + private static List args(final WitValue... values) throws Exception { return Arrays.asList(values); } From 4dba95d5e8a2f163a7233bd25e9229e764180c0f Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 19 Jul 2026 06:26:58 -0400 Subject: [PATCH 29/85] fix(bridging): detect new-shape aggregate/filter via exportsInterface fallback BridgingSparqlExtensionDispatch#aggregateIsNewShape used only ComponentInstance#hasFunction("tegmentum:webfunction/aggregate@ 0.1.0#new-aggregate") for shape detection. On wasmtime4j runtimes older than 46.0.1-1.4.6 (the majority of downstream plugin builds) the probe returned false for interface-qualified export names, so new-shape guests mis-detected as old-shape and dispatch trapped against absent flat aggregate-step exports. Plugins worked around the same issue for filter dispatch via a plugin-side instance.exportsInterface(...) check but had no equivalent for aggregate. Fix hoists the fallback into the bridging helper: shape detection now returns true when either the qualified export probe or the containing interface probe succeeds. Also: - unwrap the new-aggregate ctor's WitResult ok-arm before asCallableResource (WitOwn/WitBorrow expected) - wrap the returned resource so callers can dispatch step/finish via the neutral method vocabulary the ShimAggregateResource already accepts (wasmtime resource-method dispatch wants the fully qualified [method]aggregate-state. path) Bumps webassembly4j to 2.4.2. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- .../BridgingSparqlExtensionDispatch.java | 140 +++++++++++++++++- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 16 files changed, 152 insertions(+), 18 deletions(-) diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 3d93d46..6c9f4cb 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index d4d7f11..b567c2c 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 9b9f1e0..561729c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index d1425f2..10ee49a 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 6491fac..41a2725 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 0901d23..e04f50f 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 wasmtime4j-provider diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java index 6e01211..2850f07 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/BridgingSparqlExtensionDispatch.java @@ -50,6 +50,16 @@ public final class BridgingSparqlExtensionDispatch { static final String NEW_SHAPE_FILTER_EXPORT = "tegmentum:webfunction/extension@0.1.0#call"; + /** + * New-shape filter interface — the parent interface hosting {@link #NEW_SHAPE_FILTER_EXPORT}. + * Used as a shape-detection fallback for older wasmtime4j runtimes whose {@code hasFunction} + * probe doesn't recognize interface-qualified export names (fixed in wasmtime4j 46.0.1-1.4.6 + * via commit 30f7fc5b, but downstream plugins on older wasmtime4j jars still hit the + * limitation). + */ + static final String NEW_SHAPE_FILTER_INTERFACE = + "tegmentum:webfunction/extension@0.1.0"; + /** * Old-shape filter call — a bare {@code evaluate} function exported at the guest world's root. */ @@ -62,6 +72,13 @@ public final class BridgingSparqlExtensionDispatch { static final String NEW_SHAPE_AGGREGATE_CTOR = "tegmentum:webfunction/aggregate@0.1.0#new-aggregate"; + /** + * New-shape aggregate interface — the parent interface hosting {@link + * #NEW_SHAPE_AGGREGATE_CTOR}. Same fallback rationale as {@link #NEW_SHAPE_FILTER_INTERFACE}. + */ + static final String NEW_SHAPE_AGGREGATE_INTERFACE = + "tegmentum:webfunction/aggregate@0.1.0"; + /** * New-shape aggregate methods. The plugin-side driver calls {@code step} and {@code finish} * on the {@link WitCallableResource} returned by {@link #newAggregate(String)}; these @@ -99,8 +116,20 @@ public final class BridgingSparqlExtensionDispatch { */ public BridgingSparqlExtensionDispatch(final ComponentInstance instance) { this.instance = Objects.requireNonNull(instance, "instance"); - this.filterIsNewShape = instance.hasFunction(NEW_SHAPE_FILTER_EXPORT); - this.aggregateIsNewShape = instance.hasFunction(NEW_SHAPE_AGGREGATE_CTOR); + // Detection: try the qualified-export probe first (fast, exact — a passing + // `hasFunction` guarantees the invocation path will succeed). Fall back to + // `exportsInterface` when it misses — on wasmtime4j runtimes older than + // 46.0.1-1.4.6 the qualified-name probe returns false even when the export + // is present, so without the interface-level fallback new-shape guests get + // mis-detected as old-shape and dispatch traps on absent flat exports. The + // filter side has carried this fallback at the plugin layer since the + // limitation was diagnosed; hoisting it into the bridging helper lets the + // aggregate side (which the plugins do not work around) reach new-shape + // guests too, and lets the plugin-side filter workaround retire. + this.filterIsNewShape = detectNewShape( + instance, NEW_SHAPE_FILTER_EXPORT, NEW_SHAPE_FILTER_INTERFACE); + this.aggregateIsNewShape = detectNewShape( + instance, NEW_SHAPE_AGGREGATE_CTOR, NEW_SHAPE_AGGREGATE_INTERFACE); if (LOG.isLoggable(Level.FINE)) { LOG.log( Level.FINE, @@ -194,13 +223,66 @@ public WitValue callFilter(final String functionName, final List args) public WitCallableResource newAggregate(final String aggregateName) { Objects.requireNonNull(aggregateName, "aggregateName"); if (aggregateIsNewShape) { + // new-aggregate: func(name: string) -> result. + // The wire return is a WitResult; asCallableResource wants the ok-arm + // resource handle directly (a WitOwn/WitBorrow/WitResource, per the + // provider adapter's contract). Unwrap here so callers don't have to + // know the ctor is result-typed — the shim's error surface stays a + // plain ExecutionException carrying the guest's error message. final Object handle = instance.invokeWit( NEW_SHAPE_AGGREGATE_CTOR, witString(aggregateName)); - return instance.asCallableResource(handle); + final WitCallableResource raw = + instance.asCallableResource(unwrapResultOk(handle, NEW_SHAPE_AGGREGATE_CTOR)); + // Wrap so callers can dispatch with the neutral method names + // ("step"/"finish") the shim also accepts — the wasmtime resource + // machinery underneath wants the fully-qualified WIT method export + // name and doesn't know about the neutral vocabulary. + return new NeutralMethodNameResource(raw); } return new ShimAggregateResource(instance); } + /** + * Unwrap the ok arm of a {@link ai.tegmentum.wasmtime4j.wit.WitResult}. Non-result values pass + * through unchanged — old-shape or degenerate guest returns don't get wrapped in a result on + * the wire, and this method is only load-bearing on the new-shape ctor path where the WIT + * signature ({@code result}) forces the wrapper. An err arm becomes + * an {@link ExecutionException} carrying the guest's message; a null/absent ok payload throws + * the same to name the export that surfaced the empty result. + */ + private static Object unwrapResultOk(final Object handle, final String exportName) { + if (!(handle instanceof ai.tegmentum.wasmtime4j.wit.WitResult)) { + return handle; + } + final ai.tegmentum.wasmtime4j.wit.WitResult wr = + (ai.tegmentum.wasmtime4j.wit.WitResult) handle; + if (wr.isErr()) { + final String message = wr.getErr() + .map(v -> v instanceof WitString ? ((WitString) v).getValue() : v.toString()) + .orElse("component returned err with no payload"); + throw new ExecutionException(exportName + " returned err: " + message); + } + return wr.getOk().orElseThrow(() -> new ExecutionException( + exportName + " returned ok with no payload")); + } + + /** + * Shape-detection helper. Returns true when the guest exports the new-shape entry either + * as a qualified function ({@code hasFunction}) or transitively via its containing WIT + * interface ({@code exportsInterface}). + * + *

The two probes are not redundant: {@code hasFunction} is the exact answer but requires + * wasmtime4j 46.0.1-1.4.6+ to recognize interface-qualified names, and downstream plugins + * pin older wasmtime4j jars in ways this helper cannot control. {@code exportsInterface} + * catches every case where the qualified probe misses. + */ + private static boolean detectNewShape( + final ComponentInstance instance, + final String qualifiedExport, + final String interfaceName) { + return instance.hasFunction(qualifiedExport) || instance.exportsInterface(interfaceName); + } + /** * Adapter over {@link WitString#of(String)}, which declares a checked * {@link ai.tegmentum.wasmtime4j.exception.ValidationException} for the null-arg case that @@ -258,6 +340,58 @@ public WitValue bridge( + "' is present on the component instance"); } + /** + * Adapter over a real wasmtime {@link WitCallableResource} that also accepts the neutral + * method names ({@code "step"} / {@code "finish"}) the {@link ShimAggregateResource} accepts. + * Translates them to the fully qualified WIT method-export paths + * ({@link #NEW_SHAPE_AGGREGATE_STEP} / {@link #NEW_SHAPE_AGGREGATE_FINISH}) that + * {@code invokeResourceMethodWit} expects on wasmtime. Fully-qualified names pass through + * unchanged so a caller that isn't going through the bridging vocabulary still works. + */ + static final class NeutralMethodNameResource implements WitCallableResource { + + private final WitCallableResource delegate; + + NeutralMethodNameResource(final WitCallableResource delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public String resourceTypeName() { + return delegate.resourceTypeName(); + } + + @Override + public Object invokeMethod(final String methodExportName, final Object... args) { + return delegate.invokeMethod(translate(methodExportName), args); + } + + @Override + public Object invokeMethodWit(final String methodExportName, final Object... args) { + return delegate.invokeMethodWit(translate(methodExportName), args); + } + + private static String translate(final String methodExportName) { + if (SHIM_METHOD_STEP.equals(methodExportName)) { + return NEW_SHAPE_AGGREGATE_STEP; + } + if (SHIM_METHOD_FINISH.equals(methodExportName)) { + return NEW_SHAPE_AGGREGATE_FINISH; + } + return methodExportName; + } + + @Override + public boolean isClosed() { + return delegate.isClosed(); + } + + @Override + public void close() { + delegate.close(); + } + } + /** * Shim {@link WitCallableResource} that fronts a pair of flat exports * ({@code aggregate-step} / {@code aggregate-finish}) as if they were methods on a WIT diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index b735add..00d21f1 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index b5cb1ef..184c669 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index e66a830..ba408a9 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 5f3010a..d050bfc 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 2751256..5f395d5 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 07b16b2..b1b2e2b 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index a3ebbb5..1f0d4d8 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index cb48eb4..315bf2e 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index b30eece..64ba07b 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.1 + 2.4.2 webassembly4j-testing From 1a583bdbffb8ea83f2909dc7defdb4eae3b12b76 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 19 Jul 2026 09:45:45 -0400 Subject: [PATCH 30/85] feat(api): expose consumeFuel + fuelConsumed on ComponentInstance interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two default methods to webassembly4j-api's ComponentInstance so providers can surface the wasmtime4j 1.4.7 fuel accessors: - consumeFuel(long amount): default throws UnsupportedFeatureException. The wasmtime4j-provider overrides to delegate to the underlying wasmtime4j ComponentInstance's consumeFuel. - fuelConsumed(): default returns -1 (unsupported sentinel). The wasmtime4j-provider overrides to delegate to fuelConsumed. Providers whose engine is not fuel-metered (endive, graalwasm, wamr, wasm3) inherit the defaults — no changes. Test: WasmtimeComponentFuelAccountingTest covers the three key behaviours on a real fuel-metered component instance built from the existing counter_component: - consumeFuel(K) then fuelConsumed() returns K - consumeFuel beyond remaining throws + leaves store fuel unchanged - a real guest invocation shows non-zero fuelConsumed afterwards Bumps webassembly4j to 2.4.3 and pins wasmtime4j.version to 46.0.1-1.4.7. Closes the upstream gap that forced the stardog-plugin Phase 1 fuel- metering Java-side counter workaround. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 4 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- .../WasmtimeComponentInstanceAdapter.java | 21 ++ .../WasmtimeComponentFuelAccountingTest.java | 183 ++++++++++++++++++ webassembly4j-api/pom.xml | 2 +- .../webassembly4j/api/ComponentInstance.java | 44 +++++ webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 18 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentFuelAccountingTest.java diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 6c9f4cb..0fb3d62 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index b567c2c..2336959 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 561729c..a927fe5 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 pom WebAssembly4J Parent @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.4.6 + 46.0.1-1.4.7 2.4.4-1.0.2 1.0.1 24.2.2 diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 10ee49a..5066b53 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 41a2725..53ab1b1 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index e04f50f..29618f0 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 wasmtime4j-provider diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java index 1490f24..0fc9f4b 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentInstanceAdapter.java @@ -228,6 +228,27 @@ public Optional global(String name) { return Optional.empty(); } + @Override + public void consumeFuel(long amount) { + if (amount < 0L) { + throw new IllegalArgumentException("amount must be non-negative, got " + amount); + } + try { + nativeInstance.consumeFuel(amount); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException("Failed to consume fuel: " + e.getMessage(), e); + } + } + + @Override + public long fuelConsumed() { + try { + return nativeInstance.fuelConsumed(); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException("Failed to read fuel consumed: " + e.getMessage(), e); + } + } + @Override @SuppressWarnings("unchecked") public Optional unwrap(Class nativeType) { diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentFuelAccountingTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentFuelAccountingTest.java new file mode 100644 index 0000000..5412966 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentFuelAccountingTest.java @@ -0,0 +1,183 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.DefaultWasiContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.config.ComponentConfig; +import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; +import ai.tegmentum.webassembly4j.provider.wasmtime.config.WasmtimeConfig; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +/** + * Exercises {@link ComponentInstance#consumeFuel(long)} + {@link ComponentInstance#fuelConsumed()} + * end-to-end through the wasmtime4j provider — the wasmtime4j 1.4.7 accessors that let the host + * charge fuel against the same wasmtime store the guest consumes, and read back the amount spent. + * + *

Reuses the checked-in counter component (see {@code src/test/rust/counter_component/}) — this + * test only cares that the component instantiates + invocations actually burn fuel; the counter + * semantics themselves are covered by {@link WasmtimeCallableResourceTest}. + */ +class WasmtimeComponentFuelAccountingTest { + + private static final String COUNTER_INTERFACE = "tegmentum:test-counter/counter-api@0.1.0"; + private static final String CTOR_EXPORT = COUNTER_INTERFACE + "#[constructor]counter"; + private static final String INCREMENT_METHOD = + COUNTER_INTERFACE + "#[method]counter.increment"; + + static boolean runtimeAvailable() { + return new WasmtimeProvider().availability().available(); + } + + private static byte[] loadCounterComponent() throws IOException { + try (InputStream is = + WasmtimeComponentFuelAccountingTest.class.getResourceAsStream( + "/counter_component.wasm")) { + assertNotNull(is, "counter_component.wasm not on classpath"); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = is.read(chunk)) != -1) { + buf.write(chunk, 0, n); + } + return buf.toByteArray(); + } + } + + /** + * A cargo-component-built component imports {@code wasi:cli} / {@code wasi:io} for its + * ambient environment, so instantiation always needs a WASI context — an empty one is enough + * since these tests never touch the filesystem or network. + */ + private static DefaultLinkingContext wasiLinking() { + return DefaultLinkingContext.builder() + .wasiContext(DefaultWasiContext.builder().build()) + .build(); + } + + /** + * Enable fuel metering on the wasmtime engine so the provider actually pushes the + * {@link ComponentConfig#fuelLimit()} down as a hard cap on the store the guest runs against. + * Without this the provider treats the config as advisory only (see + * {@code WasmtimeComponentAdapter#newStore} — the {@code setFuel} call is gated on + * {@code engine.isFuelEnabled()}) and consumeFuel wouldn't have a real cap to enforce. + */ + private static Engine engineWithFuel() { + WebAssemblyConfig cfg = WebAssemblyConfig.builder() + .engineConfig(WasmtimeConfig.builder().consumeFuel(true).build()) + .build(); + return WasmtimeEngineAdapter.create(cfg); + } + + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("consumeFuel(K) then fuelConsumed() returns K (against a fresh instance)") + void consumeFuelAndReadItBack() throws Exception { + byte[] wasm = loadCounterComponent(); + final long fuelCap = 1_000_000L; + try (Engine engine = engineWithFuel(); + Component component = engine.loadComponent(wasm)) { + ComponentInstance instance = + component.instantiate( + wasiLinking(), ComponentConfig.builder().fuelLimit(fuelCap).build()); + + // Baseline is captured at instantiation; nothing has burned fuel yet. + assertEquals( + 0L, + instance.fuelConsumed(), + "fresh instance should report zero fuel consumed against the just-set baseline"); + + instance.consumeFuel(1234L); + assertEquals( + 1234L, + instance.fuelConsumed(), + "fuelConsumed() must reflect the exact host-side deduction"); + + instance.consumeFuel(766L); + assertEquals( + 2000L, + instance.fuelConsumed(), + "successive consumeFuel calls must be additive against the baseline"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("consumeFuel beyond the store's remaining fuel throws (no partial deduction)") + void consumeFuelBeyondRemainingThrows() throws Exception { + byte[] wasm = loadCounterComponent(); + // Big enough for cargo-component's wasi:cli init (~16 KiB of fuel), then leaves plenty + // of headroom for the consumeFuel arithmetic below. + final long fuelCap = 100_000L; + try (Engine engine = engineWithFuel(); + Component component = engine.loadComponent(wasm)) { + ComponentInstance instance = + component.instantiate( + wasiLinking(), ComponentConfig.builder().fuelLimit(fuelCap).build()); + + // Burn 4000 units first so the failure isn't just "asked for > cap on a fresh store" + // — it's "asked for > remaining after a real prior deduction". + instance.consumeFuel(4_000L); + assertEquals(4_000L, instance.fuelConsumed()); + + // Ask for far more than what remains and observe the failure. Use a value larger than + // the WHOLE cap so the test doesn't have to reason about exactly how much fuel wasm + // init consumed. + final long tooMuch = fuelCap + 1_000L; + ExecutionException ex = + assertThrows( + ExecutionException.class, + () -> instance.consumeFuel(tooMuch), + "deducting more than the remaining fuel must throw"); + assertTrue( + ex.getMessage().toLowerCase().contains("insufficient") + || (ex.getCause() != null + && ex.getCause() + .getMessage() + .toLowerCase() + .contains("insufficient")), + "message should identify the insufficiency; got: " + ex.getMessage()); + assertEquals( + 4_000L, + instance.fuelConsumed(), + "a failed consumeFuel must leave the store's remaining fuel unchanged"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + @DisplayName("guest invocation actually burns store fuel — fuelConsumed reflects it") + void guestInvocationBurnsStoreFuel() throws Exception { + byte[] wasm = loadCounterComponent(); + final long fuelCap = 10_000_000L; + try (Engine engine = engineWithFuel(); + Component component = engine.loadComponent(wasm)) { + ComponentInstance instance = + component.instantiate( + wasiLinking(), ComponentConfig.builder().fuelLimit(fuelCap).build()); + + // Drive a real component call. Not asserting the resource plumbing here (that's the + // callable-resource test); the value returned is a WitOwn — we only care that a wasm + // call executed, so that fuelConsumed reads a positive amount afterwards. + Object handle = instance.invokeWit(CTOR_EXPORT); + assertNotNull(handle); + + long afterCtor = instance.fuelConsumed(); + assertTrue( + afterCtor > 0L, + "constructor invocation should burn some wasm fuel; got " + afterCtor); + } + } +} diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 00d21f1..5787a1f 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-api diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java index 4435cbb..8848ec5 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ComponentInstance.java @@ -157,4 +157,48 @@ default T runConcurrent(ConcurrentTask task) { throw new UnsupportedFeatureException( "Concurrent component execution not supported"); } + + /** + * Decrement the underlying store's remaining fuel by {@code amount}. + * + *

Lets the host charge fuel against the same budget the guest consumes so a Java-side + * "toll" is enforced against real store fuel — the guest traps once the store is exhausted, + * just as if the fuel had been burned by wasm instructions. Complements + * {@link #fuelConsumed()}: what the host debits here shows up in the actual-consumed reading. + * + *

Providers whose engine is not fuel-metered (or that don't host a store per instance) + * throw {@link UnsupportedFeatureException} by default. Providers that support fuel override. + * + * @param amount the units of fuel to deduct (must be non-negative) + * @throws UnsupportedFeatureException if the provider does not support store-level fuel + * accounting + * @throws IllegalArgumentException if {@code amount} is negative + * @throws ai.tegmentum.webassembly4j.api.exception.ExecutionException if the deduction + * would exceed the currently-remaining fuel (no partial deduction) or the underlying + * fuel operation fails + * @since 2.4.3 + */ + default void consumeFuel(long amount) { + throw new UnsupportedFeatureException( + "Provider does not support store-level fuel accounting"); + } + + /** + * Report the amount of fuel consumed against the store since its fuel was last set. + * + *

Set-fuel points are: instantiation (from the caller's fuel cap) and, for providers with + * per-call fuel resets, the start of each invocation. This method returns + * {@code baseline - remaining} relative to the most recent set-fuel — the actual fuel burned + * (wasm instructions plus any {@link #consumeFuel(long)} tolls) since then. + * + *

Providers whose engine is not fuel-metered return {@code -1} (unsupported sentinel). + * Providers that support fuel override. + * + * @return the fuel consumed since the last {@code set_fuel}, or {@code -1} when the provider + * does not support store-level fuel accounting + * @since 2.4.3 + */ + default long fuelConsumed() { + return -1L; + } } diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 184c669..f1ca385 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index ba408a9..dd26079 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index d050bfc..d50d763 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 5f395d5..1ac4ffa 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index b1b2e2b..b908579 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 1f0d4d8..168445b 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index 315bf2e..b1c211c 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 64ba07b..85f289a 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.2 + 2.4.3 webassembly4j-testing From e8f503fb7cc5638fb3b1bca67218e6ac15e909c3 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 20 Jul 2026 06:37:42 -0400 Subject: [PATCH 31/85] feat(webassembly4j-api): WasiContext.wasiHttpEnabled + DefaultWasiContext.Builder.wasiHttp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce an opt-in wasi:http hint on the provider-neutral WasiContext so policies can request that the wasi:http/outgoing-handler + types import surface be published on a component's linker without a bespoke host call. Default false — no behavioural change for existing callers. The interface method has a default so third-party WasiContext implementations pick it up without a source-incompatible signature change. Publishing the imports does not itself grant network egress; the usual allowNetwork / egressRules gates still apply to any HTTP connection the guest attempts. Providers that do not support wasi:http may either surface an UnsupportedFeatureException or silently no-op when the flag is set — the policy per provider is documented on the provider's adapter. --- .../webassembly4j/api/DefaultWasiContext.java | 23 +++++++++++++++++-- .../webassembly4j/api/WasiContext.java | 17 ++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java index b9f9057..93c858d 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultWasiContext.java @@ -18,13 +18,15 @@ public final class DefaultWasiContext implements WasiContext { private final Map preopenGuestPaths; private final boolean allowNetwork; private final List egressRules; + private final boolean wasiHttpEnabled; private DefaultWasiContext(List args, Map env, boolean inheritStdin, boolean inheritStdout, boolean inheritStderr, List preopenDirs, List readOnlyPreopenDirs, Map preopenGuestPaths, - boolean allowNetwork, List egressRules) { + boolean allowNetwork, List egressRules, + boolean wasiHttpEnabled) { this.args = Collections.unmodifiableList(new ArrayList<>(args)); this.env = Collections.unmodifiableMap(new LinkedHashMap<>(env)); this.inheritStdin = inheritStdin; @@ -37,6 +39,7 @@ private DefaultWasiContext(List args, Map env, Collections.unmodifiableMap(new LinkedHashMap<>(preopenGuestPaths)); this.allowNetwork = allowNetwork; this.egressRules = Collections.unmodifiableList(new ArrayList<>(egressRules)); + this.wasiHttpEnabled = wasiHttpEnabled; } @Override @@ -89,6 +92,11 @@ public List egressRules() { return egressRules; } + @Override + public boolean wasiHttpEnabled() { + return wasiHttpEnabled; + } + public static Builder builder() { return new Builder(); } @@ -105,6 +113,7 @@ public static final class Builder { private final Map preopenGuestPaths = new LinkedHashMap<>(); private boolean allowNetwork; private final List egressRules = new ArrayList<>(); + private boolean wasiHttpEnabled; private Builder() { } @@ -190,10 +199,20 @@ public Builder allowEgress(NetworkEgressRule rule) { return this; } + /** + * Ask the provider to publish the {@code wasi:http} import surface for this instantiation. + * Default {@code false}. Publishing the imports does not by itself grant network access — the + * usual {@link #allowNetwork(boolean)} / egress rules still gate what the guest can reach. + */ + public Builder wasiHttp(boolean enable) { + this.wasiHttpEnabled = enable; + return this; + } + public DefaultWasiContext build() { return new DefaultWasiContext(args, env, inheritStdin, inheritStdout, inheritStderr, preopenDirs, readOnlyPreopenDirs, preopenGuestPaths, - allowNetwork, egressRules); + allowNetwork, egressRules, wasiHttpEnabled); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java index f0fac23..010ad92 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/WasiContext.java @@ -79,4 +79,21 @@ default List egressRules() { default Optional fsAccessObserver() { return Optional.empty(); } + + /** + * Whether the {@code wasi:http/outgoing-handler + types} import surface should be published on the + * component linker for this instantiation. Default {@code false}: no behavioural change for existing + * embedders. When {@code true}, a provider that supports {@code wasi:http} enables the surface so a + * guest that imports {@code wasi:http/outgoing-handler} can instantiate without a bespoke host call; + * providers that do not support {@code wasi:http} may either surface {@link + * ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException} or silently no-op (documented + * per-provider). + * + *

This flag only publishes the imports. It does not itself grant network egress — the + * provider's usual {@link #allowNetwork()} / {@link #egressRules()} gates still apply to any HTTP + * connection the guest attempts. + */ + default boolean wasiHttpEnabled() { + return false; + } } From 0ebc9ae68cc1f664a5301a92735d51d74ae963b2 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 20 Jul 2026 07:01:46 -0400 Subject: [PATCH 32/85] feat(wasmtime4j-provider): forward WasiContext.wasiHttpEnabled to wasmtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read wasi.wasiHttpEnabled() in WasmtimeComponentAdapter.toWasiPreview2Config and forward it to WasiPreview2Config.Builder.wasiHttp(true). The wasmtime ComponentLinker then invokes enableWasiHttp() after preview 2 is enabled, so a component that imports wasi:http/outgoing-handler + types instantiates off this provider without a bespoke host call. The flag only publishes the imports; it does not itself grant network egress. The existing allowNetwork + egressRules gates still apply to any HTTP connection the guest attempts. Non-wasmtime providers (endive/chicory/wamr/graalwasm) can pick up the API signal later; their adapters are free to no-op, log, or throw UnsupportedFeatureException on the flag — the policy is provider-local. --- .../provider/wasmtime/WasmtimeComponentAdapter.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index e2763e3..e83e56d 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -255,6 +255,13 @@ static WasiPreview2Config toWasiPreview2Config(WasiContext wasi) { (path, operation, reason, errorCode) -> neutral.onDenied(path, operation, reason, errorCode)); } + // wasi:http opt-in: forward the framework-neutral flag to wasmtime4j's WasiPreview2Config, + // which drives ComponentLinker.enableWasiHttp() after preview 2 is enabled. Default off; + // publishing the imports does not itself grant egress — the network egress rules above still + // gate what the guest can reach. + if (wasi.wasiHttpEnabled()) { + b.wasiHttp(true); + } return b.build(); } From cf5bfaaec6373177363aa5dcb0d12624640c6954 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 20 Jul 2026 07:01:54 -0400 Subject: [PATCH 33/85] test(wasmtime4j-provider): wasi:http toggle empirically verified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unit-level assertions that a WasiContext with wasiHttp(true) rides through toWasiPreview2Config into WasiPreview2Config.isWasiHttp()=true, and that the default and explicit-false paths both leave the flag off. Also assert that opting in does NOT implicitly grant network egress — publishing the imports is orthogonal from the network gate. End-to-end wasi:http linker invocation is already covered by wasmtime4j's own JniComponentLinkerValidationTest / ComponentLinkerTest; the mapping seam is the only surface the provider layer owns. --- .../WasmtimeComponentAdapterWasiTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java index a36c140..704fba1 100644 --- a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapterWasiTest.java @@ -61,5 +61,31 @@ void emptyContextIsDenyAll() { assertTrue(cfg.getPreopenDirs().isEmpty()); assertFalse(cfg.isAllowNetwork()); assertFalse(cfg.isInheritStdout()); + assertFalse(cfg.isWasiHttp(), "wasi:http must default off"); + } + + @Test + @DisplayName("WasiContext.wasiHttp(true) is forwarded to WasiPreview2Config.isWasiHttp()") + void forwardsWasiHttpEnabled() { + DefaultWasiContext wasi = DefaultWasiContext.builder().wasiHttp(true).build(); + + WasiPreview2Config cfg = WasmtimeComponentAdapter.toWasiPreview2Config(wasi); + + assertTrue(cfg.isWasiHttp(), + "wasiHttpEnabled=true on WasiContext must forward to WasiPreview2Config.isWasiHttp()"); + // Publishing the imports is orthogonal to network-egress: the flag alone must not open the + // network. Callers must still opt in via allowNetwork + egressRules. + assertFalse(cfg.isAllowNetwork(), + "wasi:http opt-in must not implicitly grant network egress"); + } + + @Test + @DisplayName("WasiContext.wasiHttp(false) leaves the flag off") + void wasiHttpFalseIsForwarded() { + DefaultWasiContext wasi = DefaultWasiContext.builder().wasiHttp(false).build(); + + WasiPreview2Config cfg = WasmtimeComponentAdapter.toWasiPreview2Config(wasi); + + assertFalse(cfg.isWasiHttp()); } } From 2b55a581c68dd8200cb4234273834fde3fa20d4e Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 20 Jul 2026 16:40:42 -0400 Subject: [PATCH 34/85] fix(gc): restore isMutable() accessor on java22 GcStructType.Field record The Java 22 MRJAR overlay converts GcStructType.Field to a record, whose component-derived accessor is mutable() (no is- prefix). The Java 8 base class exposes isMutable(). Consumers compiled against the base variant (e.g. WasmtimeGcExtension.convertStructType) call isMutable() and blow up with NoSuchMethodError on Java 22+ JVMs, which prefer the java22 class over the base entry. Add an explicit isMutable() alias to the record for binary compatibility with the base variant. Fixes WasmtimeGcExtensionTest.* NoSuchMethodError reproducible on a clean 2.4.3 build under JDK 22+. --- .../ai/tegmentum/webassembly4j/api/gc/GcStructType.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/webassembly4j-api/src/main/java22/ai/tegmentum/webassembly4j/api/gc/GcStructType.java b/webassembly4j-api/src/main/java22/ai/tegmentum/webassembly4j/api/gc/GcStructType.java index aef4ddb..3b811b5 100644 --- a/webassembly4j-api/src/main/java22/ai/tegmentum/webassembly4j/api/gc/GcStructType.java +++ b/webassembly4j-api/src/main/java22/ai/tegmentum/webassembly4j/api/gc/GcStructType.java @@ -27,6 +27,15 @@ public record Field(String name, GcFieldType type, boolean mutable) { Objects.requireNonNull(type, "type"); } + /** + * Alias for {@link #mutable()} preserved for binary compatibility with the + * base-release class variant, whose accessor is named {@code isMutable()}. + * Consumers compiled against the base variant call {@code isMutable()}; + * without this method the java22 record overlay throws + * {@link NoSuchMethodError} at runtime on Java 22+ JVMs. + */ + public boolean isMutable() { return mutable; } + @Override public String toString() { return (name != null ? name : "") + ": " + type + (mutable ? " (mut)" : ""); From bd46b86e41fe37e27c921e931d4dc3e2c98e14df Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 21 Jul 2026 17:59:08 -0400 Subject: [PATCH 35/85] chore(deps): bump wasmtime4j to 47.0.2-1.5.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a927fe5..0465af3 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 5.14.4 - 46.0.1-1.4.7 + 47.0.2-1.5.0 2.4.4-1.0.2 1.0.1 24.2.2 From 59d0e9be1a3ae5375f28e579d56597784ed1d682 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 21 Jul 2026 18:17:35 -0400 Subject: [PATCH 36/85] chore(release): 2.4.0 Cuts 2.4.0, a minor bump over 2.3.0 covering: - WASI enhancements: provider-neutral WasiNnConfig + LinkingContext.Builder.enableWasiNn, a supportsWasiNn() capability probe on LinkingContext + Component, and WasiContext.wasiHttpEnabled / DefaultWasiContext.Builder.wasiHttp forwarded through the wasmtime provider to WasiPreview2Config.wasiHttp (imports only; egress is still gated by allowNetwork + egressRules). - Component-model resource dispatch: WitCallableResource surface (invokeMethod / invokeMethodWit / close) plus a wasmtime adapter that accepts WitOwn / WitBorrow from invokeWit. - Fuel accounting on the API: ComponentInstance.consumeFuel(long) + fuelConsumed() default methods, wasmtime provider overrides. - BridgingSparqlExtensionDispatch prototype for JVM plugins straddling the new + old SPARQL extension ABIs, with an exportsInterface fallback for interface-qualified shape detection on older wasmtime4j runtimes. - GcStructType.Field.isMutable() alias restored on the Java 22 MRJAR record variant so downstream code compiled against the base variant no longer NoSuchMethodErrors on JDK 22+. - wasmtime4j bump 46.0.1-1.3.1 -> 47.0.2-1.5.0 (major; carries wasi:nn config, resource-method dispatch, interface-qualified hasFunction, and fuel accessors added across the 1.4.x line). --- CHANGELOG.md | 21 +++++++++++++++++++++ endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 16 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f199272..d887f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 2.4.0 + +### New Features + +- **Provider-neutral `wasi:nn` opt-in**: new `WasiNnConfig` marker in `webassembly4j-api` and `LinkingContext.Builder.enableWasiNn(WasiNnConfig)` let a caller enable wasi:nn on a component without touching a native `ComponentLinker`. The Wasmtime provider translates it to `ai.tegmentum.wasmtime4j.wasi.nn.WasiNnConfig` and calls the native `ComponentLinker#enableWasiNn`. Providers without wasi:nn surface a `WasmException` from their native linker which `InstantiationException` wraps at the API boundary. +- **`supportsWasiNn()` capability probe** on `LinkingContext` + `Component` (default `false`), so callers can gate `enableWasiNn(...)` on the actual provider capability rather than on their own configuration intent. The Wasmtime adapter reports the compile-time `wasi-nn` feature bit of the loaded `libwasmtime4j` native. Closes a regression where plugins calling `enableWasiNn(...)` unconditionally against a wasmtime4j native built without the `wasi-nn` cargo feature would throw at instantiation time even for guests importing zero `wasi:nn` interfaces. +- **`WitCallableResource` — component-model resource-method invocation**: new provider-neutral surface that wraps a component-model resource handle with `invokeMethod` / `invokeMethodWit` / `close`, so JVM plugins driving components via SPARQL SERVICE can address aggregate-state, iterator, and handle-then-method WIT patterns without threading the receiver + instance pair by hand. The Wasmtime provider implements this by binding a wasmtime4j `WitResource` to its originating `ComponentInstance` and delegating through the new `invokeResourceMethodWit` + `dropResource` entry points. Endive / GraalWasm / WAMR providers inherit the default `UnsupportedFeatureException` — extending them is a follow-up when their underlying runtimes grow equivalent hooks. +- **`ComponentInstance.consumeFuel(long)` + `fuelConsumed()`**: default methods on the API `ComponentInstance` surface providers can implement to expose fuel accounting. `consumeFuel` defaults to `UnsupportedFeatureException`, `fuelConsumed` defaults to `-1` (unsupported sentinel). The Wasmtime provider overrides both to delegate to the underlying wasmtime4j `ComponentInstance`. Non-fuel-metered providers (Endive, GraalWasm, WAMR, wasm3) inherit the defaults unchanged. Closes the upstream gap that forced a JVM-side counter workaround for fuel metering. +- **`WasiContext.wasiHttpEnabled` + `DefaultWasiContext.Builder.wasiHttp(...)`**: opt-in `wasi:http` hint on the provider-neutral `WasiContext` so policies can request that the `wasi:http/outgoing-handler` + `types` import surface be published on a component's linker without a bespoke host call. Default `false` — no behavioural change for existing callers. The interface method has a default so third-party `WasiContext` implementations pick it up without a source-incompatible signature change. The Wasmtime provider forwards the flag to `WasiPreview2Config.Builder.wasiHttp(true)`; the wasmtime `ComponentLinker` then invokes `enableWasiHttp()` after preview 2 is enabled. **Publishing the imports does not itself grant network egress** — the existing `allowNetwork` + `egressRules` gates still apply to any HTTP connection the guest attempts. Non-Wasmtime providers may no-op, log, or throw `UnsupportedFeatureException` on the flag; the policy is provider-local. +- **`BridgingSparqlExtensionDispatch` (Wasmtime provider)**: transitional helper that lets JVM plugins dispatch against SPARQL extension guests speaking either the new `sparql-extension` ABI (`tegmentum:webfunction@0.1.0`) or the old flat `evaluate` / `aggregate-step` / `aggregate-finish` ABI. Detects the guest's shape once at construction and caches the result. Intended to be deleted once every guest exports the new `sparql-extension` world. + +### Bug Fixes + +- **WasmGC on JDK 22+**: restore an explicit `isMutable()` accessor on the Java 22 MRJAR variant of `GcStructType.Field` (which was converted to a `record`, whose component accessor is `mutable()` with no `is-` prefix). Downstream consumers compiled against the base variant call `isMutable()` and previously blew up with `NoSuchMethodError` when the java22 class was preferred at runtime. +- **Bridging shape detection**: `BridgingSparqlExtensionDispatch#aggregateIsNewShape` now falls back to `ComponentInstance.exportsInterface(...)` when `hasFunction("...#new-aggregate")` returns `false`. On wasmtime4j runtimes older than `46.0.1-1.4.6` the qualified-export probe returned `false` for interface-qualified export names, so new-shape guests mis-detected as old-shape and dispatch trapped against absent flat aggregate-step exports. Also unwraps the `new-aggregate` ctor's `WitResult` ok-arm before `asCallableResource` and wraps the returned resource so callers can dispatch step/finish via the neutral method vocabulary the shim aggregate resource accepts. +- **Wasmtime resource adapter**: `WasmtimeComponentInstanceAdapter#asCallableResource` now accepts `WitOwn` / `WitBorrow` (discriminators 22/23) in addition to `WitResource`, translating via `WitResource.fromHandle`. The wasmtime WIT deserializer surfaces `own` / `borrow` as `WitOwn` / `WitBorrow`, not `WitResource` — the prior check rejected the shape actually returned by `invokeWit` at run time. + +### Dependency Upgrades + +- wasmtime4j: 46.0.1-1.3.1 → 47.0.2-1.5.0 (major bump — wasmtime 47 with GC + exception-handling enabled by default upstream, wasi-common removed; also carries the underlying `wasi:nn` config, resource-method dispatch, interface-qualified `hasFunction`, and fuel accessors added across the 1.4.x line). + ## 2.3.0 ### New Features diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 0fb3d62..52a94b8 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 2336959..121b1f6 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 0465af3..d682540 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 5066b53..53e57f7 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 53ab1b1..40d2902 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 29618f0..a728aeb 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 5787a1f..629394f 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index f1ca385..132c9de 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index dd26079..3e5baf4 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index d50d763..6868927 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 1ac4ffa..c75b951 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index b908579..27bc3af 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 168445b..514e126 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index b1c211c..f4c35aa 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 85f289a..32b1e8d 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.3 + 2.4.0 webassembly4j-testing From 8dcc53dae6e4c86ff40eb40207dcc7cc5ac01820 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Tue, 21 Jul 2026 19:14:55 -0400 Subject: [PATCH 37/85] ci(release): skip cargo-component rebuild in publish job wasmtime4j-provider binds an exec-maven-plugin execution to the generate-test-resources phase that shells out to `cargo component build --release --target wasm32-wasip2` to regenerate the counter test fixture. It is guarded by -Dskip.rust.build=true. The Test and Quality jobs don't run wasmtime4j-provider's full lifecycle so they never trigger the binding, but the Publish job's install / deploy / package invocations do, and the ubuntu-latest runner has no cargo-component toolchain. Pass -Dskip.rust.build=true on all four publish-job mvn commands (install, Maven Central deploy, GitHub Packages deploy, package) so they consume the checked-in counter_component.wasm and don't try to regenerate it. Reproducer: v2.4.0 tag run (29874324640) failed with `error: no such command: component` at the Install modules locally step, before any deploy step ran. --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fef2abe..5d5bc08 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,7 @@ jobs: run: | DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" ./mvnw install -B -q \ - -DskipTests -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dgpg.skip=true -DskipNativeTests \ + -DskipTests -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -Dgpg.skip=true -DskipNativeTests -Dskip.rust.build=true \ $DEPLOY_MODULES -am - name: Deploy to Maven Central @@ -141,7 +141,7 @@ jobs: run: | DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" ./mvnw deploy -B -P release \ - -DskipTests -Dmaven.test.skip=true -DskipNativeTests \ + -DskipTests -Dmaven.test.skip=true -DskipNativeTests -Dskip.rust.build=true \ $DEPLOY_MODULES - name: Deploy to GitHub Packages @@ -162,7 +162,7 @@ jobs: SETTINGS ./mvnw deploy -B \ - -DskipTests -Dmaven.test.skip=true -DskipNativeTests \ + -DskipTests -Dmaven.test.skip=true -DskipNativeTests -Dskip.rust.build=true \ -DaltDeploymentRepository=github::https://maven.pkg.github.com/${{ github.repository }} \ -Dgpg.skip=true \ -s ~/.m2/settings-github.xml \ @@ -172,7 +172,7 @@ jobs: run: | DEPLOY_MODULES="-pl webassembly4j-api,webassembly4j-spi,wasmtime4j-provider,wamr4j-provider,wasm3-provider,endive4j-provider,graalwasm4j-provider,webassembly4j-runtime,webassembly4j-testing,webassembly4j-pool,webassembly4j-spring,webassembly4j-bindgen" ./mvnw package -B -P release \ - -DskipTests -Dmaven.test.skip=true -DskipNativeTests \ + -DskipTests -Dmaven.test.skip=true -DskipNativeTests -Dskip.rust.build=true \ $DEPLOY_MODULES - name: Create checksums From 062b66e3d02e353d0932b30e5e1b5ea7d6cc8b77 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 22 Jul 2026 06:41:54 -0400 Subject: [PATCH 38/85] =?UTF-8?q?chore(release):=202.5.0=20=E2=80=94=20sur?= =?UTF-8?q?face=20wasmtime4j=2047.0.2-1.5.0=20JNI=20trap-info=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bump so downstream consumers can pick up the wasmtime4j 47.0.2-1.5.0 pin (already landed in bd46b86). Fresh version signals the substrate change — wasmtime4j 47's JNI now preserves the anyhow error chain via {:#} formatting (upstream commit 64d03041), so trap type discriminators (interrupt, fuel exhaustion, etc.) reach Java callers intact instead of being flattened to a generic trap string. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index 52a94b8..cc951c5 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 121b1f6..0446a18 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index d682540..8df1a42 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 53e57f7..2d6c0f8 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 40d2902..2f97ad6 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index a728aeb..316203b 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 629394f..25badcc 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 132c9de..9160a19 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 3e5baf4..4a05972 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 6868927..aa253ba 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index c75b951..020932e 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 27bc3af..5f14940 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 514e126..22ceedb 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index f4c35aa..1dc8b61 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 32b1e8d..2332229 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.4.0 + 2.5.0 webassembly4j-testing From cd95b135cc88ee86466d999a5bb01925eac4b7ce Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Wed, 22 Jul 2026 20:49:12 -0400 Subject: [PATCH 39/85] =?UTF-8?q?chore(deps):=20bump=20wasmtime4j=20to=204?= =?UTF-8?q?7.0.2-1.5.1=20=E2=80=94=20pick=20up=20typed=20trap=20discrimina?= =?UTF-8?q?tors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cascades the 2.5.0 -> 2.5.1 self-version through parent + 14 submodules. Patch bump — the [trap_code:N] prefix embedded by wasmtime4j 1.5.1 is additive on the wire and does not change any Java API surface. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 4 ++-- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index cc951c5..a4c9e12 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 0446a18..258382b 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 8df1a42..9b7d8a7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 pom WebAssembly4J Parent @@ -63,7 +63,7 @@ 5.14.4 - 47.0.2-1.5.0 + 47.0.2-1.5.1 2.4.4-1.0.2 1.0.1 24.2.2 diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index 2d6c0f8..da9909c 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 2f97ad6..3046c75 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 wasm3-provider diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 316203b..496ced7 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index 25badcc..b5c9c35 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 9160a19..6dcf55f 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 4a05972..3f74572 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index aa253ba..1769124 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 020932e..6a2b4a1 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 5f14940..77b3241 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 22ceedb..724afae 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index 1dc8b61..ff60f72 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 2332229..39d1c05 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.0 + 2.5.1 webassembly4j-testing From 1c9e3760e89f410130dbee43954dce20dbbb48ad Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 16:43:48 -0400 Subject: [PATCH 40/85] feat(endive4j-provider): implement Component Model via wasmcm runtime guest Land a real SPI Component surface for endive4j-provider by embedding the portable wasmcm_runtime_guest.wasm inside a companion Endive instance. loadComponent() no longer throws UnsupportedFeatureException; the layered runtime handles parse / instantiate / call-export end to end. Runtime guest wiring - WasmcmGuestBlobLocator resolves wasmcm_runtime_guest.wasm from -Dwasmcm.runtime.guest.wasm, WASMCM_RUNTIME_GUEST_WASM, WASMCM_HOME, the classpath resource, or a ~/git/wasm-cm sibling checkout; falls back cleanly with a lookup-order string embedded in errors. - EndiveEngineAdapter.loadComponent locates the blob, hands it to run.tegmentum.wasmcm.endive.WasmcmRuntimeGuestLoader, drives wasmcm_parse_v2, and returns an EndiveComponentAdapter. Missing blob surfaces as UnsupportedFeatureException naming every knob. - EndiveComponentAdapter.instantiate(LinkingContext) groups WitHostFunctionDefinitions by interface prefix and calls registerHostProvider + registerHostCallback per group, then invokes wasmcm_instantiate_with_imports_v2. Function-index order within a provider is the caller's insertion order. - EndiveComponentInstanceAdapter drives wasmcm_call_export_v2 for invoke() and reads wasmcm_instance_export_count / wasmcm_instance_export_name for hasFunction / exportedFunctions. Codec - WasmcmValueCodec encodes / decodes the guest's [u32 total][u32 count] frame with all 23 tags. Java natural mapping covers Boolean, numeric wrappers, Character, String, byte[], List, Map; a Boxed(tag, payload) envelope is the escape hatch for unsigned widths, variants, options, results, enums, flags, and resource handles. Capabilities - EndiveEngineCapabilities.supportsComponents now reflects runtime-guest presence (delegates to WasmcmGuestBlobLocator). Tests - EndiveComponentIntegrationTest covers the scalar-component round trip (parse, instantiate, invoke add(2,3) = 5), a wasi_p2_clocks instantiation attempt that locks in today's honest failure (Wall 2 on the wasm-cm Rust side is unfinished), and the lookup-order error message shape. - EndiveProviderTest.loadComponentSurfacesFailureCleanly replaces the loadComponentThrowsUnsupported test. - Surefire wires -Dwasmcm.runtime.guest.wasm from a maven property defaulted to the sibling wasm-cm target dir; overridable per-run. Known gaps carried forward as honest failures (no fake success): - Components with real component-level imports (wasi_p2_clocks) surface as HOST_ERR_UNLINKABLE until Wall 2 lands on the wasm-cm side. - Component.exportedInterfaces / importedInterfaces return empty pre-instantiation; the runtime guest exposes exports only after instantiate() succeeds. - Component.serialize and ComponentInstance.asCallableResource keep the SPI defaults (unsupported). Dependency: run.tegmentum.wasmcm:wasmcm-endive-adapter:0.1.0-SNAPSHOT. --- endive4j-provider/pom.xml | 44 ++ .../endive/EndiveComponentAdapter.java | 203 ++++++ .../EndiveComponentInstanceAdapter.java | 177 ++++++ .../provider/endive/EndiveEngineAdapter.java | 21 +- .../endive/EndiveEngineCapabilities.java | 4 +- .../endive/WasmcmGuestBlobLocator.java | 122 ++++ .../provider/endive/WasmcmValueCodec.java | 585 ++++++++++++++++++ .../EndiveComponentIntegrationTest.java | 171 +++++ .../provider/endive/EndiveProviderTest.java | 23 +- .../src/test/resources/scalar-component.wasm | Bin 0 -> 147 bytes .../resources/wasi_p2_clocks.component.wasm | Bin 0 -> 19229 bytes 11 files changed, 1344 insertions(+), 6 deletions(-) create mode 100644 endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentAdapter.java create mode 100644 endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentInstanceAdapter.java create mode 100644 endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmGuestBlobLocator.java create mode 100644 endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmValueCodec.java create mode 100644 endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java create mode 100644 endive4j-provider/src/test/resources/scalar-component.wasm create mode 100644 endive4j-provider/src/test/resources/wasi_p2_clocks.component.wasm diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index a4c9e12..d36fdc5 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -17,6 +17,37 @@ WebAssembly4J Endive Provider Endive pure-Java engine provider for WebAssembly4J + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${wasmcm.runtime.guest.wasm.path} + + + + + + + + + ${user.home}/git/wasm-cm/target/wasm32-unknown-unknown/release/wasmcm_runtime_guest.wasm + 0.1.0-SNAPSHOT + + ai.tegmentum.webassembly4j @@ -39,6 +70,19 @@ compiler true + + + run.tegmentum.wasmcm + wasmcm-endive-adapter + ${wasmcm-endive-adapter.version} + org.junit.jupiter junit-jupiter diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentAdapter.java new file mode 100644 index 0000000..80c490e --- /dev/null +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentAdapter.java @@ -0,0 +1,203 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.WitHostFunctionDefinition; +import ai.tegmentum.webassembly4j.api.exception.InstantiationException; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import run.tegmentum.wasmcm.endive.HostCallbackDispatcher; +import run.tegmentum.wasmcm.endive.HostCallbackException; +import run.tegmentum.wasmcm.endive.WasmcmRuntimeGuestLoader; + +/** + * SPI-facing {@link Component} that layers the Component Model on top of the + * unmodified Endive core-Wasm engine via the {@code wasmcm_runtime_guest.wasm} + * portable Rust runtime. + * + *

The heavy lifting happens in {@link WasmcmRuntimeGuestLoader}: the CM + * component bytes have already been parsed through the guest by the time this + * adapter is constructed (see {@link EndiveEngineAdapter#loadComponent(byte[])}) + * — this class just adapts the {@link Component#instantiate(LinkingContext)} + * surface onto {@code wasmcm_instantiate_v2} / + * {@code wasmcm_instantiate_with_imports_v2} and hands back an + * {@link EndiveComponentInstanceAdapter}. + * + *

Linking context translation

+ * + *

{@link LinkingContext#witHostFunctions()} entries are grouped by their + * WIT-path interface prefix (everything before the {@code '#'}), each group + * registered as a host provider via + * {@link WasmcmRuntimeGuestLoader#registerHostProvider(String, int)}. A single + * {@link HostCallbackDispatcher} per provider routes the guest's + * {@code wasmcm_host_call} extern into the caller's Java implementations by + * function index. + * + *

Function-index order matches the order in which the caller added the + * {@link WitHostFunctionDefinition}s to the linking context — the WIT interface + * is not read back from the component here, so the caller controls the mapping. + * This is stable and deterministic; callers building against a specific WIT + * world should add their host functions in the WIT interface's declared order. + * + *

Note on runtime-guest gaps at this milestone

+ * + *

Registering a host provider only records intent inside the runtime guest; + * the Wall 2 chain that routes a {@code canon lower} against a component-level + * import through {@code wasmcm_host_call} is a work in progress on the wasm-cm + * Rust side (see {@code HostCallbackDispatcher} javadoc for the specific gap). + * When a component that imports interfaces the runtime cannot yet route is + * instantiated, {@code wasmcm_instantiate_with_imports_v2} surfaces + * {@code HOST_ERR_UNLINKABLE} — this adapter re-throws as + * {@link InstantiationException} with the guest's diagnostic message, matching + * the "no fake success" contract. + */ +final class EndiveComponentAdapter implements Component { + + private final WasmcmRuntimeGuestLoader loader; + private final long componentHandle; + private final boolean ownsLoader; + private volatile boolean closed; + + EndiveComponentAdapter( + WasmcmRuntimeGuestLoader loader, long componentHandle, boolean ownsLoader) { + this.loader = loader; + this.componentHandle = componentHandle; + this.ownsLoader = ownsLoader; + } + + @Override + public ComponentInstance instantiate() { + return instantiate((LinkingContext) null); + } + + @Override + public ComponentInstance instantiate(LinkingContext linkingContext) { + checkOpen(); + try { + long instanceHandle; + if (linkingContext == null) { + instanceHandle = loader.instantiate(componentHandle); + } else { + Map imports = registerImports(linkingContext); + if (imports.isEmpty()) { + instanceHandle = loader.instantiate(componentHandle); + } else { + instanceHandle = loader.instantiateWithImports(componentHandle, imports); + } + } + return new EndiveComponentInstanceAdapter(loader, instanceHandle); + } catch (IllegalStateException e) { + throw new InstantiationException( + "Failed to instantiate Component Model component via wasmcm runtime guest: " + + e.getMessage(), + e); + } + } + + /** + * Bind each unique WIT-path interface prefix declared by the linking + * context's {@link LinkingContext#witHostFunctions()} to a provider handle + * and route the guest's {@code wasmcm_host_call} extern to the caller's Java + * implementations. Function index within a provider is the caller's + * insertion order. + */ + private Map registerImports(LinkingContext linkingContext) { + List defs = linkingContext.witHostFunctions(); + // Group by interface prefix (the segment before the final '#'). + Map> byInterface = new LinkedHashMap<>(); + for (WitHostFunctionDefinition def : defs) { + String iface = interfacePrefix(def.witPath()); + byInterface.computeIfAbsent(iface, k -> new java.util.ArrayList<>()).add(def); + } + Map providers = new LinkedHashMap<>(); + for (Map.Entry> e : byInterface.entrySet()) { + String iface = e.getKey(); + java.util.List funcs = e.getValue(); + long providerHandle = loader.registerHostProvider(iface, funcs.size()); + loader.registerHostCallback(providerHandle, new WitDispatcher(funcs)); + providers.put(iface, providerHandle); + } + return providers; + } + + private static String interfacePrefix(String witPath) { + int hash = witPath.indexOf('#'); + return hash < 0 ? "" : witPath.substring(0, hash); + } + + /** + * Routes a {@code wasmcm_host_call} for a given interface into the WIT host + * function at the guest-supplied {@code funcIdx}. Argument / result codec + * matches {@code wasmcm_runtime_guest::codec}. + */ + private static final class WitDispatcher implements HostCallbackDispatcher { + + private final List funcs; + + WitDispatcher(List funcs) { + this.funcs = Collections.unmodifiableList(new java.util.ArrayList<>(funcs)); + } + + @Override + public byte[] dispatch(int funcIdx, byte[] argFrame) throws HostCallbackException { + if (funcIdx < 0 || funcIdx >= funcs.size()) { + throw new HostCallbackException( + "no WIT host function registered at index " + funcIdx + + " (interface has " + funcs.size() + " slots)"); + } + WitHostFunctionDefinition def = funcs.get(funcIdx); + List decoded = WasmcmValueCodec.decodeFrame(argFrame); + Object[] argsArr = decoded.toArray(new Object[0]); + Object[] results; + try { + results = def.function().execute(argsArr); + } catch (RuntimeException e) { + throw new HostCallbackException( + "WIT host function '" + def.witPath() + "' threw: " + e.getMessage()); + } + if (results == null) { + results = new Object[0]; + } + return WasmcmValueCodec.encodeFrame(results); + } + } + + @Override + public List exportedInterfaces() { + // The runtime guest surfaces exports only after instantiation (they are + // properties of the instance, not the parsed component handle at this + // milestone); return empty at the pre-instantiation Component level so + // callers know to instantiate first. + return Collections.emptyList(); + } + + @Override + public List importedInterfaces() { + return Collections.emptyList(); + } + + private void checkOpen() { + if (closed) { + throw new IllegalStateException("EndiveComponentAdapter already closed"); + } + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + if (ownsLoader) { + // The loader owns an Endive Instance running the runtime guest; + // Endive Instances currently do not expose a close() (they release + // when GC'd), so the ownership boundary is intentionally soft here. + // Left as a no-op — the JVM will collect the companion instance + // with the loader. + } + } +} diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentInstanceAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentInstanceAdapter.java new file mode 100644 index 0000000..71d66e2 --- /dev/null +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentInstanceAdapter.java @@ -0,0 +1,177 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.Global; +import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import run.endive.runtime.ExportFunction; +import run.tegmentum.wasmcm.endive.WasmcmRuntimeGuestLoader; + +/** + * SPI-facing {@link ComponentInstance} whose entire concrete implementation is + * layered on top of {@code wasmcm_runtime_guest.wasm} running inside an Endive + * companion instance. All Component Model semantics — export dispatch, canon + * lift/lower, resource tables — live in the guest blob; the adapter only + * marshals values across the wire and dispatches guest exports. + * + *

Instances are minted by {@link EndiveComponentAdapter#instantiate()} + * / {@link EndiveComponentAdapter#instantiate(ai.tegmentum.webassembly4j.api.LinkingContext)} + * and hold the u32 instance handle the guest returned. Multiple instances of + * the same {@code Component} share the same {@link WasmcmRuntimeGuestLoader}. + */ +final class EndiveComponentInstanceAdapter implements ComponentInstance { + + private final WasmcmRuntimeGuestLoader loader; + private final long instanceHandle; + + EndiveComponentInstanceAdapter(WasmcmRuntimeGuestLoader loader, long instanceHandle) { + this.loader = loader; + this.instanceHandle = instanceHandle; + } + + @Override + public Object invoke(String functionName, Object... args) { + Object[] safeArgs = args == null ? new Object[0] : args; + byte[] argFrame; + try { + argFrame = safeArgs.length == 0 + ? new byte[0] + : WasmcmValueCodec.encodeFrame(safeArgs); + } catch (RuntimeException e) { + throw new ExecutionException( + "Failed to marshal arguments for component export '" + functionName + "'", e); + } + byte[] resultFrame; + try { + resultFrame = loader.callExport(instanceHandle, functionName, argFrame); + } catch (IllegalStateException e) { + throw new ExecutionException( + "Component export '" + functionName + "' failed: " + e.getMessage(), e); + } + List results; + try { + results = WasmcmValueCodec.decodeFrame(resultFrame); + } catch (RuntimeException e) { + throw new ExecutionException( + "Failed to decode results for component export '" + functionName + "'", e); + } + if (results.isEmpty()) { + return null; + } + if (results.size() == 1) { + return results.get(0); + } + return results; + } + + @Override + public boolean hasFunction(String name) { + for (String exp : exportedFunctions()) { + if (exp.equals(name)) { + return true; + } + } + return false; + } + + @Override + public List exportedFunctions() { + // The runtime guest exposes the flattened export list of the instantiated + // component through wasmcm_instance_export_count / wasmcm_instance_export_name. + // These are string names as the component itself surfaces them (function names + // for root-level exports; interface-qualified names for interface exports). + ExportFunction countFn = loader.guest().exports().function("wasmcm_instance_export_count"); + ExportFunction nameFn = loader.guest().exports().function("wasmcm_instance_export_name"); + if (countFn == null || nameFn == null) { + return Collections.emptyList(); + } + run.endive.runtime.Memory mem = loader.guest().exports().memory("memory"); + ExportFunction alloc = loader.guest().exports().function("wasmcm_alloc"); + ExportFunction free = loader.guest().exports().function("wasmcm_free"); + long[] countResult = countFn.apply(instanceHandle); + int count = (int) (countResult[0] & 0xFFFFFFFFL); + if (count == 0) { + return Collections.emptyList(); + } + int bufCap = 512; + long[] allocResult = alloc.apply((long) bufCap, 1L); + int bufPtr = (int) (allocResult[0] & 0xFFFFFFFFL); + if (bufPtr == 0) { + return Collections.emptyList(); + } + try { + java.util.List out = new java.util.ArrayList<>(count); + for (int i = 0; i < count; i++) { + long[] r = nameFn.apply(instanceHandle, (long) i, (long) bufPtr, (long) bufCap); + int n = (int) (r[0] & 0xFFFFFFFFL); + if (n == 0) { + continue; + } + byte[] bytes = mem.readBytes(bufPtr, n); + out.add(new String(bytes, StandardCharsets.UTF_8)); + } + return Collections.unmodifiableList(out); + } finally { + free.apply((long) bufPtr, (long) bufCap); + } + } + + @Override + public List exportedInterfaces() { + // The runtime guest's flattened export list mingles interface-qualified + // function exports (e.g. "wasi:clocks/monotonic-clock@0.2.3#now") with + // root-level ones. Extract the interface prefixes to give callers a + // provider-agnostic view of "which interfaces does this instance surface". + java.util.LinkedHashSet ifaces = new java.util.LinkedHashSet<>(); + for (String name : exportedFunctions()) { + int hash = name.indexOf('#'); + if (hash > 0) { + ifaces.add(name.substring(0, hash)); + } + } + return java.util.Collections.unmodifiableList(new java.util.ArrayList<>(ifaces)); + } + + @Override + public boolean exportsInterface(String name) { + return exportedInterfaces().contains(name); + } + + @Override + public Optional function(String name) { + // Component instances do not surface core-module exports; the SPI's + // ComponentInstance javadoc documents this contract explicitly. + return Optional.empty(); + } + + @Override + public Optional memory(String name) { + return Optional.empty(); + } + + @Override + public Optional

table(String name) { + return Optional.empty(); + } + + @Override + public Optional global(String name) { + return Optional.empty(); + } + + @Override + @SuppressWarnings("unchecked") + public Optional unwrap(Class nativeType) { + if (nativeType.isInstance(loader)) { + return Optional.of((T) loader); + } + return Optional.empty(); + } +} diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java index 07232ca..827ed96 100644 --- a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineAdapter.java @@ -11,6 +11,7 @@ import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; import run.endive.wasm.Parser; import run.endive.wasm.WasmModule; +import run.tegmentum.wasmcm.endive.WasmcmRuntimeGuestLoader; import java.util.Optional; import java.util.function.Function; @@ -68,8 +69,24 @@ private Function createBuilderF @Override public Component loadComponent(byte[] bytes) { - throw new UnsupportedFeatureException( - "Component model is not supported by Endive"); + byte[] guestWasm = WasmcmGuestBlobLocator.locateOrNull(); + if (guestWasm == null) { + throw new UnsupportedFeatureException( + "Component Model support requires wasmcm_runtime_guest.wasm; provide it via " + + WasmcmGuestBlobLocator.describeLookupOrder()); + } + try { + WasmcmRuntimeGuestLoader loader = new WasmcmRuntimeGuestLoader(guestWasm); + long componentHandle = loader.parseComponent(bytes); + return new EndiveComponentAdapter(loader, componentHandle, true); + } catch (IllegalStateException e) { + throw new ValidationException( + "Failed to parse Component Model binary through wasmcm runtime guest: " + + e.getMessage(), + e); + } catch (RuntimeException e) { + throw new ValidationException("Failed to load Component Model binary", e); + } } @Override diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java index df1c9ed..7242b13 100644 --- a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/EndiveEngineCapabilities.java @@ -11,7 +11,9 @@ public boolean supportsCoreModules() { @Override public boolean supportsComponents() { - return false; + // Component Model is layered in via wasmcm_runtime_guest.wasm; concretely + // available whenever the guest blob is resolvable at loadComponent time. + return WasmcmGuestBlobLocator.locateOrNull() != null; } @Override diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmGuestBlobLocator.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmGuestBlobLocator.java new file mode 100644 index 0000000..7b8fb9b --- /dev/null +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmGuestBlobLocator.java @@ -0,0 +1,122 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Locates the {@code wasmcm_runtime_guest.wasm} blob — the portable Rust Component + * Model runtime cross-compiled to {@code wasm32-unknown-unknown} — that the Endive + * provider layers on top of Endive to obtain a real Component Model surface. + * + *

Lookup order: + *

    + *
  1. System property {@code wasmcm.runtime.guest.wasm} — absolute path to the {@code .wasm}.
  2. + *
  3. Environment variable {@code WASMCM_RUNTIME_GUEST_WASM} — absolute path to the {@code .wasm}.
  4. + *
  5. Environment variable {@code WASMCM_HOME} — the guest is resolved to + * {@code $WASMCM_HOME/target/wasm32-unknown-unknown/release/wasmcm_runtime_guest.wasm}.
  6. + *
  7. Classpath resource {@code wasmcm_runtime_guest.wasm} on the thread-context class loader + * (with a fallback to this class's loader).
  8. + *
  9. Sibling checkout heuristic {@code ~/git/wasm-cm/target/wasm32-unknown-unknown/release/wasmcm_runtime_guest.wasm} + * — convenient for local development when a companion {@code wasm-cm} tree is present.
  10. + *
+ * + *

Returns {@code null} when nothing matches; callers surface a clear error to the user + * rather than pretending Component Model works with no guest present. + */ +final class WasmcmGuestBlobLocator { + + static final String SYSTEM_PROPERTY = "wasmcm.runtime.guest.wasm"; + static final String ENV_GUEST_WASM = "WASMCM_RUNTIME_GUEST_WASM"; + static final String ENV_HOME = "WASMCM_HOME"; + static final String CLASSPATH_RESOURCE = "wasmcm_runtime_guest.wasm"; + static final String HOME_RELATIVE_GUEST = + "target/wasm32-unknown-unknown/release/wasmcm_runtime_guest.wasm"; + + private WasmcmGuestBlobLocator() {} + + static byte[] locateOrNull() { + String prop = System.getProperty(SYSTEM_PROPERTY); + byte[] bytes = readIfRegular(prop); + if (bytes != null) { + return bytes; + } + + String envGuest = System.getenv(ENV_GUEST_WASM); + bytes = readIfRegular(envGuest); + if (bytes != null) { + return bytes; + } + + String home = System.getenv(ENV_HOME); + if (home != null && !home.isEmpty()) { + bytes = readIfRegular(Paths.get(home, HOME_RELATIVE_GUEST).toString()); + if (bytes != null) { + return bytes; + } + } + + bytes = readClasspath(Thread.currentThread().getContextClassLoader()); + if (bytes != null) { + return bytes; + } + bytes = readClasspath(WasmcmGuestBlobLocator.class.getClassLoader()); + if (bytes != null) { + return bytes; + } + + String userHome = System.getProperty("user.home"); + if (userHome != null && !userHome.isEmpty()) { + Path sibling = Paths.get(userHome, "git", "wasm-cm", HOME_RELATIVE_GUEST); + bytes = readIfRegular(sibling.toString()); + if (bytes != null) { + return bytes; + } + } + + return null; + } + + /** + * Human-readable summary of the lookup paths, embedded in error messages when + * no blob can be found so callers know how to point the provider at one. + */ + static String describeLookupOrder() { + return "-D" + SYSTEM_PROPERTY + "=" + + " | $" + ENV_GUEST_WASM + + " | $" + ENV_HOME + "/" + HOME_RELATIVE_GUEST + + " | classpath:" + CLASSPATH_RESOURCE + + " | ~/git/wasm-cm/" + HOME_RELATIVE_GUEST; + } + + private static byte[] readIfRegular(String path) { + if (path == null || path.isEmpty()) { + return null; + } + Path p = Paths.get(path); + if (!Files.isRegularFile(p)) { + return null; + } + try { + return Files.readAllBytes(p); + } catch (IOException e) { + return null; + } + } + + private static byte[] readClasspath(ClassLoader loader) { + if (loader == null) { + return null; + } + try (InputStream is = loader.getResourceAsStream(CLASSPATH_RESOURCE)) { + if (is == null) { + return null; + } + return is.readAllBytes(); + } catch (IOException e) { + return null; + } + } +} diff --git a/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmValueCodec.java b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmValueCodec.java new file mode 100644 index 0000000..8aa507c --- /dev/null +++ b/endive4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/endive/WasmcmValueCodec.java @@ -0,0 +1,585 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Wire codec for the {@code wasmcm_runtime_guest} argument / result frames. + * + *

Mirrors the tags and layout documented on {@code wasmcm_runtime_guest::codec} + * (see {@code crates/wasmcm-runtime-guest/src/codec.rs} in the wasm-cm tree): + * + *

+ * frame  := [u32 total_bytes_including_header][u32 count][value * count]
+ * value  := [u32 tag][payload...]
+ * 
+ * + *

All integers are little-endian and unaligned. + * + *

The Java surface exchanges natural Java types with callers of + * {@link EndiveComponentInstanceAdapter#invoke}: {@code Boolean, Byte, Short, + * Integer, Long, Float, Double, Character, String, byte[], List, Map} — mapped + * onto the tag arms below at encode time. Decoding produces the mirror shape. + * + *

Types the Java-side natural mapping cannot express unambiguously (unsigned + * widths, variants, options, results, enums, flags, resources) accept a + * pre-boxed {@link Boxed} sentinel that carries the wire tag alongside the + * payload. + */ +final class WasmcmValueCodec { + + static final int TAG_BOOL = 0; + static final int TAG_S8 = 1; + static final int TAG_U8 = 2; + static final int TAG_S16 = 3; + static final int TAG_U16 = 4; + static final int TAG_S32 = 5; + static final int TAG_U32 = 6; + static final int TAG_S64 = 7; + static final int TAG_U64 = 8; + static final int TAG_F32 = 9; + static final int TAG_F64 = 10; + static final int TAG_CHAR = 11; + static final int TAG_STRING = 12; + static final int TAG_LIST = 13; + static final int TAG_RECORD = 14; + static final int TAG_TUPLE = 15; + static final int TAG_VARIANT = 16; + static final int TAG_OPTION = 17; + static final int TAG_RESULT = 18; + static final int TAG_FLAGS = 19; + static final int TAG_ENUM = 20; + static final int TAG_OWN = 21; + static final int TAG_BORROW = 22; + + private WasmcmValueCodec() {} + + /** + * Envelope for a payload plus its explicit wire tag — the escape hatch for + * WIT types the Java-native shape can't disambiguate on its own (unsigned + * widths, variants, options, results, enums, flags, own/borrow handles). + * + *

The concrete payload shape per tag: + *

    + *
  • {@link #TAG_U8}/{@link #TAG_U16}/{@link #TAG_U32}: {@link Integer} + * carrying the unsigned bit pattern.
  • + *
  • {@link #TAG_U64}: {@link Long} carrying the unsigned bit pattern.
  • + *
  • {@link #TAG_S8}: {@link Byte}. {@link #TAG_S16}: {@link Short}. + * {@link #TAG_S32}: {@link Integer}. {@link #TAG_S64}: {@link Long}.
  • + *
  • {@link #TAG_OPTION}: {@link Boxed} for {@code some(v)} carrying the + * inner value, or {@code null} payload for {@code none}.
  • + *
  • {@link #TAG_RESULT}: {@code Object[]{isOkBoolean, payloadOrNull}}.
  • + *
  • {@link #TAG_ENUM}: {@code Object[]{caseIndexInt, caseNameString}}.
  • + *
  • {@link #TAG_VARIANT}: {@code Object[]{caseIndexInt, caseNameString, payloadOrNull}}.
  • + *
  • {@link #TAG_FLAGS}: {@link List} of {@link String}.
  • + *
  • {@link #TAG_OWN}/{@link #TAG_BORROW}: {@link Integer} handle.
  • + *
+ */ + static final class Boxed { + final int tag; + final Object payload; + + Boxed(int tag, Object payload) { + this.tag = tag; + this.payload = payload; + } + + int tag() { + return tag; + } + + Object payload() { + return payload; + } + } + + // ----------------------------------------------------------------- + // Encode + // ----------------------------------------------------------------- + + static byte[] encodeFrame(Object[] values) { + ByteArrayOutputStream inner = new ByteArrayOutputStream(); + writeU32(inner, values.length); + for (Object v : values) { + encodeValue(inner, v); + } + byte[] body = inner.toByteArray(); + int total = 4 + body.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(total); + writeU32(out, total); + try { + out.write(body); + } catch (java.io.IOException e) { + throw new IllegalStateException("BAOS never throws", e); + } + return out.toByteArray(); + } + + static void encodeValue(ByteArrayOutputStream buf, Object v) { + if (v instanceof Boxed) { + Boxed b = (Boxed) v; + encodeBoxed(buf, b); + return; + } + if (v instanceof Boolean) { + writeU32(buf, TAG_BOOL); + buf.write(((Boolean) v) ? 1 : 0); + return; + } + if (v instanceof Byte) { + writeU32(buf, TAG_S8); + buf.write(((Byte) v) & 0xFF); + return; + } + if (v instanceof Short) { + writeU32(buf, TAG_S16); + writeI16(buf, (Short) v); + return; + } + if (v instanceof Integer) { + writeU32(buf, TAG_S32); + writeI32(buf, (Integer) v); + return; + } + if (v instanceof Long) { + writeU32(buf, TAG_S64); + writeI64(buf, (Long) v); + return; + } + if (v instanceof Float) { + writeU32(buf, TAG_F32); + writeI32(buf, Float.floatToRawIntBits((Float) v)); + return; + } + if (v instanceof Double) { + writeU32(buf, TAG_F64); + writeI64(buf, Double.doubleToRawLongBits((Double) v)); + return; + } + if (v instanceof Character) { + writeU32(buf, TAG_CHAR); + writeU32(buf, (Character) v); + return; + } + if (v instanceof String) { + writeU32(buf, TAG_STRING); + writeBytes(buf, ((String) v).getBytes(StandardCharsets.UTF_8)); + return; + } + if (v instanceof byte[]) { + byte[] bytes = (byte[]) v; + writeU32(buf, TAG_LIST); + writeU32(buf, bytes.length); + for (byte b : bytes) { + writeU32(buf, TAG_U8); + buf.write(b & 0xFF); + } + return; + } + if (v instanceof List) { + List list = (List) v; + writeU32(buf, TAG_LIST); + writeU32(buf, list.size()); + for (Object item : list) { + encodeValue(buf, item); + } + return; + } + if (v instanceof Map) { + Map map = (Map) v; + writeU32(buf, TAG_RECORD); + writeU32(buf, map.size()); + for (Map.Entry e : map.entrySet()) { + writeBytes(buf, e.getKey().toString().getBytes(StandardCharsets.UTF_8)); + encodeValue(buf, e.getValue()); + } + return; + } + throw new IllegalArgumentException( + "cannot encode Java type " + + (v == null ? "null" : v.getClass().getName()) + + " as a wasmcm ComponentValue; wrap in " + + Boxed.class.getName() + + " with an explicit wire tag"); + } + + private static void encodeBoxed(ByteArrayOutputStream buf, Boxed b) { + switch (b.tag) { + case TAG_BOOL: + writeU32(buf, TAG_BOOL); + buf.write(((Boolean) b.payload) ? 1 : 0); + return; + case TAG_S8: + writeU32(buf, TAG_S8); + buf.write(((Number) b.payload).byteValue() & 0xFF); + return; + case TAG_U8: + writeU32(buf, TAG_U8); + buf.write(((Number) b.payload).intValue() & 0xFF); + return; + case TAG_S16: + writeU32(buf, TAG_S16); + writeI16(buf, ((Number) b.payload).shortValue()); + return; + case TAG_U16: + writeU32(buf, TAG_U16); + writeI16(buf, (short) (((Number) b.payload).intValue() & 0xFFFF)); + return; + case TAG_S32: + writeU32(buf, TAG_S32); + writeI32(buf, ((Number) b.payload).intValue()); + return; + case TAG_U32: + writeU32(buf, TAG_U32); + writeI32(buf, ((Number) b.payload).intValue()); + return; + case TAG_S64: + writeU32(buf, TAG_S64); + writeI64(buf, ((Number) b.payload).longValue()); + return; + case TAG_U64: + writeU32(buf, TAG_U64); + writeI64(buf, ((Number) b.payload).longValue()); + return; + case TAG_F32: + writeU32(buf, TAG_F32); + writeI32(buf, Float.floatToRawIntBits(((Number) b.payload).floatValue())); + return; + case TAG_F64: + writeU32(buf, TAG_F64); + writeI64(buf, Double.doubleToRawLongBits(((Number) b.payload).doubleValue())); + return; + case TAG_CHAR: + writeU32(buf, TAG_CHAR); + writeU32(buf, (int) ((Character) b.payload).charValue()); + return; + case TAG_STRING: + writeU32(buf, TAG_STRING); + writeBytes(buf, ((String) b.payload).getBytes(StandardCharsets.UTF_8)); + return; + case TAG_LIST: + writeU32(buf, TAG_LIST); + { + List list = (List) b.payload; + writeU32(buf, list.size()); + for (Object item : list) { + encodeValue(buf, item); + } + } + return; + case TAG_RECORD: + writeU32(buf, TAG_RECORD); + { + @SuppressWarnings("unchecked") + Map map = (Map) b.payload; + writeU32(buf, map.size()); + for (Map.Entry e : map.entrySet()) { + writeBytes(buf, e.getKey().getBytes(StandardCharsets.UTF_8)); + encodeValue(buf, e.getValue()); + } + } + return; + case TAG_TUPLE: + writeU32(buf, TAG_TUPLE); + { + List list = (List) b.payload; + writeU32(buf, list.size()); + for (Object item : list) { + encodeValue(buf, item); + } + } + return; + case TAG_VARIANT: + writeU32(buf, TAG_VARIANT); + { + Object[] parts = (Object[]) b.payload; + int caseIdx = ((Number) parts[0]).intValue(); + String caseName = (String) parts[1]; + Object innerPayload = parts.length > 2 ? parts[2] : null; + writeU32(buf, caseIdx); + writeBytes(buf, caseName.getBytes(StandardCharsets.UTF_8)); + if (innerPayload == null) { + buf.write(0); + } else { + buf.write(1); + encodeValue(buf, innerPayload); + } + } + return; + case TAG_OPTION: + writeU32(buf, TAG_OPTION); + if (b.payload == null) { + buf.write(0); + } else { + buf.write(1); + encodeValue(buf, b.payload); + } + return; + case TAG_RESULT: + writeU32(buf, TAG_RESULT); + { + Object[] parts = (Object[]) b.payload; + boolean isOk = ((Boolean) parts[0]); + Object inner = parts.length > 1 ? parts[1] : null; + buf.write(isOk ? 1 : 0); + if (inner == null) { + buf.write(0); + } else { + buf.write(1); + encodeValue(buf, inner); + } + } + return; + case TAG_FLAGS: + writeU32(buf, TAG_FLAGS); + { + List labels = (List) b.payload; + writeU32(buf, labels.size()); + for (Object label : labels) { + writeBytes(buf, ((String) label).getBytes(StandardCharsets.UTF_8)); + } + } + return; + case TAG_ENUM: + writeU32(buf, TAG_ENUM); + { + Object[] parts = (Object[]) b.payload; + int caseIdx = ((Number) parts[0]).intValue(); + String caseName = (String) parts[1]; + writeU32(buf, caseIdx); + writeBytes(buf, caseName.getBytes(StandardCharsets.UTF_8)); + } + return; + case TAG_OWN: + writeU32(buf, TAG_OWN); + writeU32(buf, ((Number) b.payload).intValue()); + return; + case TAG_BORROW: + writeU32(buf, TAG_BORROW); + writeU32(buf, ((Number) b.payload).intValue()); + return; + default: + throw new IllegalArgumentException("unknown wire tag " + b.tag); + } + } + + // ----------------------------------------------------------------- + // Decode + // ----------------------------------------------------------------- + + static List decodeFrame(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return java.util.Collections.emptyList(); + } + Cursor c = new Cursor(bytes, 0); + readU32(c); // total_bytes — discard, callers already sized the array + int count = readU32(c); + List out = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + out.add(decodeValue(c)); + } + return out; + } + + static Object decodeValue(Cursor c) { + int tag = readU32(c); + switch (tag) { + case TAG_BOOL: + return c.readU8() != 0; + case TAG_S8: + return (byte) c.readU8(); + case TAG_U8: + return c.readU8() & 0xFF; + case TAG_S16: + return (short) readI16(c); + case TAG_U16: + return readI16(c) & 0xFFFF; + case TAG_S32: + return readU32(c); + case TAG_U32: + // Represent unsigned u32 as a Long to keep the natural mapping non-lossy. + return ((long) readU32(c)) & 0xFFFFFFFFL; + case TAG_S64: + return readI64(c); + case TAG_U64: + // No unsigned long in Java; caller can reinterpret bits. + return readI64(c); + case TAG_F32: + return Float.intBitsToFloat(readU32(c)); + case TAG_F64: + return Double.longBitsToDouble(readI64(c)); + case TAG_CHAR: + return (char) readU32(c); + case TAG_STRING: + return new String(readBytes(c), StandardCharsets.UTF_8); + case TAG_LIST: { + int n = readU32(c); + List list = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + list.add(decodeValue(c)); + } + return list; + } + case TAG_RECORD: { + int n = readU32(c); + Map record = new LinkedHashMap<>(); + for (int i = 0; i < n; i++) { + String name = new String(readBytes(c), StandardCharsets.UTF_8); + Object value = decodeValue(c); + record.put(name, value); + } + return record; + } + case TAG_TUPLE: { + int n = readU32(c); + List list = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + list.add(decodeValue(c)); + } + return new Boxed(TAG_TUPLE, list); + } + case TAG_VARIANT: { + int caseIdx = readU32(c); + String caseName = new String(readBytes(c), StandardCharsets.UTF_8); + int hasPayload = c.readU8(); + Object payload = hasPayload == 0 ? null : decodeValue(c); + return new Boxed(TAG_VARIANT, new Object[] {caseIdx, caseName, payload}); + } + case TAG_OPTION: { + int has = c.readU8(); + return has == 0 ? new Boxed(TAG_OPTION, null) : new Boxed(TAG_OPTION, decodeValue(c)); + } + case TAG_RESULT: { + boolean isOk = c.readU8() != 0; + int has = c.readU8(); + Object payload = has == 0 ? null : decodeValue(c); + return new Boxed(TAG_RESULT, new Object[] {isOk, payload}); + } + case TAG_FLAGS: { + int n = readU32(c); + List labels = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + labels.add(new String(readBytes(c), StandardCharsets.UTF_8)); + } + return new Boxed(TAG_FLAGS, labels); + } + case TAG_ENUM: { + int caseIdx = readU32(c); + String caseName = new String(readBytes(c), StandardCharsets.UTF_8); + return new Boxed(TAG_ENUM, new Object[] {caseIdx, caseName}); + } + case TAG_OWN: + return new Boxed(TAG_OWN, readU32(c)); + case TAG_BORROW: + return new Boxed(TAG_BORROW, readU32(c)); + default: + throw new IllegalStateException("unknown wire tag " + tag); + } + } + + // ----------------------------------------------------------------- + // Low-level LE readers / writers + // ----------------------------------------------------------------- + + static final class Cursor { + final byte[] bytes; + int off; + + Cursor(byte[] bytes, int off) { + this.bytes = bytes; + this.off = off; + } + + int readU8() { + require(1); + return bytes[off++] & 0xFF; + } + + void require(int n) { + if (off + n > bytes.length) { + throw new IllegalStateException( + "wire frame truncated: need " + n + " at " + off + " of " + bytes.length); + } + } + } + + private static void writeU32(ByteArrayOutputStream buf, int v) { + buf.write(v & 0xFF); + buf.write((v >>> 8) & 0xFF); + buf.write((v >>> 16) & 0xFF); + buf.write((v >>> 24) & 0xFF); + } + + private static void writeI16(ByteArrayOutputStream buf, int v) { + buf.write(v & 0xFF); + buf.write((v >>> 8) & 0xFF); + } + + private static void writeI32(ByteArrayOutputStream buf, int v) { + writeU32(buf, v); + } + + private static void writeI64(ByteArrayOutputStream buf, long v) { + buf.write((int) (v & 0xFF)); + buf.write((int) ((v >>> 8) & 0xFF)); + buf.write((int) ((v >>> 16) & 0xFF)); + buf.write((int) ((v >>> 24) & 0xFF)); + buf.write((int) ((v >>> 32) & 0xFF)); + buf.write((int) ((v >>> 40) & 0xFF)); + buf.write((int) ((v >>> 48) & 0xFF)); + buf.write((int) ((v >>> 56) & 0xFF)); + } + + private static void writeBytes(ByteArrayOutputStream buf, byte[] bytes) { + writeU32(buf, bytes.length); + try { + buf.write(bytes); + } catch (java.io.IOException e) { + throw new IllegalStateException("BAOS never throws", e); + } + } + + private static int readU32(Cursor c) { + c.require(4); + int v = (c.bytes[c.off] & 0xFF) + | ((c.bytes[c.off + 1] & 0xFF) << 8) + | ((c.bytes[c.off + 2] & 0xFF) << 16) + | ((c.bytes[c.off + 3] & 0xFF) << 24); + c.off += 4; + return v; + } + + private static int readI16(Cursor c) { + c.require(2); + int v = (c.bytes[c.off] & 0xFF) | ((c.bytes[c.off + 1] & 0xFF) << 8); + c.off += 2; + return v; + } + + private static long readI64(Cursor c) { + c.require(8); + long v = (long) (c.bytes[c.off] & 0xFF) + | ((long) (c.bytes[c.off + 1] & 0xFF) << 8) + | ((long) (c.bytes[c.off + 2] & 0xFF) << 16) + | ((long) (c.bytes[c.off + 3] & 0xFF) << 24) + | ((long) (c.bytes[c.off + 4] & 0xFF) << 32) + | ((long) (c.bytes[c.off + 5] & 0xFF) << 40) + | ((long) (c.bytes[c.off + 6] & 0xFF) << 48) + | ((long) (c.bytes[c.off + 7] & 0xFF) << 56); + c.off += 8; + return v; + } + + private static byte[] readBytes(Cursor c) { + int n = readU32(c); + c.require(n); + byte[] out = new byte[n]; + System.arraycopy(c.bytes, c.off, out, 0, n); + c.off += n; + return out; + } +} diff --git a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java new file mode 100644 index 0000000..5076109 --- /dev/null +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java @@ -0,0 +1,171 @@ +package ai.tegmentum.webassembly4j.provider.endive; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.WitHostFunctionDefinition; +import ai.tegmentum.webassembly4j.api.exception.InstantiationException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +/** + * End-to-end verification of Endive's Component Model surface: parse a real CM + * binary, instantiate it under the layered runtime guest, invoke an exported + * function and inspect exported names / interfaces. + * + *

The tests are skipped when the {@code wasmcm_runtime_guest.wasm} blob is + * not resolvable via {@link WasmcmGuestBlobLocator} — a fresh checkout that has + * not built the sibling {@code wasm-cm} tree gets a green {@code mvn test} + * rather than a red failure. + */ +final class EndiveComponentIntegrationTest { + + @Test + void lookupOrderStringNamesBlobExplicitly() { + // The UnsupportedFeatureException raised when the guest is missing embeds + // this string; verify it stays actionable ("names the blob and every knob"). + String s = WasmcmGuestBlobLocator.describeLookupOrder(); + assertTrue(s.contains("wasmcm_runtime_guest.wasm"), + "lookup order should name the blob: " + s); + assertTrue(s.contains(WasmcmGuestBlobLocator.SYSTEM_PROPERTY), + "lookup order should name the system property: " + s); + assertTrue(s.contains(WasmcmGuestBlobLocator.ENV_HOME), + "lookup order should name WASMCM_HOME: " + s); + assertTrue(s.contains("classpath"), + "lookup order should mention classpath: " + s); + } + + @Test + void scalarComponentRoundTripsAddThroughGuest() throws Exception { + assumeGuestPresent(); + byte[] bytes = loadClasspath("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = EndiveEngineAdapter.create(null); + Component component = engine.loadComponent(bytes)) { + assertNotNull(component, "loadComponent must not return null"); + ComponentInstance instance = component.instantiate(); + assertNotNull(instance, "instantiate must not return null"); + + // The scalar-component fixture exposes exactly `add: func(u32, u32) -> u32` + // at the component root. hasFunction / exportedFunctions rely on the guest's + // wasmcm_instance_export_count / wasmcm_instance_export_name. + assertTrue(instance.hasFunction("add"), + "scalar-component should export 'add', got " + instance.exportedFunctions()); + + // U32 args round-trip through Boxed(TAG_U32, int); the fixture returns U32. + Object result = instance.invoke( + "add", + new WasmcmValueCodec.Boxed(WasmcmValueCodec.TAG_U32, 2), + new WasmcmValueCodec.Boxed(WasmcmValueCodec.TAG_U32, 3)); + assertEquals(5L, ((Number) result).longValue(), + "add(2, 3) should be 5; got " + result); + } + } + + /** + * Attempt to instantiate a real WASI Preview 2 component that imports + * {@code wasip2:host/primitives@0.1.0} and {@code wasi:io/poll@0.2.3}. + * + *

Full round-trip host-callback dispatch is gated behind Wall 2 on the + * wasm-cm Rust side (see {@code HostCallbackDispatcher} javadoc). Until + * Wall 2 lands, the guest surfaces {@code HOST_ERR_UNLINKABLE} even when + * host providers are registered — this adapter re-throws as + * {@link InstantiationException}. The test locks in the honest failure so + * the day Wall 2 arrives the assertion here flips and this becomes a real + * success path. + */ + @Test + void wasiClocksComponentReachesInstantiationSite() throws Exception { + assumeGuestPresent(); + byte[] bytes = loadClasspath("wasi_p2_clocks.component.wasm"); + assumeTrue(bytes != null, "wasi_p2_clocks.component.wasm missing from test resources"); + + try (Engine engine = EndiveEngineAdapter.create(null); + Component component = engine.loadComponent(bytes)) { + assertNotNull(component, "loadComponent should succeed on a WASI P2 component binary"); + + DefaultLinkingContext ctx = DefaultLinkingContext.builder() + .addWitHostFunction( + new WitHostFunctionDefinition( + "wasip2:host/primitives@0.1.0#clock-monotonic-now-ns", + args -> new Object[] { + new WasmcmValueCodec.Boxed( + WasmcmValueCodec.TAG_U64, 42L) + })) + .addWitHostFunction( + new WitHostFunctionDefinition( + "wasip2:host/primitives@0.1.0#clock-wall-now-ns", + args -> new Object[] { + new WasmcmValueCodec.Boxed( + WasmcmValueCodec.TAG_U64, 43L) + })) + .addWitHostFunction( + new WitHostFunctionDefinition( + "wasip2:host/primitives@0.1.0#clock-monotonic-resolution-ns", + args -> new Object[] { + new WasmcmValueCodec.Boxed( + WasmcmValueCodec.TAG_U64, 1L) + })) + .build(); + + // Instantiation is expected to fail today with an UNLINKABLE from the + // guest — wasi:io/poll@0.2.3 imports a resource type the runtime + // cannot yet satisfy, and Wall 2 (canon-lower routing to a host + // callback) is not yet wired. When Wall 2 lands, flip this to + // assertNotNull(component.instantiate(ctx)). + InstantiationException ex = assertThrows( + InstantiationException.class, + () -> component.instantiate(ctx)); + assertNotNull(ex.getMessage(), "guest should have written a diagnostic"); + } + } + + private static void assumeGuestPresent() { + byte[] guest = WasmcmGuestBlobLocator.locateOrNull(); + assumeTrue( + guest != null, + () -> + "wasmcm_runtime_guest.wasm not found; searched: " + + WasmcmGuestBlobLocator.describeLookupOrder()); + } + + private static byte[] loadClasspath(String resource) throws IOException { + try (InputStream is = EndiveComponentIntegrationTest.class + .getClassLoader() + .getResourceAsStream(resource)) { + if (is == null) { + // Fallback: check the module test resources on disk. Convenience path so + // running from an IDE without a fresh test-compile still works. + Path fallback = Paths.get( + System.getProperty("user.dir"), + "src", "test", "resources", resource); + if (Files.isRegularFile(fallback)) { + return Files.readAllBytes(fallback); + } + return null; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = is.read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + } +} diff --git a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java index 8b6acdc..401ea84 100644 --- a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveProviderTest.java @@ -5,6 +5,8 @@ import ai.tegmentum.webassembly4j.api.EngineInfo; import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; +import ai.tegmentum.webassembly4j.api.exception.ValidationException; +import ai.tegmentum.webassembly4j.api.exception.WebAssemblyException; import ai.tegmentum.webassembly4j.provider.endive.config.EndiveConfig; import ai.tegmentum.webassembly4j.spi.ProviderAvailability; import ai.tegmentum.webassembly4j.spi.ProviderDescriptor; @@ -55,7 +57,9 @@ void capabilities() { try (Engine engine = provider.create(null)) { EngineCapabilities caps = engine.capabilities(); assertTrue(caps.supportsCoreModules()); - assertFalse(caps.supportsComponents()); + // supportsComponents() reflects runtime-guest presence; asserting a + // hard value would make the test order-dependent on the env + // (see WasmcmGuestBlobLocator). Locking in the rest of the surface. assertTrue(caps.supportsWasi()); assertFalse(caps.supportsFuel()); assertFalse(caps.supportsNativeInterop()); @@ -66,10 +70,23 @@ void capabilities() { } @Test - void loadComponentThrowsUnsupported() { + void loadComponentSurfacesFailureCleanly() { try (Engine engine = provider.create(null)) { - assertThrows(UnsupportedFeatureException.class, + // Two honest outcomes depending on whether the wasmcm_runtime_guest.wasm + // blob is resolvable at test time: + // - guest present -> the guest rejects the empty payload with a + // malformed/invalid diagnostic; the provider + // wraps that as ValidationException. + // - guest missing -> the provider surfaces UnsupportedFeatureException + // naming the blob and how to point at it. + // Both are WebAssemblyException subtypes; the test locks in the union. + WebAssemblyException ex = assertThrows( + WebAssemblyException.class, () -> engine.loadComponent(new byte[0])); + assertTrue( + ex instanceof UnsupportedFeatureException || ex instanceof ValidationException, + "expected UnsupportedFeatureException or ValidationException, got " + + ex.getClass().getName()); } } diff --git a/endive4j-provider/src/test/resources/scalar-component.wasm b/endive4j-provider/src/test/resources/scalar-component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a67fa30c2fc73f40d5a2e3cf8f1df7a309dba5f2 GIT binary patch literal 147 zcmWlSF%H5o6hrObD4!641ShE2**E}4AF=2VSHgYEw*LX?K^8Fp literal 0 HcmV?d00001 diff --git a/endive4j-provider/src/test/resources/wasi_p2_clocks.component.wasm b/endive4j-provider/src/test/resources/wasi_p2_clocks.component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..04caa87d5c1a66e7060477b414314b7c1347064d GIT binary patch literal 19229 zcmdsN+BwIEmW^U3crv23t3)w%*2V<8BJ1Z3+ZkW#evLz-fbRJ_M~%B2ybs=xm;=iC>Ol47@?DuFy_W}bQGd7o!q4&B`P*$L-e<1hIKQa|h5`P99C z{>;k!($v|N<&}+<<%RjF<(2bO%j@2yZf|TlKX>NLQ0<4V?y%NdUpe#S#=^?7^&9JW zckuk&`og)xM^CS;Z%m(CTR6M0v9Q@&pFMQf$L=~*bB(rtz^5)dw{qsp+{rV&#E<=g zcl*@o=)%ghssTNG*WESmzxP=O)!sShADwtSY;C#6ldY{SzomD6JAd(U=N|VLdG|Cr zo&s=>r~DQ~O;5^od-CF`4Zr7;>r#1rHr@3*Uqh%j=(SVcE=i6*oTjx}JxOY{Mk8d+ zG;OBM;Oh0UI{#W@&V?|ZX7#3Xb%ZodnMVjFvCO$N%R)1FPtTe^=O-s?iFbSJO@vTv zZ(nw;#_OS3KifOIvUVZ3KW92H4X;lRY{Dv{N^ZqIT=vl$!wt;BHSf7G`1+G4*XP$3 zPWGlwJ-Id~YjR_Ezng9g%j+9+%Nwqh5BD-+ph~UMES%m^K&N`POSCtftC3a z=NC3koLpEw^_kxCi9;vuo;h*I-8?=F;T~yzz`Me?-j_bo+5d@;|MHQUd+zCe-x3^tuzbl^q+~sXod_EK}e4dx~_f&5UFR5I7Arvn^JJj$I zA1>yu{W*bk{R7)C{Pc}vyBm%qE_b=lL#(;@+51D*y2-U)5M28i{T>u4QJnPtVr`>k88Q_)`Py4&ov?CRQI5>&zPcZ6&v{P;_kW9iPs9Z7zqQtpcO zV(e5{MeDYbumU;tyF5_Amp~K7QTrbX{hrnyE9Ig6^;q6pmh+_jk7G>&6tevl7iRT8 z_aI0foCxi|t@b>j@88l}Cye`66c(MIk5#QaOd$3=v^w-J+?4x?NS|=+&kI0=9*tG4 zR(mk?-%(?$>DvEDFRpcf&ak_+SX}&7hWEVt_HP(aD@TfIy(6Hw2gThTA)%d>$S>&<#aaqkujCp32ju$A((ejdnALkk@j5CQtij*@&GJSYw3o z(CXWATH11>l{aK3Ci-aC4H#$OdaKAfG`qa9m?u#`bsnuh4g-1SOWj%}&qzu28?*eT zBvV}Rt2ZU7cfKX9y=_{n7h9j6Nb-7t(KYh=!xIdcGp@A?>>YtUIN>pd4ExLd9bs}v z!SIrwZXNX?Upqy_vpzDS`sSiGiZ``-%*wQpPP_g!&GuvLiG*zJE1 zLCouj5U>AD00^|!_7$}eQ*Uu$7K3UYM^PS!cuDnp&`kUH^w#=c!PmCXSTL~fU-@;S zMq5AlqFYiNQP~%*#p0<|J&zygCX2=RV(cG-i7%d76b5i8{&cbZReFYwlU4ZQ>BTOt z>$-K}pm9+Vduf$kU9VRt3riYuu}iO|u3ze;;Hnpic@_p{-h-p96=6-jMU{q0Ek0Nc z)~op7`aob(Tp9tQru|RFx9BPzHkvOO1uh$9WYnEPS{xqkRuz{BEjSN=2`mkhl34W1 zlO^0NDai|qbWwaLywwmE;>d^|v+Q6%q4m?-LlZ9^OmF@+h}(9R9{~D4J(?3m#p_po zJ^HR2Plcq#2vF8ZWM(f(_uZ5kr?{n4Q@<-$8o!DUB*ljby4mJu(B>vo)8sdniu)Hk zHS_Oks4y!umkdqaEC7waoe17kUL>2{ut~@)RZ?uN3ma@Uiev-EzxvmqK>ACHOZF;- zBY>=%9fJ-6od7opp|ag&qSVWzz0~gr`#ZJBRZd|Xit3s`>cu2%Q3Kq8h;ahr!$cv5YUmsx5d)#i zBEQ-MkprDZGMhI#&7AP9YNZu2lgVyI8%1HyD#T3Y%>ssb)?h~Dq=$ z*e#^-o};>%Fe!12H!Z}>(BdZic-D#{#Qs32$XX+839Uqs*_1BXLWEH4H=ZEQM5%_! zvSD1E`?7>-q?w1~VuM*4DtSTroot+$u#ns?=8f~KGtoA%T3N50rLuM27Q0EF^lg2- zh6_O%KpO>4uz|!NdueU;bhjyE36-%h@Q|@URn~1t3JnoF4qb%x3^OgsXbzDfJF9$+ z@#`XJshgGF#?Xtxf#=Aq%*?vMCdE6#H)V94L^KX;zEOCHNj5jSO+ps~Hu5HpYYsuf zQ>zutfw@q@!-7Iti2)2TBHwOk){|^`j1w?mL=UDK6lnoKV=yHeF}y;oQHgKIiNib^ z*~@6ex0Ow>h&B{$1@wAf2)r#ymnHwaEn4<5;J(3(s8Zw}!3=ZXgnRrL?rV|zW?t)a z-x%T^4-ogN!ttZe;}ng78a3@VWrAq0a;Ro>{ZgBdZ8k$JvCxRMDQ=iiTCj{7MSW@{yNq+Nag$kSEvmRueCI)oAN+)C(L(ul&BNPhxC#(5c}CTlpZP=w!5 zZfYDO1M+#9Bq;;DmN(?+faoQQjEJmUF=W_XkglYlppcLVpmhS)AhuFaY|Ru~vl#t` zVrzX6TLH}QPb@;=ACs3ys|y}_Rcx(a9b4;z*jkUVRU)inD;yGCA==cRiN4|ZiaLkm zE7Q;#1{qy|H%onn!XNmUfuObsLMvCg?8oq#L+>Uk9&724o;~ zk)JRqhGO-K7EuZIu_Z>;nedIWShiYz1@OzN zo(bO?1-4pRzZqMyfa#SyDZvs zCOrE$Oc_O4+%AjuL_ed&=gOizQRHax`LbxwnegIh@vCLgp6ItMUeXi&j<#AY(s((x zerpuiYH9suZ2eBzS^`@wt>2EVua>PPu+`FfIkvuDwwAzFOY3W~^@n9^32e2r{vfu# zQMQ)AR!i%TV(XujttGJ4()#1r`exZ$0$VMuKZ&hBD_cuotEKg=*!oV{S^`@wt#8NH zXQQK4z*bA^wyIHR*;)cyEv?VR))&gw64+{KeLl9nShkkHRx3|0_Go2HG-NSLgtS;P z==IguOQB(jY3nZ%nidrtNp39i5lX9^6_CZ^4%69-DdX7#j$TNkbWpG+tgmPN=gAip|ANRzzIOBwAD3OjgFC=G22_H)BsgkoZQ zT!NR_5eZXRdy%b)Mgh-k2;CVYYG;hptIc-9o#A=Wek3Rt)8v$o@s9p(6p=LtT37lV zWZ@&J74X2AOjh85UosN1uwxSq9u4JZ7s=OCXrz`c>yj1AhLIf|S-! zl}oumX-2Qd8J#?>Kci#ogBjhiPE)p`I!5}cm_*Z5T(VcDFlCQuNeMY(QWl1?;sf5Y zaoeV23W6h3A*LegOT(6Y2YE^(TD#dgNRf| zom`Ar4pVRc6HV(FL9!?mc^&@&GN!4lI!%To2K}5UiCc+aWH+Ka@R_nYn}6fW0)|D)}FRKs6?)i7HMHVw%Sb&un9>-0fgftZG8IT z_Qgjnq_$s{!|Y+B_zS4MxT|!%i0&eI8^(mb)RlA8d_3FiG$kS1z{pF zQm{wOB%ugwwtA(|?Au`y9TDMeJGO6^TI|_I56+xCq&1#Y6@^@G0Y$&nU@?N5$7shH zRMF)}Rs}|aP_iBgq9f5@Wh{{wRxY8)qS%7GN<6}7MqiG^n2RDu>xin?D2-4d6jzhj z@2Ehk$fC2GGB!t}f6bD;CEU=O}1!i<7q?ePlPiBQ)%0vT}Ho!aO6!T&y zKr1B(juSK#+fb3&(r1j~_}-^D=Pk+@GjtsWfdjwTp}Jfe$>^`#V!s|e(6Pl(B{NP) zXTz+hkFy*u3~P$fQapk~H~koW)hwrzEPzL5Irti#<=`*Qay-U!CV==r@Y!JON@H4> zAplYMF8RPIQ3C|)(9YD-YW<6bubXF!#p*xwaq__#h3nRA$}>N&F%xzhrJp0H+nKLX z75j6V3^Vz<5MZZvXoC;LLz`}pznR?S7aRvXI>81Cukmb#=W!&?pd7{+Mi6qz0(&OK zdyrrC+&>l{OJtagL`s575R(_nBSavSXtF8R_dze(R5lCI9*R;N7(!|65{fT2zBu5? zIjLY%DQ`wEryEu5QN^uFxs)V9u!(0lN@qEqv3*BN^*G)Y7WTrcc|<_8lV2@d*d?3iybC><1FClIy!O!97Ko!wz~XO;~~$%8N=DVQoUWF3knl??nq23~vfE>5K6IIZ7I zY@7SN;L7Yo4A~Ia(11*3P!(lSsQracMOeS1)YZ354zg`GcrdZW zNi`JSRK<~OoK&HzKdJiRg%7?U^RznLuUtAJhM`GSo5f102-SKFEqox_OhnZ?vK>Xp zD%*klN&-WCYxbn4X(Yay-pn+FvW%PRQKlegkx@Yeoku{1q$#VOoI?KWDMj%=pj(O(M>Hm z76pU=^F1Dvq!W=9z12G8DH^x8XdPiz(Shp$@(A7Hkb9(+`gJ4*Q5=bJcR)vCiDDGC z98a=zbEB=ZQ<*oXSOgG7sv%VD7g5ZVXAVip^t&J8ET$O67z_bT=jM?K!i)fhm}vb~ zd|xwfv9Boyvpk}ofqsd$0LlE>;cP3-yRm*{Ez;#(jU&3)?^8BLo( zFKMlGL^~oXy%(49xDN~mf(Yuqxn^$30_vPZR*mh; zLdpbTL08!XQF*r!7}kr7k`|-4^|e6OY7E3sE2C8cSorGd7!_?*1gj{LLe8_joswxL zG2JknLD$M)!EA)vTfbwh8rz&i>oO0L6LOto`Qleri|ws7O@l>!vr~_48V9?p9d%@i zY!h7eYe%_I%$((m@lBncR{dg!D_88y$a)0zh#ecnkNX|1jr)XZ6G)@P5FvfBZ?iaO zgJ2vm#&8z?A=w-XYt*etBOc!`g@>V++~Nua!7Xwd#c1mkoJE1tQWjYVTMnovW?`BY zD)fKf+af9gjJE)7Kt!DN+L4IkO3ZVge#<@u4)`JJB1Qdd?ac2on`zgXBi)M7IM2Ngm7nq zIOQ_e5kph$idxZqBTMMC^y$mK+SifN6_>uR)TWaVjIb&z6hE-46z_R;n^P2(ZWpj?)5^TIHuMSJc zIQf^pO^Q^+07Mjat%DB4Y$wQxeI=*%cVlpo6*5J>5=+0p(UV2jTZ>oqtga;9Y<-y` znC_9x_agK|g9#M_Ps#}uUCJuU86zqDeg0rfP51LixL&AyF~P#y--vjwieLXuwXL6} zJ{!87n?~&NSGWPIWo%Q9<+mm2HA~V>CY+kcvGzVAJp61syAKz=O>4gflVH%+hAkgU zl(oyn)+0!xCJl9)#53jFO+wW}>`q~lk|;r4Avk^D5@_q1WVrsrwWoLol4h-N_kK=e z^0ST58Hdqr`KDEI#Lj(=A?g@Z>toc*{UAZt6n)WQ!9FKfSf?E;imQ82uO4hlsRwHT zuA#8f=k}ag(sX!fj8`E4;%QwEby&B} zZaFKFwBW9!?xxT)S+&gsT{QBL0wl=;1>Hn2t_9twmu>tgbM9(>1(muECD%$LD{)obZJ8MJ6E_s zx;h}h=3mTRyBy&1MwC@@@KsiZvQo!kjF2&Kldz2(xOaq;*e^SzlT1v44RT(j^;Tom zGMX}e5gyonMm~hEa&vCnw#%EkcG}p4^!nyn@yzF*&2%R@yG~nr?Kb91&~pB|aqsW&~^}NsLOXcclq=u>Dh*tS#9NC897uB%vwGjieovWQ@X5%9uyc zv(iV)S7n|7NjLe?rXfosvto=|S8L23n94Q+nQ-aFRA5ygqWC6%7S#0zRqQ%6zlT}rd$CX$U!($b1#3Tk;;ktdtt`*moY@+CosC&OU zh9z&t$os0sajAc>L{n-3RNK5^fz5E#ekvUEKz0p?umR8&GxyN?kxh7oB)i0IK#Om?e5T(3Nke=$#9d6pEtcz*lxwh}?UU+HI3LtlRVmdv=-`mYVIEb+2y{g5~3 zhJP-*>9r@Q;h87yd1B_{y+b|T4xK!Ch)lXM4*Vy;Dc+tIQGTqxpLOo6qv&&3w7nJGGwA<<*zLQ}c7@ z^o4M~_T=)$!r9(^`Nrv9{>Wc=EMK2nK6QR!{&aq7rMI4Iv~z1Kn+vCU`O0!HztG!2 zA8)$0yOrk=ekS;NcQjSsq5i)ms-o%slBU2j*v&qjGKP`R) z-{wbc;_W7W_VA;+zx-$a?Zg}5&;Ip)|L(an|6}gExBchFmybWy`}BYM!_WT4V_*5* z5C6xx_4VGGKC#W8m|Hj_<(ykz_-noV{$I}R`H}qW!t#lg&EDE+%+0y)g7ufc`_CCn zG+e9yCkOrKp4z4VzoNfrjRu`-fG5tZobR2=W!AZ@e0?8d{S{zt=7)%C|0)^mr6hhU zS?%M!#C3Wu8o|k4Z`lxm^of-f`bwAGQd=JT;nmTzQOzil`IY5U>*rg`bIU7b;X=LZi6htl{5-1(#?=q#agC47 z4aU3r`vtS}({gJLF02KgTz{}@j^O-Gv<`dr*Zr`5pW_a+3*MI>oi}QJ*14qqN%izU z=LhIOj(m5W)nPJm^@uF3AExO2yZTDqQ1G`28WUk5%=+~F>E3eUvic_s)qD9A-7wZ& zv(fS;z7%{%4c~kEBHj=-+*tZw68%wx2_~E|J~y8v&aSL&Ol@2^*PE&$XW|>dpI7ty zhMUVki#0}3g|SKR_LLA$Jh^Ot|G_8L_k&lVt^#7>(%88+c5iU+AMQN(bl%t9&BFs% zbR9~FZHTy0iX^yftn1x}hOz9J7yOV6Yjn8PyIYu)6B{=x)N~ElH<05MJ#@va0WHL19tVAu8lCRZ2Yz}h6zv7T(k`i&XRiCmRKkpxPu#Qc2O#jGNe$@DYzg9pV+u;Xf zKT1Yq$ril)*j=1l4jZH_=i06jt}~jM?Wt;nq#Pk_9SnVYtX*w|wc0W4G}%Ex<60Jz zjq0!(t=7U5EqEU-{{+a@e?Fte^!rltrvAh?wLEvW=l_(@J$8dB%U@%f3OnbJ8do zR=)w@E`Nq6SgK#y#IF83wSIcx>{JY`^S$K84xj$ga7?(`&PLJIu4|M2w>+}Aeh%|) ztWRM-9g6d{odLbROYCl_UpCawe=14bEgKi6#OSsD7?Qua1Q`9pN1>l9`Z*T$hlXW! j9r^eD(tl*Q$L`V{0RR7J6>j=kxx3$)yWe~Gu0#I^sYZ1h literal 0 HcmV?d00001 From b401e99c72a5625de425d6c910b6e9e963b87b8c Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 16:44:58 -0400 Subject: [PATCH 41/85] feat(bindgen): consume wasmos:host/embedder@0.1.0 WIT world MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend webassembly4j-bindgen to accept a full WIT world source (package declaration + world + multiple interfaces + resources with constructor / static / instance methods) and emit a compileable Java surface. First bindgen invocation against a concrete wasmos:host/ embedder world per ADR-006; the JVM lane's proof-of-shape. Parser - WitWorldPreprocessor strips package/world blocks and fans a world source into per-interface entries the existing regex parser can consume; resource bodies are extracted and returned in a side map. - WitResourceBodyParser understands the constructor / name: static func / name: func shapes inside a resource body. - WitInterfaceParser: enums/flags now parsed before records so record fields can forward-reference enum types; resolveType peels borrow and own and falls back to an opaque resource reference rather than raising when a sibling type was stripped upstream. Model - BindgenType carries an optional resourceMethods list populated when a WIT resource declares a body. Generator - CodeGenerator.parseWitSource uses the preprocessor when present (falls back to the legacy single-interface path so existing tests keep working) and materializes resource methods into BindgenFunction records with isConstructor / isStatic flags. - ModernCodeGenerator.generateResource emits the WIT constructor as a static create(...) factory and each method as an instance/static method that currently throws UnsupportedOperationException — runtime dispatch wiring is a separate follow-up (ADR-005). CLI - --no-impl disables *Impl and *BindingProvider generation for embedder-style world files whose interfaces don't map to WASM module exports. Test - WasmosEmbedderShapeTest generates against the fixture WIT, asserts the expected surface (ErrorCode, Error, ImportSatisfaction, HostProvider with the create factory, RuntimeInstance with the four methods, EmbedderCallbacks abstract), asserts determinism between runs, and compiles every generated file with the platform javac. --- .../webassembly4j/bindgen/CodeGenerator.java | 248 ++++++++++++-- .../webassembly4j/bindgen/cli/BindgenCli.java | 10 + .../generator/ModernCodeGenerator.java | 61 +++- .../bindgen/model/BindgenType.java | 28 ++ .../bindgen/wit/WitInterfaceParser.java | 56 ++- .../bindgen/wit/WitResourceBodyParser.java | 321 +++++++++++++++++ .../bindgen/wit/WitWorldPreprocessor.java | 324 ++++++++++++++++++ .../wasmos/WasmosEmbedderShapeTest.java | 238 +++++++++++++ .../resources/wasmos-embedder/embedder.wit | 208 +++++++++++ 9 files changed, 1452 insertions(+), 42 deletions(-) create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java create mode 100644 webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java index e1652e1..14257c6 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java @@ -28,12 +28,14 @@ import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; import ai.tegmentum.webassembly4j.bindgen.model.BindgenVariantCase; import ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceParser; - import ai.tegmentum.webassembly4j.bindgen.wit.WitFunction; import ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceDefinition; import ai.tegmentum.webassembly4j.bindgen.wit.WitParameter; +import ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser; +import ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser.WitResourceMethod; import ai.tegmentum.webassembly4j.bindgen.wit.WitType; import ai.tegmentum.webassembly4j.bindgen.wit.WitTypeCategory; +import ai.tegmentum.webassembly4j.bindgen.wit.WitWorldPreprocessor; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -168,39 +170,68 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException final String packageName = config.getPackageName() != null ? config.getPackageName() : "generated"; - final WitInterfaceParser parser = new WitInterfaceParser(); - final WitInterfaceDefinition definition = parser.parseInterface(witText, packageName); - - final BindgenModel.Builder modelBuilder = - BindgenModel.builder().name(definition.getName()).sourceFile(witPath.toString()); - - // Convert WIT types to bindgen types - final Map convertedTypes = new HashMap<>(); - for (Map.Entry entry : definition.getTypes().entrySet()) { - final BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); - convertedTypes.put(entry.getKey(), bindgenType); + // Fan a full WIT world source (package + world + multiple + // interfaces + resource bodies) out into per-interface entries + // the existing single-interface WIT parser can consume. + final List preprocessed = + WitWorldPreprocessor.preprocess(witText); + + // Fall back to legacy single-interface parsing if the input + // wasn't a multi-interface world (e.g. tests that pass a bare + // `interface X { ... }`). + final List interfacesToParse; + if (preprocessed.isEmpty()) { + return parseWitSourceLegacy(witPath, witText, packageName); } + interfacesToParse = preprocessed; - // Convert WIT functions to bindgen functions - final List convertedFunctions = new ArrayList<>(); - for (Map.Entry entry : definition.getFunctions().entrySet()) { - final BindgenFunction bindgenFunc = convertWitFunction(entry.getValue(), convertedTypes); - convertedFunctions.add(bindgenFunc); - } + final BindgenModel.Builder modelBuilder = + BindgenModel.builder().name(witPath.getFileName().toString()).sourceFile(witPath.toString()); + + for (final WitWorldPreprocessor.Interface pre : interfacesToParse) { + final String reconstructed = "interface " + pre.getName() + " {\n" + pre.getBody() + "\n}\n"; + final WitInterfaceParser parser = new WitInterfaceParser(); + final WitInterfaceDefinition definition = parser.parseInterface(reconstructed, packageName); + + final Map convertedTypes = new HashMap<>(); + for (final Map.Entry entry : definition.getTypes().entrySet()) { + BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); + if (bindgenType.getKind() == BindgenType.Kind.RESOURCE + && pre.getResourceBodies().containsKey(entry.getKey())) { + final BindgenType.Builder rebuilt = + BindgenType.builder() + .name(bindgenType.getName()) + .kind(BindgenType.Kind.RESOURCE) + .documentation(bindgenType.getDocumentation().orElse(null)); + final List methods = + WitResourceBodyParser.parse( + entry.getKey(), pre.getResourceBodies().get(entry.getKey())); + for (final WitResourceMethod m : methods) { + rebuilt.addResourceMethod(convertResourceMethod(m, convertedTypes)); + } + bindgenType = rebuilt.build(); + } + convertedTypes.put(entry.getKey(), bindgenType); + } - // Build interface containing types and functions - final BindgenInterface.Builder ifaceBuilder = - BindgenInterface.builder().name(definition.getName()).packageName(packageName); + final List convertedFunctions = new ArrayList<>(); + for (final Map.Entry entry : definition.getFunctions().entrySet()) { + final BindgenFunction bindgenFunc = + convertWitFunction(entry.getValue(), convertedTypes); + convertedFunctions.add(bindgenFunc); + } - for (BindgenFunction func : convertedFunctions) { - ifaceBuilder.addFunction(func); - } - for (BindgenType type : convertedTypes.values()) { - ifaceBuilder.addType(type); + final BindgenInterface.Builder ifaceBuilder = + BindgenInterface.builder().name(definition.getName()).packageName(packageName); + for (final BindgenFunction func : convertedFunctions) { + ifaceBuilder.addFunction(func); + } + for (final BindgenType type : convertedTypes.values()) { + ifaceBuilder.addType(type); + } + modelBuilder.addInterface(ifaceBuilder.build()); } - modelBuilder.addInterface(ifaceBuilder.build()); - return modelBuilder.build(); } catch (final IOException e) { throw new BindgenException("Failed to read WIT file: " + witPath, e); @@ -209,6 +240,167 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException } } + private BindgenModel parseWitSourceLegacy( + final Path witPath, final String witText, final String packageName) + throws BindgenException { + final WitInterfaceParser parser = new WitInterfaceParser(); + final WitInterfaceDefinition definition = parser.parseInterface(witText, packageName); + + final BindgenModel.Builder modelBuilder = + BindgenModel.builder().name(definition.getName()).sourceFile(witPath.toString()); + + final Map convertedTypes = new HashMap<>(); + for (final Map.Entry entry : definition.getTypes().entrySet()) { + final BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); + convertedTypes.put(entry.getKey(), bindgenType); + } + + final List convertedFunctions = new ArrayList<>(); + for (final Map.Entry entry : definition.getFunctions().entrySet()) { + final BindgenFunction bindgenFunc = convertWitFunction(entry.getValue(), convertedTypes); + convertedFunctions.add(bindgenFunc); + } + + final BindgenInterface.Builder ifaceBuilder = + BindgenInterface.builder().name(definition.getName()).packageName(packageName); + for (final BindgenFunction func : convertedFunctions) { + ifaceBuilder.addFunction(func); + } + for (final BindgenType type : convertedTypes.values()) { + ifaceBuilder.addType(type); + } + modelBuilder.addInterface(ifaceBuilder.build()); + return modelBuilder.build(); + } + + /** + * Convert a parsed WIT resource method into a {@link BindgenFunction}. Type + * expressions in parameters and the return position are resolved against the + * interface's already-materialized type table. Anything unresolved falls back + * to an opaque reference — bindgen doesn't run a full second-pass symbol + * table today, so cross-resource references need the caller to have declared + * the sibling resource first. + */ + private BindgenFunction convertResourceMethod( + final WitResourceMethod method, final Map knownTypes) { + final BindgenFunction.Builder builder = + BindgenFunction.builder() + .name(method.getName()) + .constructor(method.getKind() == WitResourceMethod.Kind.CONSTRUCTOR) + .staticMethod(method.getKind() == WitResourceMethod.Kind.STATIC); + for (final WitParameter p : method.getParameters()) { + builder.addParameter( + new BindgenParameter(p.getName(), resolveTypeExpression(p.getType().getName(), knownTypes))); + } + if (method.getReturnTypeExpression().isPresent()) { + builder.returnType( + resolveTypeExpression(method.getReturnTypeExpression().get(), knownTypes)); + } + return builder.build(); + } + + /** + * Resolve a raw WIT type expression string (e.g. {@code list}, + * {@code borrow}, {@code result}) + * into a bindgen type. Names that match already-known types resolve as + * references; primitives resolve as primitives; container expressions + * (list/option/result) recurse. Anything else lands as an opaque reference. + */ + private BindgenType resolveTypeExpression( + final String expression, final Map knownTypes) { + String expr = expression.trim(); + // Unwrap borrow and own — resource-lifetime qualifiers are ABI + // concerns; the generated Java surface only cares about the referenced + // type. + while (true) { + if (expr.startsWith("borrow<") && expr.endsWith(">")) { + expr = expr.substring("borrow<".length(), expr.length() - 1).trim(); + } else if (expr.startsWith("own<") && expr.endsWith(">")) { + expr = expr.substring("own<".length(), expr.length() - 1).trim(); + } else { + break; + } + } + if (knownTypes.containsKey(expr)) { + return BindgenType.reference(expr); + } + // Primitive? + if (isKnownPrimitive(expr)) { + return BindgenType.primitive(expr); + } + // list + if (expr.startsWith("list<") && expr.endsWith(">")) { + final String inner = expr.substring(5, expr.length() - 1).trim(); + return BindgenType.list(resolveTypeExpression(inner, knownTypes)); + } + // option + if (expr.startsWith("option<") && expr.endsWith(">")) { + final String inner = expr.substring(7, expr.length() - 1).trim(); + return BindgenType.option(resolveTypeExpression(inner, knownTypes)); + } + // result, result<_, E>, result, result<_>, result + if (expr.equals("result")) { + return BindgenType.result(null, null); + } + if (expr.startsWith("result<") && expr.endsWith(">")) { + final String inner = expr.substring(7, expr.length() - 1).trim(); + if (inner.isEmpty()) { + return BindgenType.result(null, null); + } + final int comma = topLevelComma(inner); + final String okPart = comma < 0 ? inner : inner.substring(0, comma).trim(); + final String errPart = comma < 0 ? "" : inner.substring(comma + 1).trim(); + final BindgenType ok = + okPart.isEmpty() || okPart.equals("_") + ? null + : resolveTypeExpression(okPart, knownTypes); + final BindgenType err = + errPart.isEmpty() || errPart.equals("_") + ? null + : resolveTypeExpression(errPart, knownTypes); + return BindgenType.result(ok, err); + } + return BindgenType.reference(expr); + } + + private static int topLevelComma(final String s) { + int angle = 0; + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + if (c == '<') { + angle++; + } else if (c == '>') { + angle = Math.max(0, angle - 1); + } else if (c == ',' && angle == 0) { + return i; + } + } + return -1; + } + + private static boolean isKnownPrimitive(final String name) { + switch (name) { + case "bool": + case "s8": + case "s16": + case "s32": + case "s64": + case "u8": + case "u16": + case "u32": + case "u64": + case "f32": + case "f64": + case "float32": + case "float64": + case "char": + case "string": + return true; + default: + return false; + } + } + /** * Converts a WIT type to a bindgen type. * diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java index d160630..71107c4 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java @@ -101,6 +101,14 @@ public final class BindgenCli implements Callable { description = "Disable builder generation (for LEGACY style)") private boolean noBuilders; + @Option( + names = {"--no-impl"}, + description = + "Disable *Impl and *BindingProvider generation. Use when the WIT world describes an" + + " abstract embedder surface (bindgen consumers write their own dispatch instead" + + " of using the marshalling-based Impl scaffolding).") + private boolean noImpl; + @Option( names = {"--verbose", "-v"}, description = "Enable verbose output") @@ -141,6 +149,8 @@ public Integer call() { .wasmSources(validWasmSources) .generateJavadoc(!noJavadoc) .generateBuilders(!noBuilders) + .generateImplementations(!noImpl) + .generateServiceLoader(!noImpl) .build(); if (verbose) { diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java index 058d99c..b87ab33 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java @@ -18,6 +18,8 @@ import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; import ai.tegmentum.webassembly4j.bindgen.model.BindgenField; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenParameter; import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; import ai.tegmentum.webassembly4j.bindgen.model.BindgenVariantCase; import ai.tegmentum.webassembly4j.bindgen.util.JavaNaming; @@ -205,10 +207,16 @@ protected TypeSpec generateResource(final BindgenType type) { // Add handle field classBuilder.addField(TypeName.LONG, "handle", Modifier.PRIVATE, Modifier.FINAL); - // Add constructor + // Emit the handle-holding constructor unless the WIT resource declared + // its own `constructor(...)`. When a WIT constructor is present it + // becomes the primary way callers get a handle; the handle-only ctor + // is still emitted as `protected` so subclasses / generated dispatch + // sites can wrap an existing handle. + final boolean hasWitConstructor = + type.getResourceMethods().stream().anyMatch(BindgenFunction::isConstructor); classBuilder.addMethod( MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) + .addModifiers(hasWitConstructor ? Modifier.PROTECTED : Modifier.PUBLIC) .addParameter(TypeName.LONG, "handle") .addStatement("this.handle = handle") .build()); @@ -229,6 +237,55 @@ protected TypeSpec generateResource(final BindgenType type) { .addComment("Resource cleanup - to be implemented by runtime") .build()); + // Emit the resource's WIT-declared methods. Bodies throw + // UnsupportedOperationException — the SPI runtime wiring that would + // route these to the guest hasn't landed yet (ADR-005 tracks it), so + // the surface is correct even though dispatch is stubbed. + for (final BindgenFunction method : type.getResourceMethods()) { + classBuilder.addMethod(generateResourceMethod(className, method)); + } + return classBuilder.build(); } + + private MethodSpec generateResourceMethod(final String className, final BindgenFunction method) { + if (method.isConstructor()) { + // Emit as a static factory `create` — matches the WIT constructor + // shape (constructs a new resource from primitive args) but doesn't + // collide with Java's ctor rules (return type, no name). + MethodSpec.Builder factory = + MethodSpec.methodBuilder("create") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.get(config.getPackageName(), className)); + for (BindgenParameter p : method.getParameters()) { + factory.addParameter(mapType(p.getType()), JavaNaming.toParameterName(p.getName())); + } + factory.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "wasmos:host/embedder constructor dispatch not yet wired"); + return factory.build(); + } + final String name = JavaNaming.toMethodName(method.getName()); + MethodSpec.Builder mb = MethodSpec.methodBuilder(name).addModifiers(Modifier.PUBLIC); + if (method.isStatic()) { + mb.addModifiers(Modifier.STATIC); + } + for (BindgenParameter p : method.getParameters()) { + mb.addParameter(mapType(p.getType()), JavaNaming.toParameterName(p.getName())); + } + if (method.hasReturnType()) { + mb.returns(mapType(method.getReturnType().get())); + mb.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "wasmos:host/embedder method dispatch not yet wired"); + } else { + mb.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "wasmos:host/embedder method dispatch not yet wired"); + } + return mb.build(); + } } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/model/BindgenType.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/model/BindgenType.java index 80ee8e2..a549915 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/model/BindgenType.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/model/BindgenType.java @@ -69,6 +69,7 @@ public enum Kind { private final BindgenType errorType; private final List tupleElements; private final String referencedTypeName; + private final List resourceMethods; private BindgenType(final Builder builder) { this.name = builder.name; @@ -82,6 +83,8 @@ private BindgenType(final Builder builder) { this.errorType = builder.errorType; this.tupleElements = Collections.unmodifiableList(new ArrayList<>(builder.tupleElements)); this.referencedTypeName = builder.referencedTypeName; + this.resourceMethods = + Collections.unmodifiableList(new ArrayList<>(builder.resourceMethods)); } /** @@ -257,6 +260,20 @@ public Optional getReferencedTypeName() { return Optional.ofNullable(referencedTypeName); } + /** + * Returns the methods attached to this resource type. + * + *

Populated only for {@link Kind#RESOURCE} types parsed from WIT resource + * declarations that carry a constructor / static / instance method body. The + * list is empty for every other kind and for resources declared without a + * body. + * + * @return the ordered list of resource methods (may be empty) + */ + public List getResourceMethods() { + return resourceMethods; + } + /** * Checks if this is a primitive type. * @@ -328,6 +345,7 @@ public static final class Builder { private BindgenType errorType; private List tupleElements = new ArrayList<>(); private String referencedTypeName; + private List resourceMethods = new ArrayList<>(); private Builder() {} @@ -401,6 +419,16 @@ public Builder referencedTypeName(final String typeName) { return this; } + public Builder resourceMethods(final List methods) { + this.resourceMethods = new ArrayList<>(methods); + return this; + } + + public Builder addResourceMethod(final BindgenFunction method) { + this.resourceMethods.add(method); + return this; + } + public BindgenType build() { return new BindgenType(this); } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java index 9010d5b..2290f48 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java @@ -153,26 +153,33 @@ private Map parseTypes(final String interfaceBody) throws Bindg // `flags name { ... }`). This is the canonical WIT syntax — // running it first lets the second pass resolve aliases that // reference them. + // + // Enums and flags come first so that records/variants whose + // fields or case payloads reference them can resolve without + // requiring authors to declare their WIT types in dependency + // order. Records still can't forward-reference other records + // in this simple regex parser — that would need a proper + // two-phase symbol-table build. collectStandalone( - RECORD_DECLARATION_PATTERN, + ENUM_DECLARATION_PATTERN, interfaceBody, types, - (name, body) -> parseRecordType(name, body)); + (name, body) -> parseEnumType(name, body)); collectStandalone( - VARIANT_DECLARATION_PATTERN, + FLAGS_DECLARATION_PATTERN, interfaceBody, types, - (name, body) -> parseVariantType(name, body)); + (name, body) -> parseFlagsType(name, body)); collectStandalone( - ENUM_DECLARATION_PATTERN, + RECORD_DECLARATION_PATTERN, interfaceBody, types, - (name, body) -> parseEnumType(name, body)); + (name, body) -> parseRecordType(name, body)); collectStandalone( - FLAGS_DECLARATION_PATTERN, + VARIANT_DECLARATION_PATTERN, interfaceBody, types, - (name, body) -> parseFlagsType(name, body)); + (name, body) -> parseVariantType(name, body)); // Pass 2: `type X = ...;` alias declarations. final Matcher typeMatcher = TYPE_PATTERN.matcher(interfaceBody); @@ -643,15 +650,17 @@ private List parseExports(final String interfaceBody) { * @throws BindgenException if type cannot be resolved */ private WitType resolveType(final String typeName) throws BindgenException { + final String stripped = stripResourceQualifier(typeName); + // Check cache first - final WitType cachedType = typeCache.get(typeName); + final WitType cachedType = typeCache.get(stripped); if (cachedType != null) { return cachedType; } // Try to parse as primitive type try { - final WitPrimitiveType primitive = WitPrimitiveType.fromString(typeName); + final WitPrimitiveType primitive = WitPrimitiveType.fromString(stripped); return WitType.primitive(primitive); } catch (final IllegalArgumentException e) { // Not a primitive type @@ -659,12 +668,35 @@ private WitType resolveType(final String typeName) throws BindgenException { // Try to parse as inline type definition (list, option, etc.) try { - return parseTypeDefinition(typeName, typeName); + return parseTypeDefinition(stripped, stripped); } catch (final BindgenException e) { // Not a parseable type definition } - throw new BindgenException("Unknown type: " + typeName); + // Fall back to a synthetic resource reference. WIT identifiers + // that don't resolve in the local scope typically come from a + // sibling resource declaration whose body the caller stripped + // out before feeding this interface body in — treat them as + // opaque handles rather than failing the whole parse. + return WitType.resource(stripped, stripped); + } + + /** + * Peel resource-qualifier wrappers ({@code borrow}, {@code own}) off + * a type expression. Both wrappers are ABI-level lifetime hints; at the + * generated-Java surface the referenced type is what matters. + */ + private static String stripResourceQualifier(final String typeName) { + String current = typeName.trim(); + while (true) { + if (current.startsWith("borrow<") && current.endsWith(">")) { + current = current.substring("borrow<".length(), current.length() - 1).trim(); + } else if (current.startsWith("own<") && current.endsWith(">")) { + current = current.substring("own<".length(), current.length() - 1).trim(); + } else { + return current; + } + } } /** Initializes primitive types in the type cache. */ diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java new file mode 100644 index 0000000..bfea68d --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java @@ -0,0 +1,321 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Parses the body of a WIT {@code resource} declaration into a list of + * {@link WitResourceMethod}s. + * + *

Understands the three declaration shapes: + * + *

    + *
  • {@code constructor(params)} + *
  • {@code name: static func(params) -> ret} + *
  • {@code name: func(params) -> ret} + *
+ * + *

Each declaration is terminated by a comma or semicolon (both are + * accepted). Doc comments and line comments are stripped before parsing. + * + *

This lives alongside {@link WitInterfaceParser} rather than inside it + * because the interface parser's regex machinery can't cleanly express + * the {@code constructor}/{@code static func} shapes; keeping it separate + * makes the split cheap to grow. + */ +public final class WitResourceBodyParser { + + private WitResourceBodyParser() {} + + /** + * A single method declaration on a resource, retaining enough shape for + * downstream Java code generation to decide static vs instance vs + * constructor emission. + */ + public static final class WitResourceMethod { + private final String name; + private final Kind kind; + private final List parameters; + private final Optional returnTypeExpression; + + WitResourceMethod( + final String name, + final Kind kind, + final List parameters, + final Optional returnTypeExpression) { + this.name = Objects.requireNonNull(name, "name"); + this.kind = Objects.requireNonNull(kind, "kind"); + this.parameters = List.copyOf(Objects.requireNonNull(parameters, "parameters")); + this.returnTypeExpression = Objects.requireNonNull(returnTypeExpression, "returnTypeExpression"); + } + + public String getName() { + return name; + } + + public Kind getKind() { + return kind; + } + + public List getParameters() { + return parameters; + } + + /** + * The raw return-type expression as written in the WIT source, or empty + * if the method returned no value. Kept as text so the downstream + * pipeline can resolve it against the interface's already-parsed type + * table. + */ + public Optional getReturnTypeExpression() { + return returnTypeExpression; + } + + public enum Kind { + CONSTRUCTOR, + STATIC, + INSTANCE + } + } + + /** + * Parse the body text of a WIT resource declaration. + * + * @param resourceName the name of the resource (used for diagnostics) + * @param body the body between the resource's outer braces + * @return the parsed methods, in declaration order + * @throws BindgenException if the body cannot be parsed + */ + public static List parse(final String resourceName, final String body) + throws BindgenException { + final String stripped = stripLineComments(body); + final List decls = splitDeclarations(stripped); + final List methods = new ArrayList<>(); + for (final String decl : decls) { + final String trimmed = decl.trim(); + if (trimmed.isEmpty()) { + continue; + } + methods.add(parseDeclaration(resourceName, trimmed)); + } + return methods; + } + + private static String stripLineComments(final String source) { + final StringBuilder out = new StringBuilder(source.length()); + for (final String line : source.split("\n", -1)) { + int cut = -1; + boolean inString = false; + for (int i = 0; i < line.length() - 1; i++) { + final char c = line.charAt(i); + if (c == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { + inString = !inString; + } else if (!inString && c == '/' && line.charAt(i + 1) == '/') { + cut = i; + break; + } + } + if (cut < 0) { + out.append(line).append('\n'); + } else { + out.append(line, 0, cut).append('\n'); + } + } + return out.toString(); + } + + /** + * Split a resource body on top-level {@code ,} or {@code ;} terminators. + * Angle-bracket and paren nesting are respected so {@code list} + * and {@code func(a: u32, b: u32)} don't split mid-signature. + */ + private static List splitDeclarations(final String body) { + final List out = new ArrayList<>(); + final StringBuilder current = new StringBuilder(); + int angle = 0; + int paren = 0; + boolean inString = false; + for (int i = 0; i < body.length(); i++) { + final char c = body.charAt(i); + if (c == '"' && (i == 0 || body.charAt(i - 1) != '\\')) { + inString = !inString; + current.append(c); + continue; + } + if (!inString) { + if (c == '<') { + angle++; + } else if (c == '>') { + angle = Math.max(0, angle - 1); + } else if (c == '(') { + paren++; + } else if (c == ')') { + paren = Math.max(0, paren - 1); + } + } + if (!inString && angle == 0 && paren == 0 && (c == ',' || c == ';')) { + out.add(current.toString()); + current.setLength(0); + } else { + current.append(c); + } + } + if (current.length() > 0) { + out.add(current.toString()); + } + return out; + } + + private static WitResourceMethod parseDeclaration(final String resourceName, final String decl) + throws BindgenException { + if (decl.startsWith("constructor")) { + final int open = decl.indexOf('('); + final int close = findMatchingParen(decl, open); + if (open < 0 || close < 0) { + throw new BindgenException( + "Malformed constructor on resource " + resourceName + ": " + decl); + } + final String params = decl.substring(open + 1, close); + return new WitResourceMethod( + "constructor", + WitResourceMethod.Kind.CONSTRUCTOR, + parseParameters(params), + Optional.empty()); + } + final int colon = decl.indexOf(':'); + if (colon < 0) { + throw new BindgenException( + "Resource method missing ':' on resource " + resourceName + ": " + decl); + } + final String name = decl.substring(0, colon).trim(); + final String rhs = decl.substring(colon + 1).trim(); + final boolean isStatic = rhs.startsWith("static "); + final String afterStatic = isStatic ? rhs.substring("static ".length()).trim() : rhs; + if (!afterStatic.startsWith("func")) { + throw new BindgenException( + "Expected 'func' after ':' on resource " + resourceName + "." + name + ": " + rhs); + } + final int open = afterStatic.indexOf('('); + final int close = findMatchingParen(afterStatic, open); + if (open < 0 || close < 0) { + throw new BindgenException( + "Malformed func signature on resource " + resourceName + "." + name); + } + final String params = afterStatic.substring(open + 1, close); + final String tail = afterStatic.substring(close + 1).trim(); + Optional returnExpr = Optional.empty(); + if (tail.startsWith("->")) { + returnExpr = Optional.of(tail.substring(2).trim()); + } + return new WitResourceMethod( + name, + isStatic ? WitResourceMethod.Kind.STATIC : WitResourceMethod.Kind.INSTANCE, + parseParameters(params), + returnExpr); + } + + private static int findMatchingParen(final String s, final int open) { + if (open < 0) { + return -1; + } + int depth = 0; + boolean inString = false; + for (int i = open; i < s.length(); i++) { + final char c = s.charAt(i); + if (c == '"' && (i == 0 || s.charAt(i - 1) != '\\')) { + inString = !inString; + continue; + } + if (inString) { + continue; + } + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; + } + + private static List parseParameters(final String params) throws BindgenException { + final List out = new ArrayList<>(); + if (params == null || params.trim().isEmpty()) { + return out; + } + for (final String piece : splitTopLevelCommas(params)) { + final String trimmed = piece.trim(); + if (trimmed.isEmpty()) { + continue; + } + final int colon = trimmed.indexOf(':'); + if (colon < 0) { + throw new BindgenException("Invalid parameter (no ':'): " + piece); + } + final String name = trimmed.substring(0, colon).trim(); + final String typeExpr = trimmed.substring(colon + 1).trim(); + // Store the raw type expression as the WitType name; the downstream + // code generator resolves it against the interface's type table. + out.add(new WitParameter(name, WitType.resource(typeExpr, typeExpr), false, Optional.empty())); + } + return out; + } + + private static List splitTopLevelCommas(final String s) { + final List out = new ArrayList<>(); + int angle = 0; + int paren = 0; + boolean inString = false; + final StringBuilder current = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + if (c == '"' && (i == 0 || s.charAt(i - 1) != '\\')) { + inString = !inString; + current.append(c); + continue; + } + if (!inString) { + if (c == '<') { + angle++; + } else if (c == '>') { + angle = Math.max(0, angle - 1); + } else if (c == '(') { + paren++; + } else if (c == ')') { + paren = Math.max(0, paren - 1); + } + } + if (!inString && angle == 0 && paren == 0 && c == ',') { + out.add(current.toString()); + current.setLength(0); + } else { + current.append(c); + } + } + if (current.length() > 0) { + out.add(current.toString()); + } + return out; + } +} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java new file mode 100644 index 0000000..5a2dd8d --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java @@ -0,0 +1,324 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Preprocesses a full WIT world source (package + optional {@code world} blocks + + * multiple {@code interface} blocks + resource declarations with bodies) into a + * flat list of {@link Interface} records the existing regex-based + * {@link WitInterfaceParser} can consume. + * + *

Behaviour: + * + *

    + *
  • {@code package X@Y;} declarations are removed. + *
  • {@code world X { ... }} blocks are skipped entirely — their + * {@code import} / {@code export} clauses aren't part of any interface + * surface bindgen needs to project. + *
  • {@code interface X { ... }} blocks are split into individual + * {@link Interface} entries. The existing parser is single-interface; + * this splitter fans a world file out into a stream of what it expects. + *
  • {@code resource X { body }} declarations inside an interface body have + * their bodies stripped and returned in a side-map keyed by resource + * name. The interface body then contains a bare {@code resource X;} + * token so {@link WitInterfaceParser} still sees the resource type + * declaration but doesn't choke on the constructor / static / method + * syntax it doesn't understand. + *
+ */ +public final class WitWorldPreprocessor { + + private WitWorldPreprocessor() {} + + /** A single interface extracted from a WIT world source. */ + public static final class Interface { + private final String name; + private final String body; + private final Map resourceBodies; + + Interface(final String name, final String body, final Map resourceBodies) { + this.name = name; + this.body = body; + this.resourceBodies = resourceBodies; + } + + /** The interface's name as written in WIT (e.g. {@code embedder-api}). */ + public String getName() { + return name; + } + + /** + * The interface body with resource bodies stripped — safe to hand to + * {@link WitInterfaceParser#parseInterface(String, String)} after + * wrapping in {@code interface name { ... }}. + */ + public String getBody() { + return body; + } + + /** + * Map from resource name to its unparsed body text. Callers that want + * constructor / static / instance-method definitions parse these + * themselves. Empty when the interface declares no resources with + * bodies. + */ + public Map getResourceBodies() { + return resourceBodies; + } + } + + /** + * Split a full WIT source into per-interface entries. + * + * @param witText the raw WIT world source + * @return the interfaces contained in the source, in declaration order + * @throws BindgenException if brace matching fails + */ + public static List preprocess(final String witText) throws BindgenException { + final String withoutComments = stripLineComments(witText); + final String withoutPackage = stripPackage(withoutComments); + final String withoutWorlds = stripTopLevelBlocks(withoutPackage, "world"); + return splitInterfaces(withoutWorlds); + } + + /** + * Remove {@code //…} comments (respecting string literals). Doc comments + * ({@code ///…}) are removed by the same pass since they start with + * {@code //} too. + */ + private static String stripLineComments(final String source) { + final StringBuilder out = new StringBuilder(source.length()); + for (final String line : source.split("\n", -1)) { + int cut = -1; + boolean inString = false; + for (int i = 0; i < line.length() - 1; i++) { + final char c = line.charAt(i); + if (c == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { + inString = !inString; + } else if (!inString && c == '/' && line.charAt(i + 1) == '/') { + cut = i; + break; + } + } + if (cut < 0) { + out.append(line).append('\n'); + } else { + out.append(line, 0, cut).append('\n'); + } + } + return out.toString(); + } + + /** Strip {@code package name@version;} and {@code package name;} lines. */ + private static String stripPackage(final String source) { + return source.replaceAll("(?m)^\\s*package\\s+[^;]+;\\s*$", ""); + } + + /** + * Remove every top-level {@code keyword name { … }} block (used to skip + * {@code world}). Brace-balanced so nested braces don't confuse the scan. + */ + private static String stripTopLevelBlocks(final String source, final String keyword) + throws BindgenException { + final StringBuilder out = new StringBuilder(source.length()); + int i = 0; + while (i < source.length()) { + final int idx = source.indexOf(keyword, i); + if (idx < 0) { + out.append(source, i, source.length()); + break; + } + if (!isKeywordBoundary(source, idx, keyword.length())) { + out.append(source, i, idx + 1); + i = idx + 1; + continue; + } + final int braceOpen = source.indexOf('{', idx); + if (braceOpen < 0) { + // No body — leave the fragment alone, we may be scanning garbage. + out.append(source, i, source.length()); + break; + } + out.append(source, i, idx); + final int braceClose = matchBrace(source, braceOpen); + if (braceClose < 0) { + throw new BindgenException( + "Unbalanced braces in " + keyword + " block starting at offset " + braceOpen); + } + i = braceClose + 1; + } + return out.toString(); + } + + private static boolean isKeywordBoundary(final String source, final int at, final int keywordLen) { + if (at > 0) { + final char before = source.charAt(at - 1); + if (Character.isLetterOrDigit(before) || before == '-' || before == '_') { + return false; + } + } + final int after = at + keywordLen; + if (after < source.length()) { + final char afterCh = source.charAt(after); + if (Character.isLetterOrDigit(afterCh) || afterCh == '-' || afterCh == '_') { + return false; + } + } + return true; + } + + private static int matchBrace(final String source, final int open) { + int depth = 0; + boolean inString = false; + for (int i = open; i < source.length(); i++) { + final char c = source.charAt(i); + if (c == '"' && (i == 0 || source.charAt(i - 1) != '\\')) { + inString = !inString; + continue; + } + if (inString) { + continue; + } + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; + } + + /** + * Walk the (comment-and-world-free) source and split it into + * {@code interface X { body }} blocks. Resource declarations inside each + * body are extracted and returned separately. + */ + private static List splitInterfaces(final String source) throws BindgenException { + final List result = new ArrayList<>(); + int i = 0; + while (i < source.length()) { + final int idx = source.indexOf("interface", i); + if (idx < 0) { + break; + } + if (!isKeywordBoundary(source, idx, "interface".length())) { + i = idx + 1; + continue; + } + // Read the identifier that follows. + int cursor = idx + "interface".length(); + while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { + cursor++; + } + final int nameStart = cursor; + while (cursor < source.length() && isIdentChar(source.charAt(cursor))) { + cursor++; + } + final String name = source.substring(nameStart, cursor); + // Skip whitespace to the opening brace. + while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { + cursor++; + } + if (cursor >= source.length() || source.charAt(cursor) != '{') { + throw new BindgenException("Expected '{' after interface " + name); + } + final int braceOpen = cursor; + final int braceClose = matchBrace(source, braceOpen); + if (braceClose < 0) { + throw new BindgenException("Unbalanced braces in interface " + name); + } + final String body = source.substring(braceOpen + 1, braceClose); + final Map resources = new LinkedHashMap<>(); + final String bodyStripped = extractResourceBodies(body, resources); + result.add(new Interface(name, bodyStripped, resources)); + i = braceClose + 1; + } + return result; + } + + private static boolean isIdentChar(final char c) { + return Character.isLetterOrDigit(c) || c == '-' || c == '_'; + } + + /** + * Find each {@code resource name { body }} declaration in the interface body + * and replace it with a bare {@code resource name;} so the downstream + * regex-based parser still sees the type. Emit the extracted bodies into + * {@code sink} keyed by resource name. + */ + private static String extractResourceBodies(final String body, final Map sink) + throws BindgenException { + final StringBuilder out = new StringBuilder(body.length()); + int i = 0; + while (i < body.length()) { + final int idx = body.indexOf("resource", i); + if (idx < 0) { + out.append(body, i, body.length()); + break; + } + if (!isKeywordBoundary(body, idx, "resource".length())) { + out.append(body, i, idx + 1); + i = idx + 1; + continue; + } + // Copy everything up to the keyword. + out.append(body, i, idx); + int cursor = idx + "resource".length(); + while (cursor < body.length() && Character.isWhitespace(body.charAt(cursor))) { + cursor++; + } + final int nameStart = cursor; + while (cursor < body.length() && isIdentChar(body.charAt(cursor))) { + cursor++; + } + final String name = body.substring(nameStart, cursor); + // Look ahead for `{` (declaration-with-body) or `;` + // (declaration-without-body, e.g. `type X = resource;`). + int lookAhead = cursor; + while (lookAhead < body.length() && Character.isWhitespace(body.charAt(lookAhead))) { + lookAhead++; + } + if (lookAhead < body.length() && body.charAt(lookAhead) == '{') { + final int braceClose = matchBrace(body, lookAhead); + if (braceClose < 0) { + throw new BindgenException("Unbalanced braces in resource " + name); + } + sink.put(name, body.substring(lookAhead + 1, braceClose)); + // Emit a `type` alias so the downstream parser records the + // resource type but doesn't try to interpret the method body. + // The parser only understands standalone RECORD/VARIANT/ENUM/ + // FLAGS declarations plus `type X = ...` aliases; resources + // hitch a ride on the alias form. + out.append("type ").append(name).append(" = resource;\n"); + i = braceClose + 1; + } else { + // Not a body-bearing declaration — keep original text. + out.append(body, idx, cursor); + i = cursor; + } + } + return out.toString(); + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java new file mode 100644 index 0000000..0c75197 --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java @@ -0,0 +1,238 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wasmos; + +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.Assertions.fail; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import ai.tegmentum.webassembly4j.bindgen.CodeGenerator; +import ai.tegmentum.webassembly4j.bindgen.CodeStyle; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Shape-check test for the {@code wasmos:host/embedder@0.1.0} WIT world. + * + *

This is the JVM lane's proof-of-shape per ADR-006: bindgen consumes the + * embedder WIT and produces a Java surface a JVM embedder can compile + * against without hand-writing anything except the native {@code + * wasmos_start} shim. + * + *

The test: + * + *

    + *
  1. Runs {@link CodeGenerator} against the fixture copy of {@code + * embedder.wit} (kept under {@code src/test/resources/wasmos-embedder/}). + * Implementation classes are disabled because {@code + * ImplementationCodeGenerator} generates broken marshalling glue for the + * {@code hostCall} shape (the parameter-name / local-variable collision + * is a pre-existing bug in that generator, unrelated to this shape). + *
  2. Asserts each file in the expected surface exists and contains the + * right members. + *
  3. Feeds every generated file to the platform {@link JavaCompiler} and + * fails on any javac diagnostic. + *
+ */ +@DisplayName("wasmos:host/embedder@0.1.0 shape check") +class WasmosEmbedderShapeTest { + + private static final String WIT_RESOURCE = "wasmos-embedder/embedder.wit"; + private static final String PACKAGE = "ai.tegmentum.wasmos.embedder.generated"; + + private static Path witFixture; + + @BeforeAll + static void locateFixture() throws URISyntaxException { + final URL resource = WasmosEmbedderShapeTest.class.getClassLoader().getResource(WIT_RESOURCE); + assertNotNull(resource, "fixture " + WIT_RESOURCE + " missing from test resources"); + witFixture = Path.of(URLDecoder.decode(resource.toURI().getPath(), StandardCharsets.UTF_8)); + } + + @Test + @DisplayName("bindgen generates the expected embedder surface and it compiles") + void generatesAndCompiles(@TempDir Path outputDir) throws Exception { + final BindgenConfig config = + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(outputDir) + .codeStyle(CodeStyle.MODERN) + .addWitSource(witFixture) + .generateImplementations(false) + .generateServiceLoader(false) + .build(); + + final CodeGenerator generator = new CodeGenerator(config); + final List sources = generator.generate(); + assertTrue(sources.size() >= 7, "expected at least 7 generated sources, got " + sources.size()); + + final Map byClass = toSourceMap(sources); + + // Sanity: every top-level type / interface / resource is present. + assertTrue(byClass.containsKey("ErrorCode"), "ErrorCode enum missing"); + assertTrue(byClass.containsKey("Error"), "Error record missing"); + assertTrue(byClass.containsKey("ImportSatisfaction"), "ImportSatisfaction record missing"); + assertTrue(byClass.containsKey("HostProvider"), "HostProvider resource missing"); + assertTrue(byClass.containsKey("RuntimeInstance"), "RuntimeInstance resource missing"); + assertTrue(byClass.containsKey("EmbedderApi"), "EmbedderApi interface missing"); + assertTrue(byClass.containsKey("EmbedderCallbacks"), "EmbedderCallbacks interface missing"); + + // ErrorCode: all 7 WIT variants. + final String errorCode = byClass.get("ErrorCode"); + for (final String v : + List.of( + "INVALID_COMPONENT", + "UNSATISFIED_IMPORT", + "MISSING_EXPORT", + "HOST_CALLBACK_FAILED", + "TRAP", + "WIT_MISMATCH", + "OTHER")) { + assertTrue(errorCode.contains(v), "ErrorCode missing variant " + v); + } + + // Error record: has both fields. + final String errorType = byClass.get("Error"); + assertTrue(errorType.contains("ErrorCode code"), "Error missing 'code' field"); + assertTrue(errorType.contains("String message"), "Error missing 'message' field"); + + // RuntimeInstance: static instantiate + instance methods. + final String rti = byClass.get("RuntimeInstance"); + assertTrue( + rti.contains("public static") && rti.contains("instantiate("), + "RuntimeInstance.instantiate must be public static"); + assertTrue(rti.contains("callExport("), "RuntimeInstance.callExport missing"); + assertTrue(rti.contains("introspect()"), "RuntimeInstance.introspect missing"); + assertTrue(rti.contains("verifyWorld("), "RuntimeInstance.verifyWorld missing"); + assertTrue(rti.contains("implements AutoCloseable"), "RuntimeInstance must be AutoCloseable"); + + // HostProvider: static create(interfaceName, numFuncs) from the WIT constructor. + final String hp = byClass.get("HostProvider"); + assertTrue( + hp.contains("public static HostProvider create(String interfaceName, int numFuncs)"), + "HostProvider must expose a create(String, int) factory for the WIT constructor"); + assertTrue(hp.contains("implements AutoCloseable"), "HostProvider must be AutoCloseable"); + + // EmbedderCallbacks: three abstract methods. + final String cb = byClass.get("EmbedderCallbacks"); + assertTrue(cb.contains("public interface EmbedderCallbacks"), "EmbedderCallbacks must be interface"); + assertTrue(cb.contains("hostCall("), "EmbedderCallbacks missing hostCall"); + assertTrue(cb.contains("nowMonotonicNs()"), "EmbedderCallbacks missing nowMonotonicNs"); + assertTrue(cb.contains("readRandom("), "EmbedderCallbacks missing readRandom"); + + // Determinism: same input, same generated content. + final CodeGenerator generator2 = new CodeGenerator(config); + final Map secondPass = toSourceMap(generator2.generate()); + assertEquals(byClass.keySet(), secondPass.keySet(), "class set must be deterministic"); + for (final String className : byClass.keySet()) { + assertEquals(byClass.get(className), secondPass.get(className), "content drift in " + className); + } + + // Compile every generated file with the platform javac. This is what + // gives the shape check teeth — regex assertions only prove the text is + // there, not that the whole surface is a valid Java program. + for (final GeneratedSource src : sources) { + src.writeTo(outputDir); + } + compileGeneratedSurface(outputDir); + } + + private static Map toSourceMap(final List sources) { + final Map map = new HashMap<>(); + for (final GeneratedSource src : sources) { + map.put(src.getClassName(), src.getContent()); + } + return map; + } + + private static void compileGeneratedSurface(final Path outputDir) throws Exception { + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull( + compiler, + "No JavaCompiler available on the running JDK — run the test suite on a JDK, not a JRE."); + + final List javaFiles = new ArrayList<>(); + try (Stream paths = Files.walk(outputDir)) { + paths + .filter(p -> p.toString().endsWith(".java")) + .forEach(p -> javaFiles.add(p.toFile())); + } + if (javaFiles.isEmpty()) { + fail("No generated .java files found under " + outputDir); + } + + final Path classDir = outputDir.resolve("classes"); + Files.createDirectories(classDir); + + // Provide the classpath: WitResult (in bindgen module classes) is + // referenced by the generated code. Reuse the running test's classpath. + final String cp = System.getProperty("java.class.path"); + final List options = new ArrayList<>(); + options.add("-d"); + options.add(classDir.toString()); + options.add("-source"); + options.add("11"); + options.add("-target"); + options.add("11"); + options.add("-classpath"); + options.add(cp); + + final ByteArrayOutputStream errBuf = new ByteArrayOutputStream(); + final StringWriter diagnosticSink = new StringWriter(); + final int rc; + try (var fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + final Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(javaFiles); + rc = + compiler + .getTask( + new PrintWriter(diagnosticSink), fileManager, null, options, null, compilationUnits) + .call() + ? 0 + : 1; + } + if (rc != 0) { + fail( + "javac failed on generated surface:\n" + + diagnosticSink + + "\n" + + errBuf.toString(StandardCharsets.UTF_8)); + } + } +} diff --git a/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit b/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit new file mode 100644 index 0000000..223da85 --- /dev/null +++ b/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit @@ -0,0 +1,208 @@ +// wasmos:host/embedder — v0.1.0 +// +// The embedder-facing WIT world exported by wasmcm_runtime_guest.wasm. +// Per ADR-006, this is the single surface any embedder (JVM, Rust +// CLI, Node, browser, Python, embedded) integrates against; the +// only hand-written native code per embedder is a `wasmos_start` +// shim that boots the runtime guest on that embedder's Wasm engine. +// Everything else is generated by per-language bindgen consuming +// this world. +// +// The world has two halves: +// - `embedder-api` — exported by the guest, called by the host to +// load components, register providers, instantiate, invoke, and +// introspect. +// - `embedder-callbacks` — imported by the guest, called back into +// the host to dispatch registered-provider functions and to +// satisfy the narrow set of syscall-shaped primitives the +// runtime guest itself needs (clock, random). +// +// Status: draft. The shape follows ADR-006. Concrete signatures +// are expected to iterate as the first bindgen tool (webassembly4j +// per ADR-005) produces its first generated surface and calls it +// end-to-end. + +package wasmos:host@0.1.0; + +interface embedder-api { + // ----------------------------------------------------------------- + // Errors. + // + // Every fallible export returns `result` with a coded + // reason and a free-form message. Codes let generated bindings + // map cleanly to language-level exception types; the message is + // for diagnostics. + // ----------------------------------------------------------------- + + enum error-code { + // The bytes passed to `runtime-instance.instantiate` did not + // parse or validate as a Component Model binary. + invalid-component, + // A component import name was not satisfied by any entry in + // the `imports` list passed to `instantiate`. + unsatisfied-import, + // `call-export` was invoked with a name not present in the + // instantiated component's exports. + missing-export, + // A host-side dispatcher (via `embedder-callbacks.host-call`) + // returned an error. The `error.message` carries the + // host-side reason string verbatim. + host-callback-failed, + // Guest execution trapped (unreachable, out-of-bounds, + // integer overflow, div-by-zero, etc.). + trap, + // `verify-world` found a structural mismatch against the + // expected WIT source. + wit-mismatch, + // Anything else. Prefer a specific variant when the category + // is known. + other, + } + + record error { + code: error-code, + message: string, + } + + // ----------------------------------------------------------------- + // Host-provided implementations of imported WIT interfaces. + // + // Before `instantiate`, the host registers each WIT interface it + // will satisfy by constructing a `host-provider` — a triple of + // (interface name, function count, opaque handle). The `handle` + // is then referenced from the `imports` list to bind that + // interface. Dispatch of individual function calls goes through + // `embedder-callbacks.host-call`, keyed on (handle, func-idx). + // ----------------------------------------------------------------- + + resource host-provider { + // Register a host-implemented WIT interface. `interface-name` + // is the fully-qualified name (e.g. `wasip2:host/primitives@0.1.0`) + // and `num-funcs` is the count of functions in the interface + // as seen by the guest. The number is used for bounds-checking + // on dispatch; mismatches surface as + // `error-code::host-callback-failed`. + constructor(interface-name: string, num-funcs: u32); + } + + // ----------------------------------------------------------------- + // Import satisfaction records. + // + // Passed as a list to `runtime-instance.instantiate`. Each entry + // pairs an import's interface name (as it appears in the component + // binary) with the `host-provider` handle that satisfies it. + // Ordering does not matter; the runtime matches on name. + // ----------------------------------------------------------------- + + record import-satisfaction { + interface-name: string, + provider: borrow, + } + + // ----------------------------------------------------------------- + // Runtime instance. + // + // An instantiated Component Model component with all imports + // satisfied and ready to receive export invocations. Owned by + // the caller; drop the resource to release the underlying + // runtime state. + // ----------------------------------------------------------------- + + resource runtime-instance { + // Load, validate, and instantiate a Component Model binary + // with the supplied import satisfactions. Returns the + // instance on success or a coded `error` on failure. + // + // The `component-bytes` list is copied into runtime-owned + // memory; the caller may free its buffer immediately after + // this call returns. + instantiate: static func( + component-bytes: list, + imports: list, + ) -> result; + + // Invoke an exported function by fully-qualified name. + // + // `args` is a Canonical-ABI-encoded byte string; the calling + // language's generated bindings own the typed-to-bytes + // encoding. The `list` return is the encoded return + // value in the same shape. Errors surface as coded `error`. + // + // Interacts with the M10 generated adapter cache: hot paths + // dispatch through pre-compiled adapters when available and + // fall back to the interpreter otherwise. Neither behaviour + // is observable through this surface. + call-export: func( + name: string, + args: list, + ) -> result, error>; + + // Dump the instantiated component's WIT surface (imports, + // exports, resource types, records, variants, etc.) as a + // JSON string. Format matches `wasmcm inspect --wit + // --format=json` today; expected to be documented as a + // stable schema before this ADR promotes to Accepted. + introspect: func() -> string; + + // Verify the instantiated component's world structurally + // matches `expected-wit` (a WIT-source string). Returns an + // empty ok on match, or `error` with code `wit-mismatch` + // and a message describing the first divergence. + // + // Subtype rules mirror `wasmcm validate --against`: record + // and variant width subtyping, tuple positional exact, + // join-of-variants flattening, resource identity via + // qualified names + generational handles. + verify-world: func(expected-wit: string) -> result<_, error>; + } +} + +interface embedder-callbacks { + // ----------------------------------------------------------------- + // Host-implemented import dispatcher. + // + // Called by the guest during `call-export` execution whenever a + // running component invokes a function on an imported interface + // that was satisfied by a host `host-provider`. The embedder is + // responsible for looking up (provider-handle, func-idx) in its + // registered-provider table and invoking the language-level + // implementation with `args` decoded and results encoded per + // the Canonical ABI (again, generated bindings own the typed + // encoding). + // + // An error string maps back to `error-code::host-callback-failed` + // on the guest side. + // ----------------------------------------------------------------- + + host-call: func( + provider-handle: u32, + func-idx: u32, + args: list, + ) -> result, string>; + + // ----------------------------------------------------------------- + // Syscall-shaped primitives the runtime guest itself needs from + // the embedder. Distinct from `wasi:clocks` / `wasi:random` + // interfaces implemented by wasi-p2 components — those live one + // layer up and route through `host-call`. These are the runtime + // guest's *own* dependencies. + // ----------------------------------------------------------------- + + // Monotonic nanoseconds since an unspecified epoch. Used by the + // runtime guest for deadline tracking, resource-table generation + // counters, and diagnostic timestamps. No monotonicity guarantee + // across `wasmos_start` calls in the same host process. + now-monotonic-ns: func() -> u64; + + // Fill a buffer with `len` cryptographically random bytes. Used + // by the runtime guest for resource-handle randomization. If + // the host cannot satisfy the request (e.g. missing entropy on + // an embedded target) it returns an empty list; guest treats + // that as unavailable and falls back to deterministic seeding. + read-random: func(len: u32) -> list; +} + +world embedder { + export embedder-api; + import embedder-callbacks; +} From 8c8e4d927c0276694ed86f9f34f5be0eb854d79a Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 17:20:28 -0400 Subject: [PATCH 42/85] test(endive-provider): advance wasi_p2_clocks probe past unsatisfied-import wall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous assertion locked in HOST_ERR_INVALID "component-level import 'wasi:io/poll@0.2.3' was not satisfied by the caller" — the LinkingContext only registered wasip2:host/primitives, so the runtime bailed before touching the HostCallback bridge at all. Register a placeholder WitHostFunctionDefinition for wasi:io/poll@0.2.3 so the guest provider table has a row for it (matches num_funcs=0 in the sibling Rust probe_wasi_p2_clocks), and update the assertion to lock in the actual next wall: HOST_ERR_UNSUPPORTED "adapter does not implement create_host_func". Cross-repo: pairs with wasm-cm 97ad17b (WasiClocksProbeTest pins the same wall at the loader level with file:line specificity). The wall sits in the wasm-cm guest-side HostEngineWasm — updating this test's assertion is the honest reflection of where the JVM lane stops today. --- .../EndiveComponentIntegrationTest.java | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java index 5076109..2b36cef 100644 --- a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java @@ -81,13 +81,34 @@ void scalarComponentRoundTripsAddThroughGuest() throws Exception { * Attempt to instantiate a real WASI Preview 2 component that imports * {@code wasip2:host/primitives@0.1.0} and {@code wasi:io/poll@0.2.3}. * - *

Full round-trip host-callback dispatch is gated behind Wall 2 on the - * wasm-cm Rust side (see {@code HostCallbackDispatcher} javadoc). Until - * Wall 2 lands, the guest surfaces {@code HOST_ERR_UNLINKABLE} even when - * host providers are registered — this adapter re-throws as - * {@link InstantiationException}. The test locks in the honest failure so - * the day Wall 2 arrives the assertion here flips and this becomes a real - * success path. + *

Walls 2 & 3 land the guest-side {@code + * ImportedInstance::HostCallback} plumbing and the Java-side + * {@link run.tegmentum.wasmcm.endive.HostCallbackDispatcher} wiring — + * the register/dispatch surface is fully present. The remaining gap is + * inside {@code wasmcm-runtime-guest}: its {@code + * HostEngineWasm} impl of {@code wasmcm_host_api::HostEngine} does not + * override {@code create_host_func}, so the trait default at + * {@code crates/wasmcm-host-api/src/engine.rs:271-278} returns + * {@code Error::UnsupportedFeature("adapter does not implement + * create_host_func")}. The Rust runtime hits this from + * {@code Runtime::mint_host_callback_handle} at + * {@code crates/wasmcm-runtime/src/lib.rs:2114} while synthesising the + * canon-lower ⇒ HostCallback core-func bridge — before the extern + * {@code wasmcm_host_call} return path is exercised at all. Surfaces + * through the JVM lane as {@code HOST_ERR_UNSUPPORTED} (tag 7). + * + *

The sibling Rust probe {@code + * crates/wasmcm-runtime/tests/probe_wasi_p2_clocks.rs} passes cleanly + * because it drives {@code WasmtimeCoreEngine} (which overrides + * {@code create_host_func}); the JVM sandwich cannot route through + * that until the guest-side {@code HostEngineWasm} learns the same + * trick over a new extern.

+ * + *

When that gap closes (guest-side {@code create_host_func} plus + * matching {@code instantiate}-with-func-imports on both the guest + * Rust and Java sides), flip this to + * {@code assertNotNull(component.instantiate(ctx))} and assert the + * expected exports.

*/ @Test void wasiClocksComponentReachesInstantiationSite() throws Exception { @@ -121,17 +142,31 @@ void wasiClocksComponentReachesInstantiationSite() throws Exception { new WasmcmValueCodec.Boxed( WasmcmValueCodec.TAG_U64, 1L) })) + // wasi:io/poll@0.2.3 is a resource-type-only import in + // this component (num_funcs=0 in the sibling Rust + // probe); we register a placeholder entry so the guest + // provider table has a row for it. The registered + // dispatcher is never reached — the canon-lower ⇒ + // HostCallback chain bails earlier at create_host_func. + .addWitHostFunction( + new WitHostFunctionDefinition( + "wasi:io/poll@0.2.3#__placeholder", + args -> new Object[0])) .build(); - // Instantiation is expected to fail today with an UNLINKABLE from the - // guest — wasi:io/poll@0.2.3 imports a resource type the runtime - // cannot yet satisfy, and Wall 2 (canon-lower routing to a host - // callback) is not yet wired. When Wall 2 lands, flip this to - // assertNotNull(component.instantiate(ctx)). + // Instantiation is expected to fail with HOST_ERR_UNSUPPORTED + // "adapter does not implement create_host_func" — see the + // class javadoc above for file:line of the true wall. When + // the guest-side HostEngineWasm learns create_host_func, flip + // this to assertNotNull(component.instantiate(ctx)) and + // assert the exported clocks interface is present. InstantiationException ex = assertThrows( InstantiationException.class, () -> component.instantiate(ctx)); assertNotNull(ex.getMessage(), "guest should have written a diagnostic"); + assertTrue( + ex.getMessage().contains("adapter does not implement create_host_func"), + "expected the honest create_host_func wall but got: " + ex.getMessage()); } } From a393b8c92849085045e2f4a95b3440645aa8392f Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 17:21:58 -0400 Subject: [PATCH 43/85] fix(bindgen): compileable marshalling body for `args` collision and result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two blocking bugs in `ImplementationCodeGenerator#generateMarshallingBody` surfaced by the wasmos:host/embedder@0.1.0 shape check. The generated `*Impl` class used to fail javac in two independent ways: 1. Parameter/local collision on `args`. The body declared internal locals named `args`, `retptr`, and `result` unconditionally. Any WIT function whose parameter list happens to contain one of those names — e.g. `embedder-callbacks.host-call(provider-handle, func-idx, args: list)` — produced `variable args is already defined`. Now every internal local is disambiguated against the WIT parameter set via a `$N` suffix; `$` cannot appear in a `JavaNaming.toParameterName` output so the disambiguated form is guaranteed unique. 2. Comment-in-return-position for `result` return types. `MarshallingStrategy#liftReturn` has no discriminant lift for RESULT and fell through to `CodeBlock.of("/* TODO: lift RESULT */", …)`, which `addStatement("return $L", …)` splices into `return /* TODO: lift RESULT */;` — a comment, not an expression. Emit an `UnsupportedOperationException` throw referencing the ADR-005 gap; the class compiles, the placeholder can be swapped when the Canonical-ABI discriminant lift lands, and the parameter and return-type shape are still correct. MarshallingStrategy#liftReturn/liftPrimitive now accept the caller's result local name rather than hard-coding `result`, so the disambiguated local name from ImplementationCodeGenerator flows through both call sites. Regression coverage in `ImplementationCodeGeneratorBugsTest`: three focused reproducers (`args`-named param, `result`-named param, `result` return) that compile the generated `*Impl` through the platform javac against inline stubs of the api/runtime types the generator references. `WasmosEmbedderShapeTest` continues to run with `generateImplementations (false)` — not because of the fixed bugs (both are gone) but because the generated `*Impl` bodies still reference `webassembly4j-api` and `webassembly4j-runtime` types that aren't test dependencies of the bindgen module. Turning the shape test into a full impl-on compile check is a separate wiring task; the stale comment claiming the collision bug was the blocker is updated. Test count: 469 -> 472 in `webassembly4j-bindgen`. --- .../ImplementationCodeGenerator.java | 81 ++- .../generator/MarshallingStrategy.java | 35 +- .../ImplementationCodeGeneratorBugsTest.java | 467 ++++++++++++++++++ .../wasmos/WasmosEmbedderShapeTest.java | 15 +- 4 files changed, 571 insertions(+), 27 deletions(-) create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java index 172f85b..b20d2ab 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java @@ -38,8 +38,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import javax.lang.model.element.Modifier; /** @@ -335,14 +337,54 @@ private void generateDirectBody(MethodSpec.Builder method, BindgenFunction funct private void generateMarshallingBody(MethodSpec.Builder method, BindgenFunction function, String fieldName, TypeName returnType, boolean hasReturn) { BindgenType returnBindgenType = hasReturn ? function.getReturnType().get() : null; + + // TODO(bindgen): result return marshalling requires a Canonical-ABI + // discriminant lift (tag byte followed by either ok- or err-payload + // slots) that MarshallingStrategy#liftReturn doesn't emit today — the + // default arm of that switch produces the literal `/* TODO: lift RESULT */`, + // which is a comment, not an expression, so `return ;` will not + // compile. Until the discriminant lift lands (ADR-005 resource/result + // codegen), emit a throw so the class stays compileable and the impl can + // be wired up incrementally. Parameters, return type, and the resource / + // interface shape are correct; only the body is a placeholder. + if (returnBindgenType != null && returnBindgenType.getKind() == BindgenType.Kind.RESULT) { + method.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "TODO(bindgen): result marshalling not yet wired for " + + function.getName() + + " — see MarshallingStrategy#liftReturn"); + return; + } + boolean complexReturn = MarshallingStrategy.requiresMarshalling(returnBindgenType); - method.addStatement("java.util.List args = new java.util.ArrayList<>()"); + // The marshalling body used to hard-code the local names `args`, + // `retptr`, and `result`. Any WIT function that happens to declare a + // parameter with one of those names (`embedder-callbacks.host-call` in + // wasmos:host/embedder does exactly this for `args: list`) collided + // with the redeclared local and javac rejected the generated file + // (`variable args is already defined`). Compute a fresh name for every + // internal local against the parameter set. `$` cannot appear in a + // JavaNaming-produced parameter name so the disambiguated form is + // guaranteed unique. + Set reserved = new HashSet<>(); + for (BindgenParameter param : function.getParameters()) { + reserved.add(JavaNaming.toParameterName(param.getName())); + } + String argsLocal = uniqueLocalName("args", reserved); + reserved.add(argsLocal); + String retptrLocal = uniqueLocalName("retptr", reserved); + reserved.add(retptrLocal); + String resultLocal = uniqueLocalName("result", reserved); + reserved.add(resultLocal); + + method.addStatement("java.util.List $L = new java.util.ArrayList<>()", argsLocal); // Allocate return pointer for complex returns if (complexReturn) { - method.addStatement("int retptr = marshal.allocator().allocate(8, 4)"); - method.addStatement("args.add(retptr)"); + method.addStatement("int $L = marshal.allocator().allocate(8, 4)", retptrLocal); + method.addStatement("$L.add($L)", argsLocal, retptrLocal); } // Lower each parameter @@ -351,31 +393,52 @@ private void generateMarshallingBody(MethodSpec.Builder method, BindgenFunction BindgenType paramType = param.getType(); if (MarshallingStrategy.requiresMarshalling(paramType)) { - method.addCode(MarshallingStrategy.lowerArgument(paramType, paramName, "args")); + method.addCode(MarshallingStrategy.lowerArgument(paramType, paramName, argsLocal)); } else { - method.addStatement("args.add($L)", paramName); + method.addStatement("$L.add($L)", argsLocal, paramName); } } // Invoke if (!hasReturn || complexReturn) { - method.addStatement("$L.invoke(args.toArray())", fieldName); + method.addStatement("$L.invoke($L.toArray())", fieldName, argsLocal); } else { - method.addStatement("Object result = $L.invoke(args.toArray())", fieldName); + method.addStatement("Object $L = $L.invoke($L.toArray())", resultLocal, fieldName, argsLocal); } // Lift return if (hasReturn) { if (complexReturn) { - CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, "retptr"); + CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, retptrLocal); method.addStatement("return $L", liftCode); } else { - CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, "result"); + CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, resultLocal); method.addStatement("return $L", liftCode); } } } + /** + * Pick a Java local-variable name that starts with {@code desired} and does + * not appear in {@code reserved}. Disambiguation uses a {@code $}-separated + * numeric suffix, which cannot appear in a name produced by + * {@link JavaNaming#toParameterName(String)} — so the returned name is + * guaranteed distinct from every generated parameter identifier. + */ + private static String uniqueLocalName(String desired, Set reserved) { + if (!reserved.contains(desired)) { + return desired; + } + int suffix = 0; + while (true) { + String candidate = desired + "$" + suffix; + if (!reserved.contains(candidate)) { + return candidate; + } + suffix++; + } + } + private MethodSpec generateCloseMethod() { return MethodSpec.methodBuilder("close") .addModifiers(Modifier.PUBLIC) diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java index 1588959..024d6f1 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java @@ -119,11 +119,15 @@ public static CodeBlock lowerArgument(BindgenType type, String javaVar, String a /** * Generates code to lift a WASM return value to a Java value. * + *

For primitives and enums {@code valueVar} names the {@code Object} + * returned by {@code Function.invoke}; for complex types it names the + * return-pointer local in linear memory. + * * @param type the bindgen type - * @param retptrVar the variable name holding the return pointer in linear memory + * @param valueVar the variable name (result Object, or retptr for complex types) * @return the lifting code block (an expression) */ - public static CodeBlock liftReturn(BindgenType type, String retptrVar) { + public static CodeBlock liftReturn(BindgenType type, String valueVar) { if (type == null) { return CodeBlock.of("null"); } @@ -131,12 +135,12 @@ public static CodeBlock liftReturn(BindgenType type, String retptrVar) { switch (type.getKind()) { case PRIMITIVE: if ("string".equals(type.getName())) { - return CodeBlock.of("marshal.reader().readString($L)", retptrVar); + return CodeBlock.of("marshal.reader().readString($L)", valueVar); } - return liftPrimitive(type); + return liftPrimitive(type, valueVar); case LIST: - return CodeBlock.of("marshal.reader().readBytes($L)", retptrVar); + return CodeBlock.of("marshal.reader().readBytes($L)", valueVar); case ENUM: return CodeBlock.of("/* TODO: lift enum from ordinal */"); @@ -148,34 +152,37 @@ public static CodeBlock liftReturn(BindgenType type, String retptrVar) { /** * Generates code to lift a primitive WASM return value. + * + * @param type the primitive bindgen type + * @param resultVar the local variable holding the boxed {@code Object} returned by invoke */ - private static CodeBlock liftPrimitive(BindgenType type) { + private static CodeBlock liftPrimitive(BindgenType type, String resultVar) { String name = type.getName().toLowerCase(); switch (name) { case "bool": - return CodeBlock.of("((($T) result).intValue() != 0)", Number.class); + return CodeBlock.of("((($T) $L).intValue() != 0)", Number.class, resultVar); case "s8": case "u8": - return CodeBlock.of("(($T) result).byteValue()", Number.class); + return CodeBlock.of("(($T) $L).byteValue()", Number.class, resultVar); case "s16": case "u16": - return CodeBlock.of("(($T) result).shortValue()", Number.class); + return CodeBlock.of("(($T) $L).shortValue()", Number.class, resultVar); case "s32": case "u32": case "i32": - return CodeBlock.of("(($T) result).intValue()", Number.class); + return CodeBlock.of("(($T) $L).intValue()", Number.class, resultVar); case "s64": case "u64": case "i64": - return CodeBlock.of("(($T) result).longValue()", Number.class); + return CodeBlock.of("(($T) $L).longValue()", Number.class, resultVar); case "f32": case "float32": - return CodeBlock.of("(($T) result).floatValue()", Number.class); + return CodeBlock.of("(($T) $L).floatValue()", Number.class, resultVar); case "f64": case "float64": - return CodeBlock.of("(($T) result).doubleValue()", Number.class); + return CodeBlock.of("(($T) $L).doubleValue()", Number.class, resultVar); default: - return CodeBlock.of("result"); + return CodeBlock.of("$L", resultVar); } } } diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java new file mode 100644 index 0000000..108a532 --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java @@ -0,0 +1,467 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenInterface; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Regression coverage for the two blocking bindgen bugs surfaced by the + * wasmos:host/embedder@0.1.0 shape check. + * + *

Each test constructs a bindgen interface that reproduces one bug, then + * feeds the generated {@code *Impl} source (plus a small hand-written stub of + * the runtime types the generator references) to the platform + * {@link JavaCompiler}. A javac error means the generator regressed. + * + *

    + *
  1. Bug 1 — the marshalling body redeclared internal locals ({@code args}, + * {@code retptr}, {@code result}) that collide with a WIT parameter of + * the same name. Reproducer: + * {@code host-call(provider-handle, func-idx, args: string)}, matching + * {@code embedder-callbacks.host-call} in the wasmos world (the wire + * type there is {@code list}; we use {@code string} in the repro to + * exercise the same collision without depending on the separate + * {@code List} lowering bug). + *
  2. Bug 2 — a function returning {@code result} used to emit a + * literal {@code return /* TODO: lift RESULT */;} — a comment, not + * an expression. Reproducer: {@code call-export(name) -> + * result}, matching the shape of + * {@code runtime-instance.call-export}. + *
+ * + *

The stub surface below covers only the pieces {@code + * ImplementationCodeGenerator} + {@code MarshallingStrategy} name today: + * {@code Instance / Module / Engine / Function} from the api module and + * {@code MarshalContext / MemoryReader / MemoryAllocator / Memory / + * StringCodec} from the runtime module. If the generator starts naming a new + * type, that will surface as a javac error here — which is the point. + */ +@DisplayName("ImplementationCodeGenerator bindgen bugs") +class ImplementationCodeGeneratorBugsTest { + + private static final String PACKAGE = "ai.tegmentum.bindgen.generated"; + + private ImplementationCodeGenerator generator() { + return new ImplementationCodeGenerator( + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(Path.of("target/test-output")) + .addWitSource(Path.of("test.wit")) + .generateImplementations(true) + .generateServiceLoader(false) + .build()); + } + + @Test + @DisplayName( + "bug 1: WIT parameter named `args` no longer collides with the internal args local") + void paramNamedArgsDoesNotCollide() throws Exception { + // Matches wasmos:host/embedder embedder-callbacks.host-call, minus the + // list arg type (see class javadoc). + final BindgenInterface iface = + BindgenInterface.builder() + .name("embedder-callbacks") + .addFunction( + BindgenFunction.builder() + .name("host-call") + .addParameter("provider-handle", BindgenType.primitive("u32")) + .addParameter("func-idx", BindgenType.primitive("u32")) + .addParameter("args", BindgenType.primitive("string")) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + assertEquals("EmbedderCallbacksImpl", impl.getClassName()); + final String content = impl.getContent(); + + // The internal ArrayList must have a name distinct from the `args` param. + assertFalse( + content.contains("java.util.List args ="), + "internal args local must not shadow the WIT parameter named `args`\n" + content); + assertTrue( + content.contains("java.util.List args$0"), + "expected disambiguated internal args local, got:\n" + content); + // The disambiguated local threads through the invoke site too. + assertTrue( + content.contains("args$0.toArray()"), + "expected the invoke site to use the disambiguated args local; got:\n" + content); + + compileImpl(iface, impl); + } + + @Test + @DisplayName( + "bug 1 (companion): a param named `result` disambiguates the internal result local too") + void paramNamedResultDoesNotCollide() throws Exception { + // The generator declares an `Object result` local when the return type + // needs marshalling but is not stored via a retptr (the "primitive + // returned but string parameter present" branch). Confirm that a param + // literally named `result` no longer shadows it. + final BindgenInterface iface = + BindgenInterface.builder() + .name("reporter") + .addFunction( + BindgenFunction.builder() + .name("report") + .addParameter("result", BindgenType.primitive("string")) + .returnType(BindgenType.primitive("i32")) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + assertFalse( + content.contains("Object result ="), + "internal `result` local must not shadow the WIT parameter named `result`\n" + content); + assertTrue( + content.contains("Object result$0 ="), + "expected disambiguated result local; got:\n" + content); + + compileImpl(iface, impl); + } + + @Test + @DisplayName( + "bug 2: a result return no longer emits a comment where an expression must go") + void resultReturnEmitsCompileableBody() throws Exception { + // call-export: func(name: string) -> result + // We use string on both sides so we don't depend on the list + // lowering bug (out of scope for this round). + final BindgenInterface iface = + BindgenInterface.builder() + .name("runtime-instance") + .addFunction( + BindgenFunction.builder() + .name("call-export") + .addParameter("name", BindgenType.primitive("string")) + .returnType( + BindgenType.result( + BindgenType.primitive("string"), BindgenType.reference("error"))) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + // Pre-fix the body emitted this literal — the fix must not. + assertFalse( + content.contains("return /* TODO: lift RESULT */"), + "result body must not emit a bare TODO comment in return position:\n" + content); + // The honest placeholder for the currently-unwired discriminant lift is + // an UnsupportedOperationException throw — compileable, obviously not a + // working runtime path, easy to swap when the marshalling lands. + assertTrue( + content.contains("throw new UnsupportedOperationException"), + "expected an UnsupportedOperationException throw as the placeholder body:\n" + content); + assertTrue( + content.contains("TODO(bindgen): result"), + "expected the throw message to reference the ADR-005 gap:\n" + content); + // The method signature is still generated correctly — only the body + // changed. WitResult is what the type map produces here. + assertTrue( + content.contains("public WitResult callExport"), + "expected the WitResult return type on callExport:\n" + content); + + compileImpl(iface, impl); + } + + // ------------------------------------------------------------------- + // javac harness — compile the generated Impl against a hand-written stub + // of the runtime surface it references, so the test can prove + // compileability without dragging in the api / runtime modules as test + // dependencies of bindgen (a coupling that would go the wrong direction). + // ------------------------------------------------------------------- + + private static final String STUB_SOURCE = + String.join( + "\n", + "package " + PACKAGE + ";", + "import java.util.Optional;", + "// api package stubs — signatures cribbed from webassembly4j-api.", + "class ApiStubs {", + " public interface Instance {", + " Optional function(String name);", + " }", + " public interface Module { void close(); }", + " public interface Engine { void close(); }", + " public interface Function { Object invoke(Object... args); }", + " public interface Memory { void write(long offset, byte[] bytes); }", + " public interface MemoryAllocator { int allocate(int size, int alignment); }", + " public static class MemoryReader {", + " public String readString(int offset) { return null; }", + " public byte[] readBytes(int offset) { return null; }", + " }", + " public static class MarshalContext {", + " public static MarshalContext fromInstance(Instance i) { return new MarshalContext(); }", + " public Memory memory() { return null; }", + " public MemoryAllocator allocator() { return null; }", + " public MemoryReader reader() { return null; }", + " }", + " public static class StringCodec {", + " public static int[] encode(String s, Memory m, MemoryAllocator a) { return new int[2]; }", + " }", + "}"); + + private static void compileImpl(BindgenInterface iface, GeneratedSource impl) throws Exception { + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "JDK javac not available; run tests on a JDK not a JRE"); + + // Rewrite the generated Impl to point at our in-test stub package so + // javac can resolve every symbol. The rewrite is text-level (safe: the + // generator writes fully-qualified imports in a stable, deterministic + // form). + final String rewrittenImpl = + impl.getContent() + .replace( + "import ai.tegmentum.webassembly4j.api.Instance;", + "import " + PACKAGE + ".ApiStubs.Instance;") + .replace( + "import ai.tegmentum.webassembly4j.api.Module;", + "import " + PACKAGE + ".ApiStubs.Module;") + .replace( + "import ai.tegmentum.webassembly4j.api.Engine;", + "import " + PACKAGE + ".ApiStubs.Engine;") + .replace( + "import ai.tegmentum.webassembly4j.api.Function;", + "import " + PACKAGE + ".ApiStubs.Function;") + .replace( + "import ai.tegmentum.webassembly4j.runtime.marshal.MarshalContext;", + "import " + PACKAGE + ".ApiStubs.MarshalContext;") + .replace( + "import ai.tegmentum.webassembly4j.runtime.marshal.StringCodec;", + "import " + PACKAGE + ".ApiStubs.StringCodec;"); + + final List units = new ArrayList<>(); + units.add(sourceFile(PACKAGE + "." + impl.getClassName(), rewrittenImpl)); + units.add(stubInterfaceFor(iface)); + // Sibling record referenced by bug 2's Impl in the error-side of + // WitResult<..., Error>. Idempotent for tests that don't reference it. + units.add(sourceFile(PACKAGE + ".Error", "package " + PACKAGE + "; public class Error {}")); + units.add(sourceFile(PACKAGE + ".ApiStubs", STUB_SOURCE)); + + final StringWriter diagnostics = new StringWriter(); + final String cp = System.getProperty("java.class.path"); + final boolean ok; + try (var fm = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + ok = + compiler + .getTask( + new PrintWriter(diagnostics), + fm, + null, + List.of("-classpath", cp, "-proc:none"), + null, + units) + .call(); + } + if (!ok) { + fail( + "javac failed on generated " + + impl.getClassName() + + ":\n" + + diagnostics + + "\n---SOURCE---\n" + + rewrittenImpl); + } + } + + /** + * Build a minimal interface source that matches the parent surface the + * generated {@code *Impl} implements. Signatures mirror what + * {@link ImplementationCodeGenerator#generateMethod} would produce so the + * {@code @Override} annotations resolve. + */ + private static JavaFileObject stubInterfaceFor(BindgenInterface iface) { + final StringBuilder sb = new StringBuilder(); + sb.append("package ").append(PACKAGE).append(";\n"); + sb.append("import java.util.List;\n"); + sb.append("import ai.tegmentum.webassembly4j.bindgen.wit.WitResult;\n"); + sb.append("public interface ").append(pascal(iface.getName())).append(" {\n"); + for (BindgenFunction f : iface.getFunctions()) { + sb.append(" ").append(signature(f)).append(";\n"); + } + sb.append("}\n"); + return sourceFile(PACKAGE + "." + pascal(iface.getName()), sb.toString()); + } + + private static String signature(BindgenFunction f) { + final StringBuilder sb = new StringBuilder(); + sb.append(returnDecl(f)).append(' ').append(camel(f.getName())).append('('); + boolean first = true; + for (var p : f.getParameters()) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(typeDecl(p.getType())).append(' ').append(camel(p.getName())); + } + sb.append(')'); + return sb.toString(); + } + + private static String returnDecl(BindgenFunction f) { + if (!f.hasReturnType()) { + return "void"; + } + return typeDecl(f.getReturnType().get()); + } + + private static String typeDecl(BindgenType t) { + switch (t.getKind()) { + case PRIMITIVE: + return primitiveDecl(t.getName()); + case LIST: + return "List<" + boxDecl(t.getElementType().get()) + ">"; + case RESULT: + String ok = t.getOkType().map(ImplementationCodeGeneratorBugsTest::boxDecl).orElse("Void"); + String err = + t.getErrorType().map(ImplementationCodeGeneratorBugsTest::boxDecl).orElse("Void"); + return "WitResult<" + ok + ", " + err + ">"; + case REFERENCE: + return pascal(t.getReferencedTypeName().orElse(t.getName())); + default: + return "Object"; + } + } + + private static String boxDecl(BindgenType t) { + if (t.getKind() == BindgenType.Kind.PRIMITIVE) { + switch (t.getName().toLowerCase()) { + case "u8": + case "s8": + return "Byte"; + case "u16": + case "s16": + return "Short"; + case "u32": + case "s32": + case "i32": + case "char": + return "Integer"; + case "u64": + case "s64": + case "i64": + return "Long"; + case "f32": + case "float32": + return "Float"; + case "f64": + case "float64": + return "Double"; + case "bool": + return "Boolean"; + case "string": + return "String"; + default: + return "Object"; + } + } + return typeDecl(t); + } + + private static String primitiveDecl(String name) { + switch (name.toLowerCase()) { + case "u8": + case "s8": + return "byte"; + case "u16": + case "s16": + return "short"; + case "u32": + case "s32": + case "i32": + case "char": + return "int"; + case "u64": + case "s64": + case "i64": + return "long"; + case "f32": + case "float32": + return "float"; + case "f64": + case "float64": + return "double"; + case "bool": + return "boolean"; + case "string": + return "String"; + default: + throw new IllegalArgumentException("unhandled primitive: " + name); + } + } + + private static String pascal(String kebab) { + final StringBuilder sb = new StringBuilder(); + boolean up = true; + for (int i = 0; i < kebab.length(); i++) { + final char c = kebab.charAt(i); + if (c == '-' || c == '_') { + up = true; + } else if (up) { + sb.append(Character.toUpperCase(c)); + up = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private static String camel(String kebab) { + final String p = pascal(kebab); + if (p.isEmpty()) { + return p; + } + return Character.toLowerCase(p.charAt(0)) + p.substring(1); + } + + private static JavaFileObject sourceFile(String qualifiedName, String content) { + return new SimpleJavaFileObject( + URI.create("string:///" + qualifiedName.replace('.', '/') + ".java"), + JavaFileObject.Kind.SOURCE) { + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return content; + } + }; + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java index 0c75197..3b93906 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java @@ -60,10 +60,17 @@ *
    *
  1. Runs {@link CodeGenerator} against the fixture copy of {@code * embedder.wit} (kept under {@code src/test/resources/wasmos-embedder/}). - * Implementation classes are disabled because {@code - * ImplementationCodeGenerator} generates broken marshalling glue for the - * {@code hostCall} shape (the parameter-name / local-variable collision - * is a pre-existing bug in that generator, unrelated to this shape). + * Implementation classes are disabled here because the generated + * {@code *Impl} bodies reference the {@code webassembly4j-api} and + * {@code webassembly4j-runtime} types (Instance / Module / Engine / + * Function / MarshalContext) which aren't test dependencies of the + * bindgen module. The generator-level bugs the impl bodies used to hit + * (parameter/local collision on {@code args}, comment-in-return for + * {@code result}) are covered by + * {@link ai.tegmentum.webassembly4j.bindgen.generator.ImplementationCodeGeneratorBugsTest}, + * which compiles the generated Impl against inline stubs of those + * types. A full impl-on shape check is a separate wiring task + * (test-scope coupling to the api/runtime modules). *
  2. Asserts each file in the expected surface exists and contains the * right members. *
  3. Feeds every generated file to the platform {@link JavaCompiler} and From 3a3c340df0519ab09226314993781c2f57da4472 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 17:36:06 -0400 Subject: [PATCH 44/85] feat(bindgen): consume ADR-006-hoisted `world embedder` + cross-JVM determinism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `wasmos:host/embedder@0.1.0` world was restructured to hoist its types (`error-code`, `error`, `import-satisfaction`) and resources (`host-provider`, `runtime-instance`) directly onto `world embedder`, removing the empty `embedder-api` carrier interface. Two gaps surfaced: 1. `WitWorldPreprocessor` stripped `world { ... }` blocks entirely, so the hoisted types never reached the generator — output was one file (`EmbedderCallbacks`) instead of six. Now the preprocessor projects worlds as `Interface` entries with `isWorld() == true`, strips `import`/`export`/`use` clauses from the body, and reuses the existing resource-body extraction. `CodeGenerator` routes world projections to top-level `BindgenModel` types instead of building an empty Java interface carrier. 2. Byte-identical CLI reruns diverged: HashMap/Map.copyOf iteration is randomized per JVM. Switched every downstream-iterated collection (parseTypes, parseFunctions, parseRecordType, parseVariantType, WitInterfaceDefinitionImpl, RecordTypeKind, VariantTypeKind, CodeGenerator.convertedTypes) to LinkedHashMap so declaration order flows end-to-end. Generated surface after the change (6 files, all javac-clean): EmbedderCallbacks, Error, ErrorCode, HostProvider, ImportSatisfaction, RuntimeInstance. `WasmosEmbedderShapeTest` updated to drop the `EmbedderApi` assertion and to negatively assert no empty carrier is generated. Bindgen suite still 472/472 green. --- .../webassembly4j/bindgen/CodeGenerator.java | 40 +++- .../bindgen/wit/WitInterfaceParser.java | 30 ++- .../bindgen/wit/WitTypeKind.java | 12 +- .../bindgen/wit/WitWorldPreprocessor.java | 188 ++++++++++++++++-- .../wasmos/WasmosEmbedderShapeTest.java | 16 +- .../resources/wasmos-embedder/embedder.wit | 117 +++++------ 6 files changed, 300 insertions(+), 103 deletions(-) diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java index 14257c6..7fae2e5 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java @@ -193,7 +193,9 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException final WitInterfaceParser parser = new WitInterfaceParser(); final WitInterfaceDefinition definition = parser.parseInterface(reconstructed, packageName); - final Map convertedTypes = new HashMap<>(); + // LinkedHashMap preserves WIT declaration order for cross-JVM + // deterministic generation. See WitInterfaceParser.parseTypes. + final Map convertedTypes = new java.util.LinkedHashMap<>(); for (final Map.Entry entry : definition.getTypes().entrySet()) { BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); if (bindgenType.getKind() == BindgenType.Kind.RESOURCE @@ -221,15 +223,31 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException convertedFunctions.add(bindgenFunc); } - final BindgenInterface.Builder ifaceBuilder = - BindgenInterface.builder().name(definition.getName()).packageName(packageName); - for (final BindgenFunction func : convertedFunctions) { - ifaceBuilder.addFunction(func); - } - for (final BindgenType type : convertedTypes.values()) { - ifaceBuilder.addType(type); + if (pre.isWorld()) { + // A hoisted `world X { ... }` block declares types + resources at + // the top level (per ADR-006). It has no functions of its own — + // its `import`/`export` clauses reference already-declared + // interfaces. Emit the types as top-level model types so the + // generator produces one Java file per type/resource without an + // empty carrier interface. Any function that did slip in gets + // added as a top-level function. + for (final BindgenType type : convertedTypes.values()) { + modelBuilder.addType(type); + } + for (final BindgenFunction func : convertedFunctions) { + modelBuilder.addFunction(func); + } + } else { + final BindgenInterface.Builder ifaceBuilder = + BindgenInterface.builder().name(definition.getName()).packageName(packageName); + for (final BindgenFunction func : convertedFunctions) { + ifaceBuilder.addFunction(func); + } + for (final BindgenType type : convertedTypes.values()) { + ifaceBuilder.addType(type); + } + modelBuilder.addInterface(ifaceBuilder.build()); } - modelBuilder.addInterface(ifaceBuilder.build()); } return modelBuilder.build(); @@ -249,7 +267,9 @@ private BindgenModel parseWitSourceLegacy( final BindgenModel.Builder modelBuilder = BindgenModel.builder().name(definition.getName()).sourceFile(witPath.toString()); - final Map convertedTypes = new HashMap<>(); + // LinkedHashMap preserves WIT declaration order for cross-JVM + // deterministic generation. See WitInterfaceParser.parseTypes. + final Map convertedTypes = new java.util.LinkedHashMap<>(); for (final Map.Entry entry : definition.getTypes().entrySet()) { final BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); convertedTypes.put(entry.getKey(), bindgenType); diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java index 2290f48..aa42dfd 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java @@ -146,7 +146,12 @@ public WitInterfaceDefinition parseInterface(final String witText, final String * @throws BindgenException if parsing fails */ private Map parseTypes(final String interfaceBody) throws BindgenException { - final Map types = new HashMap<>(); + // LinkedHashMap for cross-JVM-deterministic type ordering — see + // parseFunctions for the same reasoning. Downstream generators iterate + // this map to emit `.java` files; a randomized order would surface as + // reshuffled inner-type declaration order in generated code between + // invocations. + final Map types = new java.util.LinkedHashMap<>(); // Pass 1: standalone declarations (`record name { ... }`, // `variant name { ... }`, `enum name { ... }`, @@ -309,7 +314,8 @@ private WitType parseTypeDefinition(final String typeName, final String definiti */ private WitType parseRecordType(final String typeName, final String fieldsText) throws BindgenException { - final Map fields = new HashMap<>(); + // LinkedHashMap for deterministic field order in generated records. + final Map fields = new java.util.LinkedHashMap<>(); final List fieldDefs = splitTopLevel(stripComments(fieldsText), ','); for (final String fieldDef : fieldDefs) { @@ -424,7 +430,9 @@ private static List splitTopLevel(final String text, final char separato */ private WitType parseVariantType(final String typeName, final String casesText) throws BindgenException { - final Map> cases = new HashMap<>(); + // LinkedHashMap for deterministic case order in generated sealed + // interfaces. + final Map> cases = new java.util.LinkedHashMap<>(); final List caseDefs = splitTopLevel(stripComments(casesText), ','); for (final String caseDef : caseDefs) { @@ -539,7 +547,12 @@ private WitType parseResultType(final String definition) throws BindgenException */ private Map parseFunctions( final String interfaceBody, final Map types) throws BindgenException { - final Map functions = new HashMap<>(); + // LinkedHashMap so downstream generation follows WIT declaration order. + // HashMap iteration is randomized per-JVM (and depends on key hashCodes + // for its bin placement); using it here reintroduces cross-JVM + // non-determinism into generated sources, which breaks the byte-identical + // guarantee CLI users depend on for artifact reproducibility. + final Map functions = new java.util.LinkedHashMap<>(); final Matcher functionMatcher = FUNCTION_PATTERN.matcher(interfaceBody); while (functionMatcher.find()) { @@ -771,8 +784,13 @@ public WitInterfaceDefinitionImpl( this.name = name; this.version = version; this.packageName = packageName; - this.functions = Map.copyOf(functions); - this.types = Map.copyOf(types); + // LinkedHashMap-backed unmodifiable maps to preserve WIT declaration + // order across JVMs. Map.copyOf builds an immutable map with + // randomized iteration, which resurfaces as non-deterministic + // generated-source ordering. + this.functions = + java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(functions)); + this.types = java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(types)); this.imports = List.copyOf(imports); this.exports = List.copyOf(exports); this.witText = witText; diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitTypeKind.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitTypeKind.java index b76f628..f57c24e 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitTypeKind.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitTypeKind.java @@ -15,6 +15,8 @@ */ package ai.tegmentum.webassembly4j.bindgen.wit; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -342,7 +344,10 @@ private static final class RecordTypeKind extends WitTypeKind { private final Map fields; private RecordTypeKind(final Map fields) { - this.fields = Map.copyOf(Objects.requireNonNull(fields, "fields")); + // LinkedHashMap-backed unmodifiable map preserves WIT declaration + // order across JVMs; Map.copyOf randomizes iteration order. + this.fields = + Collections.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(fields, "fields"))); } @Override @@ -395,7 +400,10 @@ private static final class VariantTypeKind extends WitTypeKind { private final Map> cases; private VariantTypeKind(final Map> cases) { - this.cases = Map.copyOf(Objects.requireNonNull(cases, "cases")); + // LinkedHashMap-backed unmodifiable map preserves WIT case + // declaration order across JVMs; Map.copyOf randomizes iteration. + this.cases = + Collections.unmodifiableMap(new LinkedHashMap<>(Objects.requireNonNull(cases, "cases"))); } @Override diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java index 5a2dd8d..4f20e7a 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java @@ -31,15 +31,21 @@ * *
      *
    • {@code package X@Y;} declarations are removed. - *
    • {@code world X { ... }} blocks are skipped entirely — their - * {@code import} / {@code export} clauses aren't part of any interface - * surface bindgen needs to project. + *
    • {@code world X { ... }} blocks are projected as {@link Interface} + * entries with {@link Interface#isWorld()} true, so world-level type + * and resource declarations still reach the generator. Their + * {@code import} / {@code export} / {@code use} clauses are stripped + * from the body — they refer to already-declared interfaces and don't + * contribute any new type surface. Downstream, world entries are + * flattened into top-level model types instead of being wrapped in a + * Java interface carrier (a hoisted world has no functions of its + * own; the empty carrier is dead weight). *
    • {@code interface X { ... }} blocks are split into individual * {@link Interface} entries. The existing parser is single-interface; * this splitter fans a world file out into a stream of what it expects. - *
    • {@code resource X { body }} declarations inside an interface body have - * their bodies stripped and returned in a side-map keyed by resource - * name. The interface body then contains a bare {@code resource X;} + *
    • {@code resource X { body }} declarations inside a world or interface + * body have their bodies stripped and returned in a side-map keyed by + * resource name. The body then contains a bare {@code resource X;} * token so {@link WitInterfaceParser} still sees the resource type * declaration but doesn't choke on the constructor / static / method * syntax it doesn't understand. @@ -49,16 +55,28 @@ public final class WitWorldPreprocessor { private WitWorldPreprocessor() {} - /** A single interface extracted from a WIT world source. */ + /** + * A single interface (or hoisted world) extracted from a WIT world source. + * + *

      World-projected entries have {@link #isWorld()} true; callers use this + * to decide whether to emit a Java interface carrier or to hoist the + * entry's types to top-level model types. + */ public static final class Interface { private final String name; private final String body; private final Map resourceBodies; + private final boolean world; - Interface(final String name, final String body, final Map resourceBodies) { + Interface( + final String name, + final String body, + final Map resourceBodies, + final boolean world) { this.name = name; this.body = body; this.resourceBodies = resourceBodies; + this.world = world; } /** The interface's name as written in WIT (e.g. {@code embedder-api}). */ @@ -84,20 +102,53 @@ public String getBody() { public Map getResourceBodies() { return resourceBodies; } + + /** + * True when this entry was projected from a {@code world X { ... }} + * block (rather than an {@code interface X { ... }} block). Worlds + * declare types + resources at the top level under ADR-006's hoisted + * shape; they have no functions of their own and don't warrant an + * empty Java interface carrier. + */ + public boolean isWorld() { + return world; + } } /** - * Split a full WIT source into per-interface entries. + * Split a full WIT source into per-interface entries. Worlds are projected + * as entries with {@link Interface#isWorld()} true so callers can decide + * whether to emit a Java interface carrier or hoist the world's types to + * top-level model types. * * @param witText the raw WIT world source - * @return the interfaces contained in the source, in declaration order + * @return the interfaces + hoisted worlds contained in the source, in + * declaration order * @throws BindgenException if brace matching fails */ public static List preprocess(final String witText) throws BindgenException { final String withoutComments = stripLineComments(witText); final String withoutPackage = stripPackage(withoutComments); - final String withoutWorlds = stripTopLevelBlocks(withoutPackage, "world"); - return splitInterfaces(withoutWorlds); + final List worlds = new ArrayList<>(); + final String withoutWorlds = extractTopLevelBlocks(withoutPackage, "world", worlds); + final List result = new ArrayList<>(splitInterfaces(withoutWorlds, false)); + for (final Entry world : worlds) { + final String cleanedBody = stripImportsAndExports(world.body); + final Map resources = new LinkedHashMap<>(); + final String bodyStripped = extractResourceBodies(cleanedBody, resources); + result.add(new Interface(world.name, bodyStripped, resources, true)); + } + return result; + } + + private static final class Entry { + final String name; + final String body; + + Entry(final String name, final String body) { + this.name = name; + this.body = body; + } } /** @@ -134,11 +185,12 @@ private static String stripPackage(final String source) { } /** - * Remove every top-level {@code keyword name { … }} block (used to skip - * {@code world}). Brace-balanced so nested braces don't confuse the scan. + * Remove every top-level {@code keyword name { … }} block from the source + * and, when {@code sink} is non-null, record each block's name and body + * text into it. Brace-balanced so nested braces don't confuse the scan. */ - private static String stripTopLevelBlocks(final String source, final String keyword) - throws BindgenException { + private static String extractTopLevelBlocks( + final String source, final String keyword, final List sink) throws BindgenException { final StringBuilder out = new StringBuilder(source.length()); int i = 0; while (i < source.length()) { @@ -152,23 +204,118 @@ private static String stripTopLevelBlocks(final String source, final String keyw i = idx + 1; continue; } - final int braceOpen = source.indexOf('{', idx); - if (braceOpen < 0) { + // Read the identifier that follows. + int cursor = idx + keyword.length(); + while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { + cursor++; + } + final int nameStart = cursor; + while (cursor < source.length() && isIdentChar(source.charAt(cursor))) { + cursor++; + } + final String name = source.substring(nameStart, cursor); + while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { + cursor++; + } + if (cursor >= source.length() || source.charAt(cursor) != '{') { // No body — leave the fragment alone, we may be scanning garbage. out.append(source, i, source.length()); break; } + final int braceOpen = cursor; out.append(source, i, idx); final int braceClose = matchBrace(source, braceOpen); if (braceClose < 0) { throw new BindgenException( "Unbalanced braces in " + keyword + " block starting at offset " + braceOpen); } + if (sink != null) { + sink.add(new Entry(name, source.substring(braceOpen + 1, braceClose))); + } i = braceClose + 1; } return out.toString(); } + /** + * Strip {@code import} / {@code export} / {@code use} clauses from a world + * body. These reference already-declared interfaces and don't add any new + * type surface bindgen needs to project. Handled forms: + * + *

        + *
      • {@code import name;} + *
      • {@code export name;} + *
      • {@code import name: interface { ... }} (inline interface — the + * inline body is dropped along with the whole clause) + *
      • {@code use pkg/iface.{names...};} + *
      + * + * Unrecognized {@code import}/{@code export} clauses fall through untouched; + * the regex-based downstream parser ignores lines it doesn't understand. + */ + private static String stripImportsAndExports(final String body) throws BindgenException { + final StringBuilder out = new StringBuilder(body.length()); + int i = 0; + while (i < body.length()) { + final int nextImport = findKeyword(body, "import", i); + final int nextExport = findKeyword(body, "export", i); + final int nextUse = findKeyword(body, "use", i); + int next = -1; + for (final int candidate : new int[] {nextImport, nextExport, nextUse}) { + if (candidate >= 0 && (next < 0 || candidate < next)) { + next = candidate; + } + } + if (next < 0) { + out.append(body, i, body.length()); + break; + } + out.append(body, i, next); + // Find the clause terminator: either a `;` at nesting level 0 or a + // matching `}` for an inline `import name: interface { ... }`. + int cursor = next; + int depth = 0; + boolean inString = false; + while (cursor < body.length()) { + final char c = body.charAt(cursor); + if (c == '"' && (cursor == 0 || body.charAt(cursor - 1) != '\\')) { + inString = !inString; + } else if (!inString) { + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + } else if (c == ';' && depth == 0) { + cursor++; + break; + } + } + cursor++; + } + i = cursor; + } + return out.toString(); + } + + /** + * Find the next occurrence of {@code keyword} at a keyword boundary in + * {@code source} starting from {@code from}. Returns -1 if not found. + */ + private static int findKeyword(final String source, final String keyword, final int from) { + int cursor = from; + while (cursor < source.length()) { + final int idx = source.indexOf(keyword, cursor); + if (idx < 0) { + return -1; + } + if (isKeywordBoundary(source, idx, keyword.length())) { + return idx; + } + cursor = idx + 1; + } + return -1; + } + private static boolean isKeywordBoundary(final String source, final int at, final int keywordLen) { if (at > 0) { final char before = source.charAt(at - 1); @@ -215,7 +362,8 @@ private static int matchBrace(final String source, final int open) { * {@code interface X { body }} blocks. Resource declarations inside each * body are extracted and returned separately. */ - private static List splitInterfaces(final String source) throws BindgenException { + private static List splitInterfaces(final String source, final boolean world) + throws BindgenException { final List result = new ArrayList<>(); int i = 0; while (i < source.length()) { @@ -252,7 +400,7 @@ private static List splitInterfaces(final String source) throws Bindg final String body = source.substring(braceOpen + 1, braceClose); final Map resources = new LinkedHashMap<>(); final String bodyStripped = extractResourceBodies(body, resources); - result.add(new Interface(name, bodyStripped, resources)); + result.add(new Interface(name, bodyStripped, resources, world)); i = braceClose + 1; } return result; diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java index 3b93906..a922948 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderShapeTest.java @@ -107,18 +107,28 @@ void generatesAndCompiles(@TempDir Path outputDir) throws Exception { final CodeGenerator generator = new CodeGenerator(config); final List sources = generator.generate(); - assertTrue(sources.size() >= 7, "expected at least 7 generated sources, got " + sources.size()); + // ADR-006 hoisted the embedder-api wrapper into the world root, so the + // empty `EmbedderApi` carrier is gone. The world now projects six + // top-level types/resources + the `embedder-callbacks` interface. + assertTrue(sources.size() >= 6, "expected at least 6 generated sources, got " + sources.size()); final Map byClass = toSourceMap(sources); - // Sanity: every top-level type / interface / resource is present. + // Sanity: every top-level type / resource / interface is present. assertTrue(byClass.containsKey("ErrorCode"), "ErrorCode enum missing"); assertTrue(byClass.containsKey("Error"), "Error record missing"); assertTrue(byClass.containsKey("ImportSatisfaction"), "ImportSatisfaction record missing"); assertTrue(byClass.containsKey("HostProvider"), "HostProvider resource missing"); assertTrue(byClass.containsKey("RuntimeInstance"), "RuntimeInstance resource missing"); - assertTrue(byClass.containsKey("EmbedderApi"), "EmbedderApi interface missing"); assertTrue(byClass.containsKey("EmbedderCallbacks"), "EmbedderCallbacks interface missing"); + // The hoisted world must NOT re-emit an empty `EmbedderApi` (or `Embedder`) + // carrier — that's the whole point of the hoist. + assertTrue( + !byClass.containsKey("EmbedderApi"), + "EmbedderApi carrier must not be generated after the world-level hoist"); + assertTrue( + !byClass.containsKey("Embedder"), + "empty world carrier `Embedder` must not be generated"); // ErrorCode: all 7 WIT variants. final String errorCode = byClass.get("ErrorCode"); diff --git a/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit b/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit index 223da85..770dd33 100644 --- a/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit +++ b/webassembly4j-bindgen/src/test/resources/wasmos-embedder/embedder.wit @@ -8,23 +8,64 @@ // Everything else is generated by per-language bindgen consuming // this world. // -// The world has two halves: -// - `embedder-api` — exported by the guest, called by the host to -// load components, register providers, instantiate, invoke, and -// introspect. -// - `embedder-callbacks` — imported by the guest, called back into -// the host to dispatch registered-provider functions and to -// satisfy the narrow set of syscall-shaped primitives the -// runtime guest itself needs (clock, random). -// -// Status: draft. The shape follows ADR-006. Concrete signatures -// are expected to iterate as the first bindgen tool (webassembly4j -// per ADR-005) produces its first generated surface and calls it -// end-to-end. +// Shape: +// - The `world embedder` hoists types and resources directly, so +// generated bindings expose them as top-level classes/records +// without an empty carrier interface (initial v0.1.0 draft had +// an `embedder-api` wrapper; hoisted after webassembly4j-bindgen's +// first output revealed the wrapper was dead weight). +// - `embedder-callbacks` stays as an interface — it's imported +// (the host implements it) and interface-per-import is the +// conventional shape. package wasmos:host@0.1.0; -interface embedder-api { +interface embedder-callbacks { + // ----------------------------------------------------------------- + // Host-implemented import dispatcher. + // + // Called by the guest during `runtime-instance.call-export` + // execution whenever a running component invokes a function on an + // imported interface that was satisfied by a host `host-provider`. + // The embedder is responsible for looking up + // (provider-handle, func-idx) in its registered-provider table + // and invoking the language-level implementation with `args` + // decoded and results encoded per the Canonical ABI (generated + // bindings own the typed encoding). + // + // An error string maps back to `error-code::host-callback-failed` + // on the guest side. + // ----------------------------------------------------------------- + + host-call: func( + provider-handle: u32, + func-idx: u32, + args: list, + ) -> result, string>; + + // ----------------------------------------------------------------- + // Syscall-shaped primitives the runtime guest itself needs from + // the embedder. Distinct from `wasi:clocks` / `wasi:random` + // interfaces implemented by wasi-p2 components — those live one + // layer up and route through `host-call`. These are the runtime + // guest's *own* dependencies. + // ----------------------------------------------------------------- + + // Monotonic nanoseconds since an unspecified epoch. Used by the + // runtime guest for deadline tracking, resource-table generation + // counters, and diagnostic timestamps. No monotonicity guarantee + // across `wasmos_start` calls in the same host process. + now-monotonic-ns: func() -> u64; + + // Fill a buffer with `len` cryptographically random bytes. Used + // by the runtime guest for resource-handle randomization. If + // the host cannot satisfy the request (e.g. missing entropy on + // an embedded target) it returns an empty list; guest treats + // that as unavailable and falls back to deterministic seeding. + read-random: func(len: u32) -> list; +} + +world embedder { // ----------------------------------------------------------------- // Errors. // @@ -155,54 +196,6 @@ interface embedder-api { // qualified names + generational handles. verify-world: func(expected-wit: string) -> result<_, error>; } -} - -interface embedder-callbacks { - // ----------------------------------------------------------------- - // Host-implemented import dispatcher. - // - // Called by the guest during `call-export` execution whenever a - // running component invokes a function on an imported interface - // that was satisfied by a host `host-provider`. The embedder is - // responsible for looking up (provider-handle, func-idx) in its - // registered-provider table and invoking the language-level - // implementation with `args` decoded and results encoded per - // the Canonical ABI (again, generated bindings own the typed - // encoding). - // - // An error string maps back to `error-code::host-callback-failed` - // on the guest side. - // ----------------------------------------------------------------- - host-call: func( - provider-handle: u32, - func-idx: u32, - args: list, - ) -> result, string>; - - // ----------------------------------------------------------------- - // Syscall-shaped primitives the runtime guest itself needs from - // the embedder. Distinct from `wasi:clocks` / `wasi:random` - // interfaces implemented by wasi-p2 components — those live one - // layer up and route through `host-call`. These are the runtime - // guest's *own* dependencies. - // ----------------------------------------------------------------- - - // Monotonic nanoseconds since an unspecified epoch. Used by the - // runtime guest for deadline tracking, resource-table generation - // counters, and diagnostic timestamps. No monotonicity guarantee - // across `wasmos_start` calls in the same host process. - now-monotonic-ns: func() -> u64; - - // Fill a buffer with `len` cryptographically random bytes. Used - // by the runtime guest for resource-handle randomization. If - // the host cannot satisfy the request (e.g. missing entropy on - // an embedded target) it returns an empty list; guest treats - // that as unavailable and falls back to deterministic seeding. - read-random: func(len: u32) -> list; -} - -world embedder { - export embedder-api; import embedder-callbacks; } From 9836415ef59b111356e17ce3b42a6ca73267bb28 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 17:58:51 -0400 Subject: [PATCH 45/85] test(endive): advance wasi_p2_clocks probe to wall 9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sibling wasm-cm change lands Wall 8 — HostEngineWasm::create_host_func now overrides the trait default and materialises a real core-engine FuncHandle via a new env.host_create_host_func extern plus companion wasmcm_dispatch_host_func export. With Wall 8 cleared the JVM sandwich walks past the mint_host_callback_handle synthesis and advances into Wall 9: HostEngineWasm::module_imports at crates/wasmcm-runtime-guest/src/host_engine.rs:262 still returns an empty vector (the variable-length import descriptor codec is not yet wired), so the runtime's instantiate_with_ref_imports at crates/wasmcm-runtime/src/lib.rs:828 builds an empty ext_imports for a module that actually declares imports, and Endive's own linker surfaces the mismatch as HOST_ERR_UNLINKABLE: "missing import: .". Test updated to assert that wall — the presence of a wasi_p2 interface name in the missing-import message is the strong signal Wall 8 actually works. Flip to a full assertNotNull path when Wall 9 (module_imports/exports introspection) lands. --- .../EndiveComponentIntegrationTest.java | 75 ++++++++++--------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java index 2b36cef..7ed7dc1 100644 --- a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java @@ -81,32 +81,31 @@ void scalarComponentRoundTripsAddThroughGuest() throws Exception { * Attempt to instantiate a real WASI Preview 2 component that imports * {@code wasip2:host/primitives@0.1.0} and {@code wasi:io/poll@0.2.3}. * - *

      Walls 2 & 3 land the guest-side {@code - * ImportedInstance::HostCallback} plumbing and the Java-side - * {@link run.tegmentum.wasmcm.endive.HostCallbackDispatcher} wiring — - * the register/dispatch surface is fully present. The remaining gap is - * inside {@code wasmcm-runtime-guest}: its {@code - * HostEngineWasm} impl of {@code wasmcm_host_api::HostEngine} does not - * override {@code create_host_func}, so the trait default at - * {@code crates/wasmcm-host-api/src/engine.rs:271-278} returns - * {@code Error::UnsupportedFeature("adapter does not implement - * create_host_func")}. The Rust runtime hits this from - * {@code Runtime::mint_host_callback_handle} at - * {@code crates/wasmcm-runtime/src/lib.rs:2114} while synthesising the - * canon-lower ⇒ HostCallback core-func bridge — before the extern - * {@code wasmcm_host_call} return path is exercised at all. Surfaces - * through the JVM lane as {@code HOST_ERR_UNSUPPORTED} (tag 7). + *

      Walls 2, 3 and 8 have landed: the register/dispatch surface, the + * runtime-side {@code ImportedInstance::HostCallback} plumbing, and + * the guest-side {@code HostEngineWasm::create_host_func} + + * {@code instantiate}-with-CoreFunc-imports pair are all wired + * (see the sibling wasm-cm change that adds + * {@code env.host_create_host_func}, + * {@code env.host_instantiate_with_imports}, and the + * {@code wasmcm_dispatch_host_func} export).

      * - *

      The sibling Rust probe {@code - * crates/wasmcm-runtime/tests/probe_wasi_p2_clocks.rs} passes cleanly - * because it drives {@code WasmtimeCoreEngine} (which overrides - * {@code create_host_func}); the JVM sandwich cannot route through - * that until the guest-side {@code HostEngineWasm} learns the same - * trick over a new extern.

      + *

      The current wall is Wall 9: + * {@code HostEngineWasm::module_imports} at + * {@code crates/wasmcm-runtime-guest/src/host_engine.rs:262-269} + * still returns an empty vector — the variable-length import + * descriptor codec + the paired {@code host_module_import_at} + * handler are not yet wired. The runtime's + * {@code instantiate_with_ref_imports} at + * {@code crates/wasmcm-runtime/src/lib.rs:828} therefore builds an + * empty {@code ext_imports} for a module that actually declares + * imports, and Endive's own linker surfaces that mismatch as + * {@code HOST_ERR_UNLINKABLE: "missing import: ."}.

      * - *

      When that gap closes (guest-side {@code create_host_func} plus - * matching {@code instantiate}-with-func-imports on both the guest - * Rust and Java sides), flip this to + *

      Surfaces through the WASI P2 lane as a + * {@link InstantiationException} whose message names the missing + * import. When Wall 9 closes (module_imports/exports introspection + * fully wired), flip to * {@code assertNotNull(component.instantiate(ctx))} and assert the * expected exports.

      */ @@ -145,28 +144,34 @@ void wasiClocksComponentReachesInstantiationSite() throws Exception { // wasi:io/poll@0.2.3 is a resource-type-only import in // this component (num_funcs=0 in the sibling Rust // probe); we register a placeholder entry so the guest - // provider table has a row for it. The registered - // dispatcher is never reached — the canon-lower ⇒ - // HostCallback chain bails earlier at create_host_func. + // provider table has a row for it. Wall 9 (missing + // module_imports introspection) is the current + // blocker — the callback is not yet reached. .addWitHostFunction( new WitHostFunctionDefinition( "wasi:io/poll@0.2.3#__placeholder", args -> new Object[0])) .build(); - // Instantiation is expected to fail with HOST_ERR_UNSUPPORTED - // "adapter does not implement create_host_func" — see the - // class javadoc above for file:line of the true wall. When - // the guest-side HostEngineWasm learns create_host_func, flip - // this to assertNotNull(component.instantiate(ctx)) and - // assert the exported clocks interface is present. + // Wall 9: expect HOST_ERR_UNLINKABLE from the linker step, + // carrying a "missing import: ." message. + // The message text is the strong signal Wall 8 actually + // works — the runtime got past create_host_func and reached + // Endive's own linker with a module whose imports the + // empty module_imports stub can't enumerate. InstantiationException ex = assertThrows( InstantiationException.class, () -> component.instantiate(ctx)); assertNotNull(ex.getMessage(), "guest should have written a diagnostic"); assertTrue( - ex.getMessage().contains("adapter does not implement create_host_func"), - "expected the honest create_host_func wall but got: " + ex.getMessage()); + ex.getMessage().contains("missing import"), + "expected the honest module_imports (Wall 9) wall — 'missing import' " + + "— but got: " + ex.getMessage()); + assertTrue( + ex.getMessage().contains("wasip2:host/primitives") + || ex.getMessage().contains("wasi:io/poll"), + "expected the missing-import to name a wasi_p2 interface " + + "(confirming Wall 8 was cleared); was: " + ex.getMessage()); } } From c6418ae42141b1c8eb7aacad42da580904c18bca Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 18:25:42 -0400 Subject: [PATCH 46/85] test(endive-provider): flip wasi_p2_clocks to positive assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wall 9 landed on the sibling wasm-cm side (module_imports/exports descriptor codec wired end-to-end). The wasi_p2_clocks component now instantiates cleanly through the JVM lane — Endive's linker binds every declared import from the enumerated ext_imports. Test renamed from wasiClocksComponentReachesInstantiationSite to wasiClocksComponentInstantiatesCleanly and now asserts that component.instantiate(ctx) returns a non-null instance surfacing at least one export. If a Wall 10 emerges (e.g. an ImportedInstance::Component gap, a Resource wiring issue), the positive assertion will fail with a fresh diagnostic and the milestone discipline dictates flipping again. Requires wasmcm-endive-adapter 0.1.0-SNAPSHOT rebuilt from the wasm-cm HEAD that lands Wall 9, plus the guest runtime blob SHA-256 4f15ed6918748ce16ac6823512809343439413638c4b86873ae98077b023d08f. --- .../EndiveComponentIntegrationTest.java | 81 +++++++------------ 1 file changed, 27 insertions(+), 54 deletions(-) diff --git a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java index 7ed7dc1..11e1e02 100644 --- a/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java +++ b/endive4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/endive/EndiveComponentIntegrationTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -11,7 +10,6 @@ import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; import ai.tegmentum.webassembly4j.api.Engine; import ai.tegmentum.webassembly4j.api.WitHostFunctionDefinition; -import ai.tegmentum.webassembly4j.api.exception.InstantiationException; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -78,39 +76,29 @@ void scalarComponentRoundTripsAddThroughGuest() throws Exception { } /** - * Attempt to instantiate a real WASI Preview 2 component that imports + * Instantiate a real WASI Preview 2 component that imports * {@code wasip2:host/primitives@0.1.0} and {@code wasi:io/poll@0.2.3}. * - *

      Walls 2, 3 and 8 have landed: the register/dispatch surface, the - * runtime-side {@code ImportedInstance::HostCallback} plumbing, and - * the guest-side {@code HostEngineWasm::create_host_func} + - * {@code instantiate}-with-CoreFunc-imports pair are all wired - * (see the sibling wasm-cm change that adds - * {@code env.host_create_host_func}, - * {@code env.host_instantiate_with_imports}, and the - * {@code wasmcm_dispatch_host_func} export).

      + *

      Walls 2, 3, 8 and 9 have landed: the register/dispatch + * surface, the runtime-side {@code ImportedInstance::HostCallback} + * plumbing, the guest-side {@code HostEngineWasm::create_host_func} + * + {@code instantiate}-with-CoreFunc-imports pair, and — most + * recently — the variable-length import descriptor codec that + * turns {@code HostEngineWasm::module_imports} into a real + * enumeration are all wired end-to-end. The runtime's + * {@code instantiate_with_ref_imports} builds a full + * {@code ext_imports} list from the compiled module's declared + * imports and Endive's linker successfully binds every row.

      * - *

      The current wall is Wall 9: - * {@code HostEngineWasm::module_imports} at - * {@code crates/wasmcm-runtime-guest/src/host_engine.rs:262-269} - * still returns an empty vector — the variable-length import - * descriptor codec + the paired {@code host_module_import_at} - * handler are not yet wired. The runtime's - * {@code instantiate_with_ref_imports} at - * {@code crates/wasmcm-runtime/src/lib.rs:828} therefore builds an - * empty {@code ext_imports} for a module that actually declares - * imports, and Endive's own linker surfaces that mismatch as - * {@code HOST_ERR_UNLINKABLE: "missing import: ."}.

      - * - *

      Surfaces through the WASI P2 lane as a - * {@link InstantiationException} whose message names the missing - * import. When Wall 9 closes (module_imports/exports introspection - * fully wired), flip to - * {@code assertNotNull(component.instantiate(ctx))} and assert the - * expected exports.

      + *

      Exit criterion: {@code component.instantiate(ctx)} returns a + * non-null instance and the guest surfaces at least one export. + * If a Wall 10 emerges (e.g. an ImportedInstance::Component gap or + * a Resource wiring issue), this test will fail with a fresh + * diagnostic and the assertions get flipped again per the + * milestone discipline.

      */ @Test - void wasiClocksComponentReachesInstantiationSite() throws Exception { + void wasiClocksComponentInstantiatesCleanly() throws Exception { assumeGuestPresent(); byte[] bytes = loadClasspath("wasi_p2_clocks.component.wasm"); assumeTrue(bytes != null, "wasi_p2_clocks.component.wasm missing from test resources"); @@ -141,37 +129,22 @@ void wasiClocksComponentReachesInstantiationSite() throws Exception { new WasmcmValueCodec.Boxed( WasmcmValueCodec.TAG_U64, 1L) })) - // wasi:io/poll@0.2.3 is a resource-type-only import in - // this component (num_funcs=0 in the sibling Rust - // probe); we register a placeholder entry so the guest - // provider table has a row for it. Wall 9 (missing - // module_imports introspection) is the current - // blocker — the callback is not yet reached. + // wasi:io/poll@0.2.3 is a resource-type-only import + // (num_funcs=0 in the sibling Rust probe); a + // placeholder entry keeps the guest's provider + // cross-check happy. .addWitHostFunction( new WitHostFunctionDefinition( "wasi:io/poll@0.2.3#__placeholder", args -> new Object[0])) .build(); - // Wall 9: expect HOST_ERR_UNLINKABLE from the linker step, - // carrying a "missing import: ." message. - // The message text is the strong signal Wall 8 actually - // works — the runtime got past create_host_func and reached - // Endive's own linker with a module whose imports the - // empty module_imports stub can't enumerate. - InstantiationException ex = assertThrows( - InstantiationException.class, - () -> component.instantiate(ctx)); - assertNotNull(ex.getMessage(), "guest should have written a diagnostic"); - assertTrue( - ex.getMessage().contains("missing import"), - "expected the honest module_imports (Wall 9) wall — 'missing import' " - + "— but got: " + ex.getMessage()); + ComponentInstance instance = component.instantiate(ctx); + assertNotNull(instance, "instantiate should return a live instance after Wall 9"); assertTrue( - ex.getMessage().contains("wasip2:host/primitives") - || ex.getMessage().contains("wasi:io/poll"), - "expected the missing-import to name a wasi_p2 interface " - + "(confirming Wall 8 was cleared); was: " + ex.getMessage()); + !instance.exportedFunctions().isEmpty(), + "wasi_p2_clocks should surface at least one export; got " + + instance.exportedFunctions()); } } From d79170e011d2170cabe19c7abcde9a8d63be8c55 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 19:02:17 -0400 Subject: [PATCH 47/85] feat(wasmos-embedder-generated): add ADR-006 embedder bindings module Extracts the six wasmos:host/embedder@0.1.0 generated Java sources (RuntimeInstance / HostProvider / EmbedderCallbacks / ImportSatisfaction / Error / ErrorCode) out of webassembly4j-bindgen's test resources into a first-class Maven artifact under groupId ai.tegmentum.webassembly4j. Downstream JVM embedders (wasi-p2-rs adapters/endive, wasm-cm adapters/endive) depend on this jar for the ADR-006 embedder surface; they then ship their own connector to route the generated calls into a concrete runtime. Regenerate the six files with: java -jar webassembly4j-bindgen/target/webassembly4j-bindgen-2.5.1-cli.jar \\ --wit ~/git/wasm-cm/wit/wasmos-host/embedder.wit \\ --package ai.tegmentum.wasmos.embedder.generated \\ --output wasmos-embedder-generated/src/main/java \\ --no-impl The module ships a local copy of WitResult at its original bindgen FQN so the pom depends only on webassembly4j-api and downstream classpaths do not need webassembly4j-bindgen (which is packaged as a maven-plugin and heavier than needed at runtime). --- pom.xml | 1 + wasmos-embedder-generated/pom.xml | 64 +++++++++ .../embedder/generated/EmbedderCallbacks.java | 12 ++ .../wasmos/embedder/generated/Error.java | 38 ++++++ .../wasmos/embedder/generated/ErrorCode.java | 17 +++ .../embedder/generated/HostProvider.java | 27 ++++ .../generated/ImportSatisfaction.java | 38 ++++++ .../embedder/generated/RuntimeInstance.java | 43 ++++++ .../webassembly4j/bindgen/wit/WitResult.java | 127 ++++++++++++++++++ 9 files changed, 367 insertions(+) create mode 100644 wasmos-embedder-generated/pom.xml create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderCallbacks.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/Error.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ErrorCode.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ImportSatisfaction.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java diff --git a/pom.xml b/pom.xml index 9b7d8a7..1bee832 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ webassembly4j-benchmarks webassembly4j-bindgen webassembly4j-component-builder + wasmos-embedder-generated diff --git a/wasmos-embedder-generated/pom.xml b/wasmos-embedder-generated/pom.xml new file mode 100644 index 0000000..8496f4b --- /dev/null +++ b/wasmos-embedder-generated/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + ai.tegmentum.webassembly4j + webassembly4j-parent + 2.5.1 + + + wasmos-embedder-generated + jar + + WebAssembly4J :: wasmos-embedder generated bindings + + Java bindings generated by webassembly4j-bindgen from + wasmos:host/embedder@0.1.0 (per ADR-006). The sole content of this + module is: + + - The 6 generated source files under + ai.tegmentum.wasmos.embedder.generated (RuntimeInstance, + HostProvider, EmbedderCallbacks, ImportSatisfaction, Error, + ErrorCode) — copied verbatim from bindgen output. + - A local copy of the WitResult carrier at + ai.tegmentum.webassembly4j.bindgen.wit.WitResult so the module + has no compile-time or runtime dependency on + webassembly4j-bindgen (which is a maven-plugin-packaged + build-time-only artifact). + + Downstream embedders (wasi-p2-rs, wasm-cm) depend on this jar for + the ADR-006 embedder surface and then ship their own connector to + route the generated calls into a concrete runtime. + + Regenerate the six wasmos.embedder.generated files with: + + java -jar webassembly4j-bindgen/target/webassembly4j-bindgen-2.5.1-cli.jar \ + --wit ~/git/wasm-cm/wit/wasmos-host/embedder.wit \ + --package ai.tegmentum.wasmos.embedder.generated \ + --output wasmos-embedder-generated/src/main/java \ + --no-impl + + + + + ai.tegmentum.webassembly4j + webassembly4j-api + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + + + + + diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderCallbacks.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderCallbacks.java new file mode 100644 index 0000000..ff532f4 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderCallbacks.java @@ -0,0 +1,12 @@ +package ai.tegmentum.wasmos.embedder.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +public interface EmbedderCallbacks { + WitResult, String> hostCall(int providerHandle, int funcIdx, List args); + + long nowMonotonicNs(); + + List readRandom(int len); +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/Error.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/Error.java new file mode 100644 index 0000000..09cbbd1 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/Error.java @@ -0,0 +1,38 @@ +package ai.tegmentum.wasmos.embedder.generated; + +public final class Error { + private final ErrorCode code; + + private final String message; + + public Error(ErrorCode code, String message) { + this.code = code; + this.message = message; + } + + public ErrorCode code() { + return this.code; + } + + public String message() { + return this.message; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Error that = (Error) obj; + return java.util.Objects.equals(this.code, that.code) && java.util.Objects.equals(this.message, that.message); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(code, message); + } + + @Override + public String toString() { + return "Error[code=" + code + ", message=" + message + "]"; + } +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ErrorCode.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ErrorCode.java new file mode 100644 index 0000000..13f028a --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ErrorCode.java @@ -0,0 +1,17 @@ +package ai.tegmentum.wasmos.embedder.generated; + +public enum ErrorCode { + INVALID_COMPONENT, + + UNSATISFIED_IMPORT, + + MISSING_EXPORT, + + HOST_CALLBACK_FAILED, + + TRAP, + + WIT_MISMATCH, + + OTHER +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java new file mode 100644 index 0000000..d002d48 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java @@ -0,0 +1,27 @@ +package ai.tegmentum.wasmos.embedder.generated; + +/** + * Resource type: host-provider + * + *

      This resource should be closed after use. + */ +public class HostProvider implements AutoCloseable { + private final long handle; + + protected HostProvider(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + // Resource cleanup - to be implemented by runtime + } + + public static HostProvider create(String interfaceName, int numFuncs) { + throw new UnsupportedOperationException("wasmos:host/embedder constructor dispatch not yet wired"); + } +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ImportSatisfaction.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ImportSatisfaction.java new file mode 100644 index 0000000..e8cb76e --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/ImportSatisfaction.java @@ -0,0 +1,38 @@ +package ai.tegmentum.wasmos.embedder.generated; + +public final class ImportSatisfaction { + private final String interfaceName; + + private final HostProvider provider; + + public ImportSatisfaction(String interfaceName, HostProvider provider) { + this.interfaceName = interfaceName; + this.provider = provider; + } + + public String interfaceName() { + return this.interfaceName; + } + + public HostProvider provider() { + return this.provider; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ImportSatisfaction that = (ImportSatisfaction) obj; + return java.util.Objects.equals(this.interfaceName, that.interfaceName) && java.util.Objects.equals(this.provider, that.provider); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(interfaceName, provider); + } + + @Override + public String toString() { + return "ImportSatisfaction[interfaceName=" + interfaceName + ", provider=" + provider + "]"; + } +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java new file mode 100644 index 0000000..25e999b --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java @@ -0,0 +1,43 @@ +package ai.tegmentum.wasmos.embedder.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Resource type: runtime-instance + * + *

      This resource should be closed after use. + */ +public class RuntimeInstance implements AutoCloseable { + private final long handle; + + public RuntimeInstance(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + // Resource cleanup - to be implemented by runtime + } + + public static WitResult instantiate(List componentBytes, + List imports) { + throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + } + + public WitResult, Error> callExport(String name, List args) { + throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + } + + public String introspect() { + throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + } + + public WitResult verifyWorld(String expectedWit) { + throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + } +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java new file mode 100644 index 0000000..ba37ff4 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java @@ -0,0 +1,127 @@ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +/** + * Runtime representation of the WIT {@code result} type. + * + *

      The bindgen code generator references this class by its fully-qualified + * name (see {@link + * ai.tegmentum.webassembly4j.bindgen.generator.TypeMappingRegistry}); it must + * therefore exist as a real class on the consumer's classpath. The two + * factory methods {@link #ok(Object)} and {@link #err(Object)} construct + * either side; the resulting instance is immutable. + * + *

      Generated code typically follows this pattern: + * + *

      {@code
      + * WitResult result = parser.parse(language, source);
      + * if (result.isOk()) {
      + *     ParseResult tree = result.unwrap();
      + * } else {
      + *     String msg = result.unwrapErr();
      + * }
      + * }
      + * + * @param the success-payload type ({@link Void} when the WIT signature is + * {@code result<_, err>}) + * @param the error-payload type ({@link Void} when the WIT signature is + * {@code result} or {@code result}) + */ +public final class WitResult { + + private final T ok; + private final E err; + private final boolean isOk; + + private WitResult(final T ok, final E err, final boolean isOk) { + this.ok = ok; + this.err = err; + this.isOk = isOk; + } + + /** Construct a successful result. */ + public static WitResult ok(final T value) { + return new WitResult<>(value, null, true); + } + + /** Construct an error result. */ + public static WitResult err(final E value) { + return new WitResult<>(null, value, false); + } + + /** True if this is a success result. */ + public boolean isOk() { + return isOk; + } + + /** True if this is an error result. */ + public boolean isErr() { + return !isOk; + } + + /** Returns the success payload or throws when {@link #isErr()}. */ + public T unwrap() { + if (!isOk) { + throw new NoSuchElementException("WitResult is err: " + err); + } + return ok; + } + + /** Returns the error payload or throws when {@link #isOk()}. */ + public E unwrapErr() { + if (isOk) { + throw new NoSuchElementException("WitResult is ok: " + ok); + } + return err; + } + + /** Optional view of the success side. */ + public Optional ok() { + return isOk ? Optional.ofNullable(ok) : Optional.empty(); + } + + /** Optional view of the error side. */ + public Optional err() { + return isOk ? Optional.empty() : Optional.ofNullable(err); + } + + /** Map the success payload through {@code f}, leaving an error unchanged. */ + public WitResult map(final Function f) { + Objects.requireNonNull(f, "f"); + return isOk ? WitResult.ok(f.apply(ok)) : WitResult.err(err); + } + + /** Map the error payload through {@code f}, leaving a success unchanged. */ + public WitResult mapErr(final Function f) { + Objects.requireNonNull(f, "f"); + return isOk ? WitResult.ok(ok) : WitResult.err(f.apply(err)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof WitResult)) { + return false; + } + final WitResult other = (WitResult) o; + return isOk == other.isOk + && Objects.equals(ok, other.ok) + && Objects.equals(err, other.err); + } + + @Override + public int hashCode() { + return Objects.hash(ok, err, isOk); + } + + @Override + public String toString() { + return isOk ? "Ok(" + ok + ")" : "Err(" + err + ")"; + } +} From 97294c3e9821f32765f86af2368cf5e22723222d Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 23 Jul 2026 21:40:27 -0400 Subject: [PATCH 48/85] feat(bindgen): 2.0 SPI-dispatch mode for generated resource bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under ADR-006 every embedder should write one `wasmos_start` shim and consume generated bindings for everything else. Bindgen 1.x satisfied the shape half (resource classes, records, method signatures) but every body threw `UnsupportedOperationException`, forcing each embedder to hand-write a 300+ LOC bridge (`WasmosEmbedderConnector`). Add an opt-in runtime-provider SPI mode. With `--runtime-provider ` the generator emits a `` interface plus a `Registry` carrier, and rewrites every resource method body (constructor factory, static, instance, and `close()`) to dispatch through the registry. The embedder implements the SPI once and installs it at startup; the hand-written per-method bridge disappears. Details: - New `BindgenConfig.runtimeProviderName` (nullable String, default null preserves 1.x behaviour). CLI flag `--runtime-provider NAME`. - New `RuntimeProviderCodeGenerator` emits the SPI interface + a volatile-slot registry. Method-name convention: `` in camelCase, with `long handle` prepended to instance-method signatures. Constructor factories return the resource type; the SPI impl wraps the handle. - `ModernCodeGenerator.generateResource*` branches on the SPI flag. In SPI mode, the handle-holding ctor is `public` (an out-of-package SPI impl must construct wrapped resources); in legacy mode it stays `protected` when a WIT constructor is declared. - `wasmos-embedder-generated` regenerated with the new flag — `RuntimeInstance` / `HostProvider` bodies now dispatch through `EmbedderRuntimeRegistry`; two new files (`EmbedderRuntime.java`, `EmbedderRuntimeRegistry.java`) ship the SPI. Regenerate recipe on the module's pom description. - Design writeup: `webassembly4j-bindgen/docs/design-2.0.md`. - Tests: `WasmosEmbedderSpiDispatchTest` pins the SPI-mode surface (dispatch bodies, no throws, deterministic, javac-clean) and guards that legacy mode is unchanged. Existing `WasmosEmbedderShapeTest` continues to pin the legacy contract. Backward compatible: consumers who don't set `--runtime-provider` keep the 1.x throwing-bodies behaviour bit-for-bit. Bindgen test count: 472 -> 474 (added two SPI-mode tests). --- wasmos-embedder-generated/pom.xml | 26 +- .../embedder/generated/EmbedderRuntime.java | 30 ++ .../generated/EmbedderRuntimeRegistry.java | 36 +++ .../embedder/generated/HostProvider.java | 6 +- .../embedder/generated/RuntimeInstance.java | 10 +- webassembly4j-bindgen/docs/design-2.0.md | 175 +++++++++++ .../webassembly4j/bindgen/BindgenConfig.java | 44 ++- .../webassembly4j/bindgen/cli/BindgenCli.java | 11 + .../bindgen/generator/JavaCodeGenerator.java | 25 ++ .../generator/ModernCodeGenerator.java | 151 ++++++++-- .../RuntimeProviderCodeGenerator.java | 274 +++++++++++++++++ .../wasmos/WasmosEmbedderSpiDispatchTest.java | 276 ++++++++++++++++++ 12 files changed, 1017 insertions(+), 47 deletions(-) create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntimeRegistry.java create mode 100644 webassembly4j-bindgen/docs/design-2.0.md create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/RuntimeProviderCodeGenerator.java create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderSpiDispatchTest.java diff --git a/wasmos-embedder-generated/pom.xml b/wasmos-embedder-generated/pom.xml index 8496f4b..22391c9 100644 --- a/wasmos-embedder-generated/pom.xml +++ b/wasmos-embedder-generated/pom.xml @@ -20,10 +20,18 @@ wasmos:host/embedder@0.1.0 (per ADR-006). The sole content of this module is: - - The 6 generated source files under - ai.tegmentum.wasmos.embedder.generated (RuntimeInstance, - HostProvider, EmbedderCallbacks, ImportSatisfaction, Error, - ErrorCode) — copied verbatim from bindgen output. + - The 8 generated source files under + ai.tegmentum.wasmos.embedder.generated: + * RuntimeInstance, HostProvider — resources whose bodies + dispatch through the bindgen-2.0 runtime-provider SPI + (see EmbedderRuntime below). + * EmbedderCallbacks, ImportSatisfaction, Error, ErrorCode. + * EmbedderRuntime — bindgen-2.0 runtime-provider SPI. The + embedder implements this once (e.g. + wasi-p2-rs's WasmcmEmbedderRuntime) and installs it via + EmbedderRuntimeRegistry.install(...). + * EmbedderRuntimeRegistry — single-slot registry the + generated resource bodies consult. - A local copy of the WitResult carrier at ai.tegmentum.webassembly4j.bindgen.wit.WitResult so the module has no compile-time or runtime dependency on @@ -31,16 +39,18 @@ build-time-only artifact). Downstream embedders (wasi-p2-rs, wasm-cm) depend on this jar for - the ADR-006 embedder surface and then ship their own connector to - route the generated calls into a concrete runtime. + the ADR-006 embedder surface and provide a concrete EmbedderRuntime + implementation that maps the SPI onto their runtime driver + (WasmcmRuntimeGuestLoader for the JVM lane). - Regenerate the six wasmos.embedder.generated files with: + Regenerate the wasmos.embedder.generated files with: java -jar webassembly4j-bindgen/target/webassembly4j-bindgen-2.5.1-cli.jar \ --wit ~/git/wasm-cm/wit/wasmos-host/embedder.wit \ --package ai.tegmentum.wasmos.embedder.generated \ --output wasmos-embedder-generated/src/main/java \ - --no-impl + --no-impl \ + --runtime-provider EmbedderRuntime diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java new file mode 100644 index 0000000..d61f833 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java @@ -0,0 +1,30 @@ +package ai.tegmentum.wasmos.embedder.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Runtime-provider SPI for the generated bindings. + * + *

      The embedder implements this interface once and installs the implementation + * via {@link EmbedderRuntimeRegistry#install(EmbedderRuntime)}. Generated resource method bodies dispatch + * through {@link EmbedderRuntimeRegistry#runtime()} to reach the installed provider. + * + *

      Emitted by webassembly4j-bindgen 2.0 (runtime-provider dispatch mode). + */ +public interface EmbedderRuntime { + HostProvider hostProviderCreate(String interfaceName, int numFuncs); + + void hostProviderClose(long handle); + + WitResult runtimeInstanceInstantiate(List componentBytes, + List imports); + + WitResult, Error> runtimeInstanceCallExport(long handle, String name, List args); + + String runtimeInstanceIntrospect(long handle); + + WitResult runtimeInstanceVerifyWorld(long handle, String expectedWit); + + void runtimeInstanceClose(long handle); +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntimeRegistry.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntimeRegistry.java new file mode 100644 index 0000000..a998298 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntimeRegistry.java @@ -0,0 +1,36 @@ +package ai.tegmentum.wasmos.embedder.generated; + +import java.util.Objects; + +/** + * Single-slot registry for the {@link EmbedderRuntime} runtime-provider SPI. + * + *

      Call {@link #install(EmbedderRuntime)} once from the embedder's startup path, before + * invoking any generated resource method. Generated bodies reach the installed + * provider via {@link #runtime()}, which throws {@link IllegalStateException} if + * nothing was installed. + * + *

      Emitted by webassembly4j-bindgen 2.0. + */ +public final class EmbedderRuntimeRegistry { + private static volatile EmbedderRuntime provider; + + private EmbedderRuntimeRegistry() { + } + + public static void install(EmbedderRuntime provider) { + EmbedderRuntimeRegistry.provider = Objects.requireNonNull(provider, "provider"); + } + + public static void uninstall() { + EmbedderRuntimeRegistry.provider = null; + } + + public static EmbedderRuntime runtime() { + EmbedderRuntime r = provider; + if (r == null) { + throw new IllegalStateException("EmbedderRuntime not installed; call EmbedderRuntimeRegistry.install(...) before invoking generated bindings"); + } + return r; + } +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java index d002d48..b035dc8 100644 --- a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java @@ -8,7 +8,7 @@ public class HostProvider implements AutoCloseable { private final long handle; - protected HostProvider(long handle) { + public HostProvider(long handle) { this.handle = handle; } @@ -18,10 +18,10 @@ public long handle() { @Override public void close() { - // Resource cleanup - to be implemented by runtime + EmbedderRuntimeRegistry.runtime().hostProviderClose(this.handle); } public static HostProvider create(String interfaceName, int numFuncs) { - throw new UnsupportedOperationException("wasmos:host/embedder constructor dispatch not yet wired"); + return EmbedderRuntimeRegistry.runtime().hostProviderCreate(interfaceName, numFuncs); } } diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java index 25e999b..b65247a 100644 --- a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/RuntimeInstance.java @@ -21,23 +21,23 @@ public long handle() { @Override public void close() { - // Resource cleanup - to be implemented by runtime + EmbedderRuntimeRegistry.runtime().runtimeInstanceClose(this.handle); } public static WitResult instantiate(List componentBytes, List imports) { - throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + return EmbedderRuntimeRegistry.runtime().runtimeInstanceInstantiate(componentBytes, imports); } public WitResult, Error> callExport(String name, List args) { - throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + return EmbedderRuntimeRegistry.runtime().runtimeInstanceCallExport(this.handle, name, args); } public String introspect() { - throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + return EmbedderRuntimeRegistry.runtime().runtimeInstanceIntrospect(this.handle); } public WitResult verifyWorld(String expectedWit) { - throw new UnsupportedOperationException("wasmos:host/embedder method dispatch not yet wired"); + return EmbedderRuntimeRegistry.runtime().runtimeInstanceVerifyWorld(this.handle, expectedWit); } } diff --git a/webassembly4j-bindgen/docs/design-2.0.md b/webassembly4j-bindgen/docs/design-2.0.md new file mode 100644 index 0000000..fcfa994 --- /dev/null +++ b/webassembly4j-bindgen/docs/design-2.0.md @@ -0,0 +1,175 @@ +# webassembly4j-bindgen 2.0 — SPI-dispatching resource bodies + +- Status: **Landed** +- Date: 2026-07-23 +- Depends on: bindgen 1.x (resource shape emission, world hoisting) +- Consumes: [ADR-005](../../wasm-cm/docs/decisions/ADR-005-webassembly4j-spi.md), + [ADR-006](../../wasm-cm/docs/decisions/ADR-006-wasmos-embedder-wit-surface.md) + +## Problem + +bindgen 1.x emitted the SHAPE half of resource-typed Java bindings — +class declarations, `long handle` fields, `AutoCloseable`, method +signatures, `create(...)` factories for WIT constructors — but every +method body was `throw new UnsupportedOperationException(...)`. + +Under ADR-006 the intent is that every embedder writes exactly one +`wasmos_start` native call and consumes generated bindings for +everything else. With bindings whose bodies throw, an embedder has to +hand-write a bridge from the generated surface to a real runtime +(`WasmosEmbedderConnector` under wasi-p2-rs: 392 LOC). That bridge +subclassed the generated resources, cached its own handles, mapped +error codes by hand, and routed callbacks — everything the ADR says +should NOT exist per-embedder. + +## Decision + +Add an opt-in **runtime-provider SPI** mode to the generator. When +enabled, resource method bodies dispatch through a generated SPI +interface instead of throwing. Each embedder writes one implementation +of that SPI (once) and installs it into a generated registry at +startup; the hand-written per-method bridge disappears. + +### Configuration + +- New `BindgenConfig` field: `runtimeProviderName` (String, nullable). + When null: legacy behavior (bodies throw). When set to `"Foo"`: SPI + mode emits `Foo` + `FooRegistry` and rewrites resource bodies to + dispatch through `FooRegistry.runtime()`. +- New CLI option: `--runtime-provider NAME`. +- Deterministic: `Foo` and `FooRegistry` are emitted with a fixed + method ordering derived from WIT declaration order. Same input WIT + + same `runtimeProviderName` → byte-identical output. + +### Generated surface + +For every resource `X` with methods `m1`, `m2`, ... and the intrinsic +`close`, the SPI interface gets: + +- If `X` has a WIT constructor: `X Create()` — + returns the fully-constructed resource; the impl chooses how to mint + the handle. +- For each instance method `m(P) -> R`: `R (long handle, P)`. +- For each static method `m(P) -> R`: `R (P)`. +- Intrinsic `close`: `void Close(long handle)`. + +Where `` is the resource name in camelCase and `` is the +method name in PascalCase — the concatenation is unambiguous and +avoids collisions across resources that share a method name. + +The generated body for an instance method becomes: + +```java +public WitResult, Error> callExport(String name, List args) { + return EmbedderRuntimeRegistry.runtime() + .runtimeInstanceCallExport(this.handle, name, args); +} +``` + +The static-factory body for a WIT constructor: + +```java +public static HostProvider create(String interfaceName, int numFuncs) { + return EmbedderRuntimeRegistry.runtime() + .hostProviderCreate(interfaceName, numFuncs); +} +``` + +Note that constructor factories return the resource **type** (not a +`long`), so the SPI impl is the sole locus that knows how to wrap a +runtime-issued handle into the generated class. This keeps the +`long handle`-holding constructor `protected` — no external caller +should ever construct a resource with an unbounded raw handle. + +### Registry + +`Registry` is a tiny final class with: + +- `install( provider)` — install the SPI impl; overwrite is + allowed for test setups. +- `runtime()` — return the installed impl or throw `IllegalStateException` + with an actionable message. +- `uninstall()` — for test cleanup. + +The registry uses a `volatile` field, not a full `ThreadLocal` or per- +`RuntimeInstance` provider. Rationale: an embedder installs one +provider for the lifetime of a JVM; testing multiple providers +side-by-side would require the registry to grow (out of scope for +2.0). If that need lands, extending the registry with a thread-local +override is additive. + +### Callback binding + +`EmbedderCallbacks` stays as it was — an interface the embedder +implements. The connector's constructor threads the callback impl into +the SPI provider impl; no generator support is needed. This keeps the +generator free of any "how to wire the imported interface" strategy +choice, which is deliberately embedder-specific (e.g. Endive uses +`LinkingContext`; wasmtime4j uses `Linker`; graalwasm4j uses +`ImportObject`). + +## Consequences + +**On the generator:** + +- One new config field, one new CLI flag. +- One new file (`RuntimeProviderCodeGenerator`) that emits the two SPI + classes. +- One targeted change to `ModernCodeGenerator.generateResource` / + `generateResourceMethod` — bodies now branch on + `config.getRuntimeProviderName()`. Legacy mode still emits the + throwing bodies unchanged. +- No changes to type mapping, WIT parsing, world hoisting, or + determinism guarantees. + +**On the wasmos-embedder-generated module:** + +- Regenerated with `--runtime-provider EmbedderRuntime`. New files: + `EmbedderRuntime.java`, `EmbedderRuntimeRegistry.java`. +- `RuntimeInstance` / `HostProvider` bodies now dispatch (no throws). + +**On WasmosEmbedderConnector (wasi-p2-rs):** + +- Drops from 392 LOC to ~180 LOC. The old subclasses + (`RuntimeInstanceHandle`, `HostProviderHandle`) delete outright — no + override needed once dispatch flows through the registry. The + connector becomes a thin bootstrap wrapper: load the guest bytes, + install the SPI impl, expose convenience `byte[]` variants of the + `List`-taking generated calls. + +**What 2.0 does not do (deferred):** + +- The `introspect` / `verify-world` SPI methods dispatch through the + registry, but the wasm-cm runtime-guest does not yet expose an + export that would service them from an instance handle. The provider + impl throws `UnsupportedOperationException` for those two, with a + precise message. Bindgen has done its job; the shortfall is at the + runtime-guest surface, tracked as a follow-on in ADR-005/ADR-006. +- Own/borrow lifecycle beyond `close()` — resources today are + reference-counted at the runtime-guest level; the generated Java + surface doesn't distinguish `own` from `borrow` in method + signatures (they compile to the same generated Java type). A future + bump can add the distinction if a WIT world exercises it. +- Multiple providers per JVM — the registry is single-slot volatile. + Additive change if needed. + +## Alternatives considered + +**Emit resource bodies that delegate to a `Function`/lambda field.** A +per-instance dispatcher instead of a static registry. Rejected: it +forces the SPI impl to construct every resource with a delegate, +undoing the "just wrap a handle" shape of the generated ctor. + +**Generate a full-fat abstract class the embedder subclasses.** In +this model the resource IS abstract; the concrete class the SPI impl +extends. Rejected: the sandwich test's public API is `RuntimeInstance` +(the generated class). If it were abstract, every call site would need +the concrete subclass in its imports — a subtler leak than "call the +registry once at startup." + +**Emit dispatch bodies unconditionally (no config flag).** Rejected: +consumers who want the SHAPE half only (introspection tools, +type-only tests) would be forced to write a dummy provider. +Backward compatibility with 1.x consumers matters — the throwing +bodies are documented, load-bearing behavior for the shape tests that +already exist. diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/BindgenConfig.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/BindgenConfig.java index ee801e4..b3b86e2 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/BindgenConfig.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/BindgenConfig.java @@ -50,6 +50,7 @@ public final class BindgenConfig { private final boolean generateBuilders; private final boolean generateImplementations; private final boolean generateServiceLoader; + private final String runtimeProviderName; private BindgenConfig(final Builder builder) { this.codeStyle = builder.codeStyle; @@ -61,6 +62,7 @@ private BindgenConfig(final Builder builder) { this.generateBuilders = builder.generateBuilders; this.generateImplementations = builder.generateImplementations; this.generateServiceLoader = builder.generateServiceLoader; + this.runtimeProviderName = builder.runtimeProviderName; } /** @@ -153,6 +155,21 @@ public boolean isGenerateServiceLoader() { return generateServiceLoader; } + /** + * Returns the name of the runtime-provider SPI to emit, or {@code null} to use the legacy + * "throwing-body" resource dispatch (bindgen 1.x behaviour). + * + *

      When non-null (bindgen 2.0 SPI-dispatch mode), the generator additionally emits a + * {@code } interface and a {@code Registry} class in the target package; resource + * method bodies dispatch through that registry. See + * {@code docs/design-2.0.md} for the full contract. + * + * @return the runtime-provider interface name, or {@code null} if not configured + */ + public String getRuntimeProviderName() { + return runtimeProviderName; + } + /** * Checks if there are WIT sources configured. * @@ -206,7 +223,8 @@ public boolean equals(final Object obj) { && Objects.equals(packageName, that.packageName) && Objects.equals(outputDirectory, that.outputDirectory) && Objects.equals(witSources, that.witSources) - && Objects.equals(wasmSources, that.wasmSources); + && Objects.equals(wasmSources, that.wasmSources) + && Objects.equals(runtimeProviderName, that.runtimeProviderName); } @Override @@ -220,7 +238,8 @@ public int hashCode() { generateJavadoc, generateBuilders, generateImplementations, - generateServiceLoader); + generateServiceLoader, + runtimeProviderName); } @Override @@ -247,6 +266,8 @@ public String toString() { + generateImplementations + ", generateServiceLoader=" + generateServiceLoader + + ", runtimeProviderName=" + + (runtimeProviderName == null ? "" : runtimeProviderName) + '}'; } @@ -262,6 +283,7 @@ public static final class Builder { private boolean generateBuilders = true; private boolean generateImplementations = true; private boolean generateServiceLoader = true; + private String runtimeProviderName; private Builder() {} @@ -386,6 +408,24 @@ public Builder generateServiceLoader(final boolean generate) { return this; } + /** + * Sets the name of the runtime-provider SPI to emit (bindgen 2.0 mode). + * + *

      Pass {@code null} (the default) for legacy behaviour: resource method bodies throw + * {@link UnsupportedOperationException}. Pass a non-null identifier (e.g. {@code + * "EmbedderRuntime"}) to emit a {@code } SPI interface plus a {@code Registry} + * class, and rewrite resource method bodies to dispatch through the registry. + * + *

      Determinism is preserved in both modes; SPI method ordering follows WIT declaration order. + * + * @param name the runtime-provider interface name, or {@code null} to disable SPI dispatch + * @return this builder + */ + public Builder runtimeProviderName(final String name) { + this.runtimeProviderName = name; + return this; + } + /** * Builds the BindgenConfig instance. * diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java index 71107c4..60483b9 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java @@ -109,6 +109,16 @@ public final class BindgenCli implements Callable { + " of using the marshalling-based Impl scaffolding).") private boolean noImpl; + @Option( + names = {"--runtime-provider"}, + description = + "Emit a runtime-provider SPI interface + registry (bindgen 2.0 dispatch mode). Resource" + + " method bodies will delegate to the installed provider instead of throwing" + + " UnsupportedOperationException. Give the SPI interface name (e.g." + + " 'EmbedderRuntime'). See docs/design-2.0.md.", + paramLabel = "NAME") + private String runtimeProviderName; + @Option( names = {"--verbose", "-v"}, description = "Enable verbose output") @@ -151,6 +161,7 @@ public Integer call() { .generateBuilders(!noBuilders) .generateImplementations(!noImpl) .generateServiceLoader(!noImpl) + .runtimeProviderName(runtimeProviderName) .build(); if (verbose) { diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java index 91a683b..4ffcb5b 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java @@ -88,6 +88,31 @@ public List generate(final BindgenModel model) throws BindgenEx sources.addAll(implGen.generate(model.getInterfaces())); } + // Bindgen 2.0: emit runtime-provider SPI + registry when configured. + // The SPI dispatches every method on every resource type in the model + // (world-level + interface-level), so the embedder writes one impl and + // generated resource bodies delegate to the installed provider. + if (config.getRuntimeProviderName() != null && !config.getRuntimeProviderName().isEmpty()) { + final List resources = new ArrayList<>(); + for (BindgenType type : model.getTypes()) { + if (type.getKind() == BindgenType.Kind.RESOURCE) { + resources.add(type); + } + } + for (BindgenInterface iface : model.getInterfaces()) { + for (BindgenType type : iface.getTypes()) { + if (type.getKind() == BindgenType.Kind.RESOURCE) { + resources.add(type); + } + } + } + if (!resources.isEmpty()) { + final RuntimeProviderCodeGenerator spiGen = new RuntimeProviderCodeGenerator(config); + sources.add(spiGen.generateInterface(resources)); + sources.add(spiGen.generateRegistry()); + } + } + return sources; } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java index b87ab33..ab1972b 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ModernCodeGenerator.java @@ -207,16 +207,23 @@ protected TypeSpec generateResource(final BindgenType type) { // Add handle field classBuilder.addField(TypeName.LONG, "handle", Modifier.PRIVATE, Modifier.FINAL); - // Emit the handle-holding constructor unless the WIT resource declared - // its own `constructor(...)`. When a WIT constructor is present it - // becomes the primary way callers get a handle; the handle-only ctor - // is still emitted as `protected` so subclasses / generated dispatch - // sites can wrap an existing handle. + // Emit the handle-holding constructor. When the WIT resource declared + // its own `constructor(...)`, the visibility depends on dispatch mode: + // * Legacy (throwing bodies): protected — external callers must go + // through the WIT `create(...)` factory; only subclasses (host + // adapters) may wrap an existing raw handle. + // * Bindgen 2.0 SPI-dispatch: public — the SPI impl lives outside + // the generated package and must construct resources to fulfil + // the `WitResult instantiate(...)` / + // `Resource create(...)` contracts. The registry entry-point is + // the enforcement boundary, not the ctor's visibility. final boolean hasWitConstructor = type.getResourceMethods().stream().anyMatch(BindgenFunction::isConstructor); + final Modifier ctorVis = + (hasWitConstructor && !isSpiDispatchMode()) ? Modifier.PROTECTED : Modifier.PUBLIC; classBuilder.addMethod( MethodSpec.constructorBuilder() - .addModifiers(hasWitConstructor ? Modifier.PROTECTED : Modifier.PUBLIC) + .addModifiers(ctorVis) .addParameter(TypeName.LONG, "handle") .addStatement("this.handle = handle") .build()); @@ -229,26 +236,35 @@ protected TypeSpec generateResource(final BindgenType type) { .addStatement("return this.handle") .build()); - // Add close method - classBuilder.addMethod( + // Add close method. In bindgen 2.0 SPI-dispatch mode the body routes + // through the installed runtime provider; in the legacy mode it stays a + // no-op (matches bindgen 1.x behaviour that host adapters override). + final MethodSpec.Builder closeBuilder = MethodSpec.methodBuilder("close") .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addComment("Resource cleanup - to be implemented by runtime") - .build()); + .addAnnotation(Override.class); + if (isSpiDispatchMode()) { + closeBuilder.addStatement( + "$T.runtime().$L(this.handle)", registryClass(), RuntimeProviderCodeGenerator.dispatchCloseName(type.getName())); + } else { + closeBuilder.addComment("Resource cleanup - to be implemented by runtime"); + } + classBuilder.addMethod(closeBuilder.build()); - // Emit the resource's WIT-declared methods. Bodies throw - // UnsupportedOperationException — the SPI runtime wiring that would - // route these to the guest hasn't landed yet (ADR-005 tracks it), so - // the surface is correct even though dispatch is stubbed. + // Emit the resource's WIT-declared methods. In bindgen 2.0 SPI-dispatch + // mode bodies delegate to the installed runtime provider; in the legacy + // mode they throw UnsupportedOperationException with a "not yet wired" + // message, matching bindgen 1.x behaviour that the shape tests pin down. for (final BindgenFunction method : type.getResourceMethods()) { - classBuilder.addMethod(generateResourceMethod(className, method)); + classBuilder.addMethod(generateResourceMethod(type.getName(), className, method)); } return classBuilder.build(); } - private MethodSpec generateResourceMethod(final String className, final BindgenFunction method) { + private MethodSpec generateResourceMethod( + final String resourceWitName, final String className, final BindgenFunction method) { + final boolean spi = isSpiDispatchMode(); if (method.isConstructor()) { // Emit as a static factory `create` — matches the WIT constructor // shape (constructs a new resource from primitive args) but doesn't @@ -260,10 +276,18 @@ private MethodSpec generateResourceMethod(final String className, final BindgenF for (BindgenParameter p : method.getParameters()) { factory.addParameter(mapType(p.getType()), JavaNaming.toParameterName(p.getName())); } - factory.addStatement( - "throw new $T($S)", - UnsupportedOperationException.class, - "wasmos:host/embedder constructor dispatch not yet wired"); + if (spi) { + factory.addStatement( + "return $T.runtime().$L($L)", + registryClass(), + RuntimeProviderCodeGenerator.dispatchConstructorName(resourceWitName), + parameterForwardList(method)); + } else { + factory.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "wasmos:host/embedder constructor dispatch not yet wired"); + } return factory.build(); } final String name = JavaNaming.toMethodName(method.getName()); @@ -274,18 +298,87 @@ private MethodSpec generateResourceMethod(final String className, final BindgenF for (BindgenParameter p : method.getParameters()) { mb.addParameter(mapType(p.getType()), JavaNaming.toParameterName(p.getName())); } + final String dispatchName = + RuntimeProviderCodeGenerator.dispatchMethodName(resourceWitName, method.getName()); if (method.hasReturnType()) { mb.returns(mapType(method.getReturnType().get())); - mb.addStatement( - "throw new $T($S)", - UnsupportedOperationException.class, - "wasmos:host/embedder method dispatch not yet wired"); + if (spi) { + if (method.isStatic()) { + mb.addStatement( + "return $T.runtime().$L($L)", + registryClass(), + dispatchName, + parameterForwardList(method)); + } else { + mb.addStatement( + "return $T.runtime().$L(this.handle$L)", + registryClass(), + dispatchName, + parameterForwardListPrefixed(method)); + } + } else { + mb.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "wasmos:host/embedder method dispatch not yet wired"); + } } else { - mb.addStatement( - "throw new $T($S)", - UnsupportedOperationException.class, - "wasmos:host/embedder method dispatch not yet wired"); + if (spi) { + if (method.isStatic()) { + mb.addStatement( + "$T.runtime().$L($L)", + registryClass(), + dispatchName, + parameterForwardList(method)); + } else { + mb.addStatement( + "$T.runtime().$L(this.handle$L)", + registryClass(), + dispatchName, + parameterForwardListPrefixed(method)); + } + } else { + mb.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "wasmos:host/embedder method dispatch not yet wired"); + } } return mb.build(); } + + private boolean isSpiDispatchMode() { + final String spi = config.getRuntimeProviderName(); + return spi != null && !spi.isEmpty(); + } + + private ClassName registryClass() { + return ClassName.get(config.getPackageName(), config.getRuntimeProviderName() + "Registry"); + } + + private static String parameterForwardList(final BindgenFunction method) { + final StringBuilder sb = new StringBuilder(); + boolean first = true; + for (BindgenParameter p : method.getParameters()) { + if (!first) { + sb.append(", "); + } + sb.append(JavaNaming.toParameterName(p.getName())); + first = false; + } + return sb.toString(); + } + + /** + * Same as {@link #parameterForwardList} but always prefixed with a leading ", " so it can be + * concatenated after {@code this.handle} in the dispatch call site. Empty when the method takes + * no user-supplied parameters. + */ + private static String parameterForwardListPrefixed(final BindgenFunction method) { + final String list = parameterForwardList(method); + if (list.isEmpty()) { + return ""; + } + return ", " + list; + } } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/RuntimeProviderCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/RuntimeProviderCodeGenerator.java new file mode 100644 index 0000000..4d717aa --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/RuntimeProviderCodeGenerator.java @@ -0,0 +1,274 @@ +/* + * Copyright 2026 Tegmentum AI. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ai.tegmentum.webassembly4j.bindgen.generator; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenParameter; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; +import ai.tegmentum.webassembly4j.bindgen.util.JavaNaming; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import java.util.Objects; +import javax.lang.model.element.Modifier; + +/** + * Emits the bindgen-2.0 runtime-provider SPI: a {@code } interface with one dispatch method + * per resource method, plus a {@code Registry} class that stores and hands out the installed + * provider. + * + *

      Method-name convention: {@code } in camelCase, with a {@code long + * handle} parameter prepended to instance-method dispatch signatures. Constructor factories keep + * the resource-typed return so the SPI impl is the sole locus that knows how to wrap a + * runtime-issued raw handle in the generated class. See {@code docs/design-2.0.md}. + * + *

      Determinism: methods are emitted in the same order as the model's iteration (WIT declaration + * order), matching the convention already enforced by the resource generator. + */ +public final class RuntimeProviderCodeGenerator { + + private static final String CLOSE_METHOD_NAME = "close"; + + private final BindgenConfig config; + private final TypeMappingRegistry typeRegistry; + + /** + * Creates a new runtime-provider generator. + * + * @param config the bindgen configuration (must have a non-null {@code runtimeProviderName}) + */ + public RuntimeProviderCodeGenerator(final BindgenConfig config) { + this.config = Objects.requireNonNull(config, "config"); + if (config.getRuntimeProviderName() == null || config.getRuntimeProviderName().isEmpty()) { + throw new IllegalArgumentException( + "runtimeProviderName must be set to use RuntimeProviderCodeGenerator"); + } + this.typeRegistry = new TypeMappingRegistry(config.getCodeStyle(), config.getPackageName()); + } + + /** + * Generate the SPI interface for the supplied resource types. + * + * @param resources resource types to expose in the SPI (in declaration order) + * @return the generated interface source + */ + public GeneratedSource generateInterface(final List resources) { + final String spiName = config.getRuntimeProviderName(); + final TypeSpec.Builder ifaceBuilder = + TypeSpec.interfaceBuilder(spiName).addModifiers(Modifier.PUBLIC); + if (config.isGenerateJavadoc()) { + ifaceBuilder.addJavadoc( + "Runtime-provider SPI for the generated bindings.\n\n" + + "

      The embedder implements this interface once and installs the implementation\n" + + "via {@link $LRegistry#install($L)}. Generated resource method bodies dispatch\n" + + "through {@link $LRegistry#runtime()} to reach the installed provider.\n\n" + + "

      Emitted by webassembly4j-bindgen 2.0 (runtime-provider dispatch mode).\n", + spiName, spiName, spiName); + } + + for (final BindgenType resource : resources) { + emitResourceDispatchers(ifaceBuilder, resource); + } + + final JavaFile file = + JavaFile.builder(config.getPackageName(), ifaceBuilder.build()) + .skipJavaLangImports(true) + .indent(" ") + .build(); + return new GeneratedSource(file); + } + + /** + * Generate the {@code Registry} carrier class. + * + * @return the generated registry source + */ + public GeneratedSource generateRegistry() { + final String spiName = config.getRuntimeProviderName(); + final String registryName = spiName + "Registry"; + final ClassName spiType = ClassName.get(config.getPackageName(), spiName); + + final TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(registryName).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + if (config.isGenerateJavadoc()) { + classBuilder.addJavadoc( + "Single-slot registry for the {@link $L} runtime-provider SPI.\n\n" + + "

      Call {@link #install($L)} once from the embedder's startup path, before\n" + + "invoking any generated resource method. Generated bodies reach the installed\n" + + "provider via {@link #runtime()}, which throws {@link IllegalStateException} if\n" + + "nothing was installed.\n\n" + + "

      Emitted by webassembly4j-bindgen 2.0.\n", + spiName, spiName); + } + + // Private constructor. + classBuilder.addMethod( + MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build()); + + // volatile field. + classBuilder.addField( + com.squareup.javapoet.FieldSpec.builder( + spiType, "provider", Modifier.PRIVATE, Modifier.STATIC, Modifier.VOLATILE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("install") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(TypeName.VOID) + .addParameter(spiType, "provider") + .addStatement("$T.provider = $T.requireNonNull(provider, $S)", ClassName.get(config.getPackageName(), registryName), ClassName.get(Objects.class), "provider") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("uninstall") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(TypeName.VOID) + .addStatement("$T.provider = null", ClassName.get(config.getPackageName(), registryName)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("runtime") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(spiType) + .addStatement("$T r = provider", spiType) + .beginControlFlow("if (r == null)") + .addStatement( + "throw new $T($S)", + IllegalStateException.class, + spiName + + " not installed; call " + + registryName + + ".install(...) before invoking generated bindings") + .endControlFlow() + .addStatement("return r") + .build()); + + final JavaFile file = + JavaFile.builder(config.getPackageName(), classBuilder.build()) + .skipJavaLangImports(true) + .indent(" ") + .build(); + return new GeneratedSource(file); + } + + /** + * The camel-cased prefix used for every dispatch method on the resource. E.g. + * "runtime-instance" → "runtimeInstance". + */ + static String dispatchMethodPrefix(final String resourceWitName) { + return JavaNaming.toFieldName(resourceWitName); + } + + /** + * The SPI dispatch method name for a resource method. + * + *

      E.g. resource "runtime-instance" + method "call-export" → "runtimeInstanceCallExport". + */ + static String dispatchMethodName(final String resourceWitName, final String methodWitName) { + final String prefix = dispatchMethodPrefix(resourceWitName); + final String capMethod = JavaNaming.toClassName(methodWitName); + return prefix + capMethod; + } + + /** The SPI dispatch method name for a WIT constructor. */ + static String dispatchConstructorName(final String resourceWitName) { + return dispatchMethodPrefix(resourceWitName) + "Create"; + } + + /** The SPI dispatch method name for the intrinsic {@code close()}. */ + static String dispatchCloseName(final String resourceWitName) { + return dispatchMethodPrefix(resourceWitName) + "Close"; + } + + private void emitResourceDispatchers(final TypeSpec.Builder ifaceBuilder, final BindgenType resource) { + final String resourceWit = resource.getName(); + final ClassName resourceType = + ClassName.get(config.getPackageName(), JavaNaming.toClassName(resourceWit)); + + for (final BindgenFunction method : resource.getResourceMethods()) { + if (method.isConstructor()) { + ifaceBuilder.addMethod(dispatchForConstructor(resourceWit, method, resourceType)); + } else { + ifaceBuilder.addMethod(dispatchForMethod(resourceWit, method)); + } + } + + // Always emit a close dispatcher for every resource, even if the WIT declared + // no explicit close() method — every generated resource has an + // AutoCloseable.close() body that dispatches through the SPI. + ifaceBuilder.addMethod( + MethodSpec.methodBuilder(dispatchCloseName(resourceWit)) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(TypeName.VOID) + .addParameter(TypeName.LONG, "handle") + .build()); + } + + private MethodSpec dispatchForConstructor( + final String resourceWit, final BindgenFunction ctor, final ClassName resourceType) { + final MethodSpec.Builder mb = + MethodSpec.methodBuilder(dispatchConstructorName(resourceWit)) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(resourceType); + for (final BindgenParameter p : ctor.getParameters()) { + mb.addParameter(mapType(p.getType()), JavaNaming.toParameterName(p.getName())); + } + return mb.build(); + } + + private MethodSpec dispatchForMethod(final String resourceWit, final BindgenFunction method) { + final MethodSpec.Builder mb = + MethodSpec.methodBuilder(dispatchMethodName(resourceWit, method.getName())) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT); + if (!method.isStatic()) { + mb.addParameter(TypeName.LONG, "handle"); + } + for (final BindgenParameter p : method.getParameters()) { + mb.addParameter(mapType(p.getType()), JavaNaming.toParameterName(p.getName())); + } + if (method.hasReturnType()) { + mb.returns(mapType(method.getReturnType().get())); + } else { + mb.returns(TypeName.VOID); + } + return mb.build(); + } + + private TypeName mapType(final BindgenType type) { + switch (type.getKind()) { + case PRIMITIVE: + return typeRegistry.mapWitPrimitive(type.getName()); + case LIST: + return typeRegistry.mapList(mapType(type.getElementType().get())); + case OPTION: + return typeRegistry.mapOption(mapType(type.getElementType().get())); + case RESULT: + final TypeName ok = type.getOkType().map(this::mapType).orElse(null); + final TypeName err = type.getErrorType().map(this::mapType).orElse(null); + return typeRegistry.mapResult(ok, err); + case REFERENCE: + return typeRegistry.mapGeneratedType(type.getReferencedTypeName().get()); + default: + return typeRegistry.mapGeneratedType(type.getName()); + } + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderSpiDispatchTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderSpiDispatchTest.java new file mode 100644 index 0000000..d39f064 --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosEmbedderSpiDispatchTest.java @@ -0,0 +1,276 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wasmos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.CodeGenerator; +import ai.tegmentum.webassembly4j.bindgen.CodeStyle; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Bindgen 2.0 SPI-dispatch shape check for the {@code wasmos:host/embedder@0.1.0} WIT world. + * + *

      Verifies that when {@code runtimeProviderName} is set, the generator: + * + *

        + *
      1. Emits a new {@code EmbedderRuntime} interface + {@code EmbedderRuntimeRegistry} class in + * the same target package. + *
      2. Rewrites every resource method body to dispatch through the registry (no more {@code throw + * new UnsupportedOperationException}). + *
      3. Still compiles cleanly through the platform javac. + *
      4. Produces byte-identical output across two consecutive runs (determinism). + *
      + */ +@DisplayName("bindgen 2.0 SPI dispatch: wasmos:host/embedder@0.1.0") +class WasmosEmbedderSpiDispatchTest { + + private static final String WIT_RESOURCE = "wasmos-embedder/embedder.wit"; + private static final String PACKAGE = "ai.tegmentum.wasmos.embedder.generated"; + private static final String SPI_NAME = "EmbedderRuntime"; + + private static Path witFixture; + + @BeforeAll + static void locateFixture() throws URISyntaxException { + final URL resource = + WasmosEmbedderSpiDispatchTest.class.getClassLoader().getResource(WIT_RESOURCE); + assertNotNull(resource, "fixture " + WIT_RESOURCE + " missing from test resources"); + witFixture = Path.of(URLDecoder.decode(resource.toURI().getPath(), StandardCharsets.UTF_8)); + } + + @Test + @DisplayName("SPI mode emits interface + registry and dispatches resource bodies") + void spiModeGeneratesDispatchingSurface(@TempDir Path outputDir) throws Exception { + final BindgenConfig config = + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(outputDir) + .codeStyle(CodeStyle.MODERN) + .addWitSource(witFixture) + .generateImplementations(false) + .generateServiceLoader(false) + .runtimeProviderName(SPI_NAME) + .build(); + + final CodeGenerator generator = new CodeGenerator(config); + final List sources = generator.generate(); + final Map byClass = toSourceMap(sources); + + // SPI interface + registry both present. + assertTrue(byClass.containsKey(SPI_NAME), SPI_NAME + " interface must be generated"); + assertTrue( + byClass.containsKey(SPI_NAME + "Registry"), + SPI_NAME + "Registry class must be generated"); + + final String iface = byClass.get(SPI_NAME); + assertTrue( + iface.contains("public interface " + SPI_NAME), + SPI_NAME + " must be a public interface"); + // Method-per-resource-method: HostProvider constructor + close. + assertTrue(iface.contains("HostProvider hostProviderCreate("), "SPI missing hostProviderCreate"); + assertTrue(iface.contains("void hostProviderClose(long handle)"), "SPI missing hostProviderClose"); + // Method-per-resource-method: RuntimeInstance static + instance methods + close. + assertTrue( + iface.contains("runtimeInstanceInstantiate("), "SPI missing runtimeInstanceInstantiate"); + assertTrue( + iface.contains("runtimeInstanceCallExport(long handle,"), + "SPI missing runtimeInstanceCallExport(long, ...)"); + assertTrue( + iface.contains("runtimeInstanceIntrospect(long handle)"), + "SPI missing runtimeInstanceIntrospect(long)"); + assertTrue( + iface.contains("runtimeInstanceVerifyWorld(long handle,"), + "SPI missing runtimeInstanceVerifyWorld(long, ...)"); + assertTrue( + iface.contains("void runtimeInstanceClose(long handle)"), + "SPI missing runtimeInstanceClose(long)"); + + final String registry = byClass.get(SPI_NAME + "Registry"); + assertTrue( + registry.contains("public static void install(" + SPI_NAME + " provider)"), + "Registry missing install(SPI)"); + assertTrue( + registry.contains("public static " + SPI_NAME + " runtime()"), + "Registry missing runtime()"); + assertTrue(registry.contains("public static void uninstall()"), "Registry missing uninstall()"); + assertTrue( + registry.contains("private static volatile"), + "Registry provider field must be private static volatile"); + + // RuntimeInstance dispatches, doesn't throw. + final String rti = byClass.get("RuntimeInstance"); + assertNotNull(rti, "RuntimeInstance still emitted in SPI mode"); + assertFalse( + rti.contains("throw new UnsupportedOperationException"), + "RuntimeInstance must not throw UnsupportedOperationException in SPI mode:\n" + rti); + assertTrue( + rti.contains(SPI_NAME + "Registry.runtime().runtimeInstanceInstantiate("), + "RuntimeInstance.instantiate must dispatch through registry"); + assertTrue( + rti.contains(SPI_NAME + "Registry.runtime().runtimeInstanceCallExport(this.handle,"), + "RuntimeInstance.callExport must pass this.handle to registry"); + assertTrue( + rti.contains(SPI_NAME + "Registry.runtime().runtimeInstanceClose(this.handle)"), + "RuntimeInstance.close must dispatch through registry"); + + // HostProvider constructor factory dispatches, doesn't throw. + final String hp = byClass.get("HostProvider"); + assertNotNull(hp, "HostProvider still emitted in SPI mode"); + assertFalse( + hp.contains("throw new UnsupportedOperationException"), + "HostProvider must not throw UnsupportedOperationException in SPI mode:\n" + hp); + assertTrue( + hp.contains(SPI_NAME + "Registry.runtime().hostProviderCreate("), + "HostProvider.create must dispatch through registry"); + assertTrue( + hp.contains(SPI_NAME + "Registry.runtime().hostProviderClose(this.handle)"), + "HostProvider.close must dispatch through registry"); + // SPI-mode contract: even for resources with a WIT constructor, the + // handle-holding ctor must be public so an out-of-package SPI impl can + // construct wrapped resources. Legacy mode makes it protected. + assertTrue( + hp.contains("public HostProvider(long handle)"), + "HostProvider(long) must be public in SPI mode for out-of-package impls"); + + // Determinism: same input, same generated content. + final CodeGenerator generator2 = new CodeGenerator(config); + final Map secondPass = toSourceMap(generator2.generate()); + assertEquals(byClass.keySet(), secondPass.keySet(), "class set must be deterministic"); + for (final String className : byClass.keySet()) { + assertEquals( + byClass.get(className), secondPass.get(className), "content drift in " + className); + } + + // Compile the whole SPI-mode surface with platform javac. + for (final GeneratedSource src : sources) { + src.writeTo(outputDir); + } + compileGeneratedSurface(outputDir); + } + + @Test + @DisplayName("legacy mode (no runtimeProviderName) still emits throwing bodies") + void legacyModeUnchanged(@TempDir Path outputDir) throws Exception { + final BindgenConfig config = + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(outputDir) + .codeStyle(CodeStyle.MODERN) + .addWitSource(witFixture) + .generateImplementations(false) + .generateServiceLoader(false) + .build(); + + final CodeGenerator generator = new CodeGenerator(config); + final Map byClass = toSourceMap(generator.generate()); + assertFalse(byClass.containsKey(SPI_NAME), "SPI interface must NOT emit in legacy mode"); + assertFalse( + byClass.containsKey(SPI_NAME + "Registry"), + "SPI registry must NOT emit in legacy mode"); + assertTrue( + byClass.get("RuntimeInstance").contains("throw new UnsupportedOperationException"), + "Legacy RuntimeInstance bodies must still throw"); + } + + private static Map toSourceMap(final List sources) { + final Map map = new HashMap<>(); + for (final GeneratedSource src : sources) { + map.put(src.getClassName(), src.getContent()); + } + return map; + } + + private static void compileGeneratedSurface(final Path outputDir) throws Exception { + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "No JavaCompiler available on the running JDK."); + + final List javaFiles = new ArrayList<>(); + try (Stream paths = Files.walk(outputDir)) { + paths.filter(p -> p.toString().endsWith(".java")).forEach(p -> javaFiles.add(p.toFile())); + } + if (javaFiles.isEmpty()) { + fail("No generated .java files found under " + outputDir); + } + + final Path classDir = outputDir.resolve("classes"); + Files.createDirectories(classDir); + + final String cp = System.getProperty("java.class.path"); + final List options = new ArrayList<>(); + options.add("-d"); + options.add(classDir.toString()); + options.add("-source"); + options.add("11"); + options.add("-target"); + options.add("11"); + options.add("-classpath"); + options.add(cp); + + final ByteArrayOutputStream errBuf = new ByteArrayOutputStream(); + final StringWriter diagnosticSink = new StringWriter(); + final int rc; + try (var fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + final Iterable compilationUnits = + fileManager.getJavaFileObjectsFromFiles(javaFiles); + rc = + compiler + .getTask( + new PrintWriter(diagnosticSink), + fileManager, + null, + options, + null, + compilationUnits) + .call() + ? 0 + : 1; + } + if (rc != 0) { + fail( + "javac failed on SPI-mode generated surface:\n" + + diagnosticSink + + "\n" + + errBuf.toString(StandardCharsets.UTF_8)); + } + } +} From 8730c7e1036bee33d10fb6447b6d7cf50ddc42c7 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 25 Jul 2026 07:24:03 -0400 Subject: [PATCH 49/85] feat(bindgen): real Canonical-ABI discriminant dispatch for result returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the whole-body UnsupportedOperationException stub in ImplementationCodeGenerator's marshalling path for result returns with a real Canonical-ABI discriminant lift. Generated method bodies now read the u8 tag byte at the retptr, branch on 0 (ok) vs 1 (err), lift the payload from the aligned payload slot, and return the appropriate side of WitResult. Layout follows the Canonical ABI's variant rule: 1-byte discriminant, payload aligned to max(align(T), align(E)), total size rounded up to the payload alignment. Retptr allocation is now sized per return type via MarshallingStrategy.retptrSize / retptrAlignment so result gets enough room for its payload (the historical hard-coded allocate(8, 4) was undersized for anything larger than an i64). Ok/err payload types with a wired in-memory lifter (primitives, string, list, unit) generate real read expressions; payloads without a lifter yet (records, references, options, nested results) throw a specific-tag UnsupportedOperationException inside the corresponding arm. The dispatch shape is real; per-arm marshalling gaps are honest and self-documenting. New MemoryReader.readU8(int) returns 0..255 — the natural shape for a Canonical-ABI discriminant read. Added alongside the existing readBool / readI32 / readI64 / readF32 / readF64 family. Test coverage: - ImplementationCodeGeneratorBugsTest.resultReturnEmitsCompileableBody now asserts the real dispatch shape (readU8, if (tag$ == 0), ok-arm readString, err-arm REFERENCE-payload throw). - New resultReturnWithUnitOkPayload: result<_, error> emits WitResult.ok(null) in the ok arm. - New resultReturnPrimitiveOkPayload: result lifts the payload via readI32 at the aligned offset. - Stub surface in the bugs test extended with readU8 and the remaining reader methods so javac resolves every symbol the dispatch emits. Test count: 474 -> 476 in webassembly4j-bindgen. --- .../ImplementationCodeGenerator.java | 44 +- .../generator/MarshallingStrategy.java | 413 ++++++++++++++++++ .../ImplementationCodeGeneratorBugsTest.java | 110 ++++- .../runtime/marshal/MemoryReader.java | 16 + 4 files changed, 548 insertions(+), 35 deletions(-) diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java index b20d2ab..0842200 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java @@ -337,25 +337,8 @@ private void generateDirectBody(MethodSpec.Builder method, BindgenFunction funct private void generateMarshallingBody(MethodSpec.Builder method, BindgenFunction function, String fieldName, TypeName returnType, boolean hasReturn) { BindgenType returnBindgenType = hasReturn ? function.getReturnType().get() : null; - - // TODO(bindgen): result return marshalling requires a Canonical-ABI - // discriminant lift (tag byte followed by either ok- or err-payload - // slots) that MarshallingStrategy#liftReturn doesn't emit today — the - // default arm of that switch produces the literal `/* TODO: lift RESULT */`, - // which is a comment, not an expression, so `return ;` will not - // compile. Until the discriminant lift lands (ADR-005 resource/result - // codegen), emit a throw so the class stays compileable and the impl can - // be wired up incrementally. Parameters, return type, and the resource / - // interface shape are correct; only the body is a placeholder. - if (returnBindgenType != null && returnBindgenType.getKind() == BindgenType.Kind.RESULT) { - method.addStatement( - "throw new $T($S)", - UnsupportedOperationException.class, - "TODO(bindgen): result marshalling not yet wired for " - + function.getName() - + " — see MarshallingStrategy#liftReturn"); - return; - } + final boolean isResultReturn = + returnBindgenType != null && returnBindgenType.getKind() == BindgenType.Kind.RESULT; boolean complexReturn = MarshallingStrategy.requiresMarshalling(returnBindgenType); @@ -381,9 +364,17 @@ private void generateMarshallingBody(MethodSpec.Builder method, BindgenFunction method.addStatement("java.util.List $L = new java.util.ArrayList<>()", argsLocal); - // Allocate return pointer for complex returns + // Allocate return pointer for complex returns. For result the + // Canonical-ABI layout is `[tag: u8][pad][payload]` — payload alignment + // is max(align(T), align(E)); total size = payloadOffset + max(size(T), + // size(E)) rounded up to alignment. Falling back to the historical + // (size=8, align=4) reservation for other complex returns is safe for + // (ptr, len) pairs, i64 primitives, and single u32 handles — the + // pre-refactor shape. if (complexReturn) { - method.addStatement("int $L = marshal.allocator().allocate(8, 4)", retptrLocal); + final int retSize = MarshallingStrategy.retptrSize(returnBindgenType); + final int retAlign = MarshallingStrategy.retptrAlignment(returnBindgenType); + method.addStatement("int $L = marshal.allocator().allocate($L, $L)", retptrLocal, retSize, retAlign); method.addStatement("$L.add($L)", argsLocal, retptrLocal); } @@ -408,7 +399,16 @@ private void generateMarshallingBody(MethodSpec.Builder method, BindgenFunction // Lift return if (hasReturn) { - if (complexReturn) { + if (isResultReturn) { + // Canonical-ABI discriminant lift: read tag byte at retptr, branch + // on 0 (ok) vs 1 (err), lift the payload from the aligned payload + // slot, and return a WitResult of the appropriate side. Payload + // types that lack an in-memory lifter throw an + // UnsupportedOperationException in that arm — the dispatch shape is + // real; per-arm marshalling gaps are honest. + method.addCode( + MarshallingStrategy.resultDispatch(returnBindgenType, retptrLocal, function.getName())); + } else if (complexReturn) { CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, retptrLocal); method.addStatement("return $L", liftCode); } else { diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java index 024d6f1..9a1fd2f 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java @@ -19,6 +19,7 @@ import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; +import java.util.Optional; /** * Maps bindgen types to JavaPoet code fragments for lowering (Java to WASM) @@ -28,6 +29,8 @@ public final class MarshallingStrategy { private static final ClassName STRING_CODEC = ClassName.get("ai.tegmentum.webassembly4j.runtime.marshal", "StringCodec"); + private static final ClassName WIT_RESULT_CLASS = + ClassName.get("ai.tegmentum.webassembly4j.bindgen.wit", "WitResult"); private MarshallingStrategy() { } @@ -150,6 +153,416 @@ public static CodeBlock liftReturn(BindgenType type, String valueVar) { } } + /** + * Emit an expression that lifts a Canonical-ABI-encoded {@code type} from a + * concrete linear-memory offset expression. Returns {@link Optional#empty()} + * when the payload kind has no in-memory lifter wired up yet (records, + * variants, references, options, nested results). The caller is expected to + * fall back to a statement-level placeholder (an + * {@link UnsupportedOperationException} throw) in that case rather than + * splicing a comment into an expression position. + * + * @param type the bindgen type of the value to lift + * @param offsetExpr a Java expression whose value is the byte offset in + * linear memory where the value's Canonical-ABI representation begins + * @return the lifted-value expression, or empty when unsupported + */ + public static Optional liftFromOffset(final BindgenType type, final String offsetExpr) { + if (type == null) { + return Optional.empty(); + } + switch (type.getKind()) { + case PRIMITIVE: + return Optional.of(liftPrimitiveFromOffset(type, offsetExpr)); + case LIST: + // Canonical ABI encodes list as (ptr, len). readBytes assumes u8; + // that's fine for the wasmos world's list uses but would need + // per-element lifting for list. Guardrail-conservative: bail + // to the fallback throw when the element isn't u8-shaped so we don't + // silently mis-lift. + { + final BindgenType element = type.getElementType().orElse(null); + if (element != null + && element.getKind() == BindgenType.Kind.PRIMITIVE + && isByteWidePrimitive(element.getName())) { + return Optional.of(CodeBlock.of("marshal.reader().readBytes($L)", offsetExpr)); + } + return Optional.empty(); + } + case ENUM: + case FLAGS: + case RECORD: + case VARIANT: + case OPTION: + case RESULT: + case TUPLE: + case REFERENCE: + case RESOURCE: + case FUNCTION: + default: + return Optional.empty(); + } + } + + /** + * Emit the dispatch statements that lift a {@code result} return + * whose Canonical-ABI representation lives at {@code retptrLocal}. The + * emitted block reads the discriminant byte (0 = ok, 1 = err), branches, + * and returns a {@link ai.tegmentum.webassembly4j.bindgen.wit.WitResult} + * of the appropriate side. + * + *

      Layout follows the Canonical ABI's variant rule for a 2-case variant: + * + *

      +   *   [tag: u8][pad ...][payload: max(size(T), size(E))]
      +   * 
      + * + * where {@code payload} is aligned to {@code max(align(T), align(E))} and + * the tag/pad total up to that alignment. + * + *

      When a payload type has no in-memory lifter wired up (records, + * references, options, nested results — see {@link #liftFromOffset}), the + * corresponding arm emits an {@link UnsupportedOperationException} throw + * with a specific TODO tag. The dispatch shape is real; the payload gap + * is honest. + * + * @param resultType the RESULT-kinded bindgen return type + * @param retptrLocal the Java identifier that holds the retptr int + * @param methodName the outer method's WIT name — folded into the TODO + * message so post-mortems don't have to grep across the generator + * @return statements that discharge the return with a real dispatch + */ + public static CodeBlock resultDispatch( + final BindgenType resultType, final String retptrLocal, final String methodName) { + if (resultType == null || resultType.getKind() != BindgenType.Kind.RESULT) { + throw new IllegalArgumentException( + "resultDispatch requires a RESULT-kinded bindgen type, got: " + resultType); + } + final BindgenType okType = resultType.getOkType().orElse(null); + final BindgenType errType = resultType.getErrorType().orElse(null); + final int payloadOffset = resultPayloadOffset(okType, errType); + // Unique local for the discriminant tag. The outer generator already + // guarantees `retptrLocal` doesn't collide with a WIT parameter; the + // tag local piggy-backs on the same convention (`$` cannot appear in a + // JavaNaming-produced parameter name). + final String tagLocal = "tag$"; + final CodeBlock.Builder out = CodeBlock.builder(); + out.addStatement("int $L = marshal.reader().readU8($L)", tagLocal, retptrLocal); + out.beginControlFlow("if ($L == 0)", tagLocal); + emitArm(out, okType, retptrLocal, payloadOffset, methodName, /* ok= */ true); + out.nextControlFlow("else"); + emitArm(out, errType, retptrLocal, payloadOffset, methodName, /* ok= */ false); + out.endControlFlow(); + return out.build(); + } + + /** + * Return the linear-memory allocation size (bytes) an in-memory retptr for + * {@code returnType} must reserve. Callers use this to size the + * {@code allocator().allocate(size, align)} pre-call step so the payload + * fits under the Canonical-ABI layout. Types with no computed layout get + * a conservative 8-byte fallback that matches the pre-refactor default. + */ + public static int retptrSize(final BindgenType returnType) { + if (returnType == null) { + return 8; + } + if (returnType.getKind() == BindgenType.Kind.RESULT) { + final BindgenType ok = returnType.getOkType().orElse(null); + final BindgenType err = returnType.getErrorType().orElse(null); + final int payloadOffset = resultPayloadOffset(ok, err); + final int payloadSize = Math.max(sizeOf(ok), sizeOf(err)); + final int total = payloadOffset + payloadSize; + // Round up to alignment so downstream lifts don't read past the slot. + final int align = resultAlignment(ok, err); + return alignUp(total, align); + } + // Non-result complex returns keep the historical 8-byte reservation. + return 8; + } + + /** + * Return the alignment (in bytes) an in-memory retptr for {@code returnType} + * must satisfy. Matches {@link #retptrSize}'s sizing choices. + */ + public static int retptrAlignment(final BindgenType returnType) { + if (returnType == null) { + return 4; + } + if (returnType.getKind() == BindgenType.Kind.RESULT) { + final BindgenType ok = returnType.getOkType().orElse(null); + final BindgenType err = returnType.getErrorType().orElse(null); + return resultAlignment(ok, err); + } + return 4; + } + + private static void emitArm( + final CodeBlock.Builder out, + final BindgenType payloadType, + final String retptrLocal, + final int payloadOffset, + final String methodName, + final boolean okArm) { + final String factory = okArm ? "ok" : "err"; + if (payloadType == null) { + // Unit payload — WitResult..ok(null) / err(null). The + // generator's inferred WitResult / type parameters + // pick up on the null. + out.addStatement("return $T.$L(null)", WIT_RESULT_CLASS, factory); + return; + } + final String offsetExpr = + payloadOffset == 0 ? retptrLocal : "(" + retptrLocal + " + " + payloadOffset + ")"; + final Optional lifted = liftFromOffset(payloadType, offsetExpr); + if (lifted.isPresent()) { + out.addStatement("return $T.$L($L)", WIT_RESULT_CLASS, factory, lifted.get()); + return; + } + // Payload has no in-memory lifter yet. Throw so the class compiles and + // the specific gap is discoverable via the exception message. + out.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "TODO(bindgen): " + + factory + + "-arm payload lift for " + + payloadType.getKind() + + " (" + + payloadType.getName() + + ") not yet wired in result for " + + methodName + + " — see MarshallingStrategy#liftFromOffset"); + } + + /** + * Byte offset of the payload slot within a Canonical-ABI-encoded + * {@code result}. Discriminant is a single byte at offset 0; payload + * is aligned to {@code max(align(T), align(E), 1)}. + */ + static int resultPayloadOffset(final BindgenType okType, final BindgenType errType) { + final int align = resultAlignment(okType, errType); + return alignUp(1, align); + } + + static int resultAlignment(final BindgenType okType, final BindgenType errType) { + return Math.max(1, Math.max(alignOf(okType), alignOf(errType))); + } + + private static int alignUp(final int value, final int alignment) { + if (alignment <= 1) { + return value; + } + return (value + alignment - 1) & ~(alignment - 1); + } + + /** + * Canonical-ABI {@code alignment} of {@code type} in bytes. Conservative for + * unknown / not-yet-implemented shapes: returns 4, matching the historical + * retptr alignment and safe for both handles and (ptr, len) pairs. + */ + static int alignOf(final BindgenType type) { + if (type == null) { + return 1; + } + switch (type.getKind()) { + case PRIMITIVE: + return primitiveAlign(type.getName()); + case ENUM: + // ENUM discriminant width follows discriminant_size(n_cases). Data- + // free layout so alignment == size. + return enumDiscriminantWidth(type); + case FLAGS: + // FLAGS pack into 1/2/4-byte ints depending on flag count. + return flagsWidth(type); + case LIST: + case RESOURCE: + case REFERENCE: + // list = (ptr:i32, len:i32), align 4. Resource handles + opaque + // references also lift as i32. + return 4; + case OPTION: + case RESULT: + case RECORD: + case VARIANT: + case TUPLE: + case FUNCTION: + default: + return 4; + } + } + + /** + * Canonical-ABI {@code size} of {@code type} in bytes. Conservative for + * unknown / not-yet-implemented shapes: returns 8, which is safe for both + * (ptr, len) pairs and 64-bit primitives. + */ + static int sizeOf(final BindgenType type) { + if (type == null) { + return 0; + } + switch (type.getKind()) { + case PRIMITIVE: + return primitiveSize(type.getName()); + case ENUM: + return enumDiscriminantWidth(type); + case FLAGS: + return flagsWidth(type); + case LIST: + case RESOURCE: + case REFERENCE: + return type.getKind() == BindgenType.Kind.LIST ? 8 : 4; + case OPTION: + case RESULT: + case RECORD: + case VARIANT: + case TUPLE: + case FUNCTION: + default: + return 8; + } + } + + private static int primitiveAlign(final String name) { + switch (name.toLowerCase()) { + case "bool": + case "s8": + case "u8": + return 1; + case "s16": + case "u16": + return 2; + case "s32": + case "u32": + case "i32": + case "f32": + case "float32": + case "char": + return 4; + case "s64": + case "u64": + case "i64": + case "f64": + case "float64": + return 8; + case "string": + return 4; // (ptr:i32, len:i32) + default: + return 4; + } + } + + private static int primitiveSize(final String name) { + switch (name.toLowerCase()) { + case "bool": + case "s8": + case "u8": + return 1; + case "s16": + case "u16": + return 2; + case "s32": + case "u32": + case "i32": + case "f32": + case "float32": + case "char": + return 4; + case "s64": + case "u64": + case "i64": + case "f64": + case "float64": + return 8; + case "string": + return 8; // (ptr:i32, len:i32) + default: + return 4; + } + } + + private static int enumDiscriminantWidth(final BindgenType type) { + final int n = + type.getEnumValues() != null && !type.getEnumValues().isEmpty() + ? type.getEnumValues().size() + : 2; + if (n <= 256) { + return 1; + } + if (n <= 65536) { + return 2; + } + return 4; + } + + private static int flagsWidth(final BindgenType type) { + final int n = + type.getEnumValues() != null && !type.getEnumValues().isEmpty() + ? type.getEnumValues().size() + : 1; + if (n <= 8) { + return 1; + } + if (n <= 16) { + return 2; + } + return 4; + } + + private static boolean isByteWidePrimitive(final String name) { + if (name == null) { + return false; + } + switch (name.toLowerCase()) { + case "s8": + case "u8": + return true; + default: + return false; + } + } + + /** + * Generates code to lift a primitive value from an in-memory offset. Used by + * the result-dispatch generator when the ok/err payload is a primitive. + */ + private static CodeBlock liftPrimitiveFromOffset(final BindgenType type, final String offsetExpr) { + final String name = type.getName().toLowerCase(); + switch (name) { + case "bool": + return CodeBlock.of("marshal.reader().readBool($L)", offsetExpr); + case "s8": + case "u8": + return CodeBlock.of("(byte) marshal.reader().readU8($L)", offsetExpr); + case "s16": + case "u16": + // readI16 doesn't exist yet; sign-extend from a 32-bit read of the + // low 4 bytes. Correct only when the store side zero-extended (u16) + // or sign-extended (s16); Canonical ABI does exactly that for 16-bit + // slots in variant payloads. + return CodeBlock.of("(short) marshal.reader().readI32($L)", offsetExpr); + case "s32": + case "u32": + case "i32": + case "char": + return CodeBlock.of("marshal.reader().readI32($L)", offsetExpr); + case "s64": + case "u64": + case "i64": + return CodeBlock.of("marshal.reader().readI64($L)", offsetExpr); + case "f32": + case "float32": + return CodeBlock.of("marshal.reader().readF32($L)", offsetExpr); + case "f64": + case "float64": + return CodeBlock.of("marshal.reader().readF64($L)", offsetExpr); + case "string": + return CodeBlock.of("marshal.reader().readString($L)", offsetExpr); + default: + return CodeBlock.of("marshal.reader().readI32($L)", offsetExpr); + } + } + /** * Generates code to lift a primitive WASM return value. * diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java index 108a532..e7b3a19 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java @@ -160,11 +160,13 @@ void paramNamedResultDoesNotCollide() throws Exception { @Test @DisplayName( - "bug 2: a result return no longer emits a comment where an expression must go") + "bug 2: a result return dispatches on the Canonical-ABI discriminant") void resultReturnEmitsCompileableBody() throws Exception { // call-export: func(name: string) -> result - // We use string on both sides so we don't depend on the list - // lowering bug (out of scope for this round). + // The ok arm is a `string` (in-memory lifter wired); the err arm is a + // record reference — payload lifting for records isn't in this cut, so + // that arm falls back to a specific-tag throw. The dispatch shape is + // real. final BindgenInterface iface = BindgenInterface.builder() .name("runtime-instance") @@ -185,17 +187,29 @@ void resultReturnEmitsCompileableBody() throws Exception { assertFalse( content.contains("return /* TODO: lift RESULT */"), "result body must not emit a bare TODO comment in return position:\n" + content); - // The honest placeholder for the currently-unwired discriminant lift is - // an UnsupportedOperationException throw — compileable, obviously not a - // working runtime path, easy to swap when the marshalling lands. + // The previous placeholder was a whole-body throw. Real dispatch has an + // if/else on the tag; the whole-body throw is gone. + assertFalse( + content.contains("TODO(bindgen): result"), + "expected the whole-body TODO throw to be replaced by real dispatch:\n" + content); + // Canonical-ABI discriminant read from the retptr, then branch on it. assertTrue( - content.contains("throw new UnsupportedOperationException"), - "expected an UnsupportedOperationException throw as the placeholder body:\n" + content); + content.contains("marshal.reader().readU8("), + "expected a u8 discriminant read from the retptr:\n" + content); assertTrue( - content.contains("TODO(bindgen): result"), - "expected the throw message to reference the ADR-005 gap:\n" + content); - // The method signature is still generated correctly — only the body - // changed. WitResult is what the type map produces here. + content.contains("if (tag$ == 0)"), + "expected an `if (tag$ == 0)` dispatch on the discriminant:\n" + content); + // Ok arm lifts a string from `retptr + payloadOffset` (payload alignment + // for `string` is 4, so payload offset is 4). + assertTrue( + content.contains("WitResult.ok(marshal.reader().readString"), + "expected the ok arm to lift a string via readString:\n" + content); + // Err arm can't lift a record payload today; specific-tag throw + // documents the gap without pretending the arm works. + assertTrue( + content.contains("err-arm payload lift for REFERENCE"), + "expected the err arm to throw with a REFERENCE-payload TODO:\n" + content); + // The method signature is still generated correctly. assertTrue( content.contains("public WitResult callExport"), "expected the WitResult return type on callExport:\n" + content); @@ -203,6 +217,67 @@ void resultReturnEmitsCompileableBody() throws Exception { compileImpl(iface, impl); } + @Test + @DisplayName( + "bug 2 (companion): a result<_, E> return returns WitResult.ok(null) in the ok arm") + void resultReturnWithUnitOkPayload() throws Exception { + // verify-world: func(expected-wit: string) -> result<_, error> + // Ok payload is unit; the ok arm returns WitResult.ok(null). Err arm + // still falls back to a REFERENCE-payload throw. + final BindgenInterface iface = + BindgenInterface.builder() + .name("runtime-instance") + .addFunction( + BindgenFunction.builder() + .name("verify-world") + .addParameter("expected-wit", BindgenType.primitive("string")) + .returnType(BindgenType.result(null, BindgenType.reference("error"))) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + assertTrue( + content.contains("return WitResult.ok(null)"), + "expected the unit-ok arm to return WitResult.ok(null):\n" + content); + assertTrue( + content.contains("public WitResult verifyWorld"), + "expected the WitResult return type on verifyWorld:\n" + content); + + compileImpl(iface, impl); + } + + @Test + @DisplayName( + "bug 2 (companion): a result return lifts the ok arm from the payload offset") + void resultReturnPrimitiveOkPayload() throws Exception { + // Simplest ok-arm shape: primitive payload lifts via a bare + // readI32 at the aligned payload offset. Unit err arm returns null. + final BindgenInterface iface = + BindgenInterface.builder() + .name("counter") + .addFunction( + BindgenFunction.builder() + .name("current") + .addParameter("name", BindgenType.primitive("string")) + .returnType(BindgenType.result(BindgenType.primitive("i32"), null)) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + assertTrue( + content.contains("WitResult.ok(marshal.reader().readI32"), + "expected the ok arm to lift an int via readI32:\n" + content); + assertTrue( + content.contains("return WitResult.err(null)"), + "expected the unit-err arm to return WitResult.err(null):\n" + content); + + compileImpl(iface, impl); + } + // ------------------------------------------------------------------- // javac harness — compile the generated Impl against a hand-written stub // of the runtime surface it references, so the test can prove @@ -215,7 +290,10 @@ void resultReturnEmitsCompileableBody() throws Exception { "\n", "package " + PACKAGE + ";", "import java.util.Optional;", - "// api package stubs — signatures cribbed from webassembly4j-api.", + "// api package stubs — signatures cribbed from webassembly4j-api and", + "// webassembly4j-runtime. Every symbol the generator names must exist", + "// here or javac drops the whole test. New reader methods land here", + "// alongside the generator changes that reference them.", "class ApiStubs {", " public interface Instance {", " Optional function(String name);", @@ -228,6 +306,12 @@ void resultReturnEmitsCompileableBody() throws Exception { " public static class MemoryReader {", " public String readString(int offset) { return null; }", " public byte[] readBytes(int offset) { return null; }", + " public int readU8(int offset) { return 0; }", + " public boolean readBool(int offset) { return false; }", + " public int readI32(int offset) { return 0; }", + " public long readI64(int offset) { return 0L; }", + " public float readF32(int offset) { return 0f; }", + " public double readF64(int offset) { return 0d; }", " }", " public static class MarshalContext {", " public static MarshalContext fromInstance(Instance i) { return new MarshalContext(); }", diff --git a/webassembly4j-runtime/src/main/java/ai/tegmentum/webassembly4j/runtime/marshal/MemoryReader.java b/webassembly4j-runtime/src/main/java/ai/tegmentum/webassembly4j/runtime/marshal/MemoryReader.java index b3789e7..9e590de 100644 --- a/webassembly4j-runtime/src/main/java/ai/tegmentum/webassembly4j/runtime/marshal/MemoryReader.java +++ b/webassembly4j-runtime/src/main/java/ai/tegmentum/webassembly4j/runtime/marshal/MemoryReader.java @@ -42,6 +42,22 @@ public boolean readBool(int offset) { return scratchArray[0] != 0; } + /** + * Reads an unsigned 8-bit integer from the given offset. + * + *

      Returned as an {@code int} in the range {@code 0..255} — the natural + * shape for a Canonical-ABI discriminant read on {@code variant} / {@code + * option} / {@code result} lifts, which stash the tag in a single byte at + * the payload's base offset. Callers pattern-match on the return. + * + * @param offset the byte offset in linear memory + * @return the byte value as an unsigned int (0..255) + */ + public int readU8(int offset) { + memory.read(offset, 1, scratchArray, 0); + return scratchArray[0] & 0xFF; + } + /** * Reads a 32-bit integer from the given offset (little-endian). * From 156db9d501968fe74ec549bea427b462f2635da9 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 25 Jul 2026 07:24:21 -0400 Subject: [PATCH 50/85] refactor(bindgen): lift resource-method constructor/static flags into WitFunction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BindgenFunction.isConstructor / isStatic are populated during the WitFunction -> BindgenFunction translation pass rather than through a separate resource-method translation path. The previous shape had WitResourceBodyParser emit its own WitResourceMethod class carrying a Kind (CONSTRUCTOR / STATIC / INSTANCE), and CodeGenerator ran a parallel convertResourceMethod translation that duplicated convertWitFunction with only the flag-setting bits swapped in. Two translation paths for one concept; consumers had to keep both in sync. Now: - WitFunction carries isConstructor / isStatic fields, populated via the new WitFunction.resourceMethod(...) factory. Plain WitFunction constructor still exists for interface-level functions where both flags default to false. - WitResourceBodyParser.parse now returns List; the old WitResourceMethod nested class is gone. - CodeGenerator.convertWitFunction lifts the two flags onto BindgenFunction (existing constructor(...) / staticMethod(...) builder calls, previously only invoked from convertResourceMethod). - convertResourceMethod is deleted; both interface-level and resource-body method conversion take the same convertWitFunction path. - resolveOrConvertType picks up the raw-type-expression placeholders WitResourceBodyParser produces (WitType.resource(rawExpr, rawExpr)) and routes them through resolveTypeExpression so container forms (list, option, result, borrow, own) and plain primitives land correctly. Genuine unknown-resource references still fall back to BindgenType.reference, matching prior behaviour. Test coverage: new WitResourceBodyParserTest pins the flag lift directly (constructor / static func / plain func / all-three mix), proving the fields survive parsing. The existing wasmos embedder shape test (RuntimeInstance.instantiate must be public static, etc.) and SPI-dispatch test both still pass — the isStatic flow to the generated `public static` output is verified end-to-end there. Test count: 476 -> 480 in webassembly4j-bindgen (+4 WitResourceBodyParser tests). --- .../webassembly4j/bindgen/CodeGenerator.java | 79 ++++++++------ .../bindgen/wit/WitFunction.java | 62 ++++++++++- .../bindgen/wit/WitResourceBodyParser.java | 101 +++++++----------- .../wit/WitResourceBodyParserTest.java | 98 +++++++++++++++++ 4 files changed, 244 insertions(+), 96 deletions(-) create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java index 7fae2e5..d4c5b5e 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java @@ -32,7 +32,6 @@ import ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceDefinition; import ai.tegmentum.webassembly4j.bindgen.wit.WitParameter; import ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser; -import ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser.WitResourceMethod; import ai.tegmentum.webassembly4j.bindgen.wit.WitType; import ai.tegmentum.webassembly4j.bindgen.wit.WitTypeCategory; import ai.tegmentum.webassembly4j.bindgen.wit.WitWorldPreprocessor; @@ -205,11 +204,16 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException .name(bindgenType.getName()) .kind(BindgenType.Kind.RESOURCE) .documentation(bindgenType.getDocumentation().orElse(null)); - final List methods = + // Resource-body methods flow through the same WitFunction + // translation as interface-level functions — the WitFunctions + // carry the isConstructor / isStatic flags set by + // WitResourceBodyParser, and convertWitFunction lifts them onto + // BindgenFunction. Single translation path, no side-channel. + final List methods = WitResourceBodyParser.parse( entry.getKey(), pre.getResourceBodies().get(entry.getKey())); - for (final WitResourceMethod m : methods) { - rebuilt.addResourceMethod(convertResourceMethod(m, convertedTypes)); + for (final WitFunction m : methods) { + rebuilt.addResourceMethod(convertWitFunction(m, convertedTypes)); } bindgenType = rebuilt.build(); } @@ -293,32 +297,6 @@ private BindgenModel parseWitSourceLegacy( return modelBuilder.build(); } - /** - * Convert a parsed WIT resource method into a {@link BindgenFunction}. Type - * expressions in parameters and the return position are resolved against the - * interface's already-materialized type table. Anything unresolved falls back - * to an opaque reference — bindgen doesn't run a full second-pass symbol - * table today, so cross-resource references need the caller to have declared - * the sibling resource first. - */ - private BindgenFunction convertResourceMethod( - final WitResourceMethod method, final Map knownTypes) { - final BindgenFunction.Builder builder = - BindgenFunction.builder() - .name(method.getName()) - .constructor(method.getKind() == WitResourceMethod.Kind.CONSTRUCTOR) - .staticMethod(method.getKind() == WitResourceMethod.Kind.STATIC); - for (final WitParameter p : method.getParameters()) { - builder.addParameter( - new BindgenParameter(p.getName(), resolveTypeExpression(p.getType().getName(), knownTypes))); - } - if (method.getReturnTypeExpression().isPresent()) { - builder.returnType( - resolveTypeExpression(method.getReturnTypeExpression().get(), knownTypes)); - } - return builder.build(); - } - /** * Resolve a raw WIT type expression string (e.g. {@code list}, * {@code borrow}, {@code result}) @@ -584,6 +562,15 @@ private BindgenType convertVariantType(final String name, final WitType witType) /** * Converts a WIT function to a bindgen function. * + *

      Handles both interface-level functions (parsed by + * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceParser}, fully- + * typed) and resource-body methods (parsed by + * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser}, + * with parameter and return types carried as raw expression placeholders). + * The {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()} + * flags flow directly onto the {@link BindgenFunction} without a separate + * translation path. + * * @param witFunction the WIT function * @param knownTypes previously converted types for resolving references * @return the bindgen function @@ -594,6 +581,8 @@ private BindgenFunction convertWitFunction( BindgenFunction.builder() .name(witFunction.getName()) .async(witFunction.isAsync()) + .constructor(witFunction.isConstructor()) + .staticMethod(witFunction.isStatic()) .documentation(witFunction.getDocumentation().orElse(null)); // Convert parameters @@ -630,7 +619,21 @@ private BindgenFunction convertWitFunction( /** * Resolves a type from known types or converts it from WIT. * - * @param name the type name + *

      Handles three shapes: + * + *

        + *
      1. {@code name} matches a previously converted type — emit a reference. + *
      2. {@code witType} is a raw type-expression placeholder produced by + * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser} + * (a RESOURCE-kinded WitType whose name is the raw WIT source of the + * type expression) — resolve via {@link #resolveTypeExpression} so + * container forms like {@code list} / {@code borrow} / + * {@code result} land correctly. + *
      3. Otherwise, fall through to the normal WIT-to-bindgen conversion. + *
      + * + * @param name the type name (or raw expression, for resource-method + * placeholders) * @param witType the WIT type * @param knownTypes previously converted types * @return the bindgen type @@ -641,6 +644,20 @@ private BindgenType resolveOrConvertType( if (known != null) { return BindgenType.reference(name); } + // Detect a WitResourceBodyParser-produced raw-expression placeholder: + // the parser wraps every parameter and return type in + // WitType.resource(rawExpr, rawExpr), so `witType` is RESOURCE-kinded + // with a name matching the untranslated WIT source. Route those through + // resolveTypeExpression so container forms (list, option, + // result, borrow, own) resolve, and plain primitives + // (`u32`, `string`, ...) land as primitives instead of opaque + // resources. Existing WitInterfaceParser output for a genuine unknown- + // resource reference also lands here — resolveTypeExpression falls + // back to BindgenType.reference for anything it can't classify, which + // matches the pre-refactor behaviour for those cases. + if (witType != null && witType.getKind().getCategory() == WitTypeCategory.RESOURCE) { + return resolveTypeExpression(name, knownTypes); + } return convertWitType(name, witType); } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java index 4da022b..3c6caf2 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java @@ -32,10 +32,15 @@ public final class WitFunction { private final List parameters; private final List returnTypes; private final boolean isAsync; + private final boolean isConstructor; + private final boolean isStatic; private final Optional documentation; /** - * Creates a new WIT function definition. + * Creates a new plain-function WIT function definition. Neither a + * constructor nor a static method; use + * {@link #resourceMethod(String, List, List, boolean, boolean, boolean, Optional)} + * for those. * * @param name the function name * @param parameters the function parameters @@ -49,10 +54,45 @@ public WitFunction( final List returnTypes, final boolean isAsync, final Optional documentation) { + this(name, parameters, returnTypes, isAsync, false, false, documentation); + } + + /** + * Creates a new WIT function definition with resource-method flags carried + * through translation. Used by {@link WitResourceBodyParser} so the + * constructor / static / instance distinction survives into the + * downstream {@link ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} + * without a separate translation path. + */ + public static WitFunction resourceMethod( + final String name, + final List parameters, + final List returnTypes, + final boolean isConstructor, + final boolean isStatic, + final boolean isAsync, + final Optional documentation) { + if (isConstructor && isStatic) { + throw new IllegalArgumentException( + "resource method " + name + " cannot be both a constructor and a static method"); + } + return new WitFunction(name, parameters, returnTypes, isAsync, isConstructor, isStatic, documentation); + } + + private WitFunction( + final String name, + final List parameters, + final List returnTypes, + final boolean isAsync, + final boolean isConstructor, + final boolean isStatic, + final Optional documentation) { this.name = Objects.requireNonNull(name); this.parameters = List.copyOf(Objects.requireNonNull(parameters)); this.returnTypes = List.copyOf(Objects.requireNonNull(returnTypes)); this.isAsync = isAsync; + this.isConstructor = isConstructor; + this.isStatic = isStatic; this.documentation = Objects.requireNonNull(documentation); } @@ -92,6 +132,26 @@ public boolean isAsync() { return isAsync; } + /** + * True when this WIT function represents a {@code constructor(...)} + * declaration inside a {@code resource} body. Populated during parsing + * (see {@link WitResourceBodyParser}) so downstream translation can lift + * the flag onto {@link ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} + * without a separate resource-method path. + */ + public boolean isConstructor() { + return isConstructor; + } + + /** + * True when this WIT function represents a {@code name: static func(...)} + * declaration inside a {@code resource} body. Mutually exclusive with + * {@link #isConstructor()}. + */ + public boolean isStatic() { + return isStatic; + } + /** * Gets the function documentation. * diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java index bfea68d..a3e085b 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java @@ -18,12 +18,12 @@ import ai.tegmentum.webassembly4j.bindgen.BindgenException; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; /** * Parses the body of a WIT {@code resource} declaration into a list of - * {@link WitResourceMethod}s. + * {@link WitFunction}s carrying the {@link WitFunction#isConstructor()} / + * {@link WitFunction#isStatic()} flags. * *

      Understands the three declaration shapes: * @@ -40,75 +40,35 @@ * because the interface parser's regex machinery can't cleanly express * the {@code constructor}/{@code static func} shapes; keeping it separate * makes the split cheap to grow. + * + *

      Parameter and return WitTypes are carried as raw type expressions + * wrapped in {@link WitType#resource(String, String)} placeholders — the + * WIT symbol table isn't visible at this scope, so type resolution is + * deferred to the downstream translation pass (see + * {@code CodeGenerator.convertWitFunction}). This lets a single + * {@link WitFunction} translation path serve both interface-level functions + * and resource-body methods. */ public final class WitResourceBodyParser { private WitResourceBodyParser() {} /** - * A single method declaration on a resource, retaining enough shape for - * downstream Java code generation to decide static vs instance vs - * constructor emission. - */ - public static final class WitResourceMethod { - private final String name; - private final Kind kind; - private final List parameters; - private final Optional returnTypeExpression; - - WitResourceMethod( - final String name, - final Kind kind, - final List parameters, - final Optional returnTypeExpression) { - this.name = Objects.requireNonNull(name, "name"); - this.kind = Objects.requireNonNull(kind, "kind"); - this.parameters = List.copyOf(Objects.requireNonNull(parameters, "parameters")); - this.returnTypeExpression = Objects.requireNonNull(returnTypeExpression, "returnTypeExpression"); - } - - public String getName() { - return name; - } - - public Kind getKind() { - return kind; - } - - public List getParameters() { - return parameters; - } - - /** - * The raw return-type expression as written in the WIT source, or empty - * if the method returned no value. Kept as text so the downstream - * pipeline can resolve it against the interface's already-parsed type - * table. - */ - public Optional getReturnTypeExpression() { - return returnTypeExpression; - } - - public enum Kind { - CONSTRUCTOR, - STATIC, - INSTANCE - } - } - - /** - * Parse the body text of a WIT resource declaration. + * Parse the body text of a WIT resource declaration into a list of + * {@link WitFunction}s. Constructor / static / instance shape is captured + * on {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()}; + * both {@code false} means an instance method. * * @param resourceName the name of the resource (used for diagnostics) * @param body the body between the resource's outer braces * @return the parsed methods, in declaration order * @throws BindgenException if the body cannot be parsed */ - public static List parse(final String resourceName, final String body) + public static List parse(final String resourceName, final String body) throws BindgenException { final String stripped = stripLineComments(body); final List decls = splitDeclarations(stripped); - final List methods = new ArrayList<>(); + final List methods = new ArrayList<>(); for (final String decl : decls) { final String trimmed = decl.trim(); if (trimmed.isEmpty()) { @@ -184,7 +144,7 @@ private static List splitDeclarations(final String body) { return out; } - private static WitResourceMethod parseDeclaration(final String resourceName, final String decl) + private static WitFunction parseDeclaration(final String resourceName, final String decl) throws BindgenException { if (decl.startsWith("constructor")) { final int open = decl.indexOf('('); @@ -194,10 +154,13 @@ private static WitResourceMethod parseDeclaration(final String resourceName, fin "Malformed constructor on resource " + resourceName + ": " + decl); } final String params = decl.substring(open + 1, close); - return new WitResourceMethod( + return WitFunction.resourceMethod( "constructor", - WitResourceMethod.Kind.CONSTRUCTOR, parseParameters(params), + List.of(), + /* isConstructor= */ true, + /* isStatic= */ false, + /* isAsync= */ false, Optional.empty()); } final int colon = decl.indexOf(':'); @@ -221,15 +184,25 @@ private static WitResourceMethod parseDeclaration(final String resourceName, fin } final String params = afterStatic.substring(open + 1, close); final String tail = afterStatic.substring(close + 1).trim(); - Optional returnExpr = Optional.empty(); + // Carry the raw return-type expression as a WitType.resource placeholder; + // downstream translation (CodeGenerator.convertWitFunction) resolves the + // expression against the interface's already-materialized type table via + // the same resolveTypeExpression path used for parameters. + final List returnTypes; if (tail.startsWith("->")) { - returnExpr = Optional.of(tail.substring(2).trim()); + final String returnExpr = tail.substring(2).trim(); + returnTypes = List.of(WitType.resource(returnExpr, returnExpr)); + } else { + returnTypes = List.of(); } - return new WitResourceMethod( + return WitFunction.resourceMethod( name, - isStatic ? WitResourceMethod.Kind.STATIC : WitResourceMethod.Kind.INSTANCE, parseParameters(params), - returnExpr); + returnTypes, + /* isConstructor= */ false, + isStatic, + /* isAsync= */ false, + Optional.empty()); } private static int findMatchingParen(final String s, final int open) { diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java new file mode 100644 index 0000000..26229d9 --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Direct tests for {@link WitResourceBodyParser}, and specifically for the + * fix that lifts a resource method's constructor / static / instance shape + * onto {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()} + * so downstream {@link + * ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} translation + * inherits those flags without a separate translation path. + */ +@DisplayName("WitResourceBodyParser resource-method flag lift") +class WitResourceBodyParserTest { + + @Test + @DisplayName("constructor(...) → WitFunction with isConstructor=true, isStatic=false") + void constructorFlagsSetCorrectly() throws BindgenException { + final String body = "constructor(interface-name: string, num-funcs: u32);"; + final List methods = WitResourceBodyParser.parse("host-provider", body); + assertEquals(1, methods.size(), "expected exactly one parsed method"); + final WitFunction ctor = methods.get(0); + assertEquals("constructor", ctor.getName()); + assertTrue(ctor.isConstructor(), "constructor flag must be true"); + assertFalse(ctor.isStatic(), "static flag must be false for a constructor"); + assertEquals(2, ctor.getParameters().size(), "expected 2 parsed parameters"); + // Constructor has no return type. + assertEquals(0, ctor.getReturnTypes().size(), "constructor return-type list must be empty"); + } + + @Test + @DisplayName("name: static func(...) → WitFunction with isStatic=true, isConstructor=false") + void staticFuncFlagsSetCorrectly() throws BindgenException { + final String body = + "instantiate: static func(component-bytes: list, imports: list) " + + "-> result;"; + final List methods = WitResourceBodyParser.parse("runtime-instance", body); + assertEquals(1, methods.size()); + final WitFunction m = methods.get(0); + assertEquals("instantiate", m.getName()); + assertFalse(m.isConstructor(), "static func must not be a constructor"); + assertTrue(m.isStatic(), "static flag must be true"); + assertEquals(2, m.getParameters().size()); + assertEquals(1, m.getReturnTypes().size(), "expected a single carried return-type expression"); + } + + @Test + @DisplayName("name: func(...) → WitFunction with both flags false (plain instance method)") + void instanceFuncFlagsSetCorrectly() throws BindgenException { + final String body = "call-export: func(name: string, args: list) -> result, error>;"; + final List methods = WitResourceBodyParser.parse("runtime-instance", body); + assertEquals(1, methods.size()); + final WitFunction m = methods.get(0); + assertEquals("call-export", m.getName()); + assertFalse(m.isConstructor(), "plain func must not be a constructor"); + assertFalse(m.isStatic(), "plain func must not be static"); + } + + @Test + @DisplayName("all three shapes coexist and retain declaration order") + void mixedShapesRoundTrip() throws BindgenException { + final String body = + "constructor(seed: u32);\n" + + "reset: static func();\n" + + "next: func() -> u32;"; + final List methods = WitResourceBodyParser.parse("prng", body); + assertEquals(3, methods.size()); + assertTrue(methods.get(0).isConstructor(), "first method must be the constructor"); + assertTrue(methods.get(1).isStatic(), "second method must be static"); + assertFalse(methods.get(2).isConstructor(), "third method is instance"); + assertFalse(methods.get(2).isStatic(), "third method is instance"); + assertEquals("constructor", methods.get(0).getName()); + assertEquals("reset", methods.get(1).getName()); + assertEquals("next", methods.get(2).getName()); + } +} From 4c48fb8622322d70c5b94714235ebbb94db39424 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 25 Jul 2026 07:24:32 -0400 Subject: [PATCH 51/85] docs(bindgen): TODO(bindgen 3.0) marker on WitInterfaceParser regex substrate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the constructs the WitWorldPreprocessor + WitResourceBodyParser wrappers cover today because the underlying regex parser can't cleanly express them: package declarations, world blocks with type hoisting, multi-interface files, import/export/use clauses inside a world, and resource bodies (constructor / static / instance shape). A native tokenizer + recursive-descent rewrite would collapse those two wrappers into a single parser entry point that returns a WitInterfaceDefinition list directly. Not landing in this cut — the rewrite is a design task on its own — but the TODO documents which constructs need coverage before the wrappers can retire. --- .../bindgen/wit/WitInterfaceParser.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java index aa42dfd..1e365d3 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java @@ -35,6 +35,30 @@ *

      This class provides comprehensive parsing capabilities for WIT interface definitions, * including type definitions, function signatures, and interface metadata. * + *

      TODO(bindgen 3.0): This regex-based parser is intentionally single-interface — + * a full WIT world source (multiple {@code interface} blocks, {@code world} blocks, + * {@code package} declarations, {@code use} clauses, {@code import} / {@code export} + * clauses, and {@code resource} bodies) is fanned out into single-interface + * fragments by {@link WitWorldPreprocessor} before reaching this class, and + * resource-body syntax (constructor / static / instance shape) is peeled off by + * {@link WitResourceBodyParser}. A proper native parser would collapse those two + * wrappers into a single recursive-descent walk over a tokenizer stream, which + * both simplifies the codepath and removes the "regex can't cleanly express X" + * exemptions that pushed the wrappers into existence. Constructs the wrappers + * currently cover (and this class does not on its own): + * + *

        + *
      • {@code package name@version;} + *
      • {@code world X { ... }} with type-hoisted world semantics (ADR-006) + *
      • Multiple {@code interface X { ... }} blocks in one file + *
      • {@code import} / {@code export} / {@code use} clauses inside a world + *
      • {@code resource X { constructor(...); name: static func(...); name: func(...); }} + *
      + * + * When the rewrite lands, {@link WitWorldPreprocessor} and + * {@link WitResourceBodyParser} can retire in favor of a single parser entry + * point that returns a {@link WitInterfaceDefinition} list directly. + * * @since 1.0.0 */ public final class WitInterfaceParser { From ae5c9f45583197aab23d59e011865ad58ed229d3 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 25 Jul 2026 13:53:01 -0400 Subject: [PATCH 52/85] refactor(bindgen): native WIT tokenizer + recursive-descent parser (bug 4) Replace the regex-based WitInterfaceParser and its WitWorldPreprocessor + WitResourceBodyParser wrapper chain with a proper WitLexer + WitParser covering the full grammar the wrappers used to patch: package with @version, world with type-hoisted body, multiple interfaces, use / import / export clauses (bare + inline func), resource bodies (constructor + static + instance), type aliases, standalone record / variant / enum / flags, and the full defvaltype expression grammar (list, option, result with placeholder `_`, tuple, borrow, own, inline anonymous compounds). CodeGenerator now calls WitParser.parse() once per source file and consumes the returned WitDocument directly; the wrapper chain and its per-interface reconstruction loop are gone. WitInterfaceParser survives as a thin backward-compat facade so existing single-interface fragment tests keep exercising the same code path. Semicolon leniency is preserved (some legacy fixtures omit the trailing `;` before `}`); comment stripping (line, doc, block) happens in the lexer; WIT declaration order is preserved end-to-end via LinkedHashMap backing on WitDocument.ParsedInterface for cross-JVM deterministic generated output. Delete WitWorldPreprocessor + WitResourceBodyParser + its 4-test suite. Add WitParserTest with 22 tests exercising every construct the retired wrappers used to patch. Total bindgen test count: 480 -> 498 (+18 net). WasmosEmbedderShapeTest still passes with the same top-level type surface (ErrorCode / Error / ImportSatisfaction / HostProvider / RuntimeInstance / EmbedderCallbacks) and cross-run determinism. --- .../webassembly4j/bindgen/CodeGenerator.java | 141 +-- .../wit/DefaultWitInterfaceDefinition.java | 162 ++++ .../bindgen/wit/WitDocument.java | 151 +++ .../bindgen/wit/WitFunction.java | 10 +- .../bindgen/wit/WitInterfaceParser.java | 918 +----------------- .../webassembly4j/bindgen/wit/WitLexer.java | 287 ++++++ .../webassembly4j/bindgen/wit/WitParser.java | 782 +++++++++++++++ .../bindgen/wit/WitResourceBodyParser.java | 294 ------ .../webassembly4j/bindgen/wit/WitToken.java | 75 ++ .../bindgen/wit/WitWorldPreprocessor.java | 472 --------- .../bindgen/wit/WitParserTest.java | 430 ++++++++ .../wit/WitResourceBodyParserTest.java | 98 -- 12 files changed, 1977 insertions(+), 1843 deletions(-) create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/DefaultWitInterfaceDefinition.java create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitDocument.java create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitLexer.java create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java delete mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java create mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitToken.java delete mode 100644 webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java delete mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java index d4c5b5e..829ee1f 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java @@ -27,14 +27,12 @@ import ai.tegmentum.webassembly4j.bindgen.model.BindgenParameter; import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; import ai.tegmentum.webassembly4j.bindgen.model.BindgenVariantCase; -import ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceParser; +import ai.tegmentum.webassembly4j.bindgen.wit.WitDocument; import ai.tegmentum.webassembly4j.bindgen.wit.WitFunction; -import ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceDefinition; import ai.tegmentum.webassembly4j.bindgen.wit.WitParameter; -import ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser; +import ai.tegmentum.webassembly4j.bindgen.wit.WitParser; import ai.tegmentum.webassembly4j.bindgen.wit.WitType; import ai.tegmentum.webassembly4j.bindgen.wit.WitTypeCategory; -import ai.tegmentum.webassembly4j.bindgen.wit.WitWorldPreprocessor; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -169,36 +167,33 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException final String packageName = config.getPackageName() != null ? config.getPackageName() : "generated"; - // Fan a full WIT world source (package + world + multiple - // interfaces + resource bodies) out into per-interface entries - // the existing single-interface WIT parser can consume. - final List preprocessed = - WitWorldPreprocessor.preprocess(witText); - - // Fall back to legacy single-interface parsing if the input - // wasn't a multi-interface world (e.g. tests that pass a bare - // `interface X { ... }`). - final List interfacesToParse; - if (preprocessed.isEmpty()) { - return parseWitSourceLegacy(witPath, witText, packageName); + // One-pass native parse: token stream + recursive descent covers + // package / world / multi-interface / use / import / export / + // resource-body / type-alias / record / variant / enum / flags / + // list / option / result / tuple / borrow / own. The old + // regex-parser + WitWorldPreprocessor + WitResourceBodyParser + // stack (task 56) has retired; this call takes its place. + final WitDocument document = WitParser.parse(witText); + + if (document.getInterfaces().isEmpty()) { + throw new BindgenException( + "WIT file " + witPath + " contains no interface or world declarations"); } - interfacesToParse = preprocessed; final BindgenModel.Builder modelBuilder = - BindgenModel.builder().name(witPath.getFileName().toString()).sourceFile(witPath.toString()); - - for (final WitWorldPreprocessor.Interface pre : interfacesToParse) { - final String reconstructed = "interface " + pre.getName() + " {\n" + pre.getBody() + "\n}\n"; - final WitInterfaceParser parser = new WitInterfaceParser(); - final WitInterfaceDefinition definition = parser.parseInterface(reconstructed, packageName); + BindgenModel.builder() + .name(witPath.getFileName().toString()) + .sourceFile(witPath.toString()); + for (final WitDocument.ParsedInterface pi : document.getInterfaces()) { // LinkedHashMap preserves WIT declaration order for cross-JVM - // deterministic generation. See WitInterfaceParser.parseTypes. - final Map convertedTypes = new java.util.LinkedHashMap<>(); - for (final Map.Entry entry : definition.getTypes().entrySet()) { + // deterministic generation. See the note on + // WitDocument.ParsedInterface for why order is load-bearing. + final Map convertedTypes = new java.util.LinkedHashMap<>(); + for (final Map.Entry entry : pi.getTypes().entrySet()) { BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); if (bindgenType.getKind() == BindgenType.Kind.RESOURCE - && pre.getResourceBodies().containsKey(entry.getKey())) { + && pi.getResourceMethods().containsKey(entry.getKey())) { final BindgenType.Builder rebuilt = BindgenType.builder() .name(bindgenType.getName()) @@ -206,13 +201,10 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException .documentation(bindgenType.getDocumentation().orElse(null)); // Resource-body methods flow through the same WitFunction // translation as interface-level functions — the WitFunctions - // carry the isConstructor / isStatic flags set by - // WitResourceBodyParser, and convertWitFunction lifts them onto - // BindgenFunction. Single translation path, no side-channel. - final List methods = - WitResourceBodyParser.parse( - entry.getKey(), pre.getResourceBodies().get(entry.getKey())); - for (final WitFunction m : methods) { + // carry the isConstructor / isStatic flags set at parse time, + // and convertWitFunction lifts them onto BindgenFunction. + // Single translation path, no side-channel. + for (final WitFunction m : pi.getResourceMethods().get(entry.getKey())) { rebuilt.addResourceMethod(convertWitFunction(m, convertedTypes)); } bindgenType = rebuilt.build(); @@ -221,13 +213,11 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException } final List convertedFunctions = new ArrayList<>(); - for (final Map.Entry entry : definition.getFunctions().entrySet()) { - final BindgenFunction bindgenFunc = - convertWitFunction(entry.getValue(), convertedTypes); - convertedFunctions.add(bindgenFunc); + for (final Map.Entry entry : pi.getFunctions().entrySet()) { + convertedFunctions.add(convertWitFunction(entry.getValue(), convertedTypes)); } - if (pre.isWorld()) { + if (pi.isWorld()) { // A hoisted `world X { ... }` block declares types + resources at // the top level (per ADR-006). It has no functions of its own — // its `import`/`export` clauses reference already-declared @@ -243,7 +233,7 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException } } else { final BindgenInterface.Builder ifaceBuilder = - BindgenInterface.builder().name(definition.getName()).packageName(packageName); + BindgenInterface.builder().name(pi.getName()).packageName(packageName); for (final BindgenFunction func : convertedFunctions) { ifaceBuilder.addFunction(func); } @@ -262,41 +252,6 @@ private BindgenModel parseWitSource(final Path witPath) throws BindgenException } } - private BindgenModel parseWitSourceLegacy( - final Path witPath, final String witText, final String packageName) - throws BindgenException { - final WitInterfaceParser parser = new WitInterfaceParser(); - final WitInterfaceDefinition definition = parser.parseInterface(witText, packageName); - - final BindgenModel.Builder modelBuilder = - BindgenModel.builder().name(definition.getName()).sourceFile(witPath.toString()); - - // LinkedHashMap preserves WIT declaration order for cross-JVM - // deterministic generation. See WitInterfaceParser.parseTypes. - final Map convertedTypes = new java.util.LinkedHashMap<>(); - for (final Map.Entry entry : definition.getTypes().entrySet()) { - final BindgenType bindgenType = convertWitType(entry.getKey(), entry.getValue()); - convertedTypes.put(entry.getKey(), bindgenType); - } - - final List convertedFunctions = new ArrayList<>(); - for (final Map.Entry entry : definition.getFunctions().entrySet()) { - final BindgenFunction bindgenFunc = convertWitFunction(entry.getValue(), convertedTypes); - convertedFunctions.add(bindgenFunc); - } - - final BindgenInterface.Builder ifaceBuilder = - BindgenInterface.builder().name(definition.getName()).packageName(packageName); - for (final BindgenFunction func : convertedFunctions) { - ifaceBuilder.addFunction(func); - } - for (final BindgenType type : convertedTypes.values()) { - ifaceBuilder.addType(type); - } - modelBuilder.addInterface(ifaceBuilder.build()); - return modelBuilder.build(); - } - /** * Resolve a raw WIT type expression string (e.g. {@code list}, * {@code borrow}, {@code result}) @@ -562,12 +517,10 @@ private BindgenType convertVariantType(final String name, final WitType witType) /** * Converts a WIT function to a bindgen function. * - *

      Handles both interface-level functions (parsed by - * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitInterfaceParser}, fully- - * typed) and resource-body methods (parsed by - * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser}, - * with parameter and return types carried as raw expression placeholders). - * The {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()} + *

      Handles both interface-level functions and resource-body methods — + * both come out of {@link ai.tegmentum.webassembly4j.bindgen.wit.WitParser} + * with the same {@link WitFunction} shape, and the + * {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()} * flags flow directly onto the {@link BindgenFunction} without a separate * translation path. * @@ -624,11 +577,12 @@ private BindgenFunction convertWitFunction( *

        *
      1. {@code name} matches a previously converted type — emit a reference. *
      2. {@code witType} is a raw type-expression placeholder produced by - * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitResourceBodyParser} - * (a RESOURCE-kinded WitType whose name is the raw WIT source of the - * type expression) — resolve via {@link #resolveTypeExpression} so - * container forms like {@code list} / {@code borrow} / - * {@code result} land correctly. + * {@link ai.tegmentum.webassembly4j.bindgen.wit.WitParser} for a + * named reference not yet in the type table (a RESOURCE-kinded + * WitType whose name is the raw WIT source of the type expression) + * — resolve via {@link #resolveTypeExpression} so container forms + * like {@code list} / {@code borrow} / {@code result} + * land correctly. *
      3. Otherwise, fall through to the normal WIT-to-bindgen conversion. *
      * @@ -644,17 +598,14 @@ private BindgenType resolveOrConvertType( if (known != null) { return BindgenType.reference(name); } - // Detect a WitResourceBodyParser-produced raw-expression placeholder: - // the parser wraps every parameter and return type in + // Detect a WitParser-produced unresolved named-reference placeholder: + // the parser emits every non-primitive named type as // WitType.resource(rawExpr, rawExpr), so `witType` is RESOURCE-kinded // with a name matching the untranslated WIT source. Route those through - // resolveTypeExpression so container forms (list, option, - // result, borrow, own) resolve, and plain primitives - // (`u32`, `string`, ...) land as primitives instead of opaque - // resources. Existing WitInterfaceParser output for a genuine unknown- - // resource reference also lands here — resolveTypeExpression falls - // back to BindgenType.reference for anything it can't classify, which - // matches the pre-refactor behaviour for those cases. + // resolveTypeExpression so container forms already resolved as + // WitType.list / WitType.option etc. stay resolved via convertWitType, + // while genuine unknown-resource references fall back to + // BindgenType.reference — matching pre-refactor behaviour. if (witType != null && witType.getKind().getCategory() == WitTypeCategory.RESOURCE) { return resolveTypeExpression(name, knownTypes); } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/DefaultWitInterfaceDefinition.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/DefaultWitInterfaceDefinition.java new file mode 100644 index 0000000..3349881 --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/DefaultWitInterfaceDefinition.java @@ -0,0 +1,162 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Default {@link WitInterfaceDefinition} implementation. + * + *

      Extracted from the retired {@code WitInterfaceParser} inner class so both + * the new {@link WitParser}-based path and the legacy {@link WitInterfaceParser} + * facade can share one implementation. Backed by + * {@link LinkedHashMap}-based unmodifiable maps to preserve WIT declaration + * order — cross-JVM deterministic ordering keeps generated Java source + * byte-identical. + */ +final class DefaultWitInterfaceDefinition implements WitInterfaceDefinition { + + private final String name; + private final String version; + private final String packageName; + private final Map functions; + private final Map types; + private final List imports; + private final List exports; + private final String witText; + + DefaultWitInterfaceDefinition( + final String name, + final String version, + final String packageName, + final Map functions, + final Map types, + final List imports, + final List exports, + final String witText) { + this.name = name; + this.version = version; + this.packageName = packageName; + this.functions = Collections.unmodifiableMap(new LinkedHashMap<>(functions)); + this.types = Collections.unmodifiableMap(new LinkedHashMap<>(types)); + this.imports = List.copyOf(imports); + this.exports = List.copyOf(exports); + this.witText = witText; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public String getPackageName() { + return packageName; + } + + @Override + public List getFunctionNames() { + return new ArrayList<>(functions.keySet()); + } + + @Override + public Map getFunctions() { + return functions; + } + + @Override + public List getTypeNames() { + return new ArrayList<>(types.keySet()); + } + + @Override + public Map getTypes() { + return types; + } + + @Override + public Set getDependencies() { + return Set.copyOf(imports); + } + + @Override + public WitCompatibilityResult isCompatibleWith(final WitInterfaceDefinition other) { + if (!name.equals(other.getName())) { + return new WitCompatibilityResult( + false, + "Interface names do not match: " + name + " vs " + other.getName(), + Set.of(), + Set.of()); + } + + final Set ourFunctions = Set.copyOf(functions.keySet()); + final Set otherFunctions = Set.copyOf(other.getFunctionNames()); + final Set ourTypes = Set.copyOf(types.keySet()); + final Set otherTypes = Set.copyOf(other.getTypeNames()); + + final Set missingFunctions = new HashSet<>(ourFunctions); + missingFunctions.removeAll(otherFunctions); + final Set extraFunctions = new HashSet<>(otherFunctions); + extraFunctions.removeAll(ourFunctions); + + final Set missingTypes = new HashSet<>(ourTypes); + missingTypes.removeAll(otherTypes); + final Set extraTypes = new HashSet<>(otherTypes); + extraTypes.removeAll(ourTypes); + + final Set allMissing = new HashSet<>(); + missingFunctions.forEach(f -> allMissing.add("function:" + f)); + missingTypes.forEach(t -> allMissing.add("type:" + t)); + + final Set allExtra = new HashSet<>(); + extraFunctions.forEach(f -> allExtra.add("function:" + f)); + extraTypes.forEach(t -> allExtra.add("type:" + t)); + + final boolean compatible = allMissing.isEmpty() && allExtra.isEmpty(); + final String message = + compatible + ? "Interfaces are compatible" + : "Interfaces differ: missing=" + allMissing + ", extra=" + allExtra; + + return new WitCompatibilityResult(compatible, message, allMissing, allExtra); + } + + @Override + public String getWitText() { + return witText; + } + + @Override + public List getImportNames() { + return imports; + } + + @Override + public List getExportNames() { + return exports; + } +} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitDocument.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitDocument.java new file mode 100644 index 0000000..190e530 --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitDocument.java @@ -0,0 +1,151 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * The parsed result of a full WIT source file. + * + *

      Emitted by {@link WitParser}, this replaces the wrapper-chain output + * (retired {@code WitWorldPreprocessor.Interface} + separate resource-body + * parse) with a single flat representation the {@code CodeGenerator} can + * consume directly. Interfaces and worlds are surfaced as + * {@link ParsedInterface} entries in WIT declaration order with + * {@link ParsedInterface#isWorld()} distinguishing world entries from + * plain interfaces. + * + *

      Resources declared inside an interface or world body are visible in + * two ways on the same entry: the resource {@code type} lands in + * {@link ParsedInterface#getTypes()} with a {@code RESOURCE} kind, and its + * methods land in {@link ParsedInterface#getResourceMethods()} keyed by + * resource name. This single structural view removes the second + * (WitResourceBodyParser) parse pass the previous stack required. + */ +public final class WitDocument { + + private final Optional packageName; + private final Optional packageVersion; + private final List interfaces; + + WitDocument( + final Optional packageName, + final Optional packageVersion, + final List interfaces) { + this.packageName = packageName; + this.packageVersion = packageVersion; + this.interfaces = List.copyOf(interfaces); + } + + /** The declared {@code package} name (e.g. {@code wasmos:host}), if present. */ + public Optional getPackageName() { + return packageName; + } + + /** The declared {@code @version} after the package name, if present. */ + public Optional getPackageVersion() { + return packageVersion; + } + + /** + * Every interface and world in the source, in WIT declaration order. + * Worlds carry {@link ParsedInterface#isWorld()} true; the caller + * decides whether to hoist their types onto the model root or emit + * them under a Java interface carrier. + */ + public List getInterfaces() { + return interfaces; + } + + /** + * A single interface or hoisted world, with all its types, top-level + * functions, and per-resource method lists resolved. + */ + public static final class ParsedInterface { + + private final String name; + private final boolean world; + private final Map types; + private final Map functions; + private final Map> resourceMethods; + + ParsedInterface( + final String name, + final boolean world, + final Map types, + final Map functions, + final Map> resourceMethods) { + this.name = name; + this.world = world; + // Preserve WIT declaration order across JVMs — see the note on + // WitInterfaceParser.parseTypes for why LinkedHashMap-backed + // unmodifiable maps are load-bearing for byte-identical output. + this.types = Collections.unmodifiableMap(new LinkedHashMap<>(types)); + this.functions = Collections.unmodifiableMap(new LinkedHashMap<>(functions)); + final Map> copy = new LinkedHashMap<>(); + for (final Map.Entry> e : resourceMethods.entrySet()) { + copy.put(e.getKey(), List.copyOf(e.getValue())); + } + this.resourceMethods = Collections.unmodifiableMap(copy); + } + + /** The interface / world name as written in WIT. */ + public String getName() { + return name; + } + + /** + * True when this entry was projected from a {@code world X { ... }} + * block. Worlds' import / export / use clauses reference already- + * declared interfaces and don't contribute new type surface; the + * caller typically hoists a world's types to top-level model types + * rather than wrap them in an empty Java carrier. + */ + public boolean isWorld() { + return world; + } + + /** + * All types declared in the body, keyed by WIT name. Includes + * records, variants, enums, flags, aliases, and resources. + */ + public Map getTypes() { + return types; + } + + /** + * Top-level functions declared directly in the body (not resource + * methods). + */ + public Map getFunctions() { + return functions; + } + + /** + * Methods declared inside each resource body, keyed by resource name. + * {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()} + * carry the resource-method shape onto the same translation path used + * for top-level functions. + */ + public Map> getResourceMethods() { + return resourceMethods; + } + } +} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java index 3c6caf2..e6fe4ab 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitFunction.java @@ -59,10 +59,10 @@ public WitFunction( /** * Creates a new WIT function definition with resource-method flags carried - * through translation. Used by {@link WitResourceBodyParser} so the - * constructor / static / instance distinction survives into the - * downstream {@link ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} - * without a separate translation path. + * through translation. Used by {@link WitParser} so the constructor / + * static / instance distinction survives into the downstream + * {@link ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} without + * a separate translation path. */ public static WitFunction resourceMethod( final String name, @@ -135,7 +135,7 @@ public boolean isAsync() { /** * True when this WIT function represents a {@code constructor(...)} * declaration inside a {@code resource} body. Populated during parsing - * (see {@link WitResourceBodyParser}) so downstream translation can lift + * (see {@link WitParser}) so downstream translation can lift * the flag onto {@link ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} * without a separate resource-method path. */ diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java index 1e365d3..f5bdc13 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitInterfaceParser.java @@ -17,904 +17,64 @@ package ai.tegmentum.webassembly4j.bindgen.wit; import ai.tegmentum.webassembly4j.bindgen.BindgenException; - import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** - * Parser for WebAssembly Interface Type (WIT) definitions. - * - *

      This class provides comprehensive parsing capabilities for WIT interface definitions, - * including type definitions, function signatures, and interface metadata. - * - *

      TODO(bindgen 3.0): This regex-based parser is intentionally single-interface — - * a full WIT world source (multiple {@code interface} blocks, {@code world} blocks, - * {@code package} declarations, {@code use} clauses, {@code import} / {@code export} - * clauses, and {@code resource} bodies) is fanned out into single-interface - * fragments by {@link WitWorldPreprocessor} before reaching this class, and - * resource-body syntax (constructor / static / instance shape) is peeled off by - * {@link WitResourceBodyParser}. A proper native parser would collapse those two - * wrappers into a single recursive-descent walk over a tokenizer stream, which - * both simplifies the codepath and removes the "regex can't cleanly express X" - * exemptions that pushed the wrappers into existence. Constructs the wrappers - * currently cover (and this class does not on its own): - * - *

        - *
      • {@code package name@version;} - *
      • {@code world X { ... }} with type-hoisted world semantics (ADR-006) - *
      • Multiple {@code interface X { ... }} blocks in one file - *
      • {@code import} / {@code export} / {@code use} clauses inside a world - *
      • {@code resource X { constructor(...); name: static func(...); name: func(...); }} - *
      + * Single-interface facade over {@link WitParser}, kept for backward-compatible + * use by tests and older callers that hand this parser a bare + * {@code interface X { ... }} fragment. * - * When the rewrite lands, {@link WitWorldPreprocessor} and - * {@link WitResourceBodyParser} can retire in favor of a single parser entry - * point that returns a {@link WitInterfaceDefinition} list directly. + *

      The regex-based single-interface parser, its {@code WitWorldPreprocessor} + * type-hoisting pass, and its {@code WitResourceBodyParser} resource-shape + * pass are all retired — the token-stream + recursive-descent {@link + * WitParser} covers the full WIT grammar those wrappers used to patch + * (package / world with type-hoisting / multi-interface / use / import / + * export / resource bodies). Everything real should call {@link + * WitParser#parse(String)} directly and consume the returned {@link + * WitDocument}. This facade only survives so the legacy single-interface + * tests keep exercising the same code path via a familiar entrypoint. * * @since 1.0.0 */ public final class WitInterfaceParser { - private static final Pattern INTERFACE_PATTERN = - Pattern.compile("interface\\s+([a-zA-Z0-9_-]+)\\s*\\{([\\s\\S]*)\\}", Pattern.DOTALL); - - // Simplified pattern to prevent ReDoS - uses possessive quantifiers - private static final Pattern FUNCTION_PATTERN = - Pattern.compile("([a-zA-Z0-9_-]++)\\s*:\\s*func\\s*\\(([^)]*)\\)\\s*(?:->\\s*([^;]+))?"); - - private static final Pattern TYPE_PATTERN = - Pattern.compile("type\\s+([a-zA-Z0-9_-]+)\\s*=\\s*([^;]+)", Pattern.DOTALL); - - private static final Pattern RECORD_PATTERN = - Pattern.compile("record\\s*\\{([\\s\\S]*)\\}", Pattern.DOTALL); - - private static final Pattern VARIANT_PATTERN = - Pattern.compile("variant\\s*\\{([\\s\\S]*)\\}", Pattern.DOTALL); - - private static final Pattern ENUM_PATTERN = - Pattern.compile("enum\\s*\\{([\\s\\S]*)\\}", Pattern.DOTALL); - - private static final Pattern FLAGS_PATTERN = - Pattern.compile("flags\\s*\\{([\\s\\S]*)\\}", Pattern.DOTALL); - - // Standalone declarations: `record name { fields }` etc. — the - // canonical WIT syntax used by wit-bindgen output and most - // real-world WIT files. The `type X = ...` alias forms above are - // also valid but less common in practice. Non-greedy braces are - // safe because records/variants/enums/flags don't nest braces in - // their bodies. - private static final Pattern RECORD_DECLARATION_PATTERN = - Pattern.compile("record\\s+([a-zA-Z0-9_-]+)\\s*\\{([\\s\\S]*?)\\}", Pattern.DOTALL); - - private static final Pattern VARIANT_DECLARATION_PATTERN = - Pattern.compile("variant\\s+([a-zA-Z0-9_-]+)\\s*\\{([\\s\\S]*?)\\}", Pattern.DOTALL); - - private static final Pattern ENUM_DECLARATION_PATTERN = - Pattern.compile("enum\\s+([a-zA-Z0-9_-]+)\\s*\\{([\\s\\S]*?)\\}", Pattern.DOTALL); - - private static final Pattern FLAGS_DECLARATION_PATTERN = - Pattern.compile("flags\\s+([a-zA-Z0-9_-]+)\\s*\\{([\\s\\S]*?)\\}", Pattern.DOTALL); - - private static final int MAX_WIT_TEXT_LENGTH = 1024 * 1024; // 1MB limit - - private final Map typeCache; - - /** Creates a new WIT interface parser. */ - public WitInterfaceParser() { - this.typeCache = new HashMap<>(); - initializePrimitiveTypes(); - } + /** Creates a new WIT interface parser facade. */ + public WitInterfaceParser() {} /** - * Parses a WIT interface definition from text. + * Parse a single WIT interface fragment. * - * @param witText the WIT interface text - * @param packageName the package name + * @param witText the WIT source — must contain exactly one + * {@code interface X { ... }} block + * @param packageName the package name to attach to the returned + * definition * @return the parsed interface definition - * @throws BindgenException if parsing fails + * @throws BindgenException if parsing fails or the fragment doesn't + * resolve to exactly one interface */ public WitInterfaceDefinition parseInterface(final String witText, final String packageName) throws BindgenException { Objects.requireNonNull(witText, "witText"); Objects.requireNonNull(packageName, "packageName"); - - // Prevent ReDoS by rejecting excessively large WIT definitions - if (witText.length() > MAX_WIT_TEXT_LENGTH) { - throw new BindgenException( - "WIT definition exceeds maximum length of " + MAX_WIT_TEXT_LENGTH + " characters"); - } - - try { - // Parse the interface declaration - final Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(witText); - if (!interfaceMatcher.find()) { - throw new BindgenException("Invalid WIT interface: no interface declaration found"); - } - - final String interfaceName = interfaceMatcher.group(1); - final String interfaceBody = interfaceMatcher.group(2); - - // Parse types, functions, and exports - final Map types = parseTypes(interfaceBody); - final Map functions = parseFunctions(interfaceBody, types); - final List exports = parseExports(interfaceBody); - - return new WitInterfaceDefinitionImpl( - interfaceName, - "1.0", // Default version - packageName, - functions, - types, - List.of(), - exports, - witText); - - } catch (final Exception e) { - throw new BindgenException("Failed to parse WIT interface", e); - } - } - - /** - * Parses type definitions from WIT interface body. - * - * @param interfaceBody the interface body text - * @return map of type name to type definition - * @throws BindgenException if parsing fails - */ - private Map parseTypes(final String interfaceBody) throws BindgenException { - // LinkedHashMap for cross-JVM-deterministic type ordering — see - // parseFunctions for the same reasoning. Downstream generators iterate - // this map to emit `.java` files; a randomized order would surface as - // reshuffled inner-type declaration order in generated code between - // invocations. - final Map types = new java.util.LinkedHashMap<>(); - - // Pass 1: standalone declarations (`record name { ... }`, - // `variant name { ... }`, `enum name { ... }`, - // `flags name { ... }`). This is the canonical WIT syntax — - // running it first lets the second pass resolve aliases that - // reference them. - // - // Enums and flags come first so that records/variants whose - // fields or case payloads reference them can resolve without - // requiring authors to declare their WIT types in dependency - // order. Records still can't forward-reference other records - // in this simple regex parser — that would need a proper - // two-phase symbol-table build. - collectStandalone( - ENUM_DECLARATION_PATTERN, - interfaceBody, - types, - (name, body) -> parseEnumType(name, body)); - collectStandalone( - FLAGS_DECLARATION_PATTERN, - interfaceBody, - types, - (name, body) -> parseFlagsType(name, body)); - collectStandalone( - RECORD_DECLARATION_PATTERN, - interfaceBody, - types, - (name, body) -> parseRecordType(name, body)); - collectStandalone( - VARIANT_DECLARATION_PATTERN, - interfaceBody, - types, - (name, body) -> parseVariantType(name, body)); - - // Pass 2: `type X = ...;` alias declarations. - final Matcher typeMatcher = TYPE_PATTERN.matcher(interfaceBody); - - while (typeMatcher.find()) { - final String typeName = typeMatcher.group(1); - final String typeDefinition = typeMatcher.group(2).trim(); - - final WitType witType = parseTypeDefinition(typeName, typeDefinition); - types.put(typeName, witType); - typeCache.put(typeName, witType); - } - - return types; - } - - /** - * Functional interface for parsing a standalone declaration's body. Matches the - * {@code parseRecordType} / {@code parseVariantType} / {@code parseEnumType} / - * {@code parseFlagsType} signatures so they compose with {@link - * #collectStandalone(Pattern, String, Map, StandaloneBodyParser)}. - */ - @FunctionalInterface - private interface StandaloneBodyParser { - WitType parse(String typeName, String body) throws BindgenException; - } - - /** - * Run a standalone-declaration pattern (e.g. {@code record name { fields }}) across - * the interface body and register every match into {@code types} and the resolution - * cache. - */ - private void collectStandalone( - final Pattern pattern, - final String interfaceBody, - final Map types, - final StandaloneBodyParser bodyParser) - throws BindgenException { - final Matcher m = pattern.matcher(interfaceBody); - while (m.find()) { - final String typeName = m.group(1); - final String body = m.group(2); - final WitType witType = bodyParser.parse(typeName, body); - types.put(typeName, witType); - typeCache.put(typeName, witType); - } - } - - /** - * Parses a single type definition. - * - * @param typeName the type name - * @param definition the type definition - * @return the parsed WIT type - * @throws BindgenException if parsing fails - */ - private WitType parseTypeDefinition(final String typeName, final String definition) - throws BindgenException { - final String trimmed = definition.trim(); - - // Check for primitive types - try { - final WitPrimitiveType primitive = WitPrimitiveType.fromString(trimmed); - return WitType.primitive(primitive); - } catch (final IllegalArgumentException e) { - // Not a primitive type, continue parsing - } - - // Check for record type - final Matcher recordMatcher = RECORD_PATTERN.matcher(trimmed); - if (recordMatcher.matches()) { - return parseRecordType(typeName, recordMatcher.group(1)); - } - - // Check for variant type - final Matcher variantMatcher = VARIANT_PATTERN.matcher(trimmed); - if (variantMatcher.matches()) { - return parseVariantType(typeName, variantMatcher.group(1)); - } - - // Check for enum type - final Matcher enumMatcher = ENUM_PATTERN.matcher(trimmed); - if (enumMatcher.matches()) { - return parseEnumType(typeName, enumMatcher.group(1)); - } - - // Check for flags type - final Matcher flagsMatcher = FLAGS_PATTERN.matcher(trimmed); - if (flagsMatcher.matches()) { - return parseFlagsType(typeName, flagsMatcher.group(1)); - } - - // Check for list type - if (trimmed.startsWith("list<") && trimmed.endsWith(">")) { - final String elementTypeName = trimmed.substring(5, trimmed.length() - 1).trim(); - final WitType elementType = resolveType(elementTypeName); - return WitType.list(elementType); - } - - // Check for option type - if (trimmed.startsWith("option<") && trimmed.endsWith(">")) { - final String innerTypeName = trimmed.substring(7, trimmed.length() - 1).trim(); - final WitType innerType = resolveType(innerTypeName); - return WitType.option(innerType); - } - - // Check for result type - if (trimmed.startsWith("result<") && trimmed.endsWith(">")) { - return parseResultType(trimmed); - } - - // Check for resource type - if (trimmed.startsWith("resource")) { - return WitType.resource(typeName, typeName); - } - - throw new BindgenException("Unknown type definition: " + definition); - } - - /** - * Parses a record type definition. - * - * @param typeName the type name - * @param fieldsText the fields definition text - * @return the parsed record type - * @throws BindgenException if parsing fails - */ - private WitType parseRecordType(final String typeName, final String fieldsText) - throws BindgenException { - // LinkedHashMap for deterministic field order in generated records. - final Map fields = new java.util.LinkedHashMap<>(); - final List fieldDefs = splitTopLevel(stripComments(fieldsText), ','); - - for (final String fieldDef : fieldDefs) { - final String trimmed = fieldDef.trim(); - if (trimmed.isEmpty()) { - continue; - } - - // Split on the FIRST colon — type expressions like - // `list` don't contain colons but type aliases that - // resolve to records aren't allowed inline either, so the - // first `:` is the field/type separator. - final int colon = trimmed.indexOf(':'); - if (colon < 0) { - throw new BindgenException("Invalid record field definition: " + fieldDef); - } - final String fieldName = trimmed.substring(0, colon).trim(); - final String fieldTypeName = trimmed.substring(colon + 1).trim(); - final WitType fieldType = resolveType(fieldTypeName); - fields.put(fieldName, fieldType); - } - - return WitType.record(typeName, fields); - } - - /** - * Strip WIT doc-comment lines ({@code ///…}) and plain {@code //…} comments - * from declaration bodies. Mutating the body before field/case splitting avoids - * comma-inside-comment surprises. - */ - private static String stripComments(final String body) { - final StringBuilder out = new StringBuilder(body.length()); - for (final String line : body.split("\n", -1)) { - final int commentStart = indexOfLineComment(line); - if (commentStart < 0) { - out.append(line).append('\n'); - } else { - out.append(line, 0, commentStart).append('\n'); - } - } - return out.toString(); - } - - /** - * Find the first {@code //} that starts a WIT line comment, respecting - * "…" strings so a {@code //} inside a string literal isn't misread. - */ - private static int indexOfLineComment(final String line) { - boolean inString = false; - for (int i = 0; i < line.length() - 1; i++) { - final char c = line.charAt(i); - if (c == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { - inString = !inString; - continue; - } - if (!inString && c == '/' && line.charAt(i + 1) == '/') { - return i; - } - } - return -1; - } - - /** - * Split {@code text} on top-level occurrences of {@code separator}, respecting - * angle-bracket nesting ({@code list}), paren nesting - * ({@code case(payload)}), and double-quoted strings. - */ - private static List splitTopLevel(final String text, final char separator) { - final List out = new ArrayList<>(); - int depthAngle = 0; - int depthParen = 0; - boolean inString = false; - final StringBuilder current = new StringBuilder(); - for (int i = 0; i < text.length(); i++) { - final char c = text.charAt(i); - if (c == '"' && (i == 0 || text.charAt(i - 1) != '\\')) { - inString = !inString; - current.append(c); - continue; - } - if (!inString) { - if (c == '<') { - depthAngle++; - } else if (c == '>') { - depthAngle = Math.max(0, depthAngle - 1); - } else if (c == '(') { - depthParen++; - } else if (c == ')') { - depthParen = Math.max(0, depthParen - 1); - } - } - if (!inString && depthAngle == 0 && depthParen == 0 && c == separator) { - out.add(current.toString()); - current.setLength(0); - } else { - current.append(c); - } - } - if (current.length() > 0) { - out.add(current.toString()); - } - return out; - } - - /** - * Parses a variant type definition. - * - * @param typeName the type name - * @param casesText the cases definition text - * @return the parsed variant type - * @throws BindgenException if parsing fails - */ - private WitType parseVariantType(final String typeName, final String casesText) - throws BindgenException { - // LinkedHashMap for deterministic case order in generated sealed - // interfaces. - final Map> cases = new java.util.LinkedHashMap<>(); - final List caseDefs = splitTopLevel(stripComments(casesText), ','); - - for (final String caseDef : caseDefs) { - final String trimmed = caseDef.trim(); - if (trimmed.isEmpty()) { - continue; - } - - if (trimmed.contains("(")) { - // Case with payload - final int parenIndex = trimmed.indexOf('('); - final String caseName = trimmed.substring(0, parenIndex).trim(); - final String payloadType = - trimmed.substring(parenIndex + 1, trimmed.lastIndexOf(')')).trim(); - final WitType caseType = resolveType(payloadType); - cases.put(caseName, Optional.of(caseType)); - } else { - // Case without payload - cases.put(trimmed, Optional.empty()); - } - } - - return WitType.variant(typeName, cases); - } - - /** - * Parses an enum type definition. - * - * @param typeName the type name - * @param valuesText the values definition text - * @return the parsed enum type - * @throws BindgenException if parsing fails - */ - private WitType parseEnumType(final String typeName, final String valuesText) - throws BindgenException { - final List values = new ArrayList<>(); - final List valueDefs = splitTopLevel(stripComments(valuesText), ','); - - for (final String valueDef : valueDefs) { - final String trimmed = valueDef.trim(); - if (!trimmed.isEmpty()) { - values.add(trimmed); - } - } - - return WitType.enumType(typeName, values); - } - - /** - * Parses a flags type definition. - * - * @param typeName the type name - * @param flagsText the flags definition text - * @return the parsed flags type - * @throws BindgenException if parsing fails - */ - private WitType parseFlagsType(final String typeName, final String flagsText) - throws BindgenException { - final List flags = new ArrayList<>(); - final List flagDefs = splitTopLevel(stripComments(flagsText), ','); - - for (final String flagDef : flagDefs) { - final String trimmed = flagDef.trim(); - if (!trimmed.isEmpty()) { - flags.add(trimmed); - } - } - - return WitType.flags(typeName, flags); - } - - /** - * Parses a result type definition. - * - * @param definition the result type definition - * @return the parsed result type - * @throws BindgenException if parsing fails - */ - private WitType parseResultType(final String definition) throws BindgenException { - final String content = definition.substring(7, definition.length() - 1).trim(); - - if (content.isEmpty()) { - return WitType.result(Optional.empty(), Optional.empty()); - } - - final String[] parts = content.split(","); - if (parts.length > 2) { - throw new BindgenException("Result type can have at most two type parameters"); - } - - Optional okType = Optional.empty(); - Optional errorType = Optional.empty(); - - if (parts.length >= 1 && !parts[0].trim().equals("_")) { - okType = Optional.of(resolveType(parts[0].trim())); - } - - if (parts.length == 2 && !parts[1].trim().equals("_")) { - errorType = Optional.of(resolveType(parts[1].trim())); - } - - return WitType.result(okType, errorType); - } - - /** - * Parses function definitions from WIT interface body. - * - * @param interfaceBody the interface body text - * @param types the available types - * @return map of function name to function definition - * @throws BindgenException if parsing fails - */ - private Map parseFunctions( - final String interfaceBody, final Map types) throws BindgenException { - // LinkedHashMap so downstream generation follows WIT declaration order. - // HashMap iteration is randomized per-JVM (and depends on key hashCodes - // for its bin placement); using it here reintroduces cross-JVM - // non-determinism into generated sources, which breaks the byte-identical - // guarantee CLI users depend on for artifact reproducibility. - final Map functions = new java.util.LinkedHashMap<>(); - final Matcher functionMatcher = FUNCTION_PATTERN.matcher(interfaceBody); - - while (functionMatcher.find()) { - final String functionName = functionMatcher.group(1); - final String parametersText = functionMatcher.group(2); - final String returnTypeText = functionMatcher.group(3); - - final List parameters = parseParameters(parametersText); - final List returnTypes = parseReturnTypes(returnTypeText); - - final WitFunction function = - new WitFunction( - functionName, - parameters, - returnTypes, - false, // Not async by default - Optional.empty()); - - functions.put(functionName, function); - } - - return functions; - } - - /** - * Parses function parameters. - * - * @param parametersText the parameters definition text - * @return list of parsed parameters - * @throws BindgenException if parsing fails - */ - private List parseParameters(final String parametersText) throws BindgenException { - final List parameters = new ArrayList<>(); - - if (parametersText == null || parametersText.trim().isEmpty()) { - return parameters; - } - - final String[] paramDefs = splitRespectingBraces(parametersText); - for (final String paramDef : paramDefs) { - final String trimmed = paramDef.trim(); - if (trimmed.isEmpty()) { - continue; - } - - final String[] parts = trimmed.split(":"); - if (parts.length != 2) { - throw new BindgenException("Invalid parameter definition: " + paramDef); - } - - final String paramName = parts[0].trim(); - final String paramTypeName = parts[1].trim(); - final WitType paramType = resolveType(paramTypeName); - - parameters.add( - new WitParameter( - paramName, - paramType, - false, // Not optional by default - Optional.empty())); - } - - return parameters; - } - - /** - * Parses function return types. - * - * @param returnTypeText the return type definition text - * @return list of parsed return types - * @throws BindgenException if parsing fails - */ - private List parseReturnTypes(final String returnTypeText) throws BindgenException { - final List returnTypes = new ArrayList<>(); - - if (returnTypeText == null || returnTypeText.trim().isEmpty()) { - return returnTypes; - } - - // For now, assume single return type - final WitType returnType = resolveType(returnTypeText.trim()); - returnTypes.add(returnType); - - return returnTypes; - } - - /** - * Parses export declarations (simplified implementation). - * - * @param interfaceBody the interface body text - * @return list of export names - */ - private List parseExports(final String interfaceBody) { - final List exports = new ArrayList<>(); - // Simplified - exports are typically function names - final Matcher functionMatcher = FUNCTION_PATTERN.matcher(interfaceBody); - while (functionMatcher.find()) { - exports.add(functionMatcher.group(1)); - } - return exports; - } - - /** - * Resolves a type name to a WIT type. - * - * @param typeName the type name - * @return the resolved type - * @throws BindgenException if type cannot be resolved - */ - private WitType resolveType(final String typeName) throws BindgenException { - final String stripped = stripResourceQualifier(typeName); - - // Check cache first - final WitType cachedType = typeCache.get(stripped); - if (cachedType != null) { - return cachedType; - } - - // Try to parse as primitive type - try { - final WitPrimitiveType primitive = WitPrimitiveType.fromString(stripped); - return WitType.primitive(primitive); - } catch (final IllegalArgumentException e) { - // Not a primitive type - } - - // Try to parse as inline type definition (list, option, etc.) - try { - return parseTypeDefinition(stripped, stripped); - } catch (final BindgenException e) { - // Not a parseable type definition - } - - // Fall back to a synthetic resource reference. WIT identifiers - // that don't resolve in the local scope typically come from a - // sibling resource declaration whose body the caller stripped - // out before feeding this interface body in — treat them as - // opaque handles rather than failing the whole parse. - return WitType.resource(stripped, stripped); - } - - /** - * Peel resource-qualifier wrappers ({@code borrow}, {@code own}) off - * a type expression. Both wrappers are ABI-level lifetime hints; at the - * generated-Java surface the referenced type is what matters. - */ - private static String stripResourceQualifier(final String typeName) { - String current = typeName.trim(); - while (true) { - if (current.startsWith("borrow<") && current.endsWith(">")) { - current = current.substring("borrow<".length(), current.length() - 1).trim(); - } else if (current.startsWith("own<") && current.endsWith(">")) { - current = current.substring("own<".length(), current.length() - 1).trim(); - } else { - return current; - } - } - } - - /** Initializes primitive types in the type cache. */ - private void initializePrimitiveTypes() { - for (final WitPrimitiveType primitive : WitPrimitiveType.values()) { - final WitType type = WitType.primitive(primitive); - typeCache.put(primitive.getWitTypeName(), type); - typeCache.put(primitive.name().toLowerCase(Locale.ROOT), type); - } - } - - /** - * Splits a comma-separated string while respecting braces and angle brackets. - * - * @param text the text to split - * @return array of split parts - */ - private static String[] splitRespectingBraces(final String text) { - final List parts = new ArrayList<>(); - final StringBuilder current = new StringBuilder(); - int braceDepth = 0; - int angleDepth = 0; - - for (final char c : text.toCharArray()) { - if (c == '{') { - braceDepth++; - current.append(c); - } else if (c == '}') { - braceDepth--; - current.append(c); - } else if (c == '<') { - angleDepth++; - current.append(c); - } else if (c == '>') { - angleDepth--; - current.append(c); - } else if (c == ',' && braceDepth == 0 && angleDepth == 0) { - parts.add(current.toString()); - current.setLength(0); - } else { - current.append(c); - } - } - - if (current.length() > 0) { - parts.add(current.toString()); - } - - return parts.toArray(new String[0]); - } - - /** Implementation of WitInterfaceDefinition. */ - private static final class WitInterfaceDefinitionImpl implements WitInterfaceDefinition { - private final String name; - private final String version; - private final String packageName; - private final Map functions; - private final Map types; - private final List imports; - private final List exports; - private final String witText; - - public WitInterfaceDefinitionImpl( - final String name, - final String version, - final String packageName, - final Map functions, - final Map types, - final List imports, - final List exports, - final String witText) { - this.name = name; - this.version = version; - this.packageName = packageName; - // LinkedHashMap-backed unmodifiable maps to preserve WIT declaration - // order across JVMs. Map.copyOf builds an immutable map with - // randomized iteration, which resurfaces as non-deterministic - // generated-source ordering. - this.functions = - java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(functions)); - this.types = java.util.Collections.unmodifiableMap(new java.util.LinkedHashMap<>(types)); - this.imports = List.copyOf(imports); - this.exports = List.copyOf(exports); - this.witText = witText; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getVersion() { - return version; - } - - @Override - public String getPackageName() { - return packageName; - } - - @Override - public List getFunctionNames() { - return new ArrayList<>(functions.keySet()); - } - - @Override - public Map getFunctions() { - return functions; - } - - @Override - public List getTypeNames() { - return new ArrayList<>(types.keySet()); - } - - @Override - public Map getTypes() { - return types; - } - - @Override - public Set getDependencies() { - return Set.copyOf(imports); - } - - @Override - public WitCompatibilityResult isCompatibleWith(final WitInterfaceDefinition other) { - if (!name.equals(other.getName())) { - return new WitCompatibilityResult( - false, - "Interface names do not match: " + name + " vs " + other.getName(), - Set.of(), - Set.of()); - } - - final Set ourFunctions = Set.copyOf(functions.keySet()); - final Set otherFunctions = Set.copyOf(other.getFunctionNames()); - final Set ourTypes = Set.copyOf(types.keySet()); - final Set otherTypes = Set.copyOf(other.getTypeNames()); - - final Set missingFunctions = new java.util.HashSet<>(ourFunctions); - missingFunctions.removeAll(otherFunctions); - final Set extraFunctions = new java.util.HashSet<>(otherFunctions); - extraFunctions.removeAll(ourFunctions); - - final Set missingTypes = new java.util.HashSet<>(ourTypes); - missingTypes.removeAll(otherTypes); - final Set extraTypes = new java.util.HashSet<>(otherTypes); - extraTypes.removeAll(ourTypes); - - final Set allMissing = new java.util.HashSet<>(); - missingFunctions.forEach(f -> allMissing.add("function:" + f)); - missingTypes.forEach(t -> allMissing.add("type:" + t)); - - final Set allExtra = new java.util.HashSet<>(); - extraFunctions.forEach(f -> allExtra.add("function:" + f)); - extraTypes.forEach(t -> allExtra.add("type:" + t)); - - final boolean compatible = allMissing.isEmpty() && allExtra.isEmpty(); - final String message = - compatible - ? "Interfaces are compatible" - : "Interfaces differ: missing=" + allMissing + ", extra=" + allExtra; - - return new WitCompatibilityResult(compatible, message, allMissing, allExtra); - } - - @Override - public String getWitText() { - return witText; - } - - @Override - public List getImportNames() { - return imports; - } - - @Override - public List getExportNames() { - return exports; - } + final WitDocument doc = WitParser.parse(witText); + if (doc.getInterfaces().isEmpty()) { + throw new BindgenException("Invalid WIT interface: no interface declaration found"); + } + // Legacy single-interface entrypoint — take the first interface (or world) + // in declaration order. Multi-interface sources are handled directly + // through WitParser today; this path just picks the top one. + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + final List exports = new ArrayList<>(pi.getFunctions().keySet()); + return new DefaultWitInterfaceDefinition( + pi.getName(), + "1.0", + packageName, + pi.getFunctions(), + pi.getTypes(), + List.of(), + exports, + witText); } } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitLexer.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitLexer.java new file mode 100644 index 0000000..742bd76 --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitLexer.java @@ -0,0 +1,287 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import java.util.ArrayList; +import java.util.List; + +/** + * Character-stream lexer for WebAssembly Interface Type (WIT) source. + * + *

      Produces a flat {@link WitToken} list from a WIT source string. Handles + * whitespace, {@code //} line comments (including doc-comment {@code ///} + * lines), block comments {@code /* … *}{@code /}, identifiers with WIT + * kebab-case rules, punctuation, {@code ->} arrow, and version literals + * after {@code @}. + * + *

      The lexer is intentionally permissive about identifier shape — it + * accepts anything the WIT grammar recognises as an identifier and lets + * {@link WitParser} enforce semantic constraints. Keywords are surfaced as + * plain {@link WitToken.Kind#IDENT} tokens so the parser can branch on the + * lexeme text. + * + *

      Together with {@link WitParser}, this replaces the retired + * {@code WitInterfaceParser} regex chain plus its {@code WitWorldPreprocessor} + * and {@code WitResourceBodyParser} wrappers — a single recursive-descent + * walk over a proper token stream now covers the WIT constructs the wrappers + * used to patch (package / world / multi-interface / use / import / export / + * resource bodies). + */ +final class WitLexer { + + private final String source; + private int pos; + private int line = 1; + private int column = 1; + + WitLexer(final String source) { + this.source = source; + } + + /** + * Tokenise the input in one pass. + * + * @return the token stream, terminated by an {@link WitToken.Kind#EOF} token + * @throws BindgenException if the input contains an unterminated string, + * unterminated block comment, or an unrecognised character + */ + List tokenize() throws BindgenException { + final List tokens = new ArrayList<>(); + while (pos < source.length()) { + skipWhitespaceAndComments(); + if (pos >= source.length()) { + break; + } + final int startLine = line; + final int startCol = column; + final char c = source.charAt(pos); + if (c == '{') { + advance(); + tokens.add(new WitToken(WitToken.Kind.LBRACE, "{", startLine, startCol)); + } else if (c == '}') { + advance(); + tokens.add(new WitToken(WitToken.Kind.RBRACE, "}", startLine, startCol)); + } else if (c == '(') { + advance(); + tokens.add(new WitToken(WitToken.Kind.LPAREN, "(", startLine, startCol)); + } else if (c == ')') { + advance(); + tokens.add(new WitToken(WitToken.Kind.RPAREN, ")", startLine, startCol)); + } else if (c == '<') { + advance(); + tokens.add(new WitToken(WitToken.Kind.LT, "<", startLine, startCol)); + } else if (c == '>') { + advance(); + tokens.add(new WitToken(WitToken.Kind.GT, ">", startLine, startCol)); + } else if (c == ',') { + advance(); + tokens.add(new WitToken(WitToken.Kind.COMMA, ",", startLine, startCol)); + } else if (c == ';') { + advance(); + tokens.add(new WitToken(WitToken.Kind.SEMI, ";", startLine, startCol)); + } else if (c == ':') { + advance(); + tokens.add(new WitToken(WitToken.Kind.COLON, ":", startLine, startCol)); + } else if (c == '=') { + advance(); + tokens.add(new WitToken(WitToken.Kind.EQ, "=", startLine, startCol)); + } else if (c == '.') { + advance(); + tokens.add(new WitToken(WitToken.Kind.DOT, ".", startLine, startCol)); + } else if (c == '/') { + advance(); + tokens.add(new WitToken(WitToken.Kind.SLASH, "/", startLine, startCol)); + } else if (c == '@') { + advance(); + tokens.add(new WitToken(WitToken.Kind.AT, "@", startLine, startCol)); + // Everything after @ up to the next whitespace / structural + // char is the version literal (e.g. `0.1.0`, `1.2.3-alpha`). + // Kept as an opaque string; downstream doesn't need semver + // semantics. Emitted as its own token so the parser branches + // on AT before consuming the VERSION. + final int vLine = line; + final int vCol = column; + if (pos < source.length() && isVersionChar(source.charAt(pos))) { + tokens.add(readVersion(vLine, vCol)); + } + } else if (c == '_') { + // Bare underscore is a valid placeholder in `result<_, err>`. + // If followed by an ident char it's part of an identifier. + if (pos + 1 < source.length() && isIdentChar(source.charAt(pos + 1))) { + tokens.add(readIdent(startLine, startCol)); + } else { + advance(); + tokens.add(new WitToken(WitToken.Kind.UNDERSCORE, "_", startLine, startCol)); + } + } else if (c == '-') { + // `->` is the func return arrow. A lone `-` doesn't appear in + // WIT except inside kebab-case identifiers (`host-provider`), + // and identifiers start with a letter, so a `-` at token-start + // is either the arrow or an error. + if (pos + 1 < source.length() && source.charAt(pos + 1) == '>') { + advance(); + advance(); + tokens.add(new WitToken(WitToken.Kind.ARROW, "->", startLine, startCol)); + } else { + throw error("unexpected '-' at token start (expected '->')", startLine, startCol); + } + } else if (c == '"') { + tokens.add(readString(startLine, startCol)); + } else if (isIdentStart(c)) { + tokens.add(readIdent(startLine, startCol)); + } else if (Character.isDigit(c)) { + tokens.add(readInt(startLine, startCol)); + } else { + throw error("unexpected character '" + c + "'", startLine, startCol); + } + } + tokens.add(new WitToken(WitToken.Kind.EOF, "", line, column)); + return tokens; + } + + private void skipWhitespaceAndComments() throws BindgenException { + while (pos < source.length()) { + final char c = source.charAt(pos); + if (Character.isWhitespace(c)) { + advance(); + } else if (c == '/' && pos + 1 < source.length() && source.charAt(pos + 1) == '/') { + // Line comment — includes `///` doc comments. + while (pos < source.length() && source.charAt(pos) != '\n') { + advance(); + } + } else if (c == '/' && pos + 1 < source.length() && source.charAt(pos + 1) == '*') { + skipBlockComment(); + } else { + return; + } + } + } + + private void skipBlockComment() throws BindgenException { + final int startLine = line; + final int startCol = column; + advance(); + advance(); + int depth = 1; + while (pos < source.length() && depth > 0) { + if (pos + 1 < source.length() + && source.charAt(pos) == '/' + && source.charAt(pos + 1) == '*') { + advance(); + advance(); + depth++; + } else if (pos + 1 < source.length() + && source.charAt(pos) == '*' + && source.charAt(pos + 1) == '/') { + advance(); + advance(); + depth--; + } else { + advance(); + } + } + if (depth > 0) { + throw error("unterminated block comment", startLine, startCol); + } + } + + private WitToken readIdent(final int startLine, final int startCol) { + // Handle explicit-identifier `%name` prefix (WIT syntax for using a + // keyword as an identifier). The `%` is stripped; the parser sees the + // raw name. + boolean explicit = false; + if (source.charAt(pos) == '%') { + explicit = true; + advance(); + } + final int start = pos; + while (pos < source.length() && isIdentChar(source.charAt(pos))) { + advance(); + } + final String text = source.substring(start, pos); + // Guard against a lone `%` with no ident after it. + if (explicit && text.isEmpty()) { + // Fall through — the parser will report an error on the empty ident. + } + return new WitToken(WitToken.Kind.IDENT, text, startLine, startCol); + } + + private WitToken readInt(final int startLine, final int startCol) { + final int start = pos; + while (pos < source.length() && Character.isDigit(source.charAt(pos))) { + advance(); + } + return new WitToken(WitToken.Kind.INT, source.substring(start, pos), startLine, startCol); + } + + private WitToken readString(final int startLine, final int startCol) throws BindgenException { + advance(); // consume opening quote + final StringBuilder sb = new StringBuilder(); + while (pos < source.length()) { + final char c = source.charAt(pos); + if (c == '\\' && pos + 1 < source.length()) { + sb.append(source.charAt(pos + 1)); + advance(); + advance(); + } else if (c == '"') { + advance(); + return new WitToken(WitToken.Kind.STRING, sb.toString(), startLine, startCol); + } else { + sb.append(c); + advance(); + } + } + throw error("unterminated string literal", startLine, startCol); + } + + private WitToken readVersion(final int startLine, final int startCol) { + final int start = pos; + while (pos < source.length() && isVersionChar(source.charAt(pos))) { + advance(); + } + return new WitToken(WitToken.Kind.VERSION, source.substring(start, pos), startLine, startCol); + } + + private static boolean isVersionChar(final char c) { + // Semver-ish body: digits, dots, hyphens, plus, letters (pre-release + // tags like `1.2.3-alpha.1+build`). + return Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '+'; + } + + private static boolean isIdentStart(final char c) { + return Character.isLetter(c) || c == '_' || c == '%'; + } + + private static boolean isIdentChar(final char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-'; + } + + private void advance() { + final char c = source.charAt(pos); + pos++; + if (c == '\n') { + line++; + column = 1; + } else { + column++; + } + } + + private BindgenException error(final String message, final int atLine, final int atCol) { + return new BindgenException("WIT lex error at " + atLine + ":" + atCol + ": " + message); + } +} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java new file mode 100644 index 0000000..1175d2e --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java @@ -0,0 +1,782 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +/** + * Recursive-descent parser for the WebAssembly Interface Type (WIT) grammar. + * + *

      Replaces the previous regex parser + {@code WitWorldPreprocessor} + + * {@code WitResourceBodyParser} wrapper chain with a single walk over a + * {@link WitLexer}-produced token stream. Consumes: + * + *

        + *
      • {@code package name[:sub][@version];} declarations + *
      • {@code world { ... }} blocks with type items directly in the + * body (post-hoist WIT shape per ADR-006). {@code import} / {@code + * export} / {@code use} clauses inside a world are accepted and + * discarded — they reference already-declared interfaces and don't + * contribute new type surface. + *
      • {@code interface { ... }} blocks + *
      • {@code use [:][/][.{}];} statements + *
      • {@code import : func(...) -> ...;} / {@code import : + * ;} / {@code import [;]} + *
      • The matching {@code export} shapes + *
      • Resource bodies: {@code resource { constructor(...); + * : func(...); [static] : func(...); }}. Methods + * land on the same {@link WitFunction} type as top-level functions + * with {@link WitFunction#isConstructor()} / {@link + * WitFunction#isStatic()} flags set — one translation path serves + * both. + *
      • Type aliases: {@code type = ;} + *
      • Standalone type declarations: {@code record}, {@code variant}, + * {@code enum}, {@code flags} + *
      • Type expressions: primitives, {@code list}, {@code option}, + * {@code result} (both operands optional), {@code tuple}, {@code borrow}, {@code own} + *
      + * + *

      Types are emitted in WIT declaration order — the {@code + * LinkedHashMap} backing on {@link WitDocument.ParsedInterface#getTypes()} + * preserves this for byte-identical cross-JVM generated output. + */ +public final class WitParser { + + private static final int MAX_WIT_TEXT_LENGTH = 1024 * 1024; // 1 MB + + private final List tokens; + private int pos; + + private WitParser(final List tokens) { + this.tokens = tokens; + } + + /** + * Parse a full WIT source document. + * + * @param source the WIT source text + * @return the parsed document + * @throws BindgenException if parsing fails + */ + public static WitDocument parse(final String source) throws BindgenException { + if (source == null) { + throw new BindgenException("WIT source is null"); + } + if (source.length() > MAX_WIT_TEXT_LENGTH) { + throw new BindgenException( + "WIT source exceeds maximum length of " + MAX_WIT_TEXT_LENGTH + " characters"); + } + final List tokens = new WitLexer(source).tokenize(); + final WitParser parser = new WitParser(tokens); + return parser.parseDocument(); + } + + // --------------------------------------------------------------------------- + // Top-level document + // --------------------------------------------------------------------------- + + private WitDocument parseDocument() throws BindgenException { + Optional packageName = Optional.empty(); + Optional packageVersion = Optional.empty(); + final List interfaces = new ArrayList<>(); + + while (!atEnd()) { + if (matchesIdent("package")) { + final String[] pkg = parsePackageDecl(); + packageName = Optional.ofNullable(pkg[0]); + packageVersion = pkg[1] == null ? Optional.empty() : Optional.of(pkg[1]); + } else if (matchesIdent("interface")) { + interfaces.add(parseInterfaceOrWorld(/* world= */ false)); + } else if (matchesIdent("world")) { + interfaces.add(parseInterfaceOrWorld(/* world= */ true)); + } else { + throw error("unexpected token at document top-level: " + peek().text); + } + } + return new WitDocument(packageName, packageVersion, interfaces); + } + + private String[] parsePackageDecl() throws BindgenException { + expectIdent("package"); + final StringBuilder name = new StringBuilder(expectIdentToken().text); + // Optional `:sub` (e.g. `wasmos:host`). + while (peek().kind == WitToken.Kind.COLON) { + advance(); + name.append(':').append(expectIdentToken().text); + } + // Optional `/path` fragment path (rare in package decls but permitted). + while (peek().kind == WitToken.Kind.SLASH) { + advance(); + name.append('/').append(expectIdentToken().text); + } + String version = null; + if (peek().kind == WitToken.Kind.AT) { + advance(); + version = expect(WitToken.Kind.VERSION).text; + } + expect(WitToken.Kind.SEMI); + return new String[] {name.toString(), version}; + } + + // --------------------------------------------------------------------------- + // Interface / world body + // --------------------------------------------------------------------------- + + private WitDocument.ParsedInterface parseInterfaceOrWorld(final boolean world) + throws BindgenException { + expectIdent(world ? "world" : "interface"); + final String name = expectIdentToken().text; + expect(WitToken.Kind.LBRACE); + + final Map types = new LinkedHashMap<>(); + final Map functions = new LinkedHashMap<>(); + final Map> resourceMethods = new LinkedHashMap<>(); + + while (peek().kind != WitToken.Kind.RBRACE && !atEnd()) { + parseInterfaceItem(types, functions, resourceMethods); + } + expect(WitToken.Kind.RBRACE); + return new WitDocument.ParsedInterface(name, world, types, functions, resourceMethods); + } + + private void parseInterfaceItem( + final Map types, + final Map functions, + final Map> resourceMethods) + throws BindgenException { + final WitToken t = peek(); + if (t.kind != WitToken.Kind.IDENT) { + throw error("expected declaration, got " + t.kind); + } + final String kw = t.text; + switch (kw) { + case "use": + skipUseClause(); + return; + case "import": + case "export": + // Import/export clauses inside a world (or interface, though rare) + // don't declare new types on their own — they reference already- + // declared interfaces or functions. We consume-and-discard the + // clause and, when it's an inline `import name: func(...) -> ...`, + // register the function so a downstream generator sees it. + parseImportExportClause(functions); + return; + case "type": + parseTypeAlias(types); + return; + case "record": + parseRecordDecl(types); + return; + case "variant": + parseVariantDecl(types); + return; + case "enum": + parseEnumDecl(types); + return; + case "flags": + parseFlagsDecl(types); + return; + case "resource": + parseResourceDecl(types, resourceMethods); + return; + default: + // Anything else must be `name: func(...)` or `name: ` — + // a plain function declaration. + parseFunctionDecl(functions); + } + } + + // ---- use / import / export ----------------------------------------------- + + private void skipUseClause() throws BindgenException { + expectIdent("use"); + // Consume until the terminating semicolon at depth 0. `use` clauses + // may contain `{a, b, c}` item lists so we track brace depth. + int depth = 0; + while (!atEnd()) { + final WitToken t = peek(); + if (t.kind == WitToken.Kind.LBRACE) { + depth++; + advance(); + } else if (t.kind == WitToken.Kind.RBRACE) { + depth--; + advance(); + } else if (t.kind == WitToken.Kind.SEMI && depth == 0) { + advance(); + return; + } else { + advance(); + } + } + throw error("unterminated 'use' clause"); + } + + private void parseImportExportClause(final Map functions) + throws BindgenException { + // Consume `import` / `export`. + advance(); + // Two shapes: + // 1. `import ;` — bare interface reference + // 2. `import : ;` — inline binding + // 3. `import : interface { ... }` — inline interface (dropped) + // (import / export take the same grammar.) + if (peek().kind != WitToken.Kind.IDENT) { + // Anything else — swallow the rest of the clause. + skipToSemicolon(); + return; + } + final String name = expectIdentToken().text; + if (peek().kind == WitToken.Kind.SEMI) { + advance(); + return; + } + if (peek().kind != WitToken.Kind.COLON) { + // Something we don't understand — best-effort skip. + skipToSemicolon(); + return; + } + expect(WitToken.Kind.COLON); + // If it's `func(...)`, register it as a function (matches how the + // regex parser used to walk `name: func(...)` inside a world body + // before the wrappers stripped it). + if (matchesIdent("func")) { + final WitFunction fn = parseFuncSignature(name); + functions.put(name, fn); + expect(WitToken.Kind.SEMI); + return; + } + if (matchesIdent("interface")) { + // `import name: interface { ... }` — drop the inline body wholesale. + advance(); // consume `interface` + expect(WitToken.Kind.LBRACE); + int depth = 1; + while (!atEnd() && depth > 0) { + if (peek().kind == WitToken.Kind.LBRACE) { + depth++; + } else if (peek().kind == WitToken.Kind.RBRACE) { + depth--; + } + advance(); + } + // Optional trailing semicolon. + if (peek().kind == WitToken.Kind.SEMI) { + advance(); + } + return; + } + // `import name: ;` — swallow the type expression, we + // don't need it downstream. + skipToSemicolon(); + } + + private void skipToSemicolon() { + int paren = 0; + int angle = 0; + int brace = 0; + while (!atEnd()) { + final WitToken t = peek(); + if (t.kind == WitToken.Kind.LPAREN) { + paren++; + } else if (t.kind == WitToken.Kind.RPAREN) { + paren = Math.max(0, paren - 1); + } else if (t.kind == WitToken.Kind.LT) { + angle++; + } else if (t.kind == WitToken.Kind.GT) { + angle = Math.max(0, angle - 1); + } else if (t.kind == WitToken.Kind.LBRACE) { + brace++; + } else if (t.kind == WitToken.Kind.RBRACE) { + if (brace == 0) { + return; + } + brace--; + } else if (t.kind == WitToken.Kind.SEMI && paren == 0 && angle == 0 && brace == 0) { + advance(); + return; + } + advance(); + } + } + + // ---- type declarations --------------------------------------------------- + + private void parseTypeAlias(final Map types) throws BindgenException { + expectIdent("type"); + final String name = expectIdentToken().text; + expect(WitToken.Kind.EQ); + // Peek to see if it's a compound: `type X = record { ... }` / + // `type X = variant { ... }` etc. Otherwise it's a type expression. + if (matchesIdent("record")) { + advance(); + types.put(name, parseRecordBody(name)); + } else if (matchesIdent("variant")) { + advance(); + types.put(name, parseVariantBody(name)); + } else if (matchesIdent("enum")) { + advance(); + types.put(name, parseEnumBody(name)); + } else if (matchesIdent("flags")) { + advance(); + types.put(name, parseFlagsBody(name)); + } else if (matchesIdent("resource")) { + advance(); + // `type X = resource;` — bare-resource alias (rare but legal). + types.put(name, WitType.resource(name, name)); + } else { + final WitType expr = parseTypeExpression(types); + // If the returned type is a primitive/list/option/result/tuple, name + // it with the alias name so the generator emits it under that name. + // For record/variant/enum/flags/resource references we keep the + // referent type — the alias name isn't the same as the referent. + types.put(name, renameIfPrimitiveLike(name, expr)); + } + // Old regex parser accepted `type X = Y` without trailing `;` at + // end of body — keep that tolerance. + consumeOptionalSemi(); + } + + private WitType renameIfPrimitiveLike(final String aliasName, final WitType src) { + final WitTypeCategory cat = src.getKind().getCategory(); + switch (cat) { + case PRIMITIVE: + case LIST: + case OPTION: + case RESULT: + case TUPLE: + case RESOURCE: + // Container / primitive aliases keep the alias name where possible. + // We rebuild only for primitives (WitType.primitive() carries its + // own textual name); list/option/result/tuple were built with + // container names by parseTypeExpression already. + if (cat == WitTypeCategory.PRIMITIVE) { + // Represent an alias-to-primitive as-is; the alias name is what + // matters for downstream deduplication. + return src; + } + return src; + default: + return src; + } + } + + private void parseRecordDecl(final Map types) throws BindgenException { + expectIdent("record"); + final String name = expectIdentToken().text; + types.put(name, parseRecordBody(name)); + } + + private WitType parseRecordBody(final String name) throws BindgenException { + expect(WitToken.Kind.LBRACE); + final Map fields = new LinkedHashMap<>(); + while (peek().kind != WitToken.Kind.RBRACE && !atEnd()) { + final String fieldName = expectIdentToken().text; + expect(WitToken.Kind.COLON); + final WitType fieldType = parseTypeExpression(null); + fields.put(fieldName, fieldType); + // Trailing comma is optional; both `,` and end-of-body terminate. + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + expect(WitToken.Kind.RBRACE); + return WitType.record(name, fields); + } + + private void parseVariantDecl(final Map types) throws BindgenException { + expectIdent("variant"); + final String name = expectIdentToken().text; + types.put(name, parseVariantBody(name)); + } + + private WitType parseVariantBody(final String name) throws BindgenException { + expect(WitToken.Kind.LBRACE); + final Map> cases = new LinkedHashMap<>(); + while (peek().kind != WitToken.Kind.RBRACE && !atEnd()) { + final String caseName = expectIdentToken().text; + Optional payload = Optional.empty(); + if (peek().kind == WitToken.Kind.LPAREN) { + advance(); + payload = Optional.of(parseTypeExpression(null)); + expect(WitToken.Kind.RPAREN); + } + cases.put(caseName, payload); + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + expect(WitToken.Kind.RBRACE); + return WitType.variant(name, cases); + } + + private void parseEnumDecl(final Map types) throws BindgenException { + expectIdent("enum"); + final String name = expectIdentToken().text; + types.put(name, parseEnumBody(name)); + } + + private WitType parseEnumBody(final String name) throws BindgenException { + expect(WitToken.Kind.LBRACE); + final List values = new ArrayList<>(); + while (peek().kind != WitToken.Kind.RBRACE && !atEnd()) { + values.add(expectIdentToken().text); + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + expect(WitToken.Kind.RBRACE); + return WitType.enumType(name, values); + } + + private void parseFlagsDecl(final Map types) throws BindgenException { + expectIdent("flags"); + final String name = expectIdentToken().text; + types.put(name, parseFlagsBody(name)); + } + + private WitType parseFlagsBody(final String name) throws BindgenException { + expect(WitToken.Kind.LBRACE); + final List flags = new ArrayList<>(); + while (peek().kind != WitToken.Kind.RBRACE && !atEnd()) { + flags.add(expectIdentToken().text); + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + expect(WitToken.Kind.RBRACE); + return WitType.flags(name, flags); + } + + // ---- resource declarations ----------------------------------------------- + + private void parseResourceDecl( + final Map types, final Map> resourceMethods) + throws BindgenException { + expectIdent("resource"); + final String name = expectIdentToken().text; + if (peek().kind == WitToken.Kind.SEMI) { + // `resource X;` — no body. + advance(); + types.put(name, WitType.resource(name, name)); + return; + } + expect(WitToken.Kind.LBRACE); + final List methods = new ArrayList<>(); + while (peek().kind != WitToken.Kind.RBRACE && !atEnd()) { + methods.add(parseResourceMethod(name)); + // WIT tolerates both `;` and `,` between resource-body items, plus + // a trailing separator before `}`. + if (peek().kind == WitToken.Kind.SEMI || peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + expect(WitToken.Kind.RBRACE); + types.put(name, WitType.resource(name, name)); + resourceMethods.put(name, methods); + } + + private WitFunction parseResourceMethod(final String resourceName) throws BindgenException { + if (matchesIdent("constructor")) { + advance(); + expect(WitToken.Kind.LPAREN); + final List params = parseFuncParameters(); + expect(WitToken.Kind.RPAREN); + return WitFunction.resourceMethod( + "constructor", + params, + List.of(), + /* isConstructor= */ true, + /* isStatic= */ false, + /* isAsync= */ false, + Optional.empty()); + } + final String methodName = expectIdentToken().text; + expect(WitToken.Kind.COLON); + boolean isStatic = false; + if (matchesIdent("static")) { + advance(); + isStatic = true; + } + expectIdent("func"); + expect(WitToken.Kind.LPAREN); + final List params = parseFuncParameters(); + expect(WitToken.Kind.RPAREN); + final List returnTypes = parseOptionalReturnTypes(); + return WitFunction.resourceMethod( + methodName, params, returnTypes, false, isStatic, false, Optional.empty()); + } + + // ---- function declarations ------------------------------------------------ + + private void parseFunctionDecl(final Map functions) throws BindgenException { + final String name = expectIdentToken().text; + expect(WitToken.Kind.COLON); + // Optional `async` keyword before `func`. + boolean isAsync = false; + if (matchesIdent("async")) { + advance(); + isAsync = true; + } + if (!matchesIdent("func")) { + // `name: ;` — not a function, but a typed binding. We + // don't have a first-class binding-type slot; skip it so the + // caller doesn't crash. This matches the tolerance of the old + // wrapper chain. + skipToSemicolon(); + return; + } + final WitFunction fn = parseFuncSignature(name); + functions.put(name, fn.getName().equals(name) ? withAsync(fn, isAsync) : fn); + // Trailing `;` is required by strict WIT but the old regex parser + // tolerated its absence at end-of-body. Preserve that laxness so + // legacy fixtures (`add: func(...) -> s32\n}`) keep parsing. + consumeOptionalSemi(); + } + + private void consumeOptionalSemi() { + if (peek().kind == WitToken.Kind.SEMI) { + advance(); + } + } + + private WitFunction withAsync(final WitFunction fn, final boolean async) { + if (!async) { + return fn; + } + return new WitFunction(fn.getName(), fn.getParameters(), fn.getReturnTypes(), true, fn.getDocumentation()); + } + + private WitFunction parseFuncSignature(final String name) throws BindgenException { + expectIdent("func"); + expect(WitToken.Kind.LPAREN); + final List params = parseFuncParameters(); + expect(WitToken.Kind.RPAREN); + final List returnTypes = parseOptionalReturnTypes(); + return new WitFunction(name, params, returnTypes, false, Optional.empty()); + } + + private List parseFuncParameters() throws BindgenException { + final List params = new ArrayList<>(); + while (peek().kind != WitToken.Kind.RPAREN && !atEnd()) { + final String pname = expectIdentToken().text; + expect(WitToken.Kind.COLON); + final WitType ptype = parseTypeExpression(null); + params.add(new WitParameter(pname, ptype, false, Optional.empty())); + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + return params; + } + + private List parseOptionalReturnTypes() throws BindgenException { + if (peek().kind != WitToken.Kind.ARROW) { + return List.of(); + } + advance(); + final WitType retType = parseTypeExpression(null); + return List.of(retType); + } + + // --------------------------------------------------------------------------- + // Type expressions + // --------------------------------------------------------------------------- + + /** + * Parse a type expression: primitive, container ({@code list}, + * {@code option}, {@code result}, {@code tuple}), + * lifetime-qualified ({@code borrow}, {@code own}), or named + * reference (record / variant / enum / flags / resource / alias). + * + *

      Named references are emitted as {@code WitType.resource(name, name)} + * placeholders when they don't match a primitive — downstream + * translation ({@code CodeGenerator.resolveOrConvertType}) reroutes them + * to a {@code BindgenType.reference} once the type table is materialised. + * This mirrors what the old regex parser did for unknown names and + * keeps the byte-identical generator output. + * + * @param scope currently unused; reserved for future scope-aware lookup + * (records-referencing-records etc.). Pass {@code null} at call sites. + */ + private WitType parseTypeExpression(final Map scope) throws BindgenException { + final WitToken t = peek(); + if (t.kind != WitToken.Kind.IDENT) { + throw error("expected type expression, got " + t.kind); + } + final String head = t.text; + switch (head) { + case "list": + advance(); + expect(WitToken.Kind.LT); + final WitType listInner = parseTypeExpression(scope); + expect(WitToken.Kind.GT); + return WitType.list(listInner); + case "option": + advance(); + expect(WitToken.Kind.LT); + final WitType optionInner = parseTypeExpression(scope); + expect(WitToken.Kind.GT); + return WitType.option(optionInner); + case "result": + advance(); + return parseResultTail(scope); + case "tuple": + advance(); + expect(WitToken.Kind.LT); + final List elements = new ArrayList<>(); + while (peek().kind != WitToken.Kind.GT && !atEnd()) { + elements.add(parseTypeExpression(scope)); + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + } + } + expect(WitToken.Kind.GT); + return WitType.tuple(elements); + case "borrow": + case "own": + advance(); + expect(WitToken.Kind.LT); + final WitType inner = parseTypeExpression(scope); + expect(WitToken.Kind.GT); + // Lifetime qualifiers unwrap at the generator level. + return inner; + case "record": + // Inline anonymous record — legal in a type-expression position + // (uncommon but permitted by the WIT grammar). Use a synthetic + // name; downstream generation names it from context. + advance(); + return parseRecordBody(""); + case "variant": + advance(); + return parseVariantBody(""); + case "enum": + advance(); + return parseEnumBody(""); + case "flags": + advance(); + return parseFlagsBody(""); + default: + break; + } + // Primitive lookup. + final WitPrimitiveType prim = tryPrimitive(head); + if (prim != null) { + advance(); + return WitType.primitive(prim); + } + // Named reference — emit a resource placeholder so downstream lookup + // can convert it against the interface's type table. + advance(); + return WitType.resource(head, head); + } + + private WitType parseResultTail(final Map scope) throws BindgenException { + if (peek().kind != WitToken.Kind.LT) { + // `result` with no type parameters — both slots empty. + return WitType.result(Optional.empty(), Optional.empty()); + } + advance(); // consume `<` + Optional okType = Optional.empty(); + Optional errType = Optional.empty(); + // First slot: either `_` (unit) or a type expression. + if (peek().kind == WitToken.Kind.UNDERSCORE) { + advance(); + } else if (peek().kind == WitToken.Kind.GT) { + // `result<>` — empty tuple, both slots empty. + } else { + okType = Optional.of(parseTypeExpression(scope)); + } + if (peek().kind == WitToken.Kind.COMMA) { + advance(); + if (peek().kind == WitToken.Kind.UNDERSCORE) { + advance(); + } else if (peek().kind != WitToken.Kind.GT) { + errType = Optional.of(parseTypeExpression(scope)); + } + } + expect(WitToken.Kind.GT); + return WitType.result(okType, errType); + } + + private static WitPrimitiveType tryPrimitive(final String name) { + try { + return WitPrimitiveType.fromString(name.toLowerCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + + // --------------------------------------------------------------------------- + // Token stream helpers + // --------------------------------------------------------------------------- + + private WitToken peek() { + return tokens.get(pos); + } + + private WitToken advance() { + final WitToken t = tokens.get(pos); + if (pos < tokens.size() - 1) { + pos++; + } + return t; + } + + private boolean atEnd() { + return peek().kind == WitToken.Kind.EOF; + } + + private boolean matchesIdent(final String text) { + final WitToken t = peek(); + return t.kind == WitToken.Kind.IDENT && t.text.equals(text); + } + + private WitToken expect(final WitToken.Kind kind) throws BindgenException { + final WitToken t = peek(); + if (t.kind != kind) { + throw error("expected " + kind + " but got " + t.kind + " (\"" + t.text + "\")"); + } + return advance(); + } + + private WitToken expectIdentToken() throws BindgenException { + final WitToken t = peek(); + if (t.kind != WitToken.Kind.IDENT) { + throw error("expected identifier but got " + t.kind + " (\"" + t.text + "\")"); + } + return advance(); + } + + private void expectIdent(final String text) throws BindgenException { + final WitToken t = peek(); + if (t.kind != WitToken.Kind.IDENT || !t.text.equals(text)) { + throw error("expected keyword '" + text + "' but got \"" + t.text + "\""); + } + advance(); + } + + private BindgenException error(final String message) { + final WitToken t = peek(); + return new BindgenException("WIT parse error at " + t.line + ":" + t.column + ": " + message); + } +} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java deleted file mode 100644 index a3e085b..0000000 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParser.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2026 Tegmentum AI - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ai.tegmentum.webassembly4j.bindgen.wit; - -import ai.tegmentum.webassembly4j.bindgen.BindgenException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * Parses the body of a WIT {@code resource} declaration into a list of - * {@link WitFunction}s carrying the {@link WitFunction#isConstructor()} / - * {@link WitFunction#isStatic()} flags. - * - *

      Understands the three declaration shapes: - * - *

        - *
      • {@code constructor(params)} - *
      • {@code name: static func(params) -> ret} - *
      • {@code name: func(params) -> ret} - *
      - * - *

      Each declaration is terminated by a comma or semicolon (both are - * accepted). Doc comments and line comments are stripped before parsing. - * - *

      This lives alongside {@link WitInterfaceParser} rather than inside it - * because the interface parser's regex machinery can't cleanly express - * the {@code constructor}/{@code static func} shapes; keeping it separate - * makes the split cheap to grow. - * - *

      Parameter and return WitTypes are carried as raw type expressions - * wrapped in {@link WitType#resource(String, String)} placeholders — the - * WIT symbol table isn't visible at this scope, so type resolution is - * deferred to the downstream translation pass (see - * {@code CodeGenerator.convertWitFunction}). This lets a single - * {@link WitFunction} translation path serve both interface-level functions - * and resource-body methods. - */ -public final class WitResourceBodyParser { - - private WitResourceBodyParser() {} - - /** - * Parse the body text of a WIT resource declaration into a list of - * {@link WitFunction}s. Constructor / static / instance shape is captured - * on {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()}; - * both {@code false} means an instance method. - * - * @param resourceName the name of the resource (used for diagnostics) - * @param body the body between the resource's outer braces - * @return the parsed methods, in declaration order - * @throws BindgenException if the body cannot be parsed - */ - public static List parse(final String resourceName, final String body) - throws BindgenException { - final String stripped = stripLineComments(body); - final List decls = splitDeclarations(stripped); - final List methods = new ArrayList<>(); - for (final String decl : decls) { - final String trimmed = decl.trim(); - if (trimmed.isEmpty()) { - continue; - } - methods.add(parseDeclaration(resourceName, trimmed)); - } - return methods; - } - - private static String stripLineComments(final String source) { - final StringBuilder out = new StringBuilder(source.length()); - for (final String line : source.split("\n", -1)) { - int cut = -1; - boolean inString = false; - for (int i = 0; i < line.length() - 1; i++) { - final char c = line.charAt(i); - if (c == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { - inString = !inString; - } else if (!inString && c == '/' && line.charAt(i + 1) == '/') { - cut = i; - break; - } - } - if (cut < 0) { - out.append(line).append('\n'); - } else { - out.append(line, 0, cut).append('\n'); - } - } - return out.toString(); - } - - /** - * Split a resource body on top-level {@code ,} or {@code ;} terminators. - * Angle-bracket and paren nesting are respected so {@code list} - * and {@code func(a: u32, b: u32)} don't split mid-signature. - */ - private static List splitDeclarations(final String body) { - final List out = new ArrayList<>(); - final StringBuilder current = new StringBuilder(); - int angle = 0; - int paren = 0; - boolean inString = false; - for (int i = 0; i < body.length(); i++) { - final char c = body.charAt(i); - if (c == '"' && (i == 0 || body.charAt(i - 1) != '\\')) { - inString = !inString; - current.append(c); - continue; - } - if (!inString) { - if (c == '<') { - angle++; - } else if (c == '>') { - angle = Math.max(0, angle - 1); - } else if (c == '(') { - paren++; - } else if (c == ')') { - paren = Math.max(0, paren - 1); - } - } - if (!inString && angle == 0 && paren == 0 && (c == ',' || c == ';')) { - out.add(current.toString()); - current.setLength(0); - } else { - current.append(c); - } - } - if (current.length() > 0) { - out.add(current.toString()); - } - return out; - } - - private static WitFunction parseDeclaration(final String resourceName, final String decl) - throws BindgenException { - if (decl.startsWith("constructor")) { - final int open = decl.indexOf('('); - final int close = findMatchingParen(decl, open); - if (open < 0 || close < 0) { - throw new BindgenException( - "Malformed constructor on resource " + resourceName + ": " + decl); - } - final String params = decl.substring(open + 1, close); - return WitFunction.resourceMethod( - "constructor", - parseParameters(params), - List.of(), - /* isConstructor= */ true, - /* isStatic= */ false, - /* isAsync= */ false, - Optional.empty()); - } - final int colon = decl.indexOf(':'); - if (colon < 0) { - throw new BindgenException( - "Resource method missing ':' on resource " + resourceName + ": " + decl); - } - final String name = decl.substring(0, colon).trim(); - final String rhs = decl.substring(colon + 1).trim(); - final boolean isStatic = rhs.startsWith("static "); - final String afterStatic = isStatic ? rhs.substring("static ".length()).trim() : rhs; - if (!afterStatic.startsWith("func")) { - throw new BindgenException( - "Expected 'func' after ':' on resource " + resourceName + "." + name + ": " + rhs); - } - final int open = afterStatic.indexOf('('); - final int close = findMatchingParen(afterStatic, open); - if (open < 0 || close < 0) { - throw new BindgenException( - "Malformed func signature on resource " + resourceName + "." + name); - } - final String params = afterStatic.substring(open + 1, close); - final String tail = afterStatic.substring(close + 1).trim(); - // Carry the raw return-type expression as a WitType.resource placeholder; - // downstream translation (CodeGenerator.convertWitFunction) resolves the - // expression against the interface's already-materialized type table via - // the same resolveTypeExpression path used for parameters. - final List returnTypes; - if (tail.startsWith("->")) { - final String returnExpr = tail.substring(2).trim(); - returnTypes = List.of(WitType.resource(returnExpr, returnExpr)); - } else { - returnTypes = List.of(); - } - return WitFunction.resourceMethod( - name, - parseParameters(params), - returnTypes, - /* isConstructor= */ false, - isStatic, - /* isAsync= */ false, - Optional.empty()); - } - - private static int findMatchingParen(final String s, final int open) { - if (open < 0) { - return -1; - } - int depth = 0; - boolean inString = false; - for (int i = open; i < s.length(); i++) { - final char c = s.charAt(i); - if (c == '"' && (i == 0 || s.charAt(i - 1) != '\\')) { - inString = !inString; - continue; - } - if (inString) { - continue; - } - if (c == '(') { - depth++; - } else if (c == ')') { - depth--; - if (depth == 0) { - return i; - } - } - } - return -1; - } - - private static List parseParameters(final String params) throws BindgenException { - final List out = new ArrayList<>(); - if (params == null || params.trim().isEmpty()) { - return out; - } - for (final String piece : splitTopLevelCommas(params)) { - final String trimmed = piece.trim(); - if (trimmed.isEmpty()) { - continue; - } - final int colon = trimmed.indexOf(':'); - if (colon < 0) { - throw new BindgenException("Invalid parameter (no ':'): " + piece); - } - final String name = trimmed.substring(0, colon).trim(); - final String typeExpr = trimmed.substring(colon + 1).trim(); - // Store the raw type expression as the WitType name; the downstream - // code generator resolves it against the interface's type table. - out.add(new WitParameter(name, WitType.resource(typeExpr, typeExpr), false, Optional.empty())); - } - return out; - } - - private static List splitTopLevelCommas(final String s) { - final List out = new ArrayList<>(); - int angle = 0; - int paren = 0; - boolean inString = false; - final StringBuilder current = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - final char c = s.charAt(i); - if (c == '"' && (i == 0 || s.charAt(i - 1) != '\\')) { - inString = !inString; - current.append(c); - continue; - } - if (!inString) { - if (c == '<') { - angle++; - } else if (c == '>') { - angle = Math.max(0, angle - 1); - } else if (c == '(') { - paren++; - } else if (c == ')') { - paren = Math.max(0, paren - 1); - } - } - if (!inString && angle == 0 && paren == 0 && c == ',') { - out.add(current.toString()); - current.setLength(0); - } else { - current.append(c); - } - } - if (current.length() > 0) { - out.add(current.toString()); - } - return out; - } -} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitToken.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitToken.java new file mode 100644 index 0000000..25466de --- /dev/null +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitToken.java @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +/** + * A single lexical token emitted by {@link WitLexer}. + * + *

      Keywords (e.g. {@code interface}, {@code world}, {@code resource}) + * are surfaced as {@link Kind#IDENT} tokens; {@link WitParser} matches + * them by textual equality. Punctuation and structural tokens carry + * their own kinds so the parser can branch on kind without re-lexing. + * + *

      Line + column are 1-based and preserved for diagnostic messages. + */ +final class WitToken { + + /** Distinct token kinds produced by the lexer. */ + enum Kind { + /** An identifier or keyword — text carries the exact lexeme. */ + IDENT, + /** Integer literal (used inside {@code @version} strings). */ + INT, + /** Version literal after {@code @}, e.g. {@code 1.2.3} or {@code 0.1.0-pre}. */ + VERSION, + /** {@code "..."} — carries the unquoted content. */ + STRING, + LBRACE, + RBRACE, + LPAREN, + RPAREN, + LT, + GT, + COMMA, + SEMI, + COLON, + EQ, + ARROW, + DOT, + SLASH, + AT, + UNDERSCORE, + /** End-of-input sentinel. */ + EOF, + } + + final Kind kind; + final String text; + final int line; + final int column; + + WitToken(final Kind kind, final String text, final int line, final int column) { + this.kind = kind; + this.text = text; + this.line = line; + this.column = column; + } + + @Override + public String toString() { + return kind + "(" + text + ")@" + line + ":" + column; + } +} diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java deleted file mode 100644 index 4f20e7a..0000000 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitWorldPreprocessor.java +++ /dev/null @@ -1,472 +0,0 @@ -/* - * Copyright 2026 Tegmentum AI - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ai.tegmentum.webassembly4j.bindgen.wit; - -import ai.tegmentum.webassembly4j.bindgen.BindgenException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Preprocesses a full WIT world source (package + optional {@code world} blocks + - * multiple {@code interface} blocks + resource declarations with bodies) into a - * flat list of {@link Interface} records the existing regex-based - * {@link WitInterfaceParser} can consume. - * - *

      Behaviour: - * - *

        - *
      • {@code package X@Y;} declarations are removed. - *
      • {@code world X { ... }} blocks are projected as {@link Interface} - * entries with {@link Interface#isWorld()} true, so world-level type - * and resource declarations still reach the generator. Their - * {@code import} / {@code export} / {@code use} clauses are stripped - * from the body — they refer to already-declared interfaces and don't - * contribute any new type surface. Downstream, world entries are - * flattened into top-level model types instead of being wrapped in a - * Java interface carrier (a hoisted world has no functions of its - * own; the empty carrier is dead weight). - *
      • {@code interface X { ... }} blocks are split into individual - * {@link Interface} entries. The existing parser is single-interface; - * this splitter fans a world file out into a stream of what it expects. - *
      • {@code resource X { body }} declarations inside a world or interface - * body have their bodies stripped and returned in a side-map keyed by - * resource name. The body then contains a bare {@code resource X;} - * token so {@link WitInterfaceParser} still sees the resource type - * declaration but doesn't choke on the constructor / static / method - * syntax it doesn't understand. - *
      - */ -public final class WitWorldPreprocessor { - - private WitWorldPreprocessor() {} - - /** - * A single interface (or hoisted world) extracted from a WIT world source. - * - *

      World-projected entries have {@link #isWorld()} true; callers use this - * to decide whether to emit a Java interface carrier or to hoist the - * entry's types to top-level model types. - */ - public static final class Interface { - private final String name; - private final String body; - private final Map resourceBodies; - private final boolean world; - - Interface( - final String name, - final String body, - final Map resourceBodies, - final boolean world) { - this.name = name; - this.body = body; - this.resourceBodies = resourceBodies; - this.world = world; - } - - /** The interface's name as written in WIT (e.g. {@code embedder-api}). */ - public String getName() { - return name; - } - - /** - * The interface body with resource bodies stripped — safe to hand to - * {@link WitInterfaceParser#parseInterface(String, String)} after - * wrapping in {@code interface name { ... }}. - */ - public String getBody() { - return body; - } - - /** - * Map from resource name to its unparsed body text. Callers that want - * constructor / static / instance-method definitions parse these - * themselves. Empty when the interface declares no resources with - * bodies. - */ - public Map getResourceBodies() { - return resourceBodies; - } - - /** - * True when this entry was projected from a {@code world X { ... }} - * block (rather than an {@code interface X { ... }} block). Worlds - * declare types + resources at the top level under ADR-006's hoisted - * shape; they have no functions of their own and don't warrant an - * empty Java interface carrier. - */ - public boolean isWorld() { - return world; - } - } - - /** - * Split a full WIT source into per-interface entries. Worlds are projected - * as entries with {@link Interface#isWorld()} true so callers can decide - * whether to emit a Java interface carrier or hoist the world's types to - * top-level model types. - * - * @param witText the raw WIT world source - * @return the interfaces + hoisted worlds contained in the source, in - * declaration order - * @throws BindgenException if brace matching fails - */ - public static List preprocess(final String witText) throws BindgenException { - final String withoutComments = stripLineComments(witText); - final String withoutPackage = stripPackage(withoutComments); - final List worlds = new ArrayList<>(); - final String withoutWorlds = extractTopLevelBlocks(withoutPackage, "world", worlds); - final List result = new ArrayList<>(splitInterfaces(withoutWorlds, false)); - for (final Entry world : worlds) { - final String cleanedBody = stripImportsAndExports(world.body); - final Map resources = new LinkedHashMap<>(); - final String bodyStripped = extractResourceBodies(cleanedBody, resources); - result.add(new Interface(world.name, bodyStripped, resources, true)); - } - return result; - } - - private static final class Entry { - final String name; - final String body; - - Entry(final String name, final String body) { - this.name = name; - this.body = body; - } - } - - /** - * Remove {@code //…} comments (respecting string literals). Doc comments - * ({@code ///…}) are removed by the same pass since they start with - * {@code //} too. - */ - private static String stripLineComments(final String source) { - final StringBuilder out = new StringBuilder(source.length()); - for (final String line : source.split("\n", -1)) { - int cut = -1; - boolean inString = false; - for (int i = 0; i < line.length() - 1; i++) { - final char c = line.charAt(i); - if (c == '"' && (i == 0 || line.charAt(i - 1) != '\\')) { - inString = !inString; - } else if (!inString && c == '/' && line.charAt(i + 1) == '/') { - cut = i; - break; - } - } - if (cut < 0) { - out.append(line).append('\n'); - } else { - out.append(line, 0, cut).append('\n'); - } - } - return out.toString(); - } - - /** Strip {@code package name@version;} and {@code package name;} lines. */ - private static String stripPackage(final String source) { - return source.replaceAll("(?m)^\\s*package\\s+[^;]+;\\s*$", ""); - } - - /** - * Remove every top-level {@code keyword name { … }} block from the source - * and, when {@code sink} is non-null, record each block's name and body - * text into it. Brace-balanced so nested braces don't confuse the scan. - */ - private static String extractTopLevelBlocks( - final String source, final String keyword, final List sink) throws BindgenException { - final StringBuilder out = new StringBuilder(source.length()); - int i = 0; - while (i < source.length()) { - final int idx = source.indexOf(keyword, i); - if (idx < 0) { - out.append(source, i, source.length()); - break; - } - if (!isKeywordBoundary(source, idx, keyword.length())) { - out.append(source, i, idx + 1); - i = idx + 1; - continue; - } - // Read the identifier that follows. - int cursor = idx + keyword.length(); - while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { - cursor++; - } - final int nameStart = cursor; - while (cursor < source.length() && isIdentChar(source.charAt(cursor))) { - cursor++; - } - final String name = source.substring(nameStart, cursor); - while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { - cursor++; - } - if (cursor >= source.length() || source.charAt(cursor) != '{') { - // No body — leave the fragment alone, we may be scanning garbage. - out.append(source, i, source.length()); - break; - } - final int braceOpen = cursor; - out.append(source, i, idx); - final int braceClose = matchBrace(source, braceOpen); - if (braceClose < 0) { - throw new BindgenException( - "Unbalanced braces in " + keyword + " block starting at offset " + braceOpen); - } - if (sink != null) { - sink.add(new Entry(name, source.substring(braceOpen + 1, braceClose))); - } - i = braceClose + 1; - } - return out.toString(); - } - - /** - * Strip {@code import} / {@code export} / {@code use} clauses from a world - * body. These reference already-declared interfaces and don't add any new - * type surface bindgen needs to project. Handled forms: - * - *

        - *
      • {@code import name;} - *
      • {@code export name;} - *
      • {@code import name: interface { ... }} (inline interface — the - * inline body is dropped along with the whole clause) - *
      • {@code use pkg/iface.{names...};} - *
      - * - * Unrecognized {@code import}/{@code export} clauses fall through untouched; - * the regex-based downstream parser ignores lines it doesn't understand. - */ - private static String stripImportsAndExports(final String body) throws BindgenException { - final StringBuilder out = new StringBuilder(body.length()); - int i = 0; - while (i < body.length()) { - final int nextImport = findKeyword(body, "import", i); - final int nextExport = findKeyword(body, "export", i); - final int nextUse = findKeyword(body, "use", i); - int next = -1; - for (final int candidate : new int[] {nextImport, nextExport, nextUse}) { - if (candidate >= 0 && (next < 0 || candidate < next)) { - next = candidate; - } - } - if (next < 0) { - out.append(body, i, body.length()); - break; - } - out.append(body, i, next); - // Find the clause terminator: either a `;` at nesting level 0 or a - // matching `}` for an inline `import name: interface { ... }`. - int cursor = next; - int depth = 0; - boolean inString = false; - while (cursor < body.length()) { - final char c = body.charAt(cursor); - if (c == '"' && (cursor == 0 || body.charAt(cursor - 1) != '\\')) { - inString = !inString; - } else if (!inString) { - if (c == '{') { - depth++; - } else if (c == '}') { - depth--; - } else if (c == ';' && depth == 0) { - cursor++; - break; - } - } - cursor++; - } - i = cursor; - } - return out.toString(); - } - - /** - * Find the next occurrence of {@code keyword} at a keyword boundary in - * {@code source} starting from {@code from}. Returns -1 if not found. - */ - private static int findKeyword(final String source, final String keyword, final int from) { - int cursor = from; - while (cursor < source.length()) { - final int idx = source.indexOf(keyword, cursor); - if (idx < 0) { - return -1; - } - if (isKeywordBoundary(source, idx, keyword.length())) { - return idx; - } - cursor = idx + 1; - } - return -1; - } - - private static boolean isKeywordBoundary(final String source, final int at, final int keywordLen) { - if (at > 0) { - final char before = source.charAt(at - 1); - if (Character.isLetterOrDigit(before) || before == '-' || before == '_') { - return false; - } - } - final int after = at + keywordLen; - if (after < source.length()) { - final char afterCh = source.charAt(after); - if (Character.isLetterOrDigit(afterCh) || afterCh == '-' || afterCh == '_') { - return false; - } - } - return true; - } - - private static int matchBrace(final String source, final int open) { - int depth = 0; - boolean inString = false; - for (int i = open; i < source.length(); i++) { - final char c = source.charAt(i); - if (c == '"' && (i == 0 || source.charAt(i - 1) != '\\')) { - inString = !inString; - continue; - } - if (inString) { - continue; - } - if (c == '{') { - depth++; - } else if (c == '}') { - depth--; - if (depth == 0) { - return i; - } - } - } - return -1; - } - - /** - * Walk the (comment-and-world-free) source and split it into - * {@code interface X { body }} blocks. Resource declarations inside each - * body are extracted and returned separately. - */ - private static List splitInterfaces(final String source, final boolean world) - throws BindgenException { - final List result = new ArrayList<>(); - int i = 0; - while (i < source.length()) { - final int idx = source.indexOf("interface", i); - if (idx < 0) { - break; - } - if (!isKeywordBoundary(source, idx, "interface".length())) { - i = idx + 1; - continue; - } - // Read the identifier that follows. - int cursor = idx + "interface".length(); - while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { - cursor++; - } - final int nameStart = cursor; - while (cursor < source.length() && isIdentChar(source.charAt(cursor))) { - cursor++; - } - final String name = source.substring(nameStart, cursor); - // Skip whitespace to the opening brace. - while (cursor < source.length() && Character.isWhitespace(source.charAt(cursor))) { - cursor++; - } - if (cursor >= source.length() || source.charAt(cursor) != '{') { - throw new BindgenException("Expected '{' after interface " + name); - } - final int braceOpen = cursor; - final int braceClose = matchBrace(source, braceOpen); - if (braceClose < 0) { - throw new BindgenException("Unbalanced braces in interface " + name); - } - final String body = source.substring(braceOpen + 1, braceClose); - final Map resources = new LinkedHashMap<>(); - final String bodyStripped = extractResourceBodies(body, resources); - result.add(new Interface(name, bodyStripped, resources, world)); - i = braceClose + 1; - } - return result; - } - - private static boolean isIdentChar(final char c) { - return Character.isLetterOrDigit(c) || c == '-' || c == '_'; - } - - /** - * Find each {@code resource name { body }} declaration in the interface body - * and replace it with a bare {@code resource name;} so the downstream - * regex-based parser still sees the type. Emit the extracted bodies into - * {@code sink} keyed by resource name. - */ - private static String extractResourceBodies(final String body, final Map sink) - throws BindgenException { - final StringBuilder out = new StringBuilder(body.length()); - int i = 0; - while (i < body.length()) { - final int idx = body.indexOf("resource", i); - if (idx < 0) { - out.append(body, i, body.length()); - break; - } - if (!isKeywordBoundary(body, idx, "resource".length())) { - out.append(body, i, idx + 1); - i = idx + 1; - continue; - } - // Copy everything up to the keyword. - out.append(body, i, idx); - int cursor = idx + "resource".length(); - while (cursor < body.length() && Character.isWhitespace(body.charAt(cursor))) { - cursor++; - } - final int nameStart = cursor; - while (cursor < body.length() && isIdentChar(body.charAt(cursor))) { - cursor++; - } - final String name = body.substring(nameStart, cursor); - // Look ahead for `{` (declaration-with-body) or `;` - // (declaration-without-body, e.g. `type X = resource;`). - int lookAhead = cursor; - while (lookAhead < body.length() && Character.isWhitespace(body.charAt(lookAhead))) { - lookAhead++; - } - if (lookAhead < body.length() && body.charAt(lookAhead) == '{') { - final int braceClose = matchBrace(body, lookAhead); - if (braceClose < 0) { - throw new BindgenException("Unbalanced braces in resource " + name); - } - sink.put(name, body.substring(lookAhead + 1, braceClose)); - // Emit a `type` alias so the downstream parser records the - // resource type but doesn't try to interpret the method body. - // The parser only understands standalone RECORD/VARIANT/ENUM/ - // FLAGS declarations plus `type X = ...` aliases; resources - // hitch a ride on the alias form. - out.append("type ").append(name).append(" = resource;\n"); - i = braceClose + 1; - } else { - // Not a body-bearing declaration — keep original text. - out.append(body, idx, cursor); - i = cursor; - } - } - return out.toString(); - } -} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java new file mode 100644 index 0000000..891807f --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java @@ -0,0 +1,430 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Native-parser suite for {@link WitParser}. + * + *

      Covers every WIT construct the retired + * {@code WitInterfaceParser} + {@code WitWorldPreprocessor} + + * {@code WitResourceBodyParser} wrapper chain used to patch (bug 4 on the + * bindgen bug board): + * + *

        + *
      • {@code package foo:bar@1.2.3;} + *
      • {@code world { ... }} with type items directly in body + *
      • {@code interface { ... }} inside packages or worlds + *
      • {@code use} clauses (skipped, don't confuse the item scanner) + *
      • {@code import} / {@code export} clauses (bare + inline func) + *
      • Resource bodies: constructor + static + instance methods, with + * flags carried on the emitted {@link WitFunction} + *
      • Type aliases and standalone record / variant / enum / flags + *
      • Container types: list / option / result (both operands + * optional) / tuple / borrow / own + *
      + * + *

      The suite drives {@link WitParser#parse(String)} directly and asserts + * on the {@link WitDocument} shape so regressions on any of these + * constructs surface here first. + */ +@DisplayName("WitParser — native WIT grammar") +class WitParserTest { + + // --------------------------------------------------------------------------- + // package + world + interface top-level shape + // --------------------------------------------------------------------------- + + @Test + @DisplayName("package declaration with @version parses and is exposed") + void packageWithVersion() throws BindgenException { + final String src = "package wasmos:host@0.1.0;\ninterface x { }"; + final WitDocument doc = WitParser.parse(src); + assertTrue(doc.getPackageName().isPresent(), "package name captured"); + assertEquals("wasmos:host", doc.getPackageName().get()); + assertTrue(doc.getPackageVersion().isPresent(), "package version captured"); + assertEquals("0.1.0", doc.getPackageVersion().get()); + assertEquals(1, doc.getInterfaces().size()); + } + + @Test + @DisplayName("package without version parses") + void packageWithoutVersion() throws BindgenException { + final String src = "package foo:bar;\ninterface x { }"; + final WitDocument doc = WitParser.parse(src); + assertEquals("foo:bar", doc.getPackageName().orElse("")); + assertFalse(doc.getPackageVersion().isPresent(), "no version"); + } + + @Test + @DisplayName("multiple interfaces in one file are surfaced in declaration order") + void multipleInterfaces() throws BindgenException { + final String src = + "package p:q;\n" + + "interface a { foo: func() -> u32; }\n" + + "interface b { bar: func() -> u32; }\n" + + "interface c { baz: func() -> u32; }\n"; + final WitDocument doc = WitParser.parse(src); + assertEquals(3, doc.getInterfaces().size()); + assertEquals("a", doc.getInterfaces().get(0).getName()); + assertEquals("b", doc.getInterfaces().get(1).getName()); + assertEquals("c", doc.getInterfaces().get(2).getName()); + } + + @Test + @DisplayName("world with type-hoisted body parses as an isWorld() entry") + void worldWithHoistedTypes() throws BindgenException { + final String src = + "package p:q;\n" + + "world root {\n" + + " enum color { red, green, blue }\n" + + " record point { x: u32, y: u32 }\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + assertEquals(1, doc.getInterfaces().size()); + final WitDocument.ParsedInterface w = doc.getInterfaces().get(0); + assertTrue(w.isWorld(), "world flag must be set"); + assertEquals(2, w.getTypes().size()); + assertTrue(w.getTypes().containsKey("color")); + assertTrue(w.getTypes().containsKey("point")); + } + + // --------------------------------------------------------------------------- + // use / import / export + // --------------------------------------------------------------------------- + + @Test + @DisplayName("use clause is skipped without confusing subsequent items") + void useClauseSkipped() throws BindgenException { + final String src = + "interface i {\n" + + " use foo:bar/baz.{alpha, beta};\n" + + " hello: func() -> string;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + assertEquals(1, pi.getFunctions().size()); + assertTrue(pi.getFunctions().containsKey("hello")); + } + + @Test + @DisplayName("bare import/export interface reference is accepted") + void bareImportExport() throws BindgenException { + final String src = + "world w {\n" + + " import some-callbacks;\n" + + " export some-api;\n" + + " record r { a: u32 }\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface w = doc.getInterfaces().get(0); + assertTrue(w.isWorld()); + // Bare imports/exports don't add types; the record does. + assertEquals(1, w.getTypes().size()); + assertTrue(w.getTypes().containsKey("r")); + } + + @Test + @DisplayName("import name: func(...) -> ... registers the function") + void inlineImportFunc() throws BindgenException { + final String src = + "world w {\n" + " import ping: func(msg: string) -> u32;\n" + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface w = doc.getInterfaces().get(0); + assertTrue(w.getFunctions().containsKey("ping")); + final WitFunction fn = w.getFunctions().get("ping"); + assertEquals(1, fn.getParameters().size()); + assertEquals(1, fn.getReturnTypes().size()); + } + + // --------------------------------------------------------------------------- + // resource bodies (constructor / static / instance) + // --------------------------------------------------------------------------- + + @Test + @DisplayName("resource with constructor + static + instance methods parses with flags") + void resourceMethodsCarryFlags() throws BindgenException { + final String src = + "interface i {\n" + + " resource host-provider {\n" + + " constructor(name: string, count: u32);\n" + + " make: static func(seed: u32) -> host-provider;\n" + + " call: func(idx: u32) -> u32;\n" + + " }\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + final List methods = pi.getResourceMethods().get("host-provider"); + assertNotNull(methods, "resource methods captured"); + assertEquals(3, methods.size()); + assertTrue(methods.get(0).isConstructor(), "constructor flag"); + assertFalse(methods.get(0).isStatic()); + assertFalse(methods.get(1).isConstructor()); + assertTrue(methods.get(1).isStatic(), "static flag on second method"); + assertFalse(methods.get(2).isConstructor()); + assertFalse(methods.get(2).isStatic(), "third is a plain instance method"); + // Resource itself lands in the types map with RESOURCE kind. + assertTrue(pi.getTypes().containsKey("host-provider")); + assertEquals( + WitTypeCategory.RESOURCE, pi.getTypes().get("host-provider").getKind().getCategory()); + } + + @Test + @DisplayName("empty resource {} produces an empty method list") + void emptyResourceBody() throws BindgenException { + final String src = "interface i { resource opaque { } }"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + assertTrue(pi.getResourceMethods().containsKey("opaque")); + assertTrue(pi.getResourceMethods().get("opaque").isEmpty()); + } + + @Test + @DisplayName("resource with no body (`resource X;`) still declares the type") + void resourceWithoutBody() throws BindgenException { + final String src = "interface i { resource nothing; }"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + assertTrue(pi.getTypes().containsKey("nothing")); + assertEquals( + WitTypeCategory.RESOURCE, pi.getTypes().get("nothing").getKind().getCategory()); + } + + // --------------------------------------------------------------------------- + // type aliases + standalone declarations + // --------------------------------------------------------------------------- + + @Test + @DisplayName("standalone record + type-alias record both parse") + void recordShapes() throws BindgenException { + final String src = + "interface i {\n" + + " record standalone { a: u32, b: string }\n" + + " type aliased = record { x: f64, y: f64 };\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final Map types = doc.getInterfaces().get(0).getTypes(); + assertEquals(2, types.size()); + assertEquals(WitTypeCategory.RECORD, types.get("standalone").getKind().getCategory()); + assertEquals(WitTypeCategory.RECORD, types.get("aliased").getKind().getCategory()); + } + + @Test + @DisplayName("variant cases with and without payload parse") + void variantWithMixedCases() throws BindgenException { + final String src = + "interface i {\n" + + " variant msg { empty, text(string), bytes(list) }\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitType v = doc.getInterfaces().get(0).getTypes().get("msg"); + assertEquals(WitTypeCategory.VARIANT, v.getKind().getCategory()); + assertEquals(3, v.getKind().getVariantCases().size()); + assertFalse(v.getKind().getVariantCases().get("empty").isPresent()); + assertTrue(v.getKind().getVariantCases().get("text").isPresent()); + assertTrue(v.getKind().getVariantCases().get("bytes").isPresent()); + } + + @Test + @DisplayName("enum and flags parse into ordered value lists") + void enumAndFlags() throws BindgenException { + final String src = + "interface i {\n" + + " enum color { red, green, blue }\n" + + " flags perm { read, write, execute }\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final Map types = doc.getInterfaces().get(0).getTypes(); + assertEquals(List.of("red", "green", "blue"), types.get("color").getKind().getEnumValues()); + assertEquals( + List.of("read", "write", "execute"), types.get("perm").getKind().getFlags()); + } + + // --------------------------------------------------------------------------- + // container types + // --------------------------------------------------------------------------- + + @Test + @DisplayName("list / option / result / tuple / borrow / own parse in signatures") + void containerTypesInFunc() throws BindgenException { + final String src = + "interface i {\n" + + " resource handle { }\n" + + " f: func(a: list, b: option, c: borrow, d: own) " + + "-> result, string>;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitFunction fn = doc.getInterfaces().get(0).getFunctions().get("f"); + assertNotNull(fn, "function parsed"); + assertEquals(4, fn.getParameters().size()); + assertEquals(1, fn.getReturnTypes().size()); + final WitType retType = fn.getReturnTypes().get(0); + assertEquals(WitTypeCategory.RESULT, retType.getKind().getCategory()); + } + + @Test + @DisplayName("result<_, err> and result both parse") + void resultWithPlaceholders() throws BindgenException { + final String src = + "interface i {\n" + + " a: func() -> result<_, string>;\n" + + " b: func() -> result;\n" + + " c: func() -> result;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + assertEquals(3, pi.getFunctions().size()); + for (final String name : List.of("a", "b", "c")) { + final WitFunction fn = pi.getFunctions().get(name); + assertEquals(WitTypeCategory.RESULT, fn.getReturnTypes().get(0).getKind().getCategory()); + } + } + + @Test + @DisplayName("nested list> and list> parse") + void nestedContainers() throws BindgenException { + final String src = + "interface i {\n" + + " a: func() -> list>;\n" + + " b: func() -> list>;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + assertEquals(2, doc.getInterfaces().get(0).getFunctions().size()); + } + + // --------------------------------------------------------------------------- + // resource + type-expression interplay + // --------------------------------------------------------------------------- + + @Test + @DisplayName("record field can reference a previously declared record without forward-ref") + void recordFieldReferencesRecord() throws BindgenException { + final String src = + "interface i {\n" + + " record inner { v: u32 }\n" + + " record outer { part: inner }\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitType outer = doc.getInterfaces().get(0).getTypes().get("outer"); + assertEquals(WitTypeCategory.RECORD, outer.getKind().getCategory()); + final WitType partType = outer.getKind().getRecordFields().get("part"); + assertEquals("inner", partType.getName()); + } + + @Test + @DisplayName("world with import embedder-callbacks; and resource + record body works end-to-end") + void hoistedWorldWithResource() throws BindgenException { + final String src = + "package wasmos:host@0.1.0;\n" + + "interface embedder-callbacks {\n" + + " ping: func() -> u32;\n" + + "}\n" + + "world embedder {\n" + + " enum code { ok, err }\n" + + " record error { code: code, message: string }\n" + + " resource runtime-instance {\n" + + " instantiate: static func(bytes: list) -> result;\n" + + " call: func(name: string, args: list) -> result, error>;\n" + + " }\n" + + " import embedder-callbacks;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + assertEquals(2, doc.getInterfaces().size()); + final WitDocument.ParsedInterface world = doc.getInterfaces().get(1); + assertTrue(world.isWorld()); + assertTrue(world.getTypes().containsKey("code")); + assertTrue(world.getTypes().containsKey("error")); + assertTrue(world.getTypes().containsKey("runtime-instance")); + final List methods = world.getResourceMethods().get("runtime-instance"); + assertNotNull(methods); + assertEquals(2, methods.size()); + assertTrue(methods.get(0).isStatic(), "instantiate is static"); + assertFalse(methods.get(1).isStatic()); + } + + // --------------------------------------------------------------------------- + // determinism + errors + // --------------------------------------------------------------------------- + + @Test + @DisplayName("same input produces the same interface + type order across parses") + void deterministicOutput() throws BindgenException { + final String src = + "interface i {\n" + + " enum e { a, b }\n" + + " record r { x: u32 }\n" + + " resource h { }\n" + + " first: func();\n" + + " second: func() -> u32;\n" + + "}\n"; + final WitDocument a = WitParser.parse(src); + final WitDocument b = WitParser.parse(src); + assertEquals( + List.copyOf(a.getInterfaces().get(0).getTypes().keySet()), + List.copyOf(b.getInterfaces().get(0).getTypes().keySet())); + assertEquals( + List.copyOf(a.getInterfaces().get(0).getFunctions().keySet()), + List.copyOf(b.getInterfaces().get(0).getFunctions().keySet())); + } + + @Test + @DisplayName("garbage input throws BindgenException") + void garbageThrows() { + assertThrows( + BindgenException.class, () -> WitParser.parse("this is not valid wit source")); + } + + @Test + @DisplayName("line comments and doc comments are stripped without affecting parse") + void commentsIgnored() throws BindgenException { + final String src = + "// leading line comment\n" + + "package p:q;\n" + + "/// doc comment on interface\n" + + "interface i {\n" + + " // between-item comment\n" + + " foo: func() -> u32;\n" + + " /// doc on next item\n" + + " bar: func() -> u32;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + assertEquals(2, doc.getInterfaces().get(0).getFunctions().size()); + } + + @Test + @DisplayName("block comments are stripped without affecting parse") + void blockCommentsIgnored() throws BindgenException { + final String src = + "interface i {\n" + + " /* multi-line\n" + + " block comment */\n" + + " foo: func() -> u32;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + assertEquals(1, doc.getInterfaces().get(0).getFunctions().size()); + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java deleted file mode 100644 index 26229d9..0000000 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResourceBodyParserTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2026 Tegmentum AI - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ai.tegmentum.webassembly4j.bindgen.wit; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import ai.tegmentum.webassembly4j.bindgen.BindgenException; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** - * Direct tests for {@link WitResourceBodyParser}, and specifically for the - * fix that lifts a resource method's constructor / static / instance shape - * onto {@link WitFunction#isConstructor()} / {@link WitFunction#isStatic()} - * so downstream {@link - * ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction} translation - * inherits those flags without a separate translation path. - */ -@DisplayName("WitResourceBodyParser resource-method flag lift") -class WitResourceBodyParserTest { - - @Test - @DisplayName("constructor(...) → WitFunction with isConstructor=true, isStatic=false") - void constructorFlagsSetCorrectly() throws BindgenException { - final String body = "constructor(interface-name: string, num-funcs: u32);"; - final List methods = WitResourceBodyParser.parse("host-provider", body); - assertEquals(1, methods.size(), "expected exactly one parsed method"); - final WitFunction ctor = methods.get(0); - assertEquals("constructor", ctor.getName()); - assertTrue(ctor.isConstructor(), "constructor flag must be true"); - assertFalse(ctor.isStatic(), "static flag must be false for a constructor"); - assertEquals(2, ctor.getParameters().size(), "expected 2 parsed parameters"); - // Constructor has no return type. - assertEquals(0, ctor.getReturnTypes().size(), "constructor return-type list must be empty"); - } - - @Test - @DisplayName("name: static func(...) → WitFunction with isStatic=true, isConstructor=false") - void staticFuncFlagsSetCorrectly() throws BindgenException { - final String body = - "instantiate: static func(component-bytes: list, imports: list) " - + "-> result;"; - final List methods = WitResourceBodyParser.parse("runtime-instance", body); - assertEquals(1, methods.size()); - final WitFunction m = methods.get(0); - assertEquals("instantiate", m.getName()); - assertFalse(m.isConstructor(), "static func must not be a constructor"); - assertTrue(m.isStatic(), "static flag must be true"); - assertEquals(2, m.getParameters().size()); - assertEquals(1, m.getReturnTypes().size(), "expected a single carried return-type expression"); - } - - @Test - @DisplayName("name: func(...) → WitFunction with both flags false (plain instance method)") - void instanceFuncFlagsSetCorrectly() throws BindgenException { - final String body = "call-export: func(name: string, args: list) -> result, error>;"; - final List methods = WitResourceBodyParser.parse("runtime-instance", body); - assertEquals(1, methods.size()); - final WitFunction m = methods.get(0); - assertEquals("call-export", m.getName()); - assertFalse(m.isConstructor(), "plain func must not be a constructor"); - assertFalse(m.isStatic(), "plain func must not be static"); - } - - @Test - @DisplayName("all three shapes coexist and retain declaration order") - void mixedShapesRoundTrip() throws BindgenException { - final String body = - "constructor(seed: u32);\n" - + "reset: static func();\n" - + "next: func() -> u32;"; - final List methods = WitResourceBodyParser.parse("prng", body); - assertEquals(3, methods.size()); - assertTrue(methods.get(0).isConstructor(), "first method must be the constructor"); - assertTrue(methods.get(1).isStatic(), "second method must be static"); - assertFalse(methods.get(2).isConstructor(), "third method is instance"); - assertFalse(methods.get(2).isStatic(), "third method is instance"); - assertEquals("constructor", methods.get(0).getName()); - assertEquals("reset", methods.get(1).getName()); - assertEquals("next", methods.get(2).getName()); - } -} From fd072159ad15d7eb25144a96cf27ff74630d2f54 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sat, 25 Jul 2026 14:07:23 -0400 Subject: [PATCH 53/85] feat(wasmtime-provider): expose native wasmtime4j types via Component.extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overrides `Component.extension(Class)` on `WasmtimeComponentAdapter` to hand back the underlying `ai.tegmentum.wasmtime4j.component.Component`, `ai.tegmentum.wasmtime4j.Engine`, and `ai.tegmentum.wasmtime4j.component.ComponentEngine` when requested. ADR-011 native-CM pass-through embedders (wasi-rs `WasmtimeCmEmbedderRuntime`) need `componentType().importItems()` to walk per-interface function names and bind linker thunks by declaration order — introspection the framework-neutral `Component` interface doesn't cover. Extension mechanism keeps the coupling opt-in without polluting the neutral API. --- .../wasmtime/WasmtimeComponentAdapter.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java index e83e56d..ce9e541 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeComponentAdapter.java @@ -350,6 +350,34 @@ public boolean supportsWasiNn() { return WasiNnProbeHolder.AVAILABLE; } + /** + * ADR-011 native-CM pass-through hook. Returns the underlying wasmtime4j + * {@link ai.tegmentum.wasmtime4j.component.Component} when requested, so + * pass-through embedders (e.g. wasi-rs {@code WasmtimeCmEmbedderRuntime}) + * can call {@code componentType()} and walk per-interface function names to + * bind {@code linker.defineFunction} thunks by declaration order — the + * introspection path is not exposed on the framework-neutral + * {@link ai.tegmentum.webassembly4j.api.Component} interface today. + * + *

      Also returns {@link ai.tegmentum.wasmtime4j.Engine} when asked, so + * callers can co-locate the wasmtime4j {@link ai.tegmentum.wasmtime4j.Store} + * and {@link ai.tegmentum.wasmtime4j.component.ComponentLinker} against + * the same engine instance the component was compiled on. + */ + @Override + public java.util.Optional extension(Class extensionType) { + if (extensionType == ai.tegmentum.wasmtime4j.component.Component.class) { + return java.util.Optional.of(extensionType.cast(nativeComponent)); + } + if (extensionType == ai.tegmentum.wasmtime4j.Engine.class) { + return java.util.Optional.of(extensionType.cast(engine)); + } + if (extensionType == ai.tegmentum.wasmtime4j.component.ComponentEngine.class) { + return java.util.Optional.of(extensionType.cast(componentEngine)); + } + return java.util.Optional.empty(); + } + /** Reflection-lookup holder — computed once, then a constant boolean read. */ private static final class WasiNnProbeHolder { private static final boolean AVAILABLE = probeNative(); From 243322197671cfb49fd693f549a9b682c4d0302a Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:35:51 -0400 Subject: [PATCH 54/85] docs(api): begin Table-Interface-Completion (r.1 in progress) Charter: ~/git/fijivm/doctrine/specs/f-webassembly4j-table-interface-completion-charter-2026-07-26.md Working branch. Extend api-layer Table with 4 spec-standard operations (maxSize/get/set/grow) and implement in wasmtime4j-provider. Other providers stay compile-clean via UnsupportedOperationException defaults. --- NOTES-TABLE-COMPLETION-2026-07-26.md | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 NOTES-TABLE-COMPLETION-2026-07-26.md diff --git a/NOTES-TABLE-COMPLETION-2026-07-26.md b/NOTES-TABLE-COMPLETION-2026-07-26.md new file mode 100644 index 0000000..3343950 --- /dev/null +++ b/NOTES-TABLE-COMPLETION-2026-07-26.md @@ -0,0 +1,54 @@ +# F-Webassembly4j-Table-Interface-Completion — Working Notes + +Charter: `~/git/fijivm/doctrine/specs/f-webassembly4j-table-interface-completion-charter-2026-07-26.md` + +Date opened: 2026-07-26 +Branch: `f-table-interface-completion` + +## Scope (r.1) + +Add 4 spec-standard operations to `ai.tegmentum.webassembly4j.api.Table` and +implement them in `wasmtime4j-provider`. Other provider adapters are left +unchanged — the new interface methods carry `throw UnsupportedOperationException` +defaults so downstream `wamr4j-provider`, `graalwasm4j-provider`, `wasm3-provider`, +and `endive4j-provider` continue to compile. + +| Method | Spec op | Native site (`ai.tegmentum.wasmtime4j.WasmTable`) | +|---|---|---| +| `OptionalInt maxSize()` | `table.type().max` | `int getMaxSize()` — returns `-1` when unlimited | +| `Object get(int)` | `table.get` | `Object get(int)` | +| `void set(int, Object)` | `table.set` | `void set(int, Object)` | +| `int grow(int, Object)` | `table.grow` | `int grow(int, Object)` | + +## Native convention discoveries + +`ai.tegmentum.wasmtime4j.WasmTable.getMaxSize()` Javadoc (verified in +`~/git/wasmtime4j/wasmtime4j/src/main/java/ai/tegmentum/wasmtime4j/WasmTable.java:79`): + +> "Gets the maximum size of the table. @return the maximum number of +> elements, or -1 if unlimited" + +The adapter therefore treats `max < 0` as unbounded. + +## Explicitly out of scope for r.1 + +`fill`, `copy`, `init`, `dropElementSegment`, `growAsync`, `elementType()`, +`getSize64/get64/set64/…` — deferred per charter. + +## Slice plan + +- r.1-1: extend `Table` interface (defaults throw UOE) +- r.1-2: implement in `WasmtimeTableAdapter` +- r.1-3: add `WasmtimeTableAdapterTest` +- r.1-4: `mvn -B -pl webassembly4j-api,wasmtime4j-provider -am test` +- r.1-θ: close commit + +## Guardrails followed + +- No pom.xml version bump +- No CHANGELOG.md edit (deferred to release cut) +- No touch to other provider modules, spi, runtime, testing, pool, spring, + benchmarks, bindgen, component-builder, embedder-generated +- No touch to `LinkingContext.java`, `DefaultLinkingContext.java`, + `WasmtimeModuleAdapter.java` (Charter B territory) +- No push From 2cecf6389a1045d9d28bfdb52de05b1e2cc033e7 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:36:27 -0400 Subject: [PATCH 55/85] feat(api): add spec-standard Table operations (maxSize/get/set/grow) Extends ai.tegmentum.webassembly4j.api.Table with four WebAssembly-spec operations so api-layer consumers can perform basic dynamic table management without dropping through unwrap(): - OptionalInt maxSize() maps to table.type().max - Object get(int) maps to table.get - void set(int, Object) maps to table.set - int grow(int, Object) maps to table.grow (returns -1 on failure) All four are declared as default methods that throw UnsupportedOperationException so unchanged provider adapters (wamr4j-provider, graalwasm4j-provider, wasm3-provider, endive4j-provider) continue to compile without modification. Providers opt in by overriding. The Object element type is provider-specific until a spec-based Ref abstraction lands (deferred). For funcref-typed tables, null denotes an uninitialized slot. No removals, no signature changes; existing consumers are unaffected. --- .../ai/tegmentum/webassembly4j/api/Table.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Table.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Table.java index 7112c08..6940c6d 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Table.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Table.java @@ -1,10 +1,83 @@ package ai.tegmentum.webassembly4j.api; import java.util.Optional; +import java.util.OptionalInt; +/** + * A WebAssembly table exported by (or imported into) an instance. + * + *

      Beyond {@link #size()} and the escape hatch {@link #unwrap(Class)}, + * this interface exposes the four WebAssembly-spec table operations + * ({@code table.get}, {@code table.set}, {@code table.grow}, and the + * {@code max} field of {@code table.type()}) so that api-layer consumers + * can perform basic dynamic table management without dropping to a + * provider-specific native type. + * + *

      Provider support

      + * + *

      {@link #maxSize()}, {@link #get(int)}, {@link #set(int, Object)}, and + * {@link #grow(int, Object)} carry {@code default} bodies that throw + * {@link UnsupportedOperationException}. Provider adapters override the + * ones they can honour. Consumers that need to be portable across + * providers should either catch {@link UnsupportedOperationException} at + * the call site or gate on {@link EngineCapabilities} where an equivalent + * capability flag exists; note that this interface itself does NOT + * currently publish a per-operation capability probe — {@code try/catch} + * remains the portable escape. + * + *

      Element wrapping

      + * + *

      The {@code Object} return / accept type on {@link #get(int)} and + * {@link #set(int, Object)} is provider-specific until a spec-based + * {@code Ref} abstraction lands (deferred). For {@code funcref}-typed + * tables, a {@code null} slot is legal and denotes "uninitialized". + */ public interface Table { int size(); + /** + * @return the maximum table size, or {@link OptionalInt#empty()} if the + * table has no declared maximum (unbounded). + */ + default OptionalInt maxSize() { + throw new UnsupportedOperationException( + "Table.maxSize() not implemented by this provider"); + } + + /** + * @return the element at {@code index}, or {@code null} if the slot is + * uninitialized. Element wrapping is provider-specific until a + * spec-based {@code Ref} abstraction lands. + * @throws IndexOutOfBoundsException if {@code index >= size()} + */ + default Object get(int index) { + throw new UnsupportedOperationException( + "Table.get(int) not implemented by this provider"); + } + + /** + * Sets the element at {@code index}. {@code value} must be assignable to + * the table's element type; {@code null} is accepted for funcref-typed + * tables as an uninitialized slot. + * + * @throws IndexOutOfBoundsException if {@code index >= size()} + */ + default void set(int index, Object value) { + throw new UnsupportedOperationException( + "Table.set(int, Object) not implemented by this provider"); + } + + /** + * Grows the table by {@code delta} elements initialized to {@code init}. + * + * @return the pre-grow size (which is also the index of the first newly- + * added slot), or -1 if growth would exceed {@link #maxSize()}. + */ + default int grow(int delta, Object init) { + throw new UnsupportedOperationException( + "Table.grow(int, Object) not implemented by this provider"); + } + Optional unwrap(Class nativeType); } From edeae5f9848899e562b868cec47de88e84f756d3 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:36:45 -0400 Subject: [PATCH 56/85] docs(api): begin LinkingContext-Imports-Wiring-Fix (r.1 in progress) Working notes for the semantic-hole fix on LinkingContext extern-imports. Option B.2 selected; three variants (Memory/Table/Global) fully wired in r.1, Function variant deferred pending Function.unwrap on the api layer. --- NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md diff --git a/NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md b/NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md new file mode 100644 index 0000000..8805ba8 --- /dev/null +++ b/NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md @@ -0,0 +1,49 @@ +# LinkingContext-Imports-Wiring-Fix — working notes (r.1 in progress) + +**Charter**: `f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26` +**Branch**: `f-linkingcontext-imports-wiring-fix` (branched from `main` at `fd07215`) +**Type**: bug fix — closes semantic hole in existing public API + +## Problem + +- `DefaultLinkingContext.Builder.addImport(String, Object)` is a public method +- `DefaultLinkingContext.imports()` returns the resulting map +- `LinkingContext` interface never exposes it +- Providers cannot reach the map → the call has no effect +- Also: single-string key can't represent two-part `(moduleName, name)` Wasm import keys + +## Approach — Option B.2 (typed variants) + +1. Add sealed `ExternImportDefinition` in api layer with variants + `MemoryImport / TableImport / GlobalImport / FunctionImport` +2. Add `LinkingContext.externImports()` default method +3. Add typed builder methods on `DefaultLinkingContext.Builder` +4. Deprecate untyped `addImport(String, Object)` (kept for backwards compat) +5. Wire `WasmtimeModuleAdapter.instantiate(LinkingContext)` to consume them + +## Empirical unwrap availability audit + +- `Memory.unwrap(Class)` — YES +- `Table.unwrap(Class)` — YES +- `Global.unwrap(Class)` — YES +- `Function.unwrap(Class)` — NO (interface lacks it) + +`WasmtimeMemoryAdapter`, `WasmtimeTableAdapter`, `WasmtimeGlobalAdapter` all +implement `unwrap` and hand back the native `WasmMemory` / `WasmTable` / +`WasmGlobal` handles that `Linker.defineMemory/defineTable/defineGlobal` +require. + +## Scope decision (r.1) + +- MemoryImport / TableImport / GlobalImport — fully wired +- FunctionImport — sealed-type variant lands; wasmtime provider throws + `UnsupportedOperationException` at instantiate. Consuming requires adding + `Function.unwrap(Class)` on the api interface + adapter impl — deferred + to a follow-up charter. + +## Charter A coordination + +Charter A (table interface completion) is on branch `f-table-interface-completion` +and touches `Table.java` + `WasmtimeTableAdapter.java`. Charter B (this) is +branched from `main` (identical HEAD to Charter A start) and touches disjoint +files. No file overlap. From 5d6c7a0fa611de8a64f7c454382b42e02f5747c6 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:36:45 -0400 Subject: [PATCH 57/85] feat(wasmtime-provider): implement spec-standard Table operations Delegates api-layer Table.maxSize/get/set/grow to the native ai.tegmentum.wasmtime4j.WasmTable methods. Convention note: WasmTable#getMaxSize returns -1 when the table is unbounded (per its Javadoc); the adapter maps any negative value to OptionalInt.empty() to remain defensive against future sentinel extensions. --- .../wasmtime/WasmtimeTableAdapter.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java index 43d49cf..d245992 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java @@ -4,6 +4,7 @@ import ai.tegmentum.webassembly4j.api.Table; import java.util.Optional; +import java.util.OptionalInt; final class WasmtimeTableAdapter implements Table { @@ -18,6 +19,30 @@ public int size() { return nativeTable.getSize(); } + @Override + public OptionalInt maxSize() { + // ai.tegmentum.wasmtime4j.WasmTable#getMaxSize Javadoc: "returns -1 + // if unlimited". Treat any negative as unbounded to be defensive + // against future extensions of the sentinel space. + int max = nativeTable.getMaxSize(); + return max < 0 ? OptionalInt.empty() : OptionalInt.of(max); + } + + @Override + public Object get(int index) { + return nativeTable.get(index); + } + + @Override + public void set(int index, Object value) { + nativeTable.set(index, value); + } + + @Override + public int grow(int delta, Object init) { + return nativeTable.grow(delta, init); + } + @Override @SuppressWarnings("unchecked") public Optional unwrap(Class nativeType) { From 5e5bc892ca0299e071dca423250d9641c174f476 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:36:45 -0400 Subject: [PATCH 58/85] docs(api): begin LinkingContext-Imports-Wiring-Fix (r.1 in progress) Working notes for the semantic-hole fix on LinkingContext extern-imports. Option B.2 selected; three variants (Memory/Table/Global) fully wired in r.1, Function variant deferred pending Function.unwrap on the api layer. --- NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md diff --git a/NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md b/NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md new file mode 100644 index 0000000..8805ba8 --- /dev/null +++ b/NOTES-LINKINGCONTEXT-IMPORTS-2026-07-26.md @@ -0,0 +1,49 @@ +# LinkingContext-Imports-Wiring-Fix — working notes (r.1 in progress) + +**Charter**: `f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26` +**Branch**: `f-linkingcontext-imports-wiring-fix` (branched from `main` at `fd07215`) +**Type**: bug fix — closes semantic hole in existing public API + +## Problem + +- `DefaultLinkingContext.Builder.addImport(String, Object)` is a public method +- `DefaultLinkingContext.imports()` returns the resulting map +- `LinkingContext` interface never exposes it +- Providers cannot reach the map → the call has no effect +- Also: single-string key can't represent two-part `(moduleName, name)` Wasm import keys + +## Approach — Option B.2 (typed variants) + +1. Add sealed `ExternImportDefinition` in api layer with variants + `MemoryImport / TableImport / GlobalImport / FunctionImport` +2. Add `LinkingContext.externImports()` default method +3. Add typed builder methods on `DefaultLinkingContext.Builder` +4. Deprecate untyped `addImport(String, Object)` (kept for backwards compat) +5. Wire `WasmtimeModuleAdapter.instantiate(LinkingContext)` to consume them + +## Empirical unwrap availability audit + +- `Memory.unwrap(Class)` — YES +- `Table.unwrap(Class)` — YES +- `Global.unwrap(Class)` — YES +- `Function.unwrap(Class)` — NO (interface lacks it) + +`WasmtimeMemoryAdapter`, `WasmtimeTableAdapter`, `WasmtimeGlobalAdapter` all +implement `unwrap` and hand back the native `WasmMemory` / `WasmTable` / +`WasmGlobal` handles that `Linker.defineMemory/defineTable/defineGlobal` +require. + +## Scope decision (r.1) + +- MemoryImport / TableImport / GlobalImport — fully wired +- FunctionImport — sealed-type variant lands; wasmtime provider throws + `UnsupportedOperationException` at instantiate. Consuming requires adding + `Function.unwrap(Class)` on the api interface + adapter impl — deferred + to a follow-up charter. + +## Charter A coordination + +Charter A (table interface completion) is on branch `f-table-interface-completion` +and touches `Table.java` + `WasmtimeTableAdapter.java`. Charter B (this) is +branched from `main` (identical HEAD to Charter A start) and touches disjoint +files. No file overlap. From 77f8930f10825837bb3eea1dc3aff16dc0b49318 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:39:19 -0400 Subject: [PATCH 59/85] test(wasmtime-provider): cover new Table operations Adds WasmtimeTableAdapterTest exercising all four new Table methods: - sizeReportsInitialMinimum (baseline sanity) - maxSizeBoundedReturnsDeclaredMax (max=10) - maxSizeUnboundedReturnsEmpty (funcref table with no declared max) - getUninitializedSlotReturnsNull (funcref default) - setThenGetRoundTripsFuncref (native WasmFunction obtained via Instance unwrap because api-layer Function has no unwrap) - growReturnsPreGrowSizeAndIncreasesSize - growBeyondMaxReturnsMinusOne (grow past declared max) - getOutOfBoundsThrows / setOutOfBoundsThrows (IOOBE contract) Fixtures are hand-encoded WASM binaries (three tiny tables) rather than WAT compiled through native wasmtime4j unwrap; the api Engine exposes only loadModule(byte[]) and hand-encoding keeps the test dependency graph clean. All tests are gated on WasmtimeProvider().availability().available() via @EnabledIf, matching the pattern of the sibling WasmtimeIntegrationTest. --- .../wasmtime/WasmtimeTableAdapterTest.java | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapterTest.java diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapterTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapterTest.java new file mode 100644 index 0000000..1695ef6 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapterTest.java @@ -0,0 +1,224 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.Instance; +import ai.tegmentum.webassembly4j.api.Module; +import ai.tegmentum.webassembly4j.api.Table; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exercises the four spec-standard {@link Table} operations added by the + * Table-Interface-Completion charter (r.1): {@code maxSize}, {@code get}, + * {@code set}, {@code grow}. These delegate to the native + * {@code ai.tegmentum.wasmtime4j.WasmTable}. + * + *

      The api-layer {@link Engine} intentionally exposes no WAT entry + * point, so fixture modules are supplied as hand-encoded raw wasm; they + * are small enough (three tables + one empty function) that this is more + * legible than dragging a wasm-text tool through the test dependency + * graph. + */ +class WasmtimeTableAdapterTest { + + static boolean runtimeAvailable() { + return new WasmtimeProvider().availability().available(); + } + + // ---------------- Hand-encoded WASM fixtures ---------------- + // + // Encoding a funcref table + one function is small enough to keep as + // literal byte arrays. Encoded and cross-checked against wat2wasm: + // (module + // (table (export "t") $LIMITS funcref) + // (func $f (export "f"))) + // + // Sections used: + // 1 Type : ()->() + // 3 Function: [0] + // 4 Table : [limits, funcref] + // 7 Export : "t" (table 0), "f" (func 0) + // 10 Code : one empty function + + /** funcref table with min=3, max=10. */ + private static final byte[] TABLE_BOUNDED = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // magic + version + // Type: 1 * ()->() + 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, + // Function: 1 func with type 0 + 0x03, 0x02, 0x01, 0x00, + // Table: 1 * funcref, min=3, max=10 + 0x04, 0x05, 0x01, 0x70, 0x01, 0x03, 0x0a, + // Export: "t" (table 0), "f" (func 0) + 0x07, 0x09, 0x02, + 0x01, 0x74, 0x01, 0x00, + 0x01, 0x66, 0x00, 0x00, + // Code: empty body + 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b + }; + + /** funcref table with min=2, no max. */ + private static final byte[] TABLE_UNBOUNDED = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, + 0x03, 0x02, 0x01, 0x00, + // Table: 1 * funcref, min=2, no max (flags=0x00) + 0x04, 0x04, 0x01, 0x70, 0x00, 0x02, + 0x07, 0x09, 0x02, + 0x01, 0x74, 0x01, 0x00, + 0x01, 0x66, 0x00, 0x00, + 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b + }; + + /** funcref table with min=1, max=1 (no room to grow). */ + private static final byte[] TABLE_MAX_ONE = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, + 0x03, 0x02, 0x01, 0x00, + // Table: 1 * funcref, min=1, max=1 + 0x04, 0x05, 0x01, 0x70, 0x01, 0x01, 0x01, + 0x07, 0x09, 0x02, + 0x01, 0x74, 0x01, 0x00, + 0x01, 0x66, 0x00, 0x00, + 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b + }; + + // ---------------- Tests ---------------- + + @Test + @EnabledIf("runtimeAvailable") + void sizeReportsInitialMinimum() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + assertEquals(3, table.size()); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void maxSizeBoundedReturnsDeclaredMax() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + OptionalInt max = table.maxSize(); + assertTrue(max.isPresent(), "bounded table must report a max"); + assertEquals(10, max.getAsInt()); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void maxSizeUnboundedReturnsEmpty() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_UNBOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + assertFalse(table.maxSize().isPresent(), + "unbounded table must report empty max"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void getUninitializedSlotReturnsNull() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + // funcref slots default to null (uninitialized) until set + assertNull(table.get(0)); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void setThenGetRoundTripsFuncref() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + + // The api-layer Function does not expose unwrap; reach the native + // WasmFunction (a legal funcref value) via the native Instance. + ai.tegmentum.wasmtime4j.Instance nativeInstance = instance + .unwrap(ai.tegmentum.wasmtime4j.Instance.class) + .orElseThrow(() -> new AssertionError( + "Expected to unwrap ai.tegmentum.wasmtime4j.Instance")); + Object funcref = nativeInstance.getFunction("f") + .orElseThrow(() -> new AssertionError( + "Expected exported function 'f'")); + + table.set(1, funcref); + Object slot = table.get(1); + assertNotNull(slot, "post-set slot must not be null"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void growReturnsPreGrowSizeAndIncreasesSize() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + + int preGrow = table.size(); + int returned = table.grow(2, null); + assertEquals(preGrow, returned, "grow must return pre-grow size"); + assertEquals(preGrow + 2, table.size(), + "post-grow size must equal preGrow + delta"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void growBeyondMaxReturnsMinusOne() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_MAX_ONE)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + assertEquals(1, table.size()); + int returned = table.grow(5, null); + assertEquals(-1, returned, + "grow past declared max must return -1"); + assertEquals(1, table.size(), + "failed grow must leave size unchanged"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void getOutOfBoundsThrows() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + assertThrows(IndexOutOfBoundsException.class, + () -> table.get(table.size())); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void setOutOfBoundsThrows() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(TABLE_BOUNDED)) { + Instance instance = module.instantiate(); + Table table = instance.table("t").orElseThrow(); + assertThrows(IndexOutOfBoundsException.class, + () -> table.set(table.size(), null)); + } + } +} From 1915d56bd6e7d13692fbd0299935b150dd9aea15 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:43:44 -0400 Subject: [PATCH 60/85] feat(api): add ExternImportDefinition types for typed extern-imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a typed replacement for the untyped DefaultLinkingContext.Builder.addImport(String, Object) call — which was never reachable through the LinkingContext interface, so had no effect. The new hierarchy carries both the (moduleName, name) pair that Wasm imports require and the target-typed value that a provider can hand to its native linker: ExternImportDefinition (interface) - MemoryImport(moduleName, name, Memory) - TableImport (moduleName, name, Table) - GlobalImport(moduleName, name, Global) - FunctionImport(moduleName, name, Function) Because the webassembly4j-api module targets Java 8, the interface is not `sealed` and the variants are plain final classes rather than records; callers should still treat the set of implementations as closed and dispatch via `instanceof`. Only the value types land here — the LinkingContext interface accessor and the wasmtime provider wiring follow in the next commits. Refs: f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26 --- .../api/ExternImportDefinition.java | 43 ++++++++++++ .../webassembly4j/api/FunctionImport.java | 66 +++++++++++++++++++ .../webassembly4j/api/GlobalImport.java | 61 +++++++++++++++++ .../webassembly4j/api/MemoryImport.java | 62 +++++++++++++++++ .../webassembly4j/api/TableImport.java | 61 +++++++++++++++++ 5 files changed, 293 insertions(+) create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ExternImportDefinition.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FunctionImport.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/GlobalImport.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/MemoryImport.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/TableImport.java diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ExternImportDefinition.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ExternImportDefinition.java new file mode 100644 index 0000000..f92c575 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/ExternImportDefinition.java @@ -0,0 +1,43 @@ +package ai.tegmentum.webassembly4j.api; + +/** + * A typed extern-import definition attached to a {@link LinkingContext}. + * Providers consume these during {@link Module#instantiate(LinkingContext)} + * to wire cross-instance shared imports (memory, table, global, function). + * + *

      Common Wasm-embedding patterns: WASI shim wiring (memory sharing across + * a component boundary), function-table plug-in registration, cross-module + * global state, guest-callable host functions passed through as first-class + * imports. + * + *

      The concrete implementations are a fixed set matching the WebAssembly + * spec's {@code ExternType} — {@link MemoryImport}, {@link TableImport}, + * {@link GlobalImport}, {@link FunctionImport}. Sealing is not modelled at + * the language level because this package targets Java 8; callers should + * treat the set of implementations as closed and use {@code instanceof} + * dispatch. Add-on Wasm proposals (typed function references, GC ref types) + * may add variants in a future charter. + * + *

      Provider support policy: a provider that cannot wire a given variant + * (for example because the underlying value cannot be + * {@link Memory#unwrap(Class) unwrapped} to a native handle) must throw at + * {@code instantiate} time with a clear message identifying the variant and + * import coordinates. Providers that support none of these variants should + * behave as if {@link LinkingContext#externImports()} were empty. + * + * @since 2.5.2 + */ +public interface ExternImportDefinition { + + /** + * The importing module namespace as declared in the WebAssembly + * import statement (e.g. {@code "env"}). + */ + String moduleName(); + + /** + * The import field name as declared in the WebAssembly import statement + * (e.g. {@code "memory"} in {@code (import "env" "memory" ...)}). + */ + String name(); +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FunctionImport.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FunctionImport.java new file mode 100644 index 0000000..f4e5223 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/FunctionImport.java @@ -0,0 +1,66 @@ +package ai.tegmentum.webassembly4j.api; + +import java.util.Objects; + +/** + * An {@link ExternImportDefinition} variant that wires a {@link Function} + * instance as an imported function. The importing module must declare + * {@code (import "" "" (func ...))} in its Wasm binary. + * + *

      Provider support: as of {@code 2.5.2} the wasmtime4j-provider does + * not implement this variant and will throw + * {@link UnsupportedOperationException} at + * {@link Module#instantiate(LinkingContext) instantiate} time. Wiring + * requires an {@code unwrap(Class)} method on {@link Function}, which is + * scheduled for a follow-up charter. For today's function-import needs use + * {@link DefaultLinkingContext.Builder#addHostFunction(HostFunctionDefinition) + * addHostFunction} instead. + * + * @since 2.5.2 + */ +public final class FunctionImport implements ExternImportDefinition { + + private final String moduleName; + private final String name; + private final Function function; + + public FunctionImport(String moduleName, String name, Function function) { + this.moduleName = Objects.requireNonNull(moduleName, "moduleName"); + this.name = Objects.requireNonNull(name, "name"); + this.function = Objects.requireNonNull(function, "function"); + } + + @Override + public String moduleName() { + return moduleName; + } + + @Override + public String name() { + return name; + } + + public Function function() { + return function; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FunctionImport)) return false; + FunctionImport other = (FunctionImport) o; + return moduleName.equals(other.moduleName) + && name.equals(other.name) + && function.equals(other.function); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, name, function); + } + + @Override + public String toString() { + return "FunctionImport[" + moduleName + "::" + name + "]"; + } +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/GlobalImport.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/GlobalImport.java new file mode 100644 index 0000000..933cab5 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/GlobalImport.java @@ -0,0 +1,61 @@ +package ai.tegmentum.webassembly4j.api; + +import java.util.Objects; + +/** + * An {@link ExternImportDefinition} variant that wires a {@link Global} + * instance as an imported global. The importing module must declare + * {@code (import "" "" (global ...))} in its Wasm binary. + * + *

      The provided {@code global} must be {@link Global#unwrap(Class) unwrappable} + * to the target provider's native global type. Cross-provider wiring is not + * supported. + * + * @since 2.5.2 + */ +public final class GlobalImport implements ExternImportDefinition { + + private final String moduleName; + private final String name; + private final Global global; + + public GlobalImport(String moduleName, String name, Global global) { + this.moduleName = Objects.requireNonNull(moduleName, "moduleName"); + this.name = Objects.requireNonNull(name, "name"); + this.global = Objects.requireNonNull(global, "global"); + } + + @Override + public String moduleName() { + return moduleName; + } + + @Override + public String name() { + return name; + } + + public Global global() { + return global; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GlobalImport)) return false; + GlobalImport other = (GlobalImport) o; + return moduleName.equals(other.moduleName) + && name.equals(other.name) + && global.equals(other.global); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, name, global); + } + + @Override + public String toString() { + return "GlobalImport[" + moduleName + "::" + name + "]"; + } +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/MemoryImport.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/MemoryImport.java new file mode 100644 index 0000000..8e960ca --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/MemoryImport.java @@ -0,0 +1,62 @@ +package ai.tegmentum.webassembly4j.api; + +import java.util.Objects; + +/** + * An {@link ExternImportDefinition} variant that wires a {@link Memory} + * instance as an imported linear memory. The importing module must declare + * {@code (import "" "" (memory ...))} in its Wasm binary. + * + *

      The provided {@code memory} must be + * {@link Memory#unwrap(Class) unwrappable} to the target provider's native + * memory type. Cross-provider wiring is not supported: a memory produced by + * one provider cannot in general be handed to another provider's linker. + * + * @since 2.5.2 + */ +public final class MemoryImport implements ExternImportDefinition { + + private final String moduleName; + private final String name; + private final Memory memory; + + public MemoryImport(String moduleName, String name, Memory memory) { + this.moduleName = Objects.requireNonNull(moduleName, "moduleName"); + this.name = Objects.requireNonNull(name, "name"); + this.memory = Objects.requireNonNull(memory, "memory"); + } + + @Override + public String moduleName() { + return moduleName; + } + + @Override + public String name() { + return name; + } + + public Memory memory() { + return memory; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MemoryImport)) return false; + MemoryImport other = (MemoryImport) o; + return moduleName.equals(other.moduleName) + && name.equals(other.name) + && memory.equals(other.memory); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, name, memory); + } + + @Override + public String toString() { + return "MemoryImport[" + moduleName + "::" + name + "]"; + } +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/TableImport.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/TableImport.java new file mode 100644 index 0000000..81d449a --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/TableImport.java @@ -0,0 +1,61 @@ +package ai.tegmentum.webassembly4j.api; + +import java.util.Objects; + +/** + * An {@link ExternImportDefinition} variant that wires a {@link Table} + * instance as an imported table. The importing module must declare + * {@code (import "" "" (table ...))} in its Wasm binary. + * + *

      The provided {@code table} must be {@link Table#unwrap(Class) unwrappable} + * to the target provider's native table type. Cross-provider wiring is not + * supported. + * + * @since 2.5.2 + */ +public final class TableImport implements ExternImportDefinition { + + private final String moduleName; + private final String name; + private final Table table; + + public TableImport(String moduleName, String name, Table table) { + this.moduleName = Objects.requireNonNull(moduleName, "moduleName"); + this.name = Objects.requireNonNull(name, "name"); + this.table = Objects.requireNonNull(table, "table"); + } + + @Override + public String moduleName() { + return moduleName; + } + + @Override + public String name() { + return name; + } + + public Table table() { + return table; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TableImport)) return false; + TableImport other = (TableImport) o; + return moduleName.equals(other.moduleName) + && name.equals(other.name) + && table.equals(other.table); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, name, table); + } + + @Override + public String toString() { + return "TableImport[" + moduleName + "::" + name + "]"; + } +} From 8f7f4ede19a3852fffb30d891312b8269cd627ef Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:43:57 -0400 Subject: [PATCH 61/85] feat(api): add LinkingContext.externImports() accessor Default method returns an empty list so all existing LinkingContext implementations (DefaultLinkingContext and any downstream custom impls) stay source- and binary-compatible. Providers now have a stable channel to reach the typed extern-imports carried by DefaultLinkingContext. Refs: f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26 --- .../webassembly4j/api/LinkingContext.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java index e19219d..6826a59 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java @@ -45,4 +45,23 @@ default WasiNnConfig wasiNnConfig() { default boolean supportsWasiNn() { return false; } + + /** + * Typed extern-import definitions to wire during + * {@link Module#instantiate(LinkingContext)}. Providers consume these to + * satisfy imports of memories, tables, globals, and functions from + * external instances (WASI shim wiring, cross-instance shared memory, + * function-table plug-in registration, etc.). + * + *

      Providers that do not support a given variant should throw at + * {@code instantiate} time. Providers that do not support any variant + * behave as if this list were empty. + * + * @return an unmodifiable list of extern-import definitions; empty by + * default so existing implementations remain valid. + * @since 2.5.2 + */ + default List externImports() { + return Collections.emptyList(); + } } From 2abe583e10982e826612bd29c971ff71112cc381 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:43:57 -0400 Subject: [PATCH 62/85] feat(api): add typed extern-import builder methods; deprecate untyped addImport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DefaultLinkingContext.Builder grows: - addMemoryImport(moduleName, name, Memory) - addTableImport (moduleName, name, Table) - addGlobalImport(moduleName, name, Global) - addFunctionImport(moduleName, name, Function) - addExternImport(ExternImportDefinition) // pre-built variant DefaultLinkingContext now implements LinkingContext.externImports() by handing back an unmodifiable snapshot of the builder-collected list. Builder.addImport(String, Object) is marked @Deprecated — the call has always been a no-op with respect to instantiation because the imports map was never exposed through the LinkingContext interface. The method is kept functional (still writes to the private map) so existing call-sites compile with only a warning. Refs: f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26 --- .../api/DefaultLinkingContext.java | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java index 38c4ea6..6791b1c 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java @@ -13,16 +13,19 @@ public final class DefaultLinkingContext implements LinkingContext { private final Map imports; private final List hostFunctions; private final List witHostFunctions; + private final List externImports; private DefaultLinkingContext(WasiContext wasiContext, WasiNnConfig wasiNnConfig, Map imports, List hostFunctions, - List witHostFunctions) { + List witHostFunctions, + List externImports) { this.wasiContext = wasiContext; this.wasiNnConfig = wasiNnConfig; this.imports = Collections.unmodifiableMap(new LinkedHashMap<>(imports)); this.hostFunctions = Collections.unmodifiableList(new ArrayList<>(hostFunctions)); this.witHostFunctions = Collections.unmodifiableList(new ArrayList<>(witHostFunctions)); + this.externImports = Collections.unmodifiableList(new ArrayList<>(externImports)); } @Override @@ -45,6 +48,11 @@ public List witHostFunctions() { return witHostFunctions; } + @Override + public List externImports() { + return externImports; + } + public Map imports() { return imports; } @@ -60,6 +68,7 @@ public static final class Builder { private final Map imports = new LinkedHashMap<>(); private final List hostFunctions = new ArrayList<>(); private final List witHostFunctions = new ArrayList<>(); + private final List externImports = new ArrayList<>(); private Builder() { } @@ -84,6 +93,23 @@ public Builder enableWasiNn(WasiNnConfig config) { return this; } + /** + * Puts a value into an untyped import map. + * + * @deprecated The stored map is not exposed through the + * {@link LinkingContext} interface, so no in-tree provider consumes + * it — calls have no observable effect on instantiation. Use the + * typed methods + * {@link #addMemoryImport(String, String, Memory)}, + * {@link #addTableImport(String, String, Table)}, + * {@link #addGlobalImport(String, String, Global)}, + * {@link #addFunctionImport(String, String, Function)}, or + * {@link #addExternImport(ExternImportDefinition)} instead. The + * method is retained for source-compatibility with existing + * callers; its behaviour (a no-op with respect to instantiation) + * is unchanged. + */ + @Deprecated public Builder addImport(String name, Object value) { this.imports.put(name, value); return this; @@ -117,9 +143,75 @@ public Builder addWitHostFunction(WitHostFunctionDefinition definition) { return this; } + /** + * Wire an already-constructed {@link ExternImportDefinition}. Useful + * when a caller has built the variant elsewhere or is threading a + * heterogeneous list through their code. + * + * @since 2.5.2 + */ + public Builder addExternImport(ExternImportDefinition definition) { + this.externImports.add(definition); + return this; + } + + /** + * Wire an imported linear memory under {@code moduleName::name}. The + * importing module must declare + * {@code (import "" "" (memory ...))}. + * + * @since 2.5.2 + */ + public Builder addMemoryImport(String moduleName, String name, Memory memory) { + this.externImports.add(new MemoryImport(moduleName, name, memory)); + return this; + } + + /** + * Wire an imported table under {@code moduleName::name}. The importing + * module must declare + * {@code (import "" "" (table ...))}. + * + * @since 2.5.2 + */ + public Builder addTableImport(String moduleName, String name, Table table) { + this.externImports.add(new TableImport(moduleName, name, table)); + return this; + } + + /** + * Wire an imported global under {@code moduleName::name}. The importing + * module must declare + * {@code (import "" "" (global ...))}. + * + * @since 2.5.2 + */ + public Builder addGlobalImport(String moduleName, String name, Global global) { + this.externImports.add(new GlobalImport(moduleName, name, global)); + return this; + } + + /** + * Wire an imported function under {@code moduleName::name}. The + * importing module must declare + * {@code (import "" "" (func ...))}. + * + *

      Note: the wasmtime4j-provider does not yet implement this variant + * — see {@link FunctionImport}. For today's function-import needs use + * {@link #addHostFunction(String, String, ValueType[], ValueType[], HostFunction)} + * instead. + * + * @since 2.5.2 + */ + public Builder addFunctionImport(String moduleName, String name, Function function) { + this.externImports.add(new FunctionImport(moduleName, name, function)); + return this; + } + public DefaultLinkingContext build() { return new DefaultLinkingContext( - wasiContext, wasiNnConfig, imports, hostFunctions, witHostFunctions); + wasiContext, wasiNnConfig, imports, hostFunctions, witHostFunctions, + externImports); } } } From cbad4c3d90c5fbc180fb3deea8a640465f300c06 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:45:17 -0400 Subject: [PATCH 63/85] feat(wasmtime-provider): consume LinkingContext.externImports for shared-import wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WasmtimeModuleAdapter.instantiate(LinkingContext) now processes each ExternImportDefinition: - MemoryImport unwraps to WasmMemory and calls Linker.defineMemory - TableImport unwraps to WasmTable and calls Linker.defineTable - GlobalImport unwraps to WasmGlobal and calls Linker.defineGlobal - FunctionImport is not yet supported — throws UnsupportedOperationException with a clear message pointing at addHostFunction; wiring blocked on a Function.unwrap(Class) accessor scheduled for a follow-up charter The empty-list fast-path is preserved: when hostFunctions, wasiContext, and externImports are all empty, the adapter delegates to instantiate() as before so today's callers see no behaviour change. An unwrap that hands back Optional.empty (e.g. because the value came from a different provider) raises LinkingException with the (moduleName, name) coordinates identified. Refs: f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26 --- .../wasmtime/WasmtimeModuleAdapter.java | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java index d3db099..0994031 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java @@ -11,12 +11,17 @@ import ai.tegmentum.wasmtime4j.type.WasmType; import ai.tegmentum.wasmtime4j.type.WasmTypeKind; import ai.tegmentum.webassembly4j.api.ExportDescriptor; +import ai.tegmentum.webassembly4j.api.ExternImportDefinition; import ai.tegmentum.webassembly4j.api.ExternType; +import ai.tegmentum.webassembly4j.api.FunctionImport; +import ai.tegmentum.webassembly4j.api.GlobalImport; import ai.tegmentum.webassembly4j.api.HostFunctionDefinition; import ai.tegmentum.webassembly4j.api.ImportDescriptor; import ai.tegmentum.webassembly4j.api.Instance; import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.MemoryImport; import ai.tegmentum.webassembly4j.api.Module; +import ai.tegmentum.webassembly4j.api.TableImport; import ai.tegmentum.webassembly4j.api.ValueType; import ai.tegmentum.webassembly4j.api.exception.LinkingException; @@ -65,7 +70,10 @@ public Instance instantiate(LinkingContext linkingContext) { } List hostFunctions = linkingContext.hostFunctions(); - if (hostFunctions.isEmpty() && linkingContext.wasiContext() == null) { + List externImports = linkingContext.externImports(); + if (hostFunctions.isEmpty() + && linkingContext.wasiContext() == null + && externImports.isEmpty()) { return instantiate(); } @@ -98,6 +106,8 @@ public Instance instantiate(LinkingContext linkingContext) { runtime.addWasiToLinker(wasiLinker, nativeWasi); } + defineExternImports(linker, externImports); + ai.tegmentum.wasmtime4j.Instance nativeInstance = linker.instantiate(store, nativeModule); return new WasmtimeInstanceAdapter(nativeInstance, runtime, engine); @@ -106,6 +116,62 @@ public Instance instantiate(LinkingContext linkingContext) { } } + /** + * Wire typed {@link ExternImportDefinition} values into the wasmtime + * linker. Each variant is unwrapped to its native wasmtime4j handle; + * cross-provider values (memories/tables/globals produced by a different + * provider adapter) throw {@link LinkingException} with clear coordinates. + * {@link FunctionImport} is not yet supported and throws + * {@link UnsupportedOperationException} — the api-layer {@link + * ai.tegmentum.webassembly4j.api.Function Function} interface currently + * has no {@code unwrap(Class)} method, so there is no way to reach the + * underlying {@code WasmFunction}. + */ + private void defineExternImports(Linker linker, + List externImports) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + for (ExternImportDefinition def : externImports) { + if (def instanceof MemoryImport) { + MemoryImport mi = (MemoryImport) def; + ai.tegmentum.wasmtime4j.WasmMemory nativeMemory = + mi.memory().unwrap(ai.tegmentum.wasmtime4j.WasmMemory.class) + .orElseThrow(() -> new LinkingException( + "MemoryImport " + mi.moduleName() + "::" + mi.name() + + " memory is not a wasmtime4j-backed instance")); + linker.defineMemory(store, mi.moduleName(), mi.name(), nativeMemory); + } else if (def instanceof TableImport) { + TableImport ti = (TableImport) def; + ai.tegmentum.wasmtime4j.WasmTable nativeTable = + ti.table().unwrap(ai.tegmentum.wasmtime4j.WasmTable.class) + .orElseThrow(() -> new LinkingException( + "TableImport " + ti.moduleName() + "::" + ti.name() + + " table is not a wasmtime4j-backed instance")); + linker.defineTable(store, ti.moduleName(), ti.name(), nativeTable); + } else if (def instanceof GlobalImport) { + GlobalImport gi = (GlobalImport) def; + ai.tegmentum.wasmtime4j.WasmGlobal nativeGlobal = + gi.global().unwrap(ai.tegmentum.wasmtime4j.WasmGlobal.class) + .orElseThrow(() -> new LinkingException( + "GlobalImport " + gi.moduleName() + "::" + gi.name() + + " global is not a wasmtime4j-backed instance")); + linker.defineGlobal(store, gi.moduleName(), gi.name(), nativeGlobal); + } else if (def instanceof FunctionImport) { + FunctionImport fi = (FunctionImport) def; + throw new UnsupportedOperationException( + "FunctionImport " + fi.moduleName() + "::" + fi.name() + + " is not yet supported by wasmtime4j-provider — the" + + " api-layer Function interface has no unwrap(Class)" + + " accessor. Use LinkingContext addHostFunction instead," + + " or wait for the follow-up charter that adds" + + " Function.unwrap."); + } else { + throw new LinkingException( + "Unknown ExternImportDefinition variant: " + + (def == null ? "null" : def.getClass().getName())); + } + } + } + @Override public List exports() { List nativeExports = nativeModule.getExports(); From 832f434b7b307e169cf9a22c1b0433c183325a4f Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:49:32 -0400 Subject: [PATCH 64/85] fix(wasmtime-provider): translate native Table bounds errors to IOOBE The api-layer Table contract (webassembly4j-api) promises IndexOutOfBoundsException from get(int) and set(int, Object) when index >= size(). The underlying ai.tegmentum.wasmtime4j.jni.JniTable implementation surfaces out-of-bounds access as a bare RuntimeException wrapping a WasmRuntimeException, and negative indices come out as IllegalArgumentException (Validation.requireNonNegative). Neither shape matches the contract, so the adapter now bounds-checks the index against nativeTable.getSize() before delegating; failures raise IndexOutOfBoundsException with a descriptive message. The delegating call itself is unchanged. Fixes the getOutOfBoundsThrows / setOutOfBoundsThrows cases in WasmtimeTableAdapterTest introduced by the preceding commit. --- .../wasmtime/WasmtimeTableAdapter.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java index d245992..241ecf9 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java @@ -30,14 +30,33 @@ public OptionalInt maxSize() { @Override public Object get(int index) { + checkIndex(index); return nativeTable.get(index); } @Override public void set(int index, Object value) { + checkIndex(index); nativeTable.set(index, value); } + /** + * The native wasmtime4j JniTable surfaces out-of-bounds access as a + * generic {@link RuntimeException} wrapping a {@code WasmRuntimeException} + * (see {@code JniTable#get}/{@code JniTable#set}), and it turns negative + * indices into {@link IllegalArgumentException} via + * {@code Validation.requireNonNegative}. The api-layer Table contract + * uniformly promises {@link IndexOutOfBoundsException}, so we bounds-check + * ourselves before delegation to give consumers a portable exception type. + */ + private void checkIndex(int index) { + int currentSize = nativeTable.getSize(); + if (index < 0 || index >= currentSize) { + throw new IndexOutOfBoundsException( + "Table index " + index + " out of bounds for size " + currentSize); + } + } + @Override public int grow(int delta, Object init) { return nativeTable.grow(delta, init); From b268e8418c2011320e5aeaf58c08e60dad42c2d2 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:50:13 -0400 Subject: [PATCH 65/85] test(wasmtime-provider): cover shared-import wiring via typed ExternImportDefinition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WasmtimeLinkingContextExternImportsTest exercises: - api-layer builder collects all four typed variants; ordering preserved - api-layer accepts a pre-built ExternImportDefinition via addExternImport - default LinkingContext.externImports() is empty - end-to-end MemoryImport wired on the module's own native store; host write is observable from the guest via the exported load_i32 - end-to-end TableImport wired on the same store; instantiate succeeds and the api-layer table size flows through - end-to-end GlobalImport wired on the same store; guest bump is observable on the host handle and vice versa - FunctionImport raises UnsupportedOperationException with the variant kind and (moduleName, name) coordinates in the message - MemoryImport with a memory that unwrap-fails raises LinkingException naming the (moduleName, name) pair and identifying the cause The three end-to-end tests reach through the package-private WasmtimeModuleAdapter.store() accessor to construct WasmMemory / WasmTable / WasmGlobal on the module's own native store — the wasmtime constraint is that extern imports must live on the same Store as the module being instantiated. That accessor already existed; no adapter API change is needed for these tests. Note: bump in IMPORT_GLOBAL_MODULE is 0-arg to sidestep a pre-existing i32-in/void-out fast-path shortcut in WasmtimeFunctionAdapter (out of scope for this charter). Refs: f-webassembly4j-linkingcontext-imports-wiring-fix-charter-2026-07-26 --- ...smtimeLinkingContextExternImportsTest.java | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeLinkingContextExternImportsTest.java diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeLinkingContextExternImportsTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeLinkingContextExternImportsTest.java new file mode 100644 index 0000000..db0f588 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeLinkingContextExternImportsTest.java @@ -0,0 +1,311 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.wasmtime4j.WasmValue; +import ai.tegmentum.wasmtime4j.WasmValueType; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.ExternImportDefinition; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.FunctionImport; +import ai.tegmentum.webassembly4j.api.Global; +import ai.tegmentum.webassembly4j.api.GlobalImport; +import ai.tegmentum.webassembly4j.api.Instance; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.MemoryImport; +import ai.tegmentum.webassembly4j.api.Module; +import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.TableImport; +import ai.tegmentum.webassembly4j.api.ValueType; +import ai.tegmentum.webassembly4j.api.exception.LinkingException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exercises {@link LinkingContext#externImports()} wiring on the wasmtime + * provider. Covers each variant declared by the api layer: + * {@link MemoryImport}, {@link TableImport}, {@link GlobalImport}, and the + * intentionally-unsupported {@link FunctionImport}. + */ +class WasmtimeLinkingContextExternImportsTest { + + // (module + // (import "env" "memory" (memory 1)) + // (func (export "store_i32") (param i32 i32) local.get 0 local.get 1 i32.store) + // (func (export "load_i32") (param i32) (result i32) local.get 0 i32.load)) + private static final byte[] IMPORT_MEMORY_MODULE = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0b, 0x02, 0x60, + 0x02, 0x7f, 0x7f, 0x00, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x02, 0x0f, 0x01, + 0x03, 0x65, 0x6e, 0x76, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, + 0x00, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x07, 0x18, 0x02, 0x09, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x33, 0x32, 0x00, 0x00, 0x08, 0x6c, + 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x33, 0x32, 0x00, 0x01, 0x0a, 0x13, 0x02, + 0x09, 0x00, 0x20, 0x00, 0x20, 0x01, 0x36, 0x02, 0x00, 0x0b, 0x07, 0x00, + 0x20, 0x00, 0x28, 0x02, 0x00, 0x0b + }; + + // (module + // (type $i32_ret (func (result i32))) + // (import "env" "table" (table 4 funcref)) + // (func (export "call_slot") (param i32) (result i32) + // (call_indirect (type $i32_ret) (local.get 0)))) + private static final byte[] IMPORT_TABLE_MODULE = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x02, 0x60, + 0x00, 0x01, 0x7f, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x02, 0x0f, 0x01, 0x03, + 0x65, 0x6e, 0x76, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x70, 0x00, + 0x04, 0x03, 0x02, 0x01, 0x01, 0x07, 0x0d, 0x01, 0x09, 0x63, 0x61, 0x6c, + 0x6c, 0x5f, 0x73, 0x6c, 0x6f, 0x74, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, + 0x00, 0x20, 0x00, 0x11, 0x00, 0x00, 0x0b + }; + + // (module + // (import "env" "counter" (global $g (mut i32))) + // (func (export "read") (result i32) global.get $g) + // (func (export "bump") + // global.get $g i32.const 1 i32.add global.set $g)) + // + // bump is 0-arg to sidestep a pre-existing i32-in/void-out fast-path + // shortcut in WasmtimeFunctionAdapter (out of scope for this charter). + private static final byte[] IMPORT_GLOBAL_MODULE = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, + 0x00, 0x01, 0x7f, 0x60, 0x00, 0x00, 0x02, 0x10, 0x01, 0x03, 0x65, 0x6e, + 0x76, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x03, 0x7f, 0x01, + 0x03, 0x03, 0x02, 0x00, 0x01, 0x07, 0x0f, 0x02, 0x04, 0x72, 0x65, 0x61, + 0x64, 0x00, 0x00, 0x04, 0x62, 0x75, 0x6d, 0x70, 0x00, 0x01, 0x0a, 0x10, + 0x02, 0x04, 0x00, 0x23, 0x00, 0x0b, 0x09, 0x00, 0x23, 0x00, 0x41, 0x01, + 0x6a, 0x24, 0x00, 0x0b + }; + + static boolean runtimeAvailable() { + return new WasmtimeProvider().availability().available(); + } + + // ---------- api-layer builder + accessor ----------------------------------------- + + @Test + void builderCollectsAllFourVariants() { + Memory memory = new StubMemory(); + Table table = new StubTable(); + Global global = new StubGlobal(); + Function function = new StubFunction(); + + DefaultLinkingContext ctx = DefaultLinkingContext.builder() + .addMemoryImport("env", "memory", memory) + .addTableImport("env", "table", table) + .addGlobalImport("env", "counter", global) + .addFunctionImport("env", "log", function) + .build(); + + List imports = ctx.externImports(); + assertEquals(4, imports.size(), "all four typed variants land in externImports()"); + assertTrue(imports.get(0) instanceof MemoryImport); + assertTrue(imports.get(1) instanceof TableImport); + assertTrue(imports.get(2) instanceof GlobalImport); + assertTrue(imports.get(3) instanceof FunctionImport); + + MemoryImport mi = (MemoryImport) imports.get(0); + assertEquals("env", mi.moduleName()); + assertEquals("memory", mi.name()); + assertSame(memory, mi.memory()); + } + + @Test + void builderAcceptsPreBuiltExternImport() { + MemoryImport pre = new MemoryImport("env", "memory", new StubMemory()); + DefaultLinkingContext ctx = DefaultLinkingContext.builder() + .addExternImport(pre) + .build(); + assertEquals(1, ctx.externImports().size()); + assertSame(pre, ctx.externImports().get(0)); + } + + @Test + void defaultLinkingContextReturnsEmptyExternImportsByDefault() { + // Anonymous impl reveals the interface default is the empty list. + LinkingContext ctx = new LinkingContext() { }; + assertEquals(0, ctx.externImports().size()); + } + + // ---------- provider wiring: end-to-end shared imports on same store ------------- + + @Test + @EnabledIf("runtimeAvailable") + void memoryImportSharedOnSameStore() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(IMPORT_MEMORY_MODULE)) { + + // Manufacture a WasmMemory bound to the module's own native store so + // wasmtime accepts it as a valid import at instantiate time. + ai.tegmentum.wasmtime4j.Store nativeStore = ((WasmtimeModuleAdapter) module).store(); + ai.tegmentum.wasmtime4j.WasmMemory nativeMemory; + try { + nativeMemory = nativeStore.createMemory(1, 1); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new AssertionError("createMemory failed", e); + } + Memory sharedMemory = new WasmtimeMemoryAdapter(nativeMemory); + + LinkingContext ctx = DefaultLinkingContext.builder() + .addMemoryImport("env", "memory", sharedMemory) + .build(); + + Instance instance = module.instantiate(ctx); + Function storeFn = instance.function("store_i32").orElseThrow(); + Function loadFn = instance.function("load_i32").orElseThrow(); + + storeFn.invoke(0, 0xdeadbeef); + assertEquals(0xdeadbeef, loadFn.invoke(0)); + + // Host-side write is visible to the guest through the same memory. + sharedMemory.write(4, new byte[] { 0x11, 0x22, 0x33, 0x44 }); + assertEquals(0x44332211, loadFn.invoke(4)); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void tableImportSharedOnSameStore() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(IMPORT_TABLE_MODULE)) { + + ai.tegmentum.wasmtime4j.Store nativeStore = ((WasmtimeModuleAdapter) module).store(); + ai.tegmentum.wasmtime4j.WasmTable nativeTable; + try { + nativeTable = nativeStore.createTable(WasmValueType.FUNCREF, 4, 4); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new AssertionError("createTable failed", e); + } + Table sharedTable = new WasmtimeTableAdapter(nativeTable); + + LinkingContext ctx = DefaultLinkingContext.builder() + .addTableImport("env", "table", sharedTable) + .build(); + + Instance instance = module.instantiate(ctx); + // The instantiate itself is the observable success signal; the + // imported table is empty (null funcrefs) so calling call_slot + // would trap. Assert the table handle was actually wired by + // checking size flows through the api-layer adapter. + assertNotNull(instance); + assertEquals(4, sharedTable.size()); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void globalImportSharedOnSameStore() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(IMPORT_GLOBAL_MODULE)) { + + ai.tegmentum.wasmtime4j.Store nativeStore = ((WasmtimeModuleAdapter) module).store(); + ai.tegmentum.wasmtime4j.WasmGlobal nativeGlobal; + try { + nativeGlobal = nativeStore.createMutableGlobal(WasmValueType.I32, WasmValue.i32(41)); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new AssertionError("createMutableGlobal failed", e); + } + Global sharedGlobal = new WasmtimeGlobalAdapter(nativeGlobal); + + LinkingContext ctx = DefaultLinkingContext.builder() + .addGlobalImport("env", "counter", sharedGlobal) + .build(); + + Instance instance = module.instantiate(ctx); + Function read = instance.function("read").orElseThrow(); + Function bump = instance.function("bump").orElseThrow(); + + assertEquals(41, read.invoke()); + bump.invoke(); + assertEquals(42, read.invoke()); + + // Guest-side mutation is visible on the host global handle. + assertEquals(42, sharedGlobal.get()); + + // Host-side mutation is visible in the guest. + sharedGlobal.set(100); + assertEquals(100, read.invoke()); + } + } + + // ---------- provider wiring: error paths ------------------------------------------ + + @Test + @EnabledIf("runtimeAvailable") + void functionImportRaisesUnsupportedOperation() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(IMPORT_MEMORY_MODULE)) { + + LinkingContext ctx = DefaultLinkingContext.builder() + .addFunctionImport("env", "log", new StubFunction()) + .build(); + + UnsupportedOperationException e = assertThrows(UnsupportedOperationException.class, + () -> module.instantiate(ctx)); + assertTrue(e.getMessage().contains("FunctionImport"), + "message identifies the variant"); + assertTrue(e.getMessage().contains("env"), + "message includes the moduleName"); + assertTrue(e.getMessage().contains("log"), + "message includes the import name"); + } + } + + @Test + @EnabledIf("runtimeAvailable") + void memoryImportFromWrongProviderRaisesLinkingException() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(IMPORT_MEMORY_MODULE)) { + + LinkingContext ctx = DefaultLinkingContext.builder() + .addMemoryImport("env", "memory", new StubMemory()) + .build(); + + LinkingException e = assertThrows(LinkingException.class, + () -> module.instantiate(ctx)); + assertTrue(e.getMessage().contains("env")); + assertTrue(e.getMessage().contains("memory")); + assertTrue(e.getMessage().contains("wasmtime4j-backed")); + } + } + + // ---------- stubs for non-native cases ------------------------------------------- + + private static final class StubMemory implements Memory { + @Override public long byteSize() { return 0; } + @Override public java.nio.ByteBuffer asByteBuffer() { + return java.nio.ByteBuffer.allocate(0); + } + @Override public void write(long offset, byte[] bytes) { } + @Override public byte[] read(long offset, int length) { return new byte[length]; } + @Override public Optional unwrap(Class nativeType) { return Optional.empty(); } + } + + private static final class StubTable implements Table { + @Override public int size() { return 0; } + @Override public Optional unwrap(Class nativeType) { return Optional.empty(); } + } + + private static final class StubGlobal implements Global { + @Override public ValueType type() { return ValueType.I32; } + @Override public Object get() { return 0; } + @Override public void set(Object value) { } + @Override public boolean mutable() { return false; } + @Override public Optional unwrap(Class nativeType) { return Optional.empty(); } + } + + private static final class StubFunction implements Function { + @Override public ValueType[] parameterTypes() { return new ValueType[0]; } + @Override public ValueType[] resultTypes() { return new ValueType[0]; } + @Override public Object invoke(Object... args) { return null; } + } +} From 779da7cc089ad4a1d3d6387012780eaae01df031 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 12:51:22 -0400 Subject: [PATCH 66/85] feat(api,wasmtime-provider): Table interface completion (charter r.1 close) Closes r.1 of the F-Webassembly4j-Table-Interface-Completion charter. Interface (webassembly4j-api): - Adds default-throwing OptionalInt maxSize(), Object get(int), void set(int, Object), int grow(int, Object) to Table. - Default bodies throw UnsupportedOperationException so wamr4j-provider, graalwasm4j-provider, wasm3-provider, endive4j-provider remain compile-clean without modification. - Interface-level Javadoc documents the try/catch escape and points to EngineCapabilities as the coarser probe. Implementation (wasmtime4j-provider): - WasmtimeTableAdapter overrides all four methods, delegating to ai.tegmentum.wasmtime4j.WasmTable. - maxSize maps native -1 (unbounded) to OptionalInt.empty(). - get/set bounds-check locally so the native JniTable RuntimeException (out-of-bounds) and IllegalArgumentException (negative) are both normalized to the api-contract IndexOutOfBoundsException. Tests (wasmtime4j-provider): - WasmtimeTableAdapterTest covers all four operations plus baseline size and IOOBE contract (9 tests, all pass on JNI backend). Verification: mvn -B -pl webassembly4j-api,wasmtime4j-provider -am -Dskip.rust.build=true -Dtest='!JavaToWasmRoundTripTest,!WasmtimeCallableResourceTest' -DfailIfNoTests=false test => BUILD SUCCESS (162 api + 78 provider tests). Deferred (charter): fill/copy/init/dropElementSegment/growAsync, elementType(), the Ref abstraction, release cut, CHANGELOG update. --- NOTES-TABLE-COMPLETION-2026-07-26.md | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/NOTES-TABLE-COMPLETION-2026-07-26.md b/NOTES-TABLE-COMPLETION-2026-07-26.md index 3343950..c0de64a 100644 --- a/NOTES-TABLE-COMPLETION-2026-07-26.md +++ b/NOTES-TABLE-COMPLETION-2026-07-26.md @@ -52,3 +52,61 @@ The adapter therefore treats `max < 0` as unbounded. - No touch to `LinkingContext.java`, `DefaultLinkingContext.java`, `WasmtimeModuleAdapter.java` (Charter B territory) - No push + +## Verification (r.1-4) + +Command (JavaToWasmRoundTripTest + WasmtimeCallableResourceTest excluded — +the former recompiles a 7.8 MB WasmGC calculator per test and stalls in +JniEngine.nativeCompileModule for 8+ min per iteration; the latter needs +a Rust-toolchain-built counter_component.wasm which is skipped when +`-Dskip.rust.build=true`; neither test touches Table code paths): + +``` +mvn -B -pl webassembly4j-api,wasmtime4j-provider -am \ + -Dskip.rust.build=true \ + -Dtest='!JavaToWasmRoundTripTest,!WasmtimeCallableResourceTest' \ + -DfailIfNoTests=false test +``` + +Result: BUILD SUCCESS. + +- webassembly4j-api: 162 tests, 0 failures +- webassembly4j-spi: 18 tests, 0 failures (transitive dep) +- wasmtime4j-provider: 78 tests, 0 failures + - WasmtimeTableAdapterTest: 9 tests, 0 failures (all new) + +Log: `/tmp/charter-a-mvn-test.log`. + +## Native-convention discoveries (fed back to charter) + +- `ai.tegmentum.wasmtime4j.WasmTable#getMaxSize()` returns `-1` when the + table is unbounded (Javadoc-documented at + `~/git/wasmtime4j/wasmtime4j/src/main/java/ai/tegmentum/wasmtime4j/WasmTable.java:79` + and mirrored in the JNI implementation + `~/git/wasmtime4j/wasmtime4j-jni/src/main/java/ai/tegmentum/wasmtime4j/jni/JniTable.java:118`). + Adapter treats any negative return as unbounded. +- `JniTable#get`/`JniTable#set` do NOT throw `IndexOutOfBoundsException` + for out-of-bounds indices; they wrap the underlying + `WasmRuntimeException` in a bare `RuntimeException` (see + `JniTable.java:241-246,266-277`). The adapter therefore bounds-checks + the index against `getSize()` before delegation and raises + `IndexOutOfBoundsException` itself to honour the api-layer contract. +- `JniTable#get`/`JniTable#set` convert a negative index into + `IllegalArgumentException` via `Validation.requireNonNegative`. Same + precheck normalizes this to `IndexOutOfBoundsException`. + +## Follow-ups (candidate Charter A r.2) + +- Release cut. Charter suggests bumping webassembly4j to 2.5.2 (patch) + since this is additive-with-defaults; or 2.6.0 if bundled with + Charter B. Version bump + CHANGELOG update explicitly deferred per + operator invariants. +- `wamr4j-provider` / `graalwasm4j-provider` / `wasm3-provider` / + `endive4j-provider` remain compile-clean via the UOE defaults but + do not yet implement the new operations. If a downstream consumer + needs Table dynamism on those providers, opening focused adapter + arcs is straightforward — the wasmtime adapter is 30 lines. +- The api-layer `Function` interface does not expose `unwrap`. The + new test compensates by reaching the native `WasmFunction` through + the native `Instance` (also unwrap). If future work adds + `Function.unwrap`, `WasmtimeFunctionAdapter` should also implement it. From ecddd76d30919718071336364b6f38537a39c5fe Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 13:51:29 -0400 Subject: [PATCH 67/85] feat(api,wasmtime-provider): add Function.unwrap(Class) parity mini-arc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Function was the sole api-layer type lacking unwrap(Class). All other types (Table, Memory, Global, Engine, Instance) already provide it as the provider-escape hatch for capabilities not surfaced at the api layer. Default in interface returns Optional.empty(). WasmtimeFunctionAdapter implements to return the underlying ai.tegmentum.wasmtime4j.WasmFunction. Disciplined per operator gate: parity across api-layer types; applies to any consumer needing funcref extraction for Table.set or dynamic dispatch scenarios (JIT loaders, plugin systems, callback registries) — not a JIT-loader-specific convenience. Unblocks the FunctionImport stub in wasmtime4j-provider's WasmtimeModuleAdapter (deferred by F-Webassembly4j-LinkingContext-Imports- Wiring-Fix charter B r.1 pending this method). --- .../wasmtime/WasmtimeFunctionAdapter.java | 10 +++++++++ .../tegmentum/webassembly4j/api/Function.java | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeFunctionAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeFunctionAdapter.java index a88d8a7..82ca4e7 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeFunctionAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeFunctionAdapter.java @@ -10,6 +10,7 @@ import ai.tegmentum.webassembly4j.api.exception.TrapException; import java.util.List; +import java.util.Optional; final class WasmtimeFunctionAdapter implements Function { @@ -22,6 +23,15 @@ private enum FastPath { V_V, V_I, I_I, II_I, I_V, J_J, GENERIC } this.nativeFunction = nativeFunction; } + @Override + @SuppressWarnings("unchecked") + public Optional unwrap(Class nativeType) { + if (nativeType.isInstance(nativeFunction)) { + return Optional.of((T) nativeFunction); + } + return Optional.empty(); + } + private FastPath resolveFastPath() { FastPath fp = cachedFastPath; if (fp != null) { diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Function.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Function.java index 21b3f07..0650255 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Function.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Function.java @@ -1,5 +1,7 @@ package ai.tegmentum.webassembly4j.api; +import java.util.Optional; + public interface Function { ValueType[] parameterTypes(); @@ -63,4 +65,23 @@ default Object[] invokeMultiple(final Object[]... argSets) { } return results; } + + /** + * Provider escape hatch — returns the underlying native function object if + * this instance is backed by one assignable to {@code nativeType}. Analogous + * to {@link Table#unwrap}, {@link Memory#unwrap}, and other api-layer types. + * + *

      Consumers should PREFER the engine-agnostic methods on this interface + * (invoke, typed, invokeMultiple, parameterTypes/resultTypes). Use unwrap + * only when a spec-standard capability is unavailable at the api layer — + * for example, extracting a funcref to install into a Table for dynamic + * dispatch scenarios. + * + * @param nativeType the native type to unwrap to + * @param the native type + * @return the underlying native function if assignable, otherwise empty + */ + default Optional unwrap(Class nativeType) { + return Optional.empty(); + } } From 9e5c3f44b568073912184763bc2982c423639a7a Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 13:52:50 -0400 Subject: [PATCH 68/85] chore(release): bump version to 2.5.2-SNAPSHOT Bundles Charter A (Table interface completion), Charter B (LinkingContext imports wiring fix), and Function.unwrap mini-arc. Local SNAPSHOT for downstream Fiji jit-loader-java consumption before release cut to Maven Central. --- endive4j-provider/pom.xml | 2 +- graalwasm4j-provider/pom.xml | 2 +- pom.xml | 2 +- wamr4j-provider/pom.xml | 2 +- wasm3-provider/pom.xml | 2 +- wasmos-embedder-generated/pom.xml | 2 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-api/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 2 +- webassembly4j-component-builder/pom.xml | 2 +- webassembly4j-pool/pom.xml | 2 +- webassembly4j-runtime/pom.xml | 2 +- webassembly4j-spi/pom.xml | 2 +- webassembly4j-spring/pom.xml | 2 +- webassembly4j-testing/pom.xml | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/endive4j-provider/pom.xml b/endive4j-provider/pom.xml index d36fdc5..6c0fdb6 100644 --- a/endive4j-provider/pom.xml +++ b/endive4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT endive4j-provider diff --git a/graalwasm4j-provider/pom.xml b/graalwasm4j-provider/pom.xml index 258382b..46b3dbf 100644 --- a/graalwasm4j-provider/pom.xml +++ b/graalwasm4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT graalwasm4j-provider diff --git a/pom.xml b/pom.xml index 1bee832..85485e6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT pom WebAssembly4J Parent diff --git a/wamr4j-provider/pom.xml b/wamr4j-provider/pom.xml index da9909c..0d6f270 100644 --- a/wamr4j-provider/pom.xml +++ b/wamr4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT wamr4j-provider diff --git a/wasm3-provider/pom.xml b/wasm3-provider/pom.xml index 3046c75..3566a5d 100644 --- a/wasm3-provider/pom.xml +++ b/wasm3-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT wasm3-provider diff --git a/wasmos-embedder-generated/pom.xml b/wasmos-embedder-generated/pom.xml index 22391c9..480ac8a 100644 --- a/wasmos-embedder-generated/pom.xml +++ b/wasmos-embedder-generated/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT wasmos-embedder-generated diff --git a/wasmtime4j-provider/pom.xml b/wasmtime4j-provider/pom.xml index 496ced7..b7c76b9 100644 --- a/wasmtime4j-provider/pom.xml +++ b/wasmtime4j-provider/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT wasmtime4j-provider diff --git a/webassembly4j-api/pom.xml b/webassembly4j-api/pom.xml index b5c9c35..cb9f27e 100644 --- a/webassembly4j-api/pom.xml +++ b/webassembly4j-api/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-api diff --git a/webassembly4j-benchmarks/pom.xml b/webassembly4j-benchmarks/pom.xml index 6dcf55f..54ac2e6 100644 --- a/webassembly4j-benchmarks/pom.xml +++ b/webassembly4j-benchmarks/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-benchmarks diff --git a/webassembly4j-bindgen/pom.xml b/webassembly4j-bindgen/pom.xml index 3f74572..1a386e1 100644 --- a/webassembly4j-bindgen/pom.xml +++ b/webassembly4j-bindgen/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-bindgen diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 1769124..3e1f876 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-component-builder diff --git a/webassembly4j-pool/pom.xml b/webassembly4j-pool/pom.xml index 6a2b4a1..9fa88f9 100644 --- a/webassembly4j-pool/pom.xml +++ b/webassembly4j-pool/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-pool diff --git a/webassembly4j-runtime/pom.xml b/webassembly4j-runtime/pom.xml index 77b3241..49247ef 100644 --- a/webassembly4j-runtime/pom.xml +++ b/webassembly4j-runtime/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-runtime diff --git a/webassembly4j-spi/pom.xml b/webassembly4j-spi/pom.xml index 724afae..9e27950 100644 --- a/webassembly4j-spi/pom.xml +++ b/webassembly4j-spi/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-spi diff --git a/webassembly4j-spring/pom.xml b/webassembly4j-spring/pom.xml index ff60f72..f372057 100644 --- a/webassembly4j-spring/pom.xml +++ b/webassembly4j-spring/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-spring diff --git a/webassembly4j-testing/pom.xml b/webassembly4j-testing/pom.xml index 39d1c05..c3a4cd1 100644 --- a/webassembly4j-testing/pom.xml +++ b/webassembly4j-testing/pom.xml @@ -8,7 +8,7 @@ ai.tegmentum.webassembly4j webassembly4j-parent - 2.5.1 + 2.5.2-SNAPSHOT webassembly4j-testing From 7fe7909414833e2c790b665aa91edfbdaac94762 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 14:50:11 -0400 Subject: [PATCH 69/85] feat(api,wasmtime-provider): add Engine.loadModule(byte[], Instance) cross-module store sharing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-Webassembly4j-Cross-Module-Store-Sharing r.1. Adds default-throwing overload Engine.loadModule(byte[] bytes, Instance shareStoreWith) so consumers can load a related module against an existing instance's store — required for any composition pattern where the new module imports memory/table/global exports of the shared instance (JIT install, plugin loading, incremental linking, multi-module workflows). wasmtime4j-provider implements: unwraps shareStoreWith to native Instance, extracts its Store via getStore(), reuses the store when constructing the new WasmtimeModuleAdapter (which already accepted an explicit Store — no refactor needed). Non-wasmtime providers inherit UnsupportedOperationException default. Disciplined per operator gate: cross-module store sharing is a general Wasm-embedding pattern already used internally by wasmtime4j-provider's own tests via ((WasmtimeModuleAdapter) module).store() escape. This surfaces the capability at the api layer. Compile clean. Tests to follow in a separate commit. --- .../wasmtime/WasmtimeEngineAdapter.java | 28 +++++++++++++++++++ .../tegmentum/webassembly4j/api/Engine.java | 28 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java index 0fc8e94..c1dd6a9 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeEngineAdapter.java @@ -184,6 +184,34 @@ public Module loadModule(byte[] bytes) { } } + /** + * Cross-module store sharing per F-Webassembly4j-Cross-Module-Store-Sharing + * charter 2026-07-26 (Option A). Reuses {@code shareStoreWith}'s native + * {@link ai.tegmentum.wasmtime4j.Store} for the new module so any + * memory/table/global imports referring to exports of the shared instance + * satisfy wasmtime's store-invariant check. + */ + @Override + public Module loadModule(byte[] bytes, + ai.tegmentum.webassembly4j.api.Instance shareStoreWith) { + if (shareStoreWith == null) { + throw new IllegalArgumentException("shareStoreWith must not be null"); + } + ai.tegmentum.wasmtime4j.Instance nativeInst = shareStoreWith + .unwrap(ai.tegmentum.wasmtime4j.Instance.class) + .orElseThrow(() -> new IllegalArgumentException( + "shareStoreWith is not backed by a wasmtime4j Instance")); + ai.tegmentum.wasmtime4j.Store sharedStore = nativeInst.getStore(); + try { + ai.tegmentum.wasmtime4j.Module nativeModule = engine.compileModule(bytes); + return new WasmtimeModuleAdapter(runtime, engine, nativeModule, sharedStore, + nativeConfig); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ai.tegmentum.webassembly4j.api.exception.WebAssemblyException( + "Failed to load WebAssembly module (with shared store)", e); + } + } + @Override public Component loadComponent(byte[] bytes) { try { diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Engine.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Engine.java index 39a5cc4..5620e10 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Engine.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Engine.java @@ -18,6 +18,34 @@ public interface Engine extends AutoCloseable { Module loadModule(byte[] bytes); + /** + * Loads a WebAssembly module using bytes, sharing the underlying store with + * the given related instance. The returned module can be instantiated with + * a {@link LinkingContext} whose extern-imports reference exports of the + * related instance without triggering store-mismatch failures at the + * provider layer. + * + *

      Use when the returned module needs to import memory, table, global, + * or function objects that were created in {@code shareStoreWith}'s store + * — e.g. JIT-emitted-module install, host-plus-plugin composition, + * incremental linking scenarios. See spec at + * {@code doctrine/specs/f-webassembly4j-cross-module-store-sharing-charter-2026-07-26.md}. + * + *

      Providers that do not support cross-module store sharing throw + * {@link UnsupportedOperationException} by default. Consumers may probe via + * try/catch or via {@link EngineCapabilities}. + * + * @param bytes the module bytes + * @param shareStoreWith an existing instance whose store the new module + * should share; must be an {@link Instance} produced + * by this same engine + * @return the loaded module bound to {@code shareStoreWith}'s store + */ + default Module loadModule(byte[] bytes, Instance shareStoreWith) { + throw new UnsupportedOperationException( + "Engine.loadModule(byte[], Instance) not implemented by this provider"); + } + /** * Loads a WebAssembly module from a file path. * From a553e37fbaa0e98ed3a58dd0573723834339af42 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 14:51:53 -0400 Subject: [PATCH 70/85] test(wasmtime-provider): cross-module store sharing via Engine.loadModule(byte[], Instance) Positive-witness test for F-Webassembly4j-Cross-Module-Store-Sharing r.1. Uses the new engine-agnostic overload (no provider escape) to load a consumer module against a producer instance's store, wires producer's exported memory as a MemoryImport, and asserts consumer's store/load helpers round-trip through the shared memory. 2 tests: happy-path round-trip + null-argument rejection. Contrasts with WasmtimeLinkingContextExternImportsTest which uses ((WasmtimeModuleAdapter) module).store() provider escape for the same scenario. Both patterns coexist post-charter. All 88 wasmtime-provider tests pass. 162 api + 18 spi tests unchanged. --- .../WasmtimeCrossModuleStoreSharingTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCrossModuleStoreSharingTest.java diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCrossModuleStoreSharingTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCrossModuleStoreSharingTest.java new file mode 100644 index 0000000..06718cc --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCrossModuleStoreSharingTest.java @@ -0,0 +1,107 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.Instance; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.Module; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Exercises {@link Engine#loadModule(byte[], Instance)} — the cross-module + * store-sharing overload added by F-Webassembly4j-Cross-Module-Store-Sharing + * charter r.1 (2026-07-26). + * + *

      Contrast with {@link WasmtimeLinkingContextExternImportsTest} which uses + * provider escape ({@code ((WasmtimeModuleAdapter) module).store()}) to + * manufacture same-store memory/table objects. This test uses the api-layer + * overload only — no provider escape. + * + *

      Scenario: OUTER module exports memory; CONSUMER module (loaded via the + * new overload against OUTER's instance) imports env.memory and provides + * store/load helpers. Consumer's helpers operate on OUTER's memory — proves + * cross-module store sharing works end-to-end through the api layer. + */ +class WasmtimeCrossModuleStoreSharingTest { + + // (module (memory (export "memory") 1)) + private static final byte[] PRODUCER_MEMORY_MODULE = new byte[] { + (byte)0x00, (byte)0x61, (byte)0x73, (byte)0x6d, (byte)0x01, (byte)0x00, (byte)0x00, (byte)0x00, + (byte)0x05, (byte)0x03, (byte)0x01, (byte)0x00, (byte)0x01, (byte)0x07, (byte)0x0a, (byte)0x01, + (byte)0x06, (byte)0x6d, (byte)0x65, (byte)0x6d, (byte)0x6f, (byte)0x72, (byte)0x79, (byte)0x02, + (byte)0x00 + }; + + // (module + // (import "env" "memory" (memory 1)) + // (func (export "store_i32") (param i32 i32) local.get 0 local.get 1 i32.store) + // (func (export "load_i32") (param i32) (result i32) local.get 0 i32.load)) + // Copied byte-identical from WasmtimeLinkingContextExternImportsTest. + private static final byte[] IMPORT_MEMORY_MODULE = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0b, 0x02, 0x60, + 0x02, 0x7f, 0x7f, 0x00, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x02, 0x0f, 0x01, + 0x03, 0x65, 0x6e, 0x76, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, + 0x00, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x07, 0x18, 0x02, 0x09, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x33, 0x32, 0x00, 0x00, 0x08, 0x6c, + 0x6f, 0x61, 0x64, 0x5f, 0x69, 0x33, 0x32, 0x00, 0x01, 0x0a, 0x13, 0x02, + 0x09, 0x00, 0x20, 0x00, 0x20, 0x01, 0x36, 0x02, 0x00, 0x0b, 0x07, 0x00, + 0x20, 0x00, 0x28, 0x02, 0x00, 0x0b + }; + + static boolean runtimeAvailable() { + try { + try (Engine e = WasmtimeEngineAdapter.create(null)) { + return true; + } + } catch (Throwable t) { + return false; + } + } + + @Test + @EnabledIf("runtimeAvailable") + void crossModuleStoreSharingViaEngineLoadModuleOverload() { + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module producer = engine.loadModule(PRODUCER_MEMORY_MODULE)) { + + // Instantiate producer to get an Instance whose store the consumer will share. + Instance producerInstance = producer.instantiate(); + Memory sharedMemory = producerInstance.memory("memory") + .orElseThrow(() -> new AssertionError("producer memory export missing")); + + // Load consumer via the new engine-agnostic overload — reuses producer's store. + try (Module consumer = engine.loadModule(IMPORT_MEMORY_MODULE, producerInstance)) { + LinkingContext ctx = DefaultLinkingContext.builder() + .addMemoryImport("env", "memory", sharedMemory) + .build(); + + Instance consumerInstance = consumer.instantiate(ctx); + Function storeFn = consumerInstance.function("store_i32").orElseThrow(); + Function loadFn = consumerInstance.function("load_i32").orElseThrow(); + + storeFn.invoke(0, 0xdeadbeef); + assertEquals(0xdeadbeef, ((Number) loadFn.invoke(0)).intValue(), + "consumer store/load round-trips through PRODUCER's memory"); + + // Host-side write through the shared api-layer Memory is also visible. + sharedMemory.write(4, new byte[] { 0x11, 0x22, 0x33, 0x44 }); + assertEquals(0x44332211, ((Number) loadFn.invoke(4)).intValue(), + "host-side write to shared memory visible to consumer"); + } + } + } + + @Test + void rejectsNullShareStoreWith() { + try (Engine engine = WasmtimeEngineAdapter.create(null)) { + assertThrows(IllegalArgumentException.class, + () -> engine.loadModule(PRODUCER_MEMORY_MODULE, null)); + } + } +} From 1b8b9078d094779086cbd6c2c9acc517a7853cf8 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:21:01 -0400 Subject: [PATCH 71/85] docs(caller): begin webassembly4j caller-aware charter r.1 Charter: f-webassembly4j-caller-aware-host-function-charter-2026-07-26 Propagates the wasmtime4j r.2 / r.2.b Caller pattern up to the webassembly4j api layer so any provider can offer caller-aware host functions engine-agnostically. --- NOTES-CALLER-AWARE-2026-07-26.md | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 NOTES-CALLER-AWARE-2026-07-26.md diff --git a/NOTES-CALLER-AWARE-2026-07-26.md b/NOTES-CALLER-AWARE-2026-07-26.md new file mode 100644 index 0000000..173084f --- /dev/null +++ b/NOTES-CALLER-AWARE-2026-07-26.md @@ -0,0 +1,59 @@ +# Caller-Aware-Host-Function — working notes (r.1 in progress) + +**Charter**: `f-webassembly4j-caller-aware-host-function-charter-2026-07-26` +**Branch**: `f-webassembly4j-caller-aware` (branched from `main` at `a553e37`) +**Type**: feature — engine-agnostic exposure of the wasmtime4j r.2 / r.2.b `Caller` pattern +**Version**: 2.5.2-SNAPSHOT (unchanged; accumulates before release cut) + +## Charter scope + +Propagate the `Caller` scoped-mutation pattern UP from wasmtime4j to the +webassembly4j api layer so any provider can offer caller-aware host functions +without forcing consumers to reach into wasmtime4j directly. + +Downstream unblock: F-JIT-Loader-Java-Reference r.5.c (Fiji jit-loader +switches from `HostFunction` to `CallerAwareHostFunction` for +compile+instantiate+growTable inside the callback). + +## Wasmtime4j surface reference (r.2 + r.2.b at wasmtime4j 47.0.2-1.5.1) + +- `ai.tegmentum.wasmtime4j.func.Caller` — 10 scoped methods + (data, getMemory, getTable, getFunction, getGlobal, compileModule, + growTable, setTableElement, growMemory, instantiate). +- `ai.tegmentum.wasmtime4j.func.HostFunction.multiValueWithCaller(impl)` / + `singleValueWithCaller(impl)` / `voidFunctionWithCaller(impl)` — factories + that wrap a caller-aware Java lambda into a `CallerAwareHostFunction` + which the JNI backend recognises and injects the live caller into. +- Delivery route: JNI dispatch pushes a `JniCaller` onto + `JniHostFunction.CALLER_CONTEXT` ThreadLocal, and + `CallerAwareHostFunction.getCurrentCaller` retrieves it via ServiceLoader + (`CallerContextProvider`). Consumer of the wrapped `HostFunction` never + needs to hand-plumb the caller. + +## Plan (single slice) + +### r.1-1 — webassembly4j-api additions +- New: `Caller.java` (10 methods matching wasmtime4j subset) +- New: `CallerAwareHostFunction.java` (@FunctionalInterface, `Object[] execute(Caller, Object...)`) +- New: `CallerAwareHostFunctionDefinition.java` (parallel to `HostFunctionDefinition`) +- Edit: `LinkingContext.java` — add `callerAwareHostFunctions()` default +- Edit: `DefaultLinkingContext.java` — field, accessor, builder overloads + +### r.1-2 — wasmtime4j-provider bridge +- New: `WasmtimeCallerAdapter.java` — wraps native `Caller` as api `Caller` +- Delegates each method to underlying `WasmMemory/Table/Global/Function` adapters + +### r.1-3 — WasmtimeModuleAdapter.instantiate extension +- Iterate `linkingContext.callerAwareHostFunctions()` +- For each, register via `linker.defineHostFunction(module, name, funcType, + HostFunction.multiValueWithCaller((nativeCaller, wasmArgs) -> { wrap caller, + convert args, invoke, convert results }))` + +### r.1-4 — test +- WAT: exports memory + table + `run()` that calls `env.trigger_grow(1)` +- `CallerAwareHostFunction` grows the caller-visible table via `caller.growTable` +- Assert grow observable + use-after-return throws `IllegalStateException` + +## Status + +- 2026-07-26 r.1-0 STARTED: branch created, notes stub committed. From a2fa3d9bd2290e470acbdd1acfb7f32314cb076d Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:23:23 -0400 Subject: [PATCH 72/85] feat(api): add Caller + CallerAwareHostFunction interfaces + LinkingContext plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Charter r.1-1. Engine-agnostic exposure of the wasmtime4j r.2 / r.2.b Caller scoped-mutation pattern. - Caller: 10 scoped methods (data, getMemory, getTable, getFunction, getGlobal, compileModule, instantiate, growTable, setTableElement, growMemory, unwrap). - CallerAwareHostFunction: @FunctionalInterface — Object[] execute( Caller, Object...). - CallerAwareHostFunctionDefinition: parallel to HostFunctionDefinition but wraps CallerAwareHostFunction. - LinkingContext.callerAwareHostFunctions(): default returns empty list; providers that do not support the pattern inherit safe no-op behavior. - DefaultLinkingContext.Builder.addCallerAwareHostFunction(...): two overloads matching addHostFunction shape. --- .../tegmentum/webassembly4j/api/Caller.java | 116 ++++++++++++++++++ .../api/CallerAwareHostFunction.java | 30 +++++ .../CallerAwareHostFunctionDefinition.java | 53 ++++++++ .../api/DefaultLinkingContext.java | 45 ++++++- .../webassembly4j/api/LinkingContext.java | 22 ++++ 5 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunction.java create mode 100644 webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunctionDefinition.java diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java new file mode 100644 index 0000000..fb99203 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java @@ -0,0 +1,116 @@ +package ai.tegmentum.webassembly4j.api; + +import java.util.Optional; + +/** + * Scoped access to the caller's instance during a host callback. Providers + * that support caller-aware host functions expose the caller via this + * interface; the reference is only valid for the callback duration. + * + *

      Consumers use this to safely read exports of the calling instance, + * compile / instantiate additional modules against the caller's engine and + * store, and mutate caller-visible tables and memories from within the + * callback frame without triggering reentrant store-lock corruption. Any + * attempt to invoke a method after the callback returns throws + * {@link IllegalStateException} (providers enforce this via a generation + * counter kept in the underlying store). + * + *

      Not every provider surfaces every capability. Providers that do not + * support caller-aware host functions at all return an empty list from + * {@link LinkingContext#callerAwareHostFunctions()} consumption paths and + * never construct a {@code Caller}. Providers that support some but not all + * scoped methods raise {@link UnsupportedOperationException} on the + * unsupported entrypoints. + * + * @param the type of caller-associated data (provider-specific store + * data slot; typically {@code Void} when no data is used) + * @since 2.5.2 + */ +public interface Caller { + + /** + * Returns the caller-associated data (provider-specific; may be null when + * the underlying store carries no user data). + */ + T data(); + + /** + * Returns an exported memory of the calling instance by name. + */ + Optional getMemory(String name); + + /** + * Returns an exported table of the calling instance by name. + */ + Optional

getTable(String name); + + /** + * Returns an exported function of the calling instance by name. + */ + Optional getFunction(String name); + + /** + * Returns an exported global of the calling instance by name. + */ + Optional getGlobal(String name); + + /** + * Compile a {@link Module} using the caller's engine. Safe inside a + * callback — providers route through the caller-scoped path so the + * resulting module shares the caller's engine and can be instantiated + * into the caller's store without cross-engine errors. + * + * @param wasmBytes WebAssembly binary bytes (or WAT text on providers + * that accept it) + * @return the compiled module + * @throws IllegalStateException if the callback has returned + */ + Module compileModule(byte[] wasmBytes); + + /** + * Instantiate a module in the caller's store using the given imports. + * Safe inside a callback — the provider borrows the caller's live store + * context instead of re-entering the store lock. + * + * @param module the module to instantiate; must have been produced by + * the same provider (typically via + * {@link #compileModule(byte[])}) + * @param imports linking context providing imports; may be null for a + * no-import instantiation + * @return the newly created instance + * @throws IllegalStateException if the callback has returned + */ + Instance instantiate(Module module, LinkingContext imports); + + /** + * Grow a caller-visible table by {@code delta} slots, filling new slots + * with {@code init}. Provider-specific rules apply to the {@code init} + * value's type (typically funcref must be a {@link Function} handle or + * null; externref rules vary). + * + * @return the previous size on success, {@code -1} on failure + * @throws IllegalStateException if the callback has returned + */ + int growTable(Table table, int delta, Object init); + + /** + * Write a value into a caller-visible table slot. + * + * @throws IllegalStateException if the callback has returned + */ + void setTableElement(Table table, int index, Object value); + + /** + * Grow a caller-visible memory by {@code deltaPages} 64 KiB pages. + * + * @return the previous size in pages on success, {@code -1} on failure + * @throws IllegalStateException if the callback has returned + */ + long growMemory(Memory memory, long deltaPages); + + /** + * Provider escape hatch — returns the underlying provider-specific + * caller handle when {@code nativeType} is assignable from it. + */ + Optional unwrap(Class nativeType); +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunction.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunction.java new file mode 100644 index 0000000..5582f84 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunction.java @@ -0,0 +1,30 @@ +package ai.tegmentum.webassembly4j.api; + +/** + * Host function that receives a {@link Caller} handle giving scoped access + * to the calling instance's exports, engine, and store for the duration of + * the callback. Providers that support this pattern route the caller from + * their native runtime; providers that do not throw + * {@link UnsupportedOperationException} at instantiation time when a + * {@link LinkingContext} carries any + * {@link CallerAwareHostFunctionDefinition} — see + * {@link LinkingContext#callerAwareHostFunctions()}. + * + * @param the type of caller-associated data (typically {@code Void} + * when no store data is used) + * @since 2.5.2 + */ +@FunctionalInterface +public interface CallerAwareHostFunction { + + /** + * Execute the host function with scoped access to the calling instance. + * + * @param caller scoped access to the calling instance; only valid for + * the duration of this call + * @param args arguments coerced from wasm values by the provider + * @return result values coerced back into wasm values by the provider; + * must match the declared result types + */ + Object[] execute(Caller caller, Object... args); +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunctionDefinition.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunctionDefinition.java new file mode 100644 index 0000000..8959fd6 --- /dev/null +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunctionDefinition.java @@ -0,0 +1,53 @@ +package ai.tegmentum.webassembly4j.api; + +/** + * Definition of a caller-aware host function to link into a module's + * imports. Parallel to {@link HostFunctionDefinition} but wraps a + * {@link CallerAwareHostFunction} whose implementation receives a + * {@link Caller} handle for scoped access to the calling instance. + * + *

Providers that do not support caller-aware host functions ignore + * entries of this type — see + * {@link LinkingContext#callerAwareHostFunctions()}. + * + * @since 2.5.2 + */ +public final class CallerAwareHostFunctionDefinition { + + private final String moduleName; + private final String functionName; + private final ValueType[] parameterTypes; + private final ValueType[] resultTypes; + private final CallerAwareHostFunction function; + + public CallerAwareHostFunctionDefinition(String moduleName, String functionName, + ValueType[] parameterTypes, + ValueType[] resultTypes, + CallerAwareHostFunction function) { + this.moduleName = moduleName; + this.functionName = functionName; + this.parameterTypes = parameterTypes.clone(); + this.resultTypes = resultTypes.clone(); + this.function = function; + } + + public String moduleName() { + return moduleName; + } + + public String functionName() { + return functionName; + } + + public ValueType[] parameterTypes() { + return parameterTypes.clone(); + } + + public ValueType[] resultTypes() { + return resultTypes.clone(); + } + + public CallerAwareHostFunction function() { + return function; + } +} diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java index 6791b1c..50ba79c 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java @@ -14,18 +14,22 @@ public final class DefaultLinkingContext implements LinkingContext { private final List hostFunctions; private final List witHostFunctions; private final List externImports; + private final List callerAwareHostFunctions; private DefaultLinkingContext(WasiContext wasiContext, WasiNnConfig wasiNnConfig, Map imports, List hostFunctions, List witHostFunctions, - List externImports) { + List externImports, + List callerAwareHostFunctions) { this.wasiContext = wasiContext; this.wasiNnConfig = wasiNnConfig; this.imports = Collections.unmodifiableMap(new LinkedHashMap<>(imports)); this.hostFunctions = Collections.unmodifiableList(new ArrayList<>(hostFunctions)); this.witHostFunctions = Collections.unmodifiableList(new ArrayList<>(witHostFunctions)); this.externImports = Collections.unmodifiableList(new ArrayList<>(externImports)); + this.callerAwareHostFunctions = Collections.unmodifiableList( + new ArrayList<>(callerAwareHostFunctions)); } @Override @@ -53,6 +57,11 @@ public List externImports() { return externImports; } + @Override + public List callerAwareHostFunctions() { + return callerAwareHostFunctions; + } + public Map imports() { return imports; } @@ -69,6 +78,8 @@ public static final class Builder { private final List hostFunctions = new ArrayList<>(); private final List witHostFunctions = new ArrayList<>(); private final List externImports = new ArrayList<>(); + private final List callerAwareHostFunctions = + new ArrayList<>(); private Builder() { } @@ -208,10 +219,40 @@ public Builder addFunctionImport(String moduleName, String name, Function functi return this; } + /** + * Register a caller-aware host function under {@code moduleName::functionName}. + * The provider passes a {@link Caller} handle scoped to the callback + * frame — see {@link CallerAwareHostFunction}. + * + *

Providers that do not support the caller-aware pattern raise + * {@link ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException} + * at {@code instantiate} time when the resulting list is non-empty. + * + * @since 2.5.2 + */ + public Builder addCallerAwareHostFunction(String moduleName, String functionName, + ValueType[] parameterTypes, + ValueType[] resultTypes, + CallerAwareHostFunction function) { + this.callerAwareHostFunctions.add(new CallerAwareHostFunctionDefinition( + moduleName, functionName, parameterTypes, resultTypes, function)); + return this; + } + + /** + * Register a pre-built {@link CallerAwareHostFunctionDefinition}. + * + * @since 2.5.2 + */ + public Builder addCallerAwareHostFunction(CallerAwareHostFunctionDefinition definition) { + this.callerAwareHostFunctions.add(definition); + return this; + } + public DefaultLinkingContext build() { return new DefaultLinkingContext( wasiContext, wasiNnConfig, imports, hostFunctions, witHostFunctions, - externImports); + externImports, callerAwareHostFunctions); } } } diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java index 6826a59..4a9c246 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java @@ -64,4 +64,26 @@ default boolean supportsWasiNn() { default List externImports() { return Collections.emptyList(); } + + /** + * Caller-aware host functions to link into a module's imports. Each + * definition wraps a {@link CallerAwareHostFunction} whose implementation + * receives a {@link Caller} handle for scoped access to the calling + * instance's exports, engine, and store — safe from within a host + * callback frame (see + * {@code doctrine/specs/f-webassembly4j-caller-aware-host-function-charter-2026-07-26.md}). + * + *

Providers that do not support caller-aware host functions throw + * {@link ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException} + * at {@code instantiate} time when this list is non-empty; providers + * that support it consume the list alongside {@link #hostFunctions()}, + * {@link #wasiContext()}, and {@link #externImports()}. + * + * @return an unmodifiable list of caller-aware host function definitions; + * empty by default so existing implementations remain valid. + * @since 2.5.2 + */ + default List callerAwareHostFunctions() { + return Collections.emptyList(); + } } From cb81e9b3d9a06d672e05569f3a909e3ac668081d Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:44:56 -0400 Subject: [PATCH 73/85] =?UTF-8?q?feat(wasmtime-provider):=20WasmtimeCaller?= =?UTF-8?q?Adapter=20=E2=80=94=20wraps=20wasmtime4j=20Caller=20as=20api?= =?UTF-8?q?=20Caller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridges wasmtime4j's caller-scoped host-callback handle (ai.tegmentum.wasmtime4j.func.Caller) into the webassembly4j api Caller interface added at r.1-1. Delegates each api method to the underlying wasmtime4j Caller, wrapping/unwrapping types via existing Wasmtime{Memory,Table,Function,Global,Instance}Adapter classes. For instantiate(Module, LinkingContext), reconciles the api signature against wasmtime4j's Caller.instantiate(InstancePre) by building a transient linker from the caller's engine, calling instantiatePre on it, then routing the pre through the caller's borrow-safe scoped entrypoint. externImports are rejected at this path — they require a live store binding that wasmtime4j's Caller does not expose. compileModule returns a caller-scoped Module wrapper that has no owning store; instantiation must go through Caller.instantiate. --- .../wasmtime/WasmtimeCallerAdapter.java | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java new file mode 100644 index 0000000..e1a2115 --- /dev/null +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java @@ -0,0 +1,369 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.wasmtime4j.WasmFunction; +import ai.tegmentum.wasmtime4j.WasmMemory; +import ai.tegmentum.wasmtime4j.WasmRuntime; +import ai.tegmentum.wasmtime4j.WasmTable; +import ai.tegmentum.webassembly4j.api.Caller; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.Global; +import ai.tegmentum.webassembly4j.api.Instance; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.Module; +import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; +import ai.tegmentum.webassembly4j.api.exception.LinkingException; + +import java.util.Optional; + +/** + * Bridges wasmtime4j's caller-scoped host-callback handle + * ({@link ai.tegmentum.wasmtime4j.func.Caller}) into the webassembly4j + * {@link Caller} api. Instances are constructed by + * {@link WasmtimeModuleAdapter} when it dispatches a + * {@link ai.tegmentum.webassembly4j.api.CallerAwareHostFunctionDefinition} + * and are only valid for the duration of the host callback that received + * them — all scoped mutation methods route through the underlying + * wasmtime4j Caller, which generation-checks use-after-return and throws + * {@link IllegalStateException} on stale handles. + * + *

Because {@code ai.tegmentum.wasmtime4j.func.Caller} exposes no public + * accessor for the caller's underlying {@link ai.tegmentum.wasmtime4j.Store}, + * this adapter carries only the {@link WasmRuntime} + engine references + * needed to build caller-scoped modules and linkers; the store binding is + * kept implicit in the native caller handle and used only via the borrow- + * safe wasmtime4j scoped-mutation entrypoints. See charter + * {@code f-webassembly4j-caller-aware-host-function-charter-2026-07-26.md} + * for the reconciliation with the r.2 wasmtime4j implementation. + */ +final class WasmtimeCallerAdapter implements Caller { + + private final ai.tegmentum.wasmtime4j.func.Caller nativeCaller; + private final WasmRuntime runtime; + private final ai.tegmentum.wasmtime4j.config.EngineConfig engineConfig; + + WasmtimeCallerAdapter(ai.tegmentum.wasmtime4j.func.Caller nativeCaller, + WasmRuntime runtime, + ai.tegmentum.wasmtime4j.config.EngineConfig engineConfig) { + this.nativeCaller = nativeCaller; + this.runtime = runtime; + this.engineConfig = engineConfig; + } + + @Override + public T data() { + return nativeCaller.data(); + } + + @Override + public Optional getMemory(String name) { + return nativeCaller.getMemory(name).map(WasmtimeMemoryAdapter::new); + } + + @Override + public Optional

getTable(String name) { + return nativeCaller.getTable(name).map(WasmtimeTableAdapter::new); + } + + @Override + public Optional getFunction(String name) { + return nativeCaller.getFunction(name).map(WasmtimeFunctionAdapter::new); + } + + @Override + public Optional getGlobal(String name) { + return nativeCaller.getGlobal(name).map(WasmtimeGlobalAdapter::new); + } + + /** + * Compile a module using the caller's engine. Returns a caller-scoped + * {@link WasmtimeModuleAdapter} — it carries no owning {@link + * ai.tegmentum.wasmtime4j.Store}, so the caller MUST route the + * subsequent instantiation through {@link #instantiate(Module, LinkingContext)} + * rather than calling the module's own {@code instantiate()} methods + * (those throw for caller-scoped modules). + */ + @Override + public Module compileModule(byte[] wasmBytes) { + try { + ai.tegmentum.wasmtime4j.Module nativeModule = nativeCaller.compileModule(wasmBytes); + ai.tegmentum.wasmtime4j.Engine callerEngine = nativeCaller.engine(); + return WasmtimeModuleAdapter.callerScoped( + runtime, callerEngine, nativeModule, engineConfig); + } catch (IllegalStateException e) { + throw e; + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Caller-scoped compileModule failed: " + e.getMessage(), e); + } catch (UnsupportedOperationException e) { + throw e; + } + } + + /** + * Instantiate a module in the caller's store via the borrow-safe + * scoped-instantiate path. The module's imports are resolved through a + * transient {@link ai.tegmentum.wasmtime4j.Linker} built from the + * caller's engine; the pre-linked + * {@link ai.tegmentum.wasmtime4j.InstancePre} is then passed to + * {@link ai.tegmentum.wasmtime4j.func.Caller#instantiate(ai.tegmentum.wasmtime4j.InstancePre)} + * which uses {@code caller.as_context_mut()} instead of re-acquiring + * the store lock. + * + *

Only the host-function and caller-aware-host-function slices of + * the passed {@link LinkingContext} are wired via the transient linker + * — extern imports (memories, tables, globals) require a live store + * binding that isn't reachable from a wasmtime4j Caller, so passing + * non-empty {@code externImports()} throws {@link LinkingException}. + * The typical JIT-install-loop case (compile + instantiate against + * user host imports, then table-install into the caller's outer + * table) is unaffected by this restriction. + */ + @Override + public Instance instantiate(Module module, LinkingContext imports) { + if (module == null) { + throw new IllegalArgumentException("module must not be null"); + } + if (!(module instanceof WasmtimeModuleAdapter)) { + throw new LinkingException( + "Caller.instantiate: module is not backed by a" + + " wasmtime4j-provider Module (got " + + module.getClass().getName() + ")"); + } + ai.tegmentum.wasmtime4j.Module nativeModule = + ((WasmtimeModuleAdapter) module).nativeModule(); + ai.tegmentum.wasmtime4j.Engine callerEngine = nativeCaller.engine(); + try { + ai.tegmentum.wasmtime4j.Linker linker = runtime.createLinker(callerEngine); + if (imports != null) { + if (!imports.externImports().isEmpty()) { + throw new LinkingException( + "Caller.instantiate does not support externImports" + + " (memory/table/global imports need a live" + + " store binding that the caller-scoped path" + + " does not expose). Provide these via" + + " hostFunctions or caller-aware host" + + " functions instead."); + } + WasmtimeModuleAdapter.defineHostFunctions(linker, imports.hostFunctions()); + // Nested caller-aware host functions on an inner module + // instantiated inside a callback are permitted — they + // will fire against the inner instance's own callback + // context when it invokes them. + for (ai.tegmentum.webassembly4j.api.CallerAwareHostFunctionDefinition def + : imports.callerAwareHostFunctions()) { + // Route through the same package-private helper used + // by WasmtimeModuleAdapter — the nested caller adapter + // is minted per-invocation from the wasmtime4j callback + // context, not from the outer caller. + defineNestedCallerAware(linker, def); + } + } + ai.tegmentum.wasmtime4j.InstancePre pre = linker.instantiatePre(nativeModule); + ai.tegmentum.wasmtime4j.Instance nativeInstance = nativeCaller.instantiate(pre); + return new WasmtimeInstanceAdapter(nativeInstance, runtime, callerEngine); + } catch (IllegalStateException e) { + throw e; + } catch (UnsupportedOperationException e) { + throw e; + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new LinkingException( + "Caller-scoped instantiate failed: " + e.getMessage(), e); + } + } + + /** + * Register a caller-aware host function on the transient linker built + * for the caller-scoped inner instantiate. Mirrors the wiring in + * {@link WasmtimeModuleAdapter#defineCallerAwareHostFunctions} but is + * inlined here so this path doesn't need a WasmtimeModuleAdapter + * instance (there is no api-level module yet at the caller-scope). + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private void defineNestedCallerAware( + ai.tegmentum.wasmtime4j.Linker linker, + ai.tegmentum.webassembly4j.api.CallerAwareHostFunctionDefinition def) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + ai.tegmentum.wasmtime4j.WasmValueType[] paramTypes = + convertToWasmTypes(def.parameterTypes()); + ai.tegmentum.wasmtime4j.WasmValueType[] resultTypes = + convertToWasmTypes(def.resultTypes()); + ai.tegmentum.wasmtime4j.type.FunctionType funcType = + new ai.tegmentum.wasmtime4j.type.FunctionType(paramTypes, resultTypes); + + ai.tegmentum.wasmtime4j.func.HostFunction impl = + new ai.tegmentum.wasmtime4j.func.HostFunction.CallerAwareHostFunction( + (ai.tegmentum.wasmtime4j.func.HostFunction + .MultiValueHostFunctionWithCaller) (innerCaller, + wasmArgs) -> { + WasmtimeCallerAdapter adapter = new WasmtimeCallerAdapter( + innerCaller, runtime, engineConfig); + Object[] javaArgs = extractWasmValues(wasmArgs); + ai.tegmentum.webassembly4j.api.CallerAwareHostFunction fn = + (ai.tegmentum.webassembly4j.api.CallerAwareHostFunction) + def.function(); + @SuppressWarnings("unchecked") + Object[] results = fn.execute(adapter, javaArgs); + if (results == null || results.length == 0) { + return new ai.tegmentum.wasmtime4j.WasmValue[0]; + } + return convertToWasmValues(results, def.resultTypes()); + }); + linker.defineHostFunction(def.moduleName(), def.functionName(), funcType, impl); + } + + private static ai.tegmentum.wasmtime4j.WasmValueType[] convertToWasmTypes( + ai.tegmentum.webassembly4j.api.ValueType[] types) { + ai.tegmentum.wasmtime4j.WasmValueType[] result = + new ai.tegmentum.wasmtime4j.WasmValueType[types.length]; + for (int i = 0; i < types.length; i++) { + result[i] = convertToWasmType(types[i]); + } + return result; + } + + private static ai.tegmentum.wasmtime4j.WasmValueType convertToWasmType( + ai.tegmentum.webassembly4j.api.ValueType type) { + switch (type) { + case I32: return ai.tegmentum.wasmtime4j.WasmValueType.I32; + case I64: return ai.tegmentum.wasmtime4j.WasmValueType.I64; + case F32: return ai.tegmentum.wasmtime4j.WasmValueType.F32; + case F64: return ai.tegmentum.wasmtime4j.WasmValueType.F64; + case V128: return ai.tegmentum.wasmtime4j.WasmValueType.V128; + case FUNCREF: return ai.tegmentum.wasmtime4j.WasmValueType.FUNCREF; + case EXTERNREF: return ai.tegmentum.wasmtime4j.WasmValueType.EXTERNREF; + default: throw new IllegalArgumentException("Unknown type: " + type); + } + } + + private static Object[] extractWasmValues(ai.tegmentum.wasmtime4j.WasmValue[] wasmValues) { + Object[] result = new Object[wasmValues.length]; + for (int i = 0; i < wasmValues.length; i++) { + switch (wasmValues[i].getType()) { + case I32: result[i] = wasmValues[i].asInt(); break; + case I64: result[i] = wasmValues[i].asLong(); break; + case F32: result[i] = wasmValues[i].asFloat(); break; + case F64: result[i] = wasmValues[i].asDouble(); break; + default: result[i] = wasmValues[i]; break; + } + } + return result; + } + + private static ai.tegmentum.wasmtime4j.WasmValue[] convertToWasmValues( + Object[] values, ai.tegmentum.webassembly4j.api.ValueType[] types) { + ai.tegmentum.wasmtime4j.WasmValue[] result = + new ai.tegmentum.wasmtime4j.WasmValue[values.length]; + for (int i = 0; i < values.length; i++) { + Number num = (Number) values[i]; + switch (types[i]) { + case I32: result[i] = + ai.tegmentum.wasmtime4j.WasmValue.i32(num.intValue()); break; + case I64: result[i] = + ai.tegmentum.wasmtime4j.WasmValue.i64(num.longValue()); break; + case F32: result[i] = + ai.tegmentum.wasmtime4j.WasmValue.f32(num.floatValue()); break; + case F64: result[i] = + ai.tegmentum.wasmtime4j.WasmValue.f64(num.doubleValue()); break; + default: throw new IllegalArgumentException("Unsupported return type: " + types[i]); + } + } + return result; + } + + @Override + public int growTable(Table table, int delta, Object init) { + if (table == null) { + throw new IllegalArgumentException("table must not be null"); + } + WasmTable nativeTable = table.unwrap(WasmTable.class) + .orElseThrow(() -> new LinkingException( + "Caller.growTable: table is not backed by a wasmtime4j" + + " WasmTable")); + Object nativeInit = unwrapTableSlotValue(init); + try { + return nativeCaller.growTable(nativeTable, delta, nativeInit); + } catch (IllegalStateException e) { + throw e; + } catch (UnsupportedOperationException e) { + throw e; + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Caller-scoped growTable failed: " + e.getMessage(), e); + } + } + + @Override + public void setTableElement(Table table, int index, Object value) { + if (table == null) { + throw new IllegalArgumentException("table must not be null"); + } + WasmTable nativeTable = table.unwrap(WasmTable.class) + .orElseThrow(() -> new LinkingException( + "Caller.setTableElement: table is not backed by a" + + " wasmtime4j WasmTable")); + Object nativeValue = unwrapTableSlotValue(value); + try { + nativeCaller.setTableElement(nativeTable, index, nativeValue); + } catch (IllegalStateException e) { + throw e; + } catch (UnsupportedOperationException e) { + throw e; + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Caller-scoped setTableElement failed: " + e.getMessage(), e); + } + } + + @Override + public long growMemory(Memory memory, long deltaPages) { + if (memory == null) { + throw new IllegalArgumentException("memory must not be null"); + } + WasmMemory nativeMemory = memory.unwrap(WasmMemory.class) + .orElseThrow(() -> new LinkingException( + "Caller.growMemory: memory is not backed by a wasmtime4j" + + " WasmMemory")); + try { + return nativeCaller.growMemory(nativeMemory, deltaPages); + } catch (IllegalStateException e) { + throw e; + } catch (UnsupportedOperationException e) { + throw e; + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Caller-scoped growMemory failed: " + e.getMessage(), e); + } + } + + @Override + @SuppressWarnings("unchecked") + public Optional unwrap(Class nativeType) { + if (nativeType.isInstance(nativeCaller)) { + return Optional.of((U) nativeCaller); + } + return Optional.empty(); + } + + /** + * Coerce a Java-side table slot value into the raw native handle + * expected by wasmtime4j's caller-scoped table mutation entrypoints. + * Nulls pass through unchanged (representing a null funcref / + * externref); webassembly4j {@link Function} handles are unwrapped to + * their underlying {@link WasmFunction}. Anything else is passed + * through and will be validated by the native layer. + */ + private static Object unwrapTableSlotValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof Function) { + return ((Function) value).unwrap(WasmFunction.class) + .orElseThrow(() -> new LinkingException( + "Caller table slot Function value is not backed by a" + + " wasmtime4j WasmFunction")); + } + return value; + } +} From 2aa300d8cbebc6954a0cb313f5e163a5a43b5753 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:45:10 -0400 Subject: [PATCH 74/85] feat(wasmtime-provider): WasmtimeModuleAdapter consumes callerAwareHostFunctions from LinkingContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends WasmtimeModuleAdapter.instantiate(LinkingContext) to consume the new LinkingContext.callerAwareHostFunctions() list added at r.1-1. Each CallerAwareHostFunctionDefinition is registered on the linker as a wasmtime4j CallerAwareHostFunction — the subclass of HostFunction whose execute runs with the native Caller available via the CallerContextProvider SPI wired at wasmtime4j r.2. The native caller is wrapped in a WasmtimeCallerAdapter and handed to the user's CallerAwareHostFunction implementation. Also adds a caller-scoped factory (WasmtimeModuleAdapter.callerScoped) so WasmtimeCallerAdapter.compileModule can return a Module wrapper that has no owning store; its own instantiate() paths throw with a clear message pointing at Caller.instantiate. Extracts defineHostFunctions to a package-private static helper for reuse from WasmtimeCallerAdapter and adds a package-private nativeModule() accessor for the caller-scoped instantiate bridge. --- .../wasmtime/WasmtimeModuleAdapter.java | 164 ++++++++++++++++-- 1 file changed, 147 insertions(+), 17 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java index 0994031..540d3a2 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java @@ -4,12 +4,15 @@ import ai.tegmentum.wasmtime4j.WasmRuntime; import ai.tegmentum.wasmtime4j.WasmValue; import ai.tegmentum.wasmtime4j.WasmValueType; +import ai.tegmentum.wasmtime4j.func.HostFunction; import ai.tegmentum.wasmtime4j.type.ExportType; import ai.tegmentum.wasmtime4j.type.FuncType; import ai.tegmentum.wasmtime4j.type.FunctionType; import ai.tegmentum.wasmtime4j.type.ImportType; import ai.tegmentum.wasmtime4j.type.WasmType; import ai.tegmentum.wasmtime4j.type.WasmTypeKind; +import ai.tegmentum.webassembly4j.api.CallerAwareHostFunction; +import ai.tegmentum.webassembly4j.api.CallerAwareHostFunctionDefinition; import ai.tegmentum.webassembly4j.api.ExportDescriptor; import ai.tegmentum.webassembly4j.api.ExternImportDefinition; import ai.tegmentum.webassembly4j.api.ExternType; @@ -37,6 +40,7 @@ final class WasmtimeModuleAdapter implements Module { private final ai.tegmentum.wasmtime4j.Module nativeModule; private final ai.tegmentum.wasmtime4j.Store store; private final ai.tegmentum.wasmtime4j.config.EngineConfig engineConfig; + private final boolean callerScoped; WasmtimeModuleAdapter(WasmRuntime runtime, ai.tegmentum.wasmtime4j.Engine engine, @@ -48,10 +52,46 @@ final class WasmtimeModuleAdapter implements Module { this.nativeModule = nativeModule; this.store = store; this.engineConfig = engineConfig; + this.callerScoped = false; + } + + private WasmtimeModuleAdapter(WasmRuntime runtime, + ai.tegmentum.wasmtime4j.Engine engine, + ai.tegmentum.wasmtime4j.Module nativeModule, + ai.tegmentum.wasmtime4j.config.EngineConfig engineConfig) { + this.runtime = runtime; + this.engine = engine; + this.nativeModule = nativeModule; + this.store = null; + this.engineConfig = engineConfig; + this.callerScoped = true; + } + + /** + * Construct a caller-scoped Module wrapper — used only by + * {@link WasmtimeCallerAdapter#compileModule(byte[])} to hand back a + * Module that borrows the caller's implicit store. Caller-scoped + * modules must be instantiated via + * {@link WasmtimeCallerAdapter#instantiate(Module, LinkingContext)}; + * their own {@code instantiate()} paths throw + * {@link IllegalStateException} because they have no owning store. + */ + static WasmtimeModuleAdapter callerScoped( + WasmRuntime runtime, + ai.tegmentum.wasmtime4j.Engine engine, + ai.tegmentum.wasmtime4j.Module nativeModule, + ai.tegmentum.wasmtime4j.config.EngineConfig engineConfig) { + return new WasmtimeModuleAdapter(runtime, engine, nativeModule, engineConfig); } @Override public Instance instantiate() { + if (callerScoped) { + throw new IllegalStateException( + "This Module was produced by Caller.compileModule and has no" + + " owning store. Instantiate it via" + + " Caller.instantiate(Module, LinkingContext) instead."); + } try { Linker linker = runtime.createLinker(engine); ai.tegmentum.wasmtime4j.Instance nativeInstance = @@ -65,36 +105,33 @@ public Instance instantiate() { @Override public Instance instantiate(LinkingContext linkingContext) { + if (callerScoped) { + throw new IllegalStateException( + "This Module was produced by Caller.compileModule and has no" + + " owning store. Instantiate it via" + + " Caller.instantiate(Module, LinkingContext) instead."); + } if (linkingContext == null) { return instantiate(); } List hostFunctions = linkingContext.hostFunctions(); List externImports = linkingContext.externImports(); + List callerAwareHostFunctions = + linkingContext.callerAwareHostFunctions(); if (hostFunctions.isEmpty() && linkingContext.wasiContext() == null - && externImports.isEmpty()) { + && externImports.isEmpty() + && callerAwareHostFunctions.isEmpty()) { return instantiate(); } try { Linker linker = runtime.createLinker(engine); - for (HostFunctionDefinition def : hostFunctions) { - FunctionType funcType = new FunctionType( - convertToWasmTypes(def.parameterTypes()), - convertToWasmTypes(def.resultTypes())); - - linker.defineHostFunction(def.moduleName(), def.functionName(), funcType, - wasmArgs -> { - Object[] javaArgs = extractWasmValues(wasmArgs); - Object[] results = def.function().execute(javaArgs); - if (results == null || results.length == 0) { - return new WasmValue[0]; - } - return convertToWasmValues(results, def.resultTypes()); - }); - } + defineHostFunctions(linker, hostFunctions); + + defineCallerAwareHostFunctions(linker, callerAwareHostFunctions); ai.tegmentum.webassembly4j.api.WasiContext wasiCtx = linkingContext.wasiContext(); if (wasiCtx != null) { @@ -116,6 +153,73 @@ public Instance instantiate(LinkingContext linkingContext) { } } + /** + * Wire non-caller-aware host functions into the linker. Extracted so it + * can be shared with the caller-scoped instantiate path in + * {@link WasmtimeCallerAdapter}. + */ + static void defineHostFunctions(Linker linker, + List hostFunctions) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + for (HostFunctionDefinition def : hostFunctions) { + FunctionType funcType = new FunctionType( + convertToWasmTypes(def.parameterTypes()), + convertToWasmTypes(def.resultTypes())); + + linker.defineHostFunction(def.moduleName(), def.functionName(), funcType, + wasmArgs -> { + Object[] javaArgs = extractWasmValues(wasmArgs); + Object[] results = def.function().execute(javaArgs); + if (results == null || results.length == 0) { + return new WasmValue[0]; + } + return convertToWasmValues(results, def.resultTypes()); + }); + } + } + + /** + * Wire caller-aware host functions into the linker. Each definition is + * registered as a wasmtime4j {@code CallerAwareHostFunction} — the + * subclass of {@link HostFunction} whose {@code execute} runs with the + * native {@link ai.tegmentum.wasmtime4j.func.Caller} available. The + * native caller is looked up via wasmtime4j's + * {@code CallerContextProvider} SPI (already wired for JNI in + * wasmtime4j r.2), wrapped in a {@link WasmtimeCallerAdapter}, and + * handed to the user's {@link CallerAwareHostFunction} implementation. + */ + void defineCallerAwareHostFunctions( + Linker linker, + List callerAwareHostFunctions) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + for (CallerAwareHostFunctionDefinition def : callerAwareHostFunctions) { + FunctionType funcType = new FunctionType( + convertToWasmTypes(def.parameterTypes()), + convertToWasmTypes(def.resultTypes())); + + @SuppressWarnings({"unchecked", "rawtypes"}) + HostFunction impl = new HostFunction.CallerAwareHostFunction( + (HostFunction.MultiValueHostFunctionWithCaller) (nativeCaller, + wasmArgs) -> { + @SuppressWarnings({"rawtypes"}) + WasmtimeCallerAdapter callerAdapter = new WasmtimeCallerAdapter( + nativeCaller, runtime, engineConfig); + Object[] javaArgs = extractWasmValues(wasmArgs); + @SuppressWarnings({"unchecked", "rawtypes"}) + CallerAwareHostFunction fn = + (CallerAwareHostFunction) def.function(); + @SuppressWarnings("unchecked") + Object[] results = fn.execute(callerAdapter, javaArgs); + if (results == null || results.length == 0) { + return new WasmValue[0]; + } + return convertToWasmValues(results, def.resultTypes()); + }); + + linker.defineHostFunction(def.moduleName(), def.functionName(), funcType, impl); + } + } + /** * Wire typed {@link ExternImportDefinition} values into the wasmtime * linker. Each variant is unwrapped to its native wasmtime4j handle; @@ -256,15 +360,39 @@ private static ValueType convertFromWasmValueType(WasmValueType type) { public Optional extension(Class extensionType) { if (extensionType == ai.tegmentum.webassembly4j.api.capability.FuelController.class && engine.isFuelEnabled()) { + if (callerScoped) { + // Caller-scoped modules borrow the caller's store; the + // FuelController on the caller's store is reached via the + // Caller api, not through this Module handle. + return Optional.empty(); + } return Optional.of((T) new WasmtimeFuelController(store)); } return Optional.empty(); } + /** + * Provider escape — returns the underlying native handle when the + * caller wants to reach the wasmtime4j-native Module directly (used by + * {@link WasmtimeCallerAdapter#instantiate(Module, LinkingContext)} to + * bridge back to the wasmtime4j linker). + */ + @SuppressWarnings("unchecked") + Optional unwrap(Class nativeType) { + if (nativeType.isInstance(nativeModule)) { + return Optional.of((T) nativeModule); + } + return Optional.empty(); + } + ai.tegmentum.wasmtime4j.Store store() { return store; } + ai.tegmentum.wasmtime4j.Module nativeModule() { + return nativeModule; + } + private static void configureNativeWasi(ai.tegmentum.wasmtime4j.wasi.WasiContext nativeWasi, ai.tegmentum.webassembly4j.api.WasiContext wasiCtx) { List args = wasiCtx.args(); @@ -344,6 +472,8 @@ private static WasmValue[] convertToWasmValues(Object[] values, ValueType[] type @Override public void close() { nativeModule.close(); - store.close(); + if (store != null && !callerScoped) { + store.close(); + } } } From bec629267735aa7e9f0cb1fdf6747be0e1d081f6 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:46:44 -0400 Subject: [PATCH 75/85] test(wasmtime-provider): caller-aware host function + scoped growTable from callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-authored 88-byte wasm module imports env.trigger_grow, exports a 1-slot funcref table + memory, and calls trigger_grow(1) from run(). Test registers trigger_grow as a CallerAwareHostFunction whose callback resolves the caller's table export via caller.getTable("table") then grows it by 1 via caller.growTable — the borrow-safe scoped path routed through the wasmtime4j r.2 caller handle. Asserts: - pre-grow size observed inside the callback is 1 - growTable return value is the previous size (1) - post-callback instance.table("table").size() is 2 - retained Caller handle used after callback return throws IllegalStateException with a caller-generation message (proves wasmtime4j's use-after-return generation counter propagates through the api-layer adapter unchanged) Kept in the WasmtimeLinkingContextExternImportsTest style — inline hand-authored wasm bytes so the test needs no build-time toolchain. --- .../WasmtimeCallerAwareHostFunctionTest.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java new file mode 100644 index 0000000..7c9c794 --- /dev/null +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java @@ -0,0 +1,132 @@ +package ai.tegmentum.webassembly4j.provider.wasmtime; + +import ai.tegmentum.webassembly4j.api.Caller; +import ai.tegmentum.webassembly4j.api.CallerAwareHostFunction; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.Instance; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.Module; +import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.ValueType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exercises the caller-aware host function pipeline end-to-end against + * the wasmtime4j provider. The api additions land at r.1-1 (Caller, + * CallerAwareHostFunction, CallerAwareHostFunctionDefinition, + * LinkingContext.callerAwareHostFunctions, DefaultLinkingContext.Builder + * .addCallerAwareHostFunction); the provider bridge lands at r.1-2 + + * r.1-3 (WasmtimeCallerAdapter + WasmtimeModuleAdapter caller-aware + * dispatch). This test proves the wire end-to-end and confirms that the + * generation-counter use-after-return safety propagates from the + * wasmtime4j jni-side generation check to the api-side Caller handle. + */ +class WasmtimeCallerAwareHostFunctionTest { + + // Hand-authored wasm module (compiled from): + // + // (module + // (import "env" "trigger_grow" (func $trigger_grow (param i32))) + // (memory (export "memory") 1) + // (table (export "table") 1 funcref) + // (func (export "run") (param i32) + // local.get 0 + // call $trigger_grow)) + // + // 88 bytes. Kept inline to match the pattern used by + // WasmtimeLinkingContextExternImportsTest so the test needs no build-time + // wasm-authoring toolchain. + private static final byte[] CALLER_AWARE_MODULE = new byte[] { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, + 0x01, 0x7f, 0x00, 0x02, 0x14, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x0c, 0x74, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x77, 0x00, + 0x00, 0x03, 0x02, 0x01, 0x00, 0x04, 0x04, 0x01, 0x70, 0x00, 0x01, 0x05, + 0x03, 0x01, 0x00, 0x01, 0x07, 0x18, 0x03, 0x06, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x02, 0x00, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x00, + 0x03, 0x72, 0x75, 0x6e, 0x00, 0x01, 0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, + 0x00, 0x10, 0x00, 0x0b + }; + + static boolean runtimeAvailable() { + return new WasmtimeProvider().availability().available(); + } + + @Test + @EnabledIf("runtimeAvailable") + void callerAwareHostFunctionGrowsCallerTableFromCallback() { + AtomicReference> capturedCaller = new AtomicReference<>(); + AtomicInteger observedPrevSize = new AtomicInteger(-1); + AtomicInteger observedGrowReturn = new AtomicInteger(Integer.MIN_VALUE); + + CallerAwareHostFunction triggerGrow = (caller, args) -> { + // Capture the caller so we can prove post-callback use throws. + capturedCaller.set(caller); + + Table t = caller.getTable("table").orElseThrow( + () -> new AssertionError("callback failed to reach caller table")); + observedPrevSize.set(t.size()); + int growBy = ((Number) args[0]).intValue(); + int prevSize = caller.growTable(t, growBy, null); + observedGrowReturn.set(prevSize); + return new Object[0]; + }; + + try (Engine engine = WasmtimeEngineAdapter.create(null); + Module module = engine.loadModule(CALLER_AWARE_MODULE)) { + + LinkingContext ctx = DefaultLinkingContext.builder() + .addCallerAwareHostFunction("env", "trigger_grow", + new ValueType[] { ValueType.I32 }, + new ValueType[0], + triggerGrow) + .build(); + + Instance instance = module.instantiate(ctx); + Table tableBefore = instance.table("table").orElseThrow( + () -> new AssertionError("run: table export missing")); + int sizeBefore = tableBefore.size(); + assertEquals(1, sizeBefore, "wasm-declared table starts at size 1"); + + Function run = instance.function("run").orElseThrow( + () -> new AssertionError("run: run export missing")); + + // Guest calls trigger_grow(1) — callback grows the table by 1. + run.invoke(1); + + // Callback executed and observed the pre-grow size. + assertEquals(1, observedPrevSize.get(), + "callback saw pre-grow table size"); + assertEquals(1, observedGrowReturn.get(), + "growTable return value is previous size on success"); + + // Post-callback: instance's table now reports the grown size. + Table tableAfter = instance.table("table").orElseThrow(); + assertEquals(2, tableAfter.size(), + "table grew by 1 in the caller-scoped path"); + + // Use-after-return: retained Caller handle now throws when + // any scoped method is invoked. Any of the scoped ops would + // do; growTable is convenient because it does not need + // any additional native handles beyond the caller itself. + Caller stale = capturedCaller.get(); + assertNotNull(stale, "capturedCaller set inside the callback"); + + IllegalStateException ise = assertThrows(IllegalStateException.class, () -> + stale.growTable(tableAfter, 1, null), + "wasmtime4j r.2 generation counter rejects use-after-return"); + assertTrue(ise.getMessage().toLowerCase().contains("caller"), + "message identifies the caller-generation issue: " + ise.getMessage()); + } + } +} From e84654c906cf8cf06b841edcd86c6ee6cbb78424 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:50:20 -0400 Subject: [PATCH 76/85] test(wasmtime-provider): restrict caller-aware test callback to scoped ops only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial test called t.size() from inside the caller callback via the regular WasmtimeTableAdapter delegate; that path routes through WasmTable.getSize which acquires the store lock, and inside a live caller-borrow the JniTable native SIGSEGVs — witnessed via hs_err crash at libwasmtime4j.dylib Table::size + JniTable_nativeGetSize. Callback now: - reads args via observedArg, not size() - calls only caller.growTable (borrow-safe scoped path) Also swaps the run(param i32) wasm to a run() no-param variant that calls trigger_grow with an inline i32.const 1 — sidesteps the pre-existing WasmtimeFunctionAdapter I_V fast-path bug where (i32) -> () signatures route through callVoid() and drop the arg ("Parameter validation failed for function 'run'"). Wasm bytes regenerated (88 -> 91 bytes). Assertions unchanged: pre/post-callback table size, growTable return value, use-after-return IllegalStateException from wasmtime4j r.2 generation counter. --- .../WasmtimeCallerAwareHostFunctionTest.java | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java index 7c9c794..5c2f38c 100644 --- a/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java +++ b/wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java @@ -40,22 +40,25 @@ class WasmtimeCallerAwareHostFunctionTest { // (import "env" "trigger_grow" (func $trigger_grow (param i32))) // (memory (export "memory") 1) // (table (export "table") 1 funcref) - // (func (export "run") (param i32) - // local.get 0 + // (func (export "run") + // i32.const 1 // call $trigger_grow)) // - // 88 bytes. Kept inline to match the pattern used by + // 91 bytes. Kept inline to match the pattern used by // WasmtimeLinkingContextExternImportsTest so the test needs no build-time - // wasm-authoring toolchain. + // wasm-authoring toolchain. run() intentionally takes no params (constant + // 1 is loaded inline) to sidestep a pre-existing WasmtimeFunctionAdapter + // fast-path bug where the (i32) -> () signature routes through + // callVoid() and drops the argument — out of scope for this charter. private static final byte[] CALLER_AWARE_MODULE = new byte[] { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, - 0x01, 0x7f, 0x00, 0x02, 0x14, 0x01, 0x03, 0x65, 0x6e, 0x76, 0x0c, 0x74, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, 0x77, 0x00, - 0x00, 0x03, 0x02, 0x01, 0x00, 0x04, 0x04, 0x01, 0x70, 0x00, 0x01, 0x05, - 0x03, 0x01, 0x00, 0x01, 0x07, 0x18, 0x03, 0x06, 0x6d, 0x65, 0x6d, 0x6f, - 0x72, 0x79, 0x02, 0x00, 0x05, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x01, 0x00, - 0x03, 0x72, 0x75, 0x6e, 0x00, 0x01, 0x0a, 0x08, 0x01, 0x06, 0x00, 0x20, - 0x00, 0x10, 0x00, 0x0b + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x60, + 0x01, 0x7f, 0x00, 0x60, 0x00, 0x00, 0x02, 0x14, 0x01, 0x03, 0x65, 0x6e, + 0x76, 0x0c, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x67, 0x72, + 0x6f, 0x77, 0x00, 0x00, 0x03, 0x02, 0x01, 0x01, 0x04, 0x04, 0x01, 0x70, + 0x00, 0x01, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x18, 0x03, 0x06, 0x6d, + 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x05, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x01, 0x00, 0x03, 0x72, 0x75, 0x6e, 0x00, 0x01, 0x0a, 0x08, 0x01, + 0x06, 0x00, 0x41, 0x01, 0x10, 0x00, 0x0b }; static boolean runtimeAvailable() { @@ -66,18 +69,24 @@ static boolean runtimeAvailable() { @EnabledIf("runtimeAvailable") void callerAwareHostFunctionGrowsCallerTableFromCallback() { AtomicReference> capturedCaller = new AtomicReference<>(); - AtomicInteger observedPrevSize = new AtomicInteger(-1); + AtomicReference

capturedTable = new AtomicReference<>(); AtomicInteger observedGrowReturn = new AtomicInteger(Integer.MIN_VALUE); + AtomicInteger observedArg = new AtomicInteger(Integer.MIN_VALUE); + // The callback restricts itself to caller-scoped ops only. + // Calling regular WasmTable/Memory accessors (which acquire the + // store lock) from inside the callback SIGSEGVs — the whole point + // of the caller-aware pattern is to route mutation through the + // borrow-safe scoped entrypoints, so the test does the same. CallerAwareHostFunction triggerGrow = (caller, args) -> { - // Capture the caller so we can prove post-callback use throws. capturedCaller.set(caller); + observedArg.set(((Number) args[0]).intValue()); Table t = caller.getTable("table").orElseThrow( () -> new AssertionError("callback failed to reach caller table")); - observedPrevSize.set(t.size()); - int growBy = ((Number) args[0]).intValue(); - int prevSize = caller.growTable(t, growBy, null); + capturedTable.set(t); + + int prevSize = caller.growTable(t, observedArg.get(), null); observedGrowReturn.set(prevSize); return new Object[0]; }; @@ -102,11 +111,11 @@ void callerAwareHostFunctionGrowsCallerTableFromCallback() { () -> new AssertionError("run: run export missing")); // Guest calls trigger_grow(1) — callback grows the table by 1. - run.invoke(1); + run.invoke(); - // Callback executed and observed the pre-grow size. - assertEquals(1, observedPrevSize.get(), - "callback saw pre-grow table size"); + // Callback executed. + assertEquals(1, observedArg.get(), + "callback observed the guest-side i32 argument"); assertEquals(1, observedGrowReturn.get(), "growTable return value is previous size on success"); @@ -116,14 +125,17 @@ void callerAwareHostFunctionGrowsCallerTableFromCallback() { "table grew by 1 in the caller-scoped path"); // Use-after-return: retained Caller handle now throws when - // any scoped method is invoked. Any of the scoped ops would - // do; growTable is convenient because it does not need - // any additional native handles beyond the caller itself. + // any scoped method is invoked. Reuse the callback's captured + // Table wrapper for the argument (already unwrap-friendly), + // and expect the wasmtime4j r.2 generation counter to reject + // the stale caller before any native dereference happens. Caller stale = capturedCaller.get(); + Table staleTable = capturedTable.get(); assertNotNull(stale, "capturedCaller set inside the callback"); + assertNotNull(staleTable, "capturedTable set inside the callback"); IllegalStateException ise = assertThrows(IllegalStateException.class, () -> - stale.growTable(tableAfter, 1, null), + stale.growTable(staleTable, 1, null), "wasmtime4j r.2 generation counter rejects use-after-return"); assertTrue(ise.getMessage().toLowerCase().contains("caller"), "message identifies the caller-generation issue: " + ise.getMessage()); From 6c2765f219bb7ae8b03c7782495d64fc53594d6a Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 20:52:00 -0400 Subject: [PATCH 77/85] docs(caller): webassembly4j caller-aware r.1 CLOSED_SUCCEEDED close notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the full charter close: r.1-1 (api additions, landed pre-partial), r.1-2 (WasmtimeCallerAdapter), r.1-3 (WasmtimeModuleAdapter callerAwareHostFunctions consumption), r.1-4 (integration test + native crash discovery), r.1-5 (mvn install SNAPSHOT refresh). Notes the api adjustment reconciling Caller.instantiate(Module, LinkingContext) at api layer against wasmtime4j's InstancePre-only Caller.instantiate (transient linker + linker.instantiatePre inside adapter). Notes the test-discovery finding that regular WasmTable/Memory accessors SIGSEGV from callback frames — restated the wasmtime4j r.2 doctrine that mutation from callbacks MUST use caller-scoped ops. r.5.c (F-JIT-Loader-Java-Reference Fiji retry) is UNBLOCKED. --- NOTES-CALLER-AWARE-r1-close-2026-07-26.md | 204 ++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 NOTES-CALLER-AWARE-r1-close-2026-07-26.md diff --git a/NOTES-CALLER-AWARE-r1-close-2026-07-26.md b/NOTES-CALLER-AWARE-r1-close-2026-07-26.md new file mode 100644 index 0000000..e869de1 --- /dev/null +++ b/NOTES-CALLER-AWARE-r1-close-2026-07-26.md @@ -0,0 +1,204 @@ +# NOTES — Webassembly4j Caller-Aware Host Function — r.1 CLOSE (2026-07-26) + +**Branch**: `f-webassembly4j-caller-aware` (off `main @ a553e37`) + +**Charter**: `fijivm/doctrine/specs/f-webassembly4j-caller-aware-host-function-charter-2026-07-26.md` + +**Prior partial**: `fijivm/doctrine/specs/f-webassembly4j-caller-aware-host-function-r1-partial-2026-07-26.md` +(r.1-1 landed at `a2fa3d9` before session limit; resume executed r.1-2..r.1-θ) + +## Executive summary + +Webassembly4j api layer now exposes the caller-aware host-function +pattern behind a `Caller` interface + `CallerAwareHostFunction` ++ `LinkingContext.callerAwareHostFunctions()` accumulator. Wasmtime4j +provider bridges the api into the wasmtime4j r.2 caller-scoped +implementation (JNI-only in wasmtime4j 47.0.2-1.5.1); non-wasmtime +providers inherit the empty-list default and are unaffected. + +Integration test (`WasmtimeCallerAwareHostFunctionTest`) grows the +caller's table by 1 from inside a caller-aware callback and confirms +the wasmtime4j r.2 generation counter propagates `IllegalStateException` +through the api-layer Caller adapter on use-after-return — the SIGSEGV +class first witnessed at F-JIT-Loader-Java-Reference r.5.b is now +closed at the webassembly4j api layer. + +## Commits landed on `f-webassembly4j-caller-aware` + +- `1b8b907` docs(caller): begin webassembly4j caller-aware charter r.1 [pre-partial] +- `a2fa3d9` feat(api): add Caller + CallerAwareHostFunction interfaces + LinkingContext plumbing [pre-partial, r.1-1] +- `cb81e9b` feat(wasmtime-provider): WasmtimeCallerAdapter [r.1-2] +- `2aa300d` feat(wasmtime-provider): WasmtimeModuleAdapter consumes callerAwareHostFunctions from LinkingContext [r.1-3] +- `bec6292` test(wasmtime-provider): caller-aware host function + scoped growTable from callback [r.1-4 initial] +- `e84654c` test(wasmtime-provider): restrict caller-aware test callback to scoped ops only [r.1-4 fix — see "Test discovery" below] + +## Files touched + +- **New — api (r.1-1, pre-partial)**: + - `webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java` + - `webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunction.java` + - `webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/CallerAwareHostFunctionDefinition.java` +- **Modified — api (r.1-1, pre-partial)**: + - `webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/LinkingContext.java` (+ default `callerAwareHostFunctions()`) + - `webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/DefaultLinkingContext.java` (+ Builder.addCallerAwareHostFunction) +- **New — wasmtime provider (r.1-2)**: + - `wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java` +- **Modified — wasmtime provider (r.1-3)**: + - `wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeModuleAdapter.java` + - `callerScoped(...)` factory for caller-produced Modules + - `defineHostFunctions` extracted to package-private static helper for reuse + - `defineCallerAwareHostFunctions` wires each definition through + wasmtime4j's `HostFunction.CallerAwareHostFunction` + - `nativeModule()` package-private accessor for the caller-scoped bridge + - `close()` skips `store.close()` for caller-scoped Modules (their + store is the caller's live store) +- **New — wasmtime provider test (r.1-4)**: + - `wasmtime4j-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAwareHostFunctionTest.java` + +## API adjustments from charter spec + +The charter spec proposed `Caller.instantiate(Module, LinkingContext)`. +Wasmtime4j's `Caller.instantiate(InstancePre)` takes an +`InstancePre`, not a Module + import bundle — the r.2.b close notes +document this explicitly (pre-instantiation happens outside the +callback frame so the reentrant step is minimal). + +The api-layer signature is preserved as spec'd (no api change). +`WasmtimeCallerAdapter.instantiate(Module, LinkingContext)` +reconciles the mismatch internally: + +1. Extracts the native wasmtime4j Module from the api Module +2. Builds a transient `Linker` from `caller.engine()` +3. Wires `hostFunctions()` + `callerAwareHostFunctions()` from the + passed LinkingContext (extern imports are rejected — they require + a live store binding that wasmtime4j's Caller does not expose) +4. Calls `linker.instantiatePre(nativeModule)` to build the InstancePre +5. Delegates to `nativeCaller.instantiate(pre)` — the borrow-safe + scoped instantiate landed at wasmtime4j r.2.b +6. Wraps the returned Instance in `WasmtimeInstanceAdapter` + +`WasmtimeModuleAdapter` gained a caller-scoped construction path +(package-private `callerScoped(...)` factory) so +`caller.compileModule(bytes)` can return a Module handle that borrows +the caller's implicit store. Its own `instantiate()` / `instantiate(ctx)` +methods throw `IllegalStateException("… no owning store …")` — the +caller-produced Module must be instantiated via +`Caller.instantiate(Module, LinkingContext)`, not directly. + +## Test outcome + +- `mvn -B -pl webassembly4j-api,wasmtime4j-provider -am test` + (`-Dtest='!JavaToWasmRoundTripTest,!WasmtimeCallableResourceTest'`, + `-Dskip.rust.build=true`) — **269 tests PASS**, 0 failures, 0 errors, + 0 skipped: + - api: 162 (unchanged) + - spi: 18 (unchanged) + - wasmtime4j-provider: 89 (88 pre-existing unchanged + 1 new + `WasmtimeCallerAwareHostFunctionTest`) + +## Test discovery — regular WasmTable accessors SIGSEGV from callback + +Initial test called `t.size()` from inside the caller callback via +the ordinary `WasmtimeTableAdapter.size()` delegate; that path routes +to `JniTable.getSize()` which the native side implements via +`store.try_lock_store()`. Inside a live caller-borrow the native +crashes rather than throws — witnessed via hs_err at +`libwasmtime4j.dylib Table::size + JniTable_nativeGetSize`. + +This is consistent with the wasmtime4j r.2 doctrine: **mutation from +inside a callback MUST route through caller-scoped entrypoints** +(`caller.growTable`, `caller.setTableElement`, `caller.growMemory`, +`caller.instantiate`), which use `caller.as_context_mut()` instead of +the reentrant store lock. Non-mutation reads on the ordinary handles +(e.g. `WasmMemory.readBytes`, `WasmTable.get(index)`) may or may not +be safe depending on the native impl's lock discipline. The test now +uses only caller-scoped ops in the callback and reads +`instance.table("table").size()` on the outside (post-callback) where +the store lock is available. + +Also encountered a pre-existing bug in `WasmtimeFunctionAdapter`: +the fast-path classifier maps `(i32) -> ()` to `FastPath.I_V` and +calls `callVoid()`, dropping the argument. This trips "Parameter +validation failed for function 'run'" for a `run(param i32)` export. +The test module was updated to a no-param `run()` with an inline +`i32.const 1` so the fast-path used is `V_V`. Out of scope for this +charter. + +## mvn install refreshed SNAPSHOT + +- `mvn -B install -DskipTests -Dspotbugs.skip=true -Dcheckstyle.skip=true -Dpmd.skip=true -Dskip.rust.build=true` — **BUILD SUCCESS** +- All 15 modules installed (Parent + api + spi + 5 providers + runtime + testing + pool + spring + benchmarks + bindgen + component-builder + wasmos-embedder) +- Version stays `2.5.2-SNAPSHOT` (no re-bump per charter) + +## r.5.c unblock status + +**F-JIT-Loader-Java-Reference r.5.c** is now UNBLOCKED. + +The engine-agnostic path Fiji jit-loader was waiting for is live: +- `CallerAwareHostFunction` (or any T) is registered via + `DefaultLinkingContext.Builder.addCallerAwareHostFunction` +- The callback receives an api `Caller` handle with + `compileModule(byte[])`, `instantiate(Module, LinkingContext)`, + `growTable(Table, int, Object)`, `setTableElement`, `growMemory` — + all borrow-safe via the wasmtime4j r.2 + r.2.b caller pipeline +- Non-wasmtime providers (WAMR/GraalWasm/Wasm3/Endive) inherit the + empty-list default from `LinkingContext.callerAwareHostFunctions` + and never construct a Caller — non-breaking for them; if a + non-wasmtime LinkingContext ends up carrying caller-aware + definitions in a future consumer, the provider naturally throws at + instantiate time (per charter's discipline verification) + +The known-good caller-scoped ops for r.5.c per empirical evidence +here + wasmtime4j r.2.b test suite: +- `caller.compileModule(bytes)` — routes through caller's engine +- `caller.instantiate(module, imports)` — routes through borrow-safe + scoped instantiate +- `caller.growTable(table, delta, null)` — verified in this charter's + test +- `caller.setTableElement(table, index, value)` — API present; + wasmtime4j r.2c banked follow-up for funcref-encoding parity when + non-null funcref values are needed inside the callback (workaround: + install funcref via outer-instance `Table.set` after callback + returns, per wasmtime4j r.2.b `testCallerScopedJitInstallLoop`) +- `caller.growMemory(memory, deltaPages)` — verified in wasmtime4j + r.2 test suite + +## Discipline notes preserved + +- No breaking changes to existing webassembly4j api signatures. +- Non-wasmtime providers inherit UOE-free empty-list default; no + provider outside wasmtime4j-provider was touched. +- Webassembly4j pom.xml versions untouched (still `2.5.2-SNAPSHOT`). +- CHANGELOG.md untouched. +- Wasmtime4j NOT modified (already installed as 47.0.2-1.5.1 with + r.2 + r.2.b caller additions). +- Fiji: NOT touched. +- OJ9 substrate `1e7c5205e5`: NOT touched. +- HotSpot: NOT touched. FROZEN WITs preserved. Matrix v7 unchanged. +- No push. Branch `f-webassembly4j-caller-aware` remains local. + +## Stopping-condition classification + +**CLOSED_SUCCEEDED** + +- All 5 sub-slices (r.1-1 pre-partial + r.1-2..r.1-5) land as real + functionality. +- Integration test passes on first run after the SIGSEGV-in-test + discovery and fix; no build regressions; all 88 pre-existing + wasmtime4j-provider tests still pass. +- The r.5.b SIGSEGV pattern is provably closed at the webassembly4j + api layer — use-after-return is caught by IllegalStateException, + in-callback grow succeeds via the borrow-safe path. +- r.5.c (Fiji retry) is fully unblocked — consumer-side work only + from here. + +## Related + +- Charter: `fijivm/doctrine/specs/f-webassembly4j-caller-aware-host-function-charter-2026-07-26.md` +- Prior partial (r.1-1 pre-close): `fijivm/doctrine/specs/f-webassembly4j-caller-aware-host-function-r1-partial-2026-07-26.md` +- Wasmtime4j-side companion: r.2 + r.2.b close notes at + `~/git/wasmtime4j/NOTES-CALLER-AWARE-HOST-FUNCTION-r2-implement-2026-07-26.md` + + `~/git/wasmtime4j/NOTES-CALLER-AWARE-HOST-FUNCTION-r2b-instantiate-2026-07-26.md` +- Prior sibling closures (all merged to main pre-branch): + Table, LinkingContext, Function.unwrap, Cross-Module-Store-Sharing +- Downstream: F-JIT-Loader-Java-Reference r.5.c (now unblocked) From 25d4849c6b138b81ae6a450f670ad65753d0f31e Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Sun, 26 Jul 2026 21:55:19 -0400 Subject: [PATCH 78/85] feat(api,wasmtime-provider): mirror Caller.readMemory/writeMemory (post wasmtime4j r.1 close) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the wasmtime4j Caller-Scoped-Memory-IO r.1 additions at webassembly4j api layer + wasmtime4j-provider bridge. Enables engine-agnostic scoped memory I/O from caller-aware host callbacks — unblocks Fiji jit-loader r.5.d retry. api: 2 default methods (throw UOE) provider: WasmtimeCallerAdapter delegates to native Caller.readMemory/ writeMemory Non-wasmtime providers inherit UOE default (safe). --- .../wasmtime/WasmtimeCallerAdapter.java | 20 ++++++++++ .../tegmentum/webassembly4j/api/Caller.java | 39 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java index e1a2115..086739c 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java @@ -337,6 +337,26 @@ public long growMemory(Memory memory, long deltaPages) { } } + @Override + public byte[] readMemory(String memoryName, long offset, int length) { + try { + return nativeCaller.readMemory(memoryName, offset, length); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Caller-scoped readMemory failed: " + e.getMessage(), e); + } + } + + @Override + public void writeMemory(String memoryName, long offset, byte[] bytes) { + try { + nativeCaller.writeMemory(memoryName, offset, bytes); + } catch (ai.tegmentum.wasmtime4j.exception.WasmException e) { + throw new ExecutionException( + "Caller-scoped writeMemory failed: " + e.getMessage(), e); + } + } + @Override @SuppressWarnings("unchecked") public Optional unwrap(Class nativeType) { diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java index fb99203..f4417fe 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java @@ -108,6 +108,45 @@ public interface Caller { */ long growMemory(Memory memory, long deltaPages); + /** + * Read a byte range from a caller-exported memory using the caller-scoped + * native path (safe from within a host-callback frame). + * + *

Preferable to {@code getMemory(name).get().read(...)} from inside a + * callback because the api-layer {@link Memory} adapter's native handle + * may not be registered from the callback frame, causing native failures. + * This method routes through the provider's scoped-caller memory-read + * primitive (wasmtime4j-provider: uses {@code caller.get_export(name) + * .into_memory() + Memory::read(&mut ctx, ...)}). + * + * @param memoryName name of the caller's exported memory (e.g. "memory") + * @param offset byte offset into memory + * @param length number of bytes to read + * @return the byte range + * @throws IllegalStateException if the callback has returned + * @throws UnsupportedOperationException if the backend has not implemented + * scoped memory I/O + * @since 2.5.2 + */ + default byte[] readMemory(String memoryName, long offset, int length) { + throw new UnsupportedOperationException( + "readMemory not implemented on this Caller backend"); + } + + /** + * Write a byte array into a caller-exported memory using the caller-scoped + * native path. Same safety guarantees as {@link #readMemory}. + * + * @throws IllegalStateException if the callback has returned + * @throws UnsupportedOperationException if the backend has not implemented + * scoped memory I/O + * @since 2.5.2 + */ + default void writeMemory(String memoryName, long offset, byte[] bytes) { + throw new UnsupportedOperationException( + "writeMemory not implemented on this Caller backend"); + } + /** * Provider escape hatch — returns the underlying provider-specific * caller handle when {@code nativeType} is assignable from it. From c698ae6d81074a26979cfa81938626d4e893ff64 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 27 Jul 2026 06:46:48 -0400 Subject: [PATCH 79/85] feat(provider): caller-scoped externImports wire-through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-Wasmtime4j-Caller-Scoped-Instantiate-Extern-Imports r.2. Replaces WasmtimeCallerAdapter.instantiate's blanket LinkingException on externImports with a per-def routing loop: - MemoryImport / TableImport whose source is a WasmtimeMemoryAdapter / WasmtimeTableAdapter carrying a callerExportName (minted by Caller.getMemory/getTable in a callback frame) → route to the new caller-scoped linkerDefineMemoryFromExport / TableFromExport by-name native path (avoids registry-handle mismatch). - MemoryImport / TableImport / GlobalImport with outer-scoped externs → route to linkerDefineMemory / Table / Global handle-based path. - FunctionImport remains unsupported (waits on Function.unwrap). WasmtimeMemoryAdapter / WasmtimeTableAdapter gain an optional callerExportName field to signal caller-scoped origin; getMemory / getTable on WasmtimeCallerAdapter set it. --- .../wasmtime/WasmtimeCallerAdapter.java | 106 ++++++++++++++++-- .../wasmtime/WasmtimeMemoryAdapter.java | 18 +++ .../wasmtime/WasmtimeTableAdapter.java | 13 +++ 3 files changed, 126 insertions(+), 11 deletions(-) diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java index 086739c..0292274 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeCallerAdapter.java @@ -1,17 +1,23 @@ package ai.tegmentum.webassembly4j.provider.wasmtime; import ai.tegmentum.wasmtime4j.WasmFunction; +import ai.tegmentum.wasmtime4j.WasmGlobal; import ai.tegmentum.wasmtime4j.WasmMemory; import ai.tegmentum.wasmtime4j.WasmRuntime; import ai.tegmentum.wasmtime4j.WasmTable; import ai.tegmentum.webassembly4j.api.Caller; +import ai.tegmentum.webassembly4j.api.ExternImportDefinition; import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.FunctionImport; import ai.tegmentum.webassembly4j.api.Global; +import ai.tegmentum.webassembly4j.api.GlobalImport; import ai.tegmentum.webassembly4j.api.Instance; import ai.tegmentum.webassembly4j.api.LinkingContext; import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.MemoryImport; import ai.tegmentum.webassembly4j.api.Module; import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.TableImport; import ai.tegmentum.webassembly4j.api.exception.ExecutionException; import ai.tegmentum.webassembly4j.api.exception.LinkingException; @@ -58,12 +64,14 @@ public T data() { @Override public Optional getMemory(String name) { - return nativeCaller.getMemory(name).map(WasmtimeMemoryAdapter::new); + return nativeCaller.getMemory(name) + .map(mem -> new WasmtimeMemoryAdapter(mem, name)); } @Override public Optional

getTable(String name) { - return nativeCaller.getTable(name).map(WasmtimeTableAdapter::new); + return nativeCaller.getTable(name) + .map(tbl -> new WasmtimeTableAdapter(tbl, name)); } @Override @@ -137,15 +145,7 @@ public Instance instantiate(Module module, LinkingContext imports) { try { ai.tegmentum.wasmtime4j.Linker linker = runtime.createLinker(callerEngine); if (imports != null) { - if (!imports.externImports().isEmpty()) { - throw new LinkingException( - "Caller.instantiate does not support externImports" - + " (memory/table/global imports need a live" - + " store binding that the caller-scoped path" - + " does not expose). Provide these via" - + " hostFunctions or caller-aware host" - + " functions instead."); - } + defineCallerScopedExternImports(linker, imports.externImports()); WasmtimeModuleAdapter.defineHostFunctions(linker, imports.hostFunctions()); // Nested caller-aware host functions on an inner module // instantiated inside a callback are permitted — they @@ -173,6 +173,90 @@ public Instance instantiate(Module module, LinkingContext imports) { } } + /** + * Wire {@code MemoryImport} / {@code TableImport} / {@code GlobalImport} + * defs from the caller into a transient linker, using the caller's live + * store context via + * {@link ai.tegmentum.wasmtime4j.func.Caller#linkerDefineMemory} / + * {@code linkerDefineTable} / {@code linkerDefineGlobal}. Mirrors + * {@code WasmtimeModuleAdapter.defineExternImports} but routes through + * the scoped-caller path so no {@code Store} handle is needed — + * F-Wasmtime4j-Caller-Scoped-Instantiate-Extern-Imports r.1 landed + * these ops on the wasmtime4j Caller interface at 1.5.2. + * + *

{@link FunctionImport} remains unsupported here — the api-layer + * {@link ai.tegmentum.webassembly4j.api.Function Function} interface has + * no {@code unwrap(Class)} method, matching the same limitation as the + * outer path. + */ + private void defineCallerScopedExternImports( + ai.tegmentum.wasmtime4j.Linker linker, + java.util.List externImports) + throws ai.tegmentum.wasmtime4j.exception.WasmException { + if (externImports == null || externImports.isEmpty()) { + return; + } + for (ExternImportDefinition def : externImports) { + if (def instanceof MemoryImport) { + MemoryImport mi = (MemoryImport) def; + if (mi.memory() instanceof WasmtimeMemoryAdapter + && ((WasmtimeMemoryAdapter) mi.memory()).callerExportName() != null) { + nativeCaller.linkerDefineMemoryFromExport( + linker, + mi.moduleName(), + mi.name(), + ((WasmtimeMemoryAdapter) mi.memory()).callerExportName()); + } else { + WasmMemory nativeMemory = + mi.memory().unwrap(WasmMemory.class) + .orElseThrow(() -> new LinkingException( + "MemoryImport " + mi.moduleName() + "::" + mi.name() + + " memory is not a wasmtime4j-backed instance")); + nativeCaller.linkerDefineMemory( + linker, mi.moduleName(), mi.name(), nativeMemory); + } + } else if (def instanceof TableImport) { + TableImport ti = (TableImport) def; + if (ti.table() instanceof WasmtimeTableAdapter + && ((WasmtimeTableAdapter) ti.table()).callerExportName() != null) { + nativeCaller.linkerDefineTableFromExport( + linker, + ti.moduleName(), + ti.name(), + ((WasmtimeTableAdapter) ti.table()).callerExportName()); + } else { + WasmTable nativeTable = + ti.table().unwrap(WasmTable.class) + .orElseThrow(() -> new LinkingException( + "TableImport " + ti.moduleName() + "::" + ti.name() + + " table is not a wasmtime4j-backed instance")); + nativeCaller.linkerDefineTable( + linker, ti.moduleName(), ti.name(), nativeTable); + } + } else if (def instanceof GlobalImport) { + GlobalImport gi = (GlobalImport) def; + WasmGlobal nativeGlobal = + gi.global().unwrap(WasmGlobal.class) + .orElseThrow(() -> new LinkingException( + "GlobalImport " + gi.moduleName() + "::" + gi.name() + + " global is not a wasmtime4j-backed instance")); + nativeCaller.linkerDefineGlobal(linker, gi.moduleName(), gi.name(), nativeGlobal); + } else if (def instanceof FunctionImport) { + FunctionImport fi = (FunctionImport) def; + throw new UnsupportedOperationException( + "FunctionImport " + fi.moduleName() + "::" + fi.name() + + " is not yet supported by wasmtime4j-provider caller-scoped" + + " instantiate — the api-layer Function interface has no" + + " unwrap(Class) accessor. Use LinkingContext addHostFunction" + + " or addCallerAwareHostFunction instead."); + } else { + throw new LinkingException( + "Unknown ExternImportDefinition variant: " + + (def == null ? "null" : def.getClass().getName())); + } + } + } + /** * Register a caller-aware host function on the transient linker built * for the caller-scoped inner instantiate. Mirrors the wiring in diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeMemoryAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeMemoryAdapter.java index 2878eb5..9a8390a 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeMemoryAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeMemoryAdapter.java @@ -11,9 +11,27 @@ final class WasmtimeMemoryAdapter implements Memory { private static final int PAGE_SIZE = 65536; private final WasmMemory nativeMemory; + /** + * Non-null when this adapter was minted by {@code WasmtimeCallerAdapter.getMemory(name)} + * from within a callback frame. Signals that the underlying native handle is a raw + * {@code Box} (not registry-wrapped), so + * {@code WasmtimeCallerAdapter.defineCallerScopedExternImports} must route through the + * caller-scoped {@code linkerDefineMemoryFromExport} path instead of the handle-based + * path — the registry lookup would otherwise fail. + */ + private final String callerExportName; WasmtimeMemoryAdapter(WasmMemory nativeMemory) { + this(nativeMemory, null); + } + + WasmtimeMemoryAdapter(WasmMemory nativeMemory, String callerExportName) { this.nativeMemory = nativeMemory; + this.callerExportName = callerExportName; + } + + String callerExportName() { + return callerExportName; } @Override diff --git a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java index 241ecf9..ef2dee0 100644 --- a/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java +++ b/wasmtime4j-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmtime/WasmtimeTableAdapter.java @@ -9,9 +9,22 @@ final class WasmtimeTableAdapter implements Table { private final WasmTable nativeTable; + /** + * See {@code WasmtimeMemoryAdapter.callerExportName} — same rationale. + */ + private final String callerExportName; WasmtimeTableAdapter(WasmTable nativeTable) { + this(nativeTable, null); + } + + WasmtimeTableAdapter(WasmTable nativeTable, String callerExportName) { this.nativeTable = nativeTable; + this.callerExportName = callerExportName; + } + + String callerExportName() { + return callerExportName; } @Override From 9c5bf797b896319ff8b6eddbfd72a9dc8202a1a5 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Mon, 27 Jul 2026 19:40:25 -0400 Subject: [PATCH 80/85] docs(caller): @see wasmos WIT references (Charter A r.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-Wasmtime4j-Caller-Scoped-API-Aligned-With-Wasmos-WIT r.1 CLOSED_SUCCEEDED (with scope correction: doctrine applies at webassembly4j layer — the portable engine abstraction — not at wasmtime4j which is wasmtime-specific JNI binding). Per user 2026-07-27. Class-level Javadoc gains a 'Design alignment' section citing the doctrine + translation rules (WIT kebab-case → Java camelCase, borrow removed since Caller has implicit store binding, result → throws, option → Optional, list → byte[]). Per-method @see wasmos wit://... tags on 9 caller-scoped methods: getMemory/Table/Function/Global → instance.instance.get-* compileModule → module-compile.from-binary (deviation: engine implicit) instantiate → linker.linker.instantiate (deviation: LinkingContext bundles pre-linker define-* calls) growTable → instance.wasm-table.grow (upstream via r.2 of wasmos arc) setTableElement → instance.wasm-table.set (upstream via r.2) growMemory → instance.wasm-memory.grow readMemory → instance.wasm-memory.read (deviation: name-lookup vs borrow) writeMemory → instance.wasm-memory.write 12 @see-wasmos tags in file. Discoverable via grep. mvn compile green. --- .../tegmentum/webassembly4j/api/Caller.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java index f4417fe..452c6c3 100644 --- a/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java +++ b/webassembly4j-api/src/main/java/ai/tegmentum/webassembly4j/api/Caller.java @@ -22,6 +22,31 @@ * scoped methods raise {@link UnsupportedOperationException} on the * unsupported entrypoints. * + *

Design alignment: wasmos {@code host:wasmtime@0.10.0} WIT

+ * + *

webassembly4j is a portable, provider-agnostic engine abstraction — + * the same purpose wasmos's {@code host:wasmtime@0.10.0} WIT interface + * serves for the WIT / component-model ecosystem. This {@code Caller} + * interface's method shape mirrors the equivalent + * {@code host:wasmtime@0.10.0} WIT primitives (see + * {@code ~/git/wasmos/wit/}: {@code engine.wit}, {@code store.wit}, + * {@code module-compile.wit}, {@code linker.wit}, {@code instance.wit}, + * {@code refs.wit}). The two projects independently converged on the + * same primitive set — validation that the surface is broadly useful + * for any host that wants to drive dynamic composition from a callback + * frame, not JIT-loader-bespoke. + * + *

Per {@code doctrine-wasmtime4j-caller-scoped-api-aligned-with-wasmos-wit-2026-07-27} + * (which applies at the webassembly4j layer, not wasmtime4j — the JNI + * layer stays wasmtime-specific): future caller-scoped additions to this + * interface MUST mirror the equivalent method in wasmos WIT (kebab-case + * → camelCase, {@code borrow} removed since the caller carries + * its own implicit store binding, {@code result} → {@code T} + * return + throws, {@code option} → {@code Optional}, + * {@code list} → {@code byte[]}). Deviations must be documented in + * the method's Javadoc. Discoverable via {@code grep "@see wasmos"} across + * this file. + * * @param the type of caller-associated data (provider-specific store * data slot; typically {@code Void} when no data is used) * @since 2.5.2 @@ -36,21 +61,34 @@ public interface Caller { /** * Returns an exported memory of the calling instance by name. + * + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.instance.get-memory} + * ({@code func(store: borrow<store>, name: string) -> option<wasm-memory>}). */ Optional getMemory(String name); /** * Returns an exported table of the calling instance by name. + * + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.instance.get-table}. */ Optional

getTable(String name); /** * Returns an exported function of the calling instance by name. + * + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.instance.get-func}. */ Optional getFunction(String name); /** * Returns an exported global of the calling instance by name. + * + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.instance.get-global}. */ Optional getGlobal(String name); @@ -64,6 +102,11 @@ public interface Caller { * that accept it) * @return the compiled module * @throws IllegalStateException if the callback has returned + * @see wasmos wit://module-compile.wit + * — mirror of {@code host:wasmtime@0.10.0/module-compile.from-binary} + * ({@code func(engine: borrow<engine>, bytes: list<u8>) -> result<module, error>}); + * Java-side takes only bytes since the engine is implicit in the caller + * binding. */ Module compileModule(byte[] wasmBytes); @@ -79,6 +122,12 @@ public interface Caller { * no-import instantiation * @return the newly created instance * @throws IllegalStateException if the callback has returned + * @see wasmos wit://linker.wit + * — mirror of {@code host:wasmtime@0.10.0/linker.linker.instantiate} + * ({@code func(store: borrow<store>, module: borrow<module>) + * -> result<instance, error>}); Java-side collapses linker construction + * and {@code define-memory/table/global/func} calls into a single + * {@link LinkingContext} argument. */ Instance instantiate(Module module, LinkingContext imports); @@ -90,6 +139,13 @@ public interface Caller { * * @return the previous size on success, {@code -1} on failure * @throws IllegalStateException if the callback has returned + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.wasm-table.grow} + * ({@code func(store: borrow<store>, delta: u64, init: val) -> result<u64, error>}); + * added upstream via F-Wasmos-Guest-JIT-Loader r.2 (2026-07-27). + * Java-side returns {@code int} + {@code -1}-on-failure sentinel + * rather than {@code result} since {@code int} matches the WIT + * table's practical index range and the pre-existing method signature. */ int growTable(Table table, int delta, Object init); @@ -97,6 +153,10 @@ public interface Caller { * Write a value into a caller-visible table slot. * * @throws IllegalStateException if the callback has returned + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.wasm-table.set} + * ({@code func(store: borrow<store>, index: u64, value: val) -> result<_, error>}); + * added upstream via F-Wasmos-Guest-JIT-Loader r.2 (2026-07-27). */ void setTableElement(Table table, int index, Object value); @@ -105,6 +165,9 @@ public interface Caller { * * @return the previous size in pages on success, {@code -1} on failure * @throws IllegalStateException if the callback has returned + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.wasm-memory.grow} + * ({@code func(store: borrow<store>, delta: u64) -> result<u64, error>}). */ long growMemory(Memory memory, long deltaPages); @@ -126,6 +189,12 @@ public interface Caller { * @throws IllegalStateException if the callback has returned * @throws UnsupportedOperationException if the backend has not implemented * scoped memory I/O + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.wasm-memory.read} + * ({@code func(store: borrow<store>, offset: u64, length: u64) + * -> result<list<u8>, error>}); Java-side takes {@code memoryName} + * lookup rather than {@code borrow<wasm-memory>} since the caller + * resolves the export by name internally. * @since 2.5.2 */ default byte[] readMemory(String memoryName, long offset, int length) { @@ -140,6 +209,10 @@ default byte[] readMemory(String memoryName, long offset, int length) { * @throws IllegalStateException if the callback has returned * @throws UnsupportedOperationException if the backend has not implemented * scoped memory I/O + * @see wasmos wit://instance.wit + * — mirror of {@code host:wasmtime@0.10.0/instance.wasm-memory.write} + * ({@code func(store: borrow<store>, offset: u64, data: list<u8>) + * -> result<_, error>}). * @since 2.5.2 */ default void writeMemory(String memoryName, long offset, byte[] bytes) { From 8cdf5c972579db14ba0e6db153e11c65d2345a20 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 30 Jul 2026 16:43:59 -0400 Subject: [PATCH 81/85] fix(bindgen): 6 fixes across parser + codegen + CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundled fixes shipped together during a wasmos-consumption push: - CodeGenerator.generate() merges per-file BindgenModels before code emission. Previously each WIT file's javaCodeGenerator invocation emitted its own .java, and the last write silently overwrote earlier ones — killing SPI dispatchers for every interface but the last file processed. Test: WasmosRuntimeSpiMergeTest. - BindgenCli.validateSources uses Files.walk instead of listFiles so `-w wit/deps` picks up every .wit under the tree. Combined with the merge fix, multi-file WIT packages resolve intra-package `use` refs correctly. Test: BindgenCliTest.shouldRecursivelyExpandDirectorySource. - WitParser accepts stream and future: coerces to list and option respectively with a WARNING log rather than throwing BindgenException. Async component-model types are not modeled but the surrounding file stays parseable. Test: WitParserTest.asyncTypesCoerced. - JavaCodeGenerator.mapType gains a TUPLE arm that recursively maps each element (was defaulting to embedding raw WIT text as a Java type name). TypeMappingRegistry.mapTuple emits parameterised Map.Entry for 2-tuples (fits JDK; covers wasmos http/websocket headers shape) and parameterised TupleN placeholder otherwise. Test: NestedContainerFieldTest. - ImplementationCodeGenerator passes parametrised Class/Class (not raw Class) to JavaPoet for WasmBindingProvider.supports/create overrides, resolving name-clash errors under erasure. Marshalling strategy branches on WIT element kind for list params: real unbox loop for list/list, honest TODO-fallback for other elements. Tests: ImplementationCodeGeneratorBugsTest. - MarshallingStrategy.liftReturn returns Optional; caller emits UnsupportedOperationException throw when Optional is empty (mirrors resultDispatch fallback). JavaCodeGenerator.shouldSkipInterface filters interface-and-record-with-same-name pairs (per ADR pattern in provider-manifest.wit) skipping emission with a warning rather than colliding on class name. Tests: InterfaceNestedTypeCollisionTest. Suite: 458 -> 510 tests, all pass. Full 18-module install green. --- .../webassembly4j/bindgen/CodeGenerator.java | 27 +- .../webassembly4j/bindgen/cli/BindgenCli.java | 21 +- .../ImplementationCodeGenerator.java | 58 +++- .../bindgen/generator/JavaCodeGenerator.java | 114 ++++++- .../generator/MarshallingStrategy.java | 103 +++++-- .../generator/TypeMappingRegistry.java | 39 ++- .../webassembly4j/bindgen/wit/WitParser.java | 27 ++ .../bindgen/cli/BindgenCliTest.java | 34 +++ .../ImplementationCodeGeneratorBugsTest.java | 280 ++++++++++++++++++ .../InterfaceNestedTypeCollisionTest.java | 205 +++++++++++++ .../generator/NestedContainerFieldTest.java | 120 ++++++++ .../generator/TypeMappingRegistryTest.java | 21 +- .../wasmos/WasmosRuntimeSpiMergeTest.java | 111 +++++++ .../bindgen/wit/WitParserTest.java | 41 +++ .../test/resources/tuple-fixture/tuple.wit | 14 + .../resources/wasmos-runtime-split/alpha.wit | 8 + .../resources/wasmos-runtime-split/beta.wit | 8 + 17 files changed, 1181 insertions(+), 50 deletions(-) create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/InterfaceNestedTypeCollisionTest.java create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/NestedContainerFieldTest.java create mode 100644 webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosRuntimeSpiMergeTest.java create mode 100644 webassembly4j-bindgen/src/test/resources/tuple-fixture/tuple.wit create mode 100644 webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/alpha.wit create mode 100644 webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/beta.wit diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java index 829ee1f..9504945 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/CodeGenerator.java @@ -98,14 +98,22 @@ public CodeGenerator(final BindgenConfig config) throws BindgenException { public List generate() throws BindgenException { List allSources = new ArrayList<>(); - // Process WIT sources + // Process WIT sources. Multiple WIT files that share a package (typical + // for WIT packages split by interface, e.g. wasmos:runtime@0.2.0 across + // 11 files) must be MERGED into one BindgenModel before code emission — + // otherwise per-file javaCodeGenerator invocations each emit their own + // .java, and the last write silently overwrites the + // earlier ones. Merging preserves every interface's resource dispatchers + // in the SPI and every type/interface/function in the output. if (config.hasWitSources()) { LOGGER.info("Processing WIT sources..."); + BindgenModel combined = null; for (Path witPath : config.getWitSources()) { BindgenModel model = parseWitSource(witPath); - List sources = javaCodeGenerator.generate(model); - allSources.addAll(sources); - LOGGER.fine("Generated " + sources.size() + " sources from " + witPath); + combined = (combined == null) ? model : combined.merge(model); + } + if (combined != null) { + allSources.addAll(javaCodeGenerator.generate(combined)); } } @@ -143,7 +151,16 @@ public void generateAndWrite() throws BindgenException { if (config.hasWitSources()) { for (Path witPath : config.getWitSources()) { BindgenModel model = parseWitSource(witPath); - allInterfaces.addAll(model.getInterfaces()); + for (BindgenInterface iface : model.getInterfaces()) { + // Skip interfaces whose Impl + BindingProvider weren't emitted + // upstream (e.g. an interface whose Java class-name collides + // with a nested type). Registering a non-existent provider in + // META-INF/services would fail ServiceLoader.load at runtime. + if (JavaCodeGenerator.shouldSkipInterface(iface)) { + continue; + } + allInterfaces.add(iface); + } } } if (!allInterfaces.isEmpty()) { diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java index 60483b9..8bb8c64 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCli.java @@ -22,10 +22,13 @@ import ai.tegmentum.webassembly4j.bindgen.CodeStyle; import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; +import java.util.stream.Stream; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -220,12 +223,18 @@ private List validateSources(final List sources, final String extens } if (source.isDirectory()) { - // Collect all matching files from directory - File[] files = source.listFiles((dir, name) -> name.endsWith(extension)); - if (files != null) { - for (File file : files) { - validPaths.add(file.toPath()); - } + // Walk recursively so a WIT layout that nests dep packages under + // `deps//*.wit` (the wit-bindgen / wac convention) can be picked + // up in one go — `-w wit/deps` pulls in every package the top-level + // WIT `use`s, so combined-model merging (see CodeGenerator.generate) + // has the full type closure available for reference resolution. + try (Stream walk = Files.walk(source.toPath())) { + walk.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(extension)) + .sorted() + .forEach(validPaths::add); + } catch (IOException e) { + System.err.println("Warning: Failed to walk directory " + source + ": " + e.getMessage()); } } else if (source.getName().endsWith(extension)) { validPaths.add(source.toPath()); diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java index 0842200..1a4df58 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGenerator.java @@ -30,9 +30,11 @@ import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; +import com.squareup.javapoet.WildcardTypeName; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -165,17 +167,27 @@ GeneratedSource generateBindingProvider(BindgenInterface iface) throws BindgenEx .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addSuperinterface(WASM_BINDING_PROVIDER_CLASS); - // supports() method + // supports() method — parameter type is Class to match the SPI + // signature verbatim. Emitting a raw `Class` here triggered + // -Xlint:rawtypes; downstream projects that promote raw-type warnings + // to errors could not compile the generated file. + final TypeName classWildcard = + ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(Object.class)); providerBuilder.addMethod(MethodSpec.methodBuilder("supports") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(boolean.class) - .addParameter(ClassName.get(Class.class), "iface") + .addParameter(classWildcard, "iface") .addStatement("return iface == $T.class", interfaceClass) .build()); - // create() method + // create() method — parameter type is Class, matching the SPI's + // ` T create(Class, ...)` exactly. Before the fix the parameter + // was raw `Class`, so under erasure the override signature clashed + // with the SPI's parametrised one ("name clash: same erasure, yet + // neither overrides the other") and javac rejected the class. TypeVariableName typeVar = TypeVariableName.get("T"); + final TypeName classOfT = ParameterizedTypeName.get(ClassName.get(Class.class), typeVar); providerBuilder.addMethod(MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) @@ -184,7 +196,7 @@ GeneratedSource generateBindingProvider(BindgenInterface iface) throws BindgenEx .build()) .addTypeVariable(typeVar) .returns(typeVar) - .addParameter(ClassName.get(Class.class), "iface") + .addParameter(classOfT, "iface") .addParameter(INSTANCE_CLASS, "instance") .addParameter(MODULE_CLASS, "module") .addParameter(ENGINE_CLASS, "engine") @@ -409,15 +421,45 @@ private void generateMarshallingBody(MethodSpec.Builder method, BindgenFunction method.addCode( MarshallingStrategy.resultDispatch(returnBindgenType, retptrLocal, function.getName())); } else if (complexReturn) { - CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, retptrLocal); - method.addStatement("return $L", liftCode); + emitLiftedReturn(method, returnBindgenType, retptrLocal, function.getName()); } else { - CodeBlock liftCode = MarshallingStrategy.liftReturn(returnBindgenType, resultLocal); - method.addStatement("return $L", liftCode); + emitLiftedReturn(method, returnBindgenType, resultLocal, function.getName()); } } } + /** + * Emit either {@code return ;} when a lifter is wired, or + * {@code throw new UnsupportedOperationException(...)} when + * {@link MarshallingStrategy#liftReturn} bails out. Before this helper the + * two return-lift sites blindly spliced the returned CodeBlock into + * {@code method.addStatement("return $L", liftCode)}, so an unwired kind + * produced {@code return /* TODO: lift REFERENCE */;} — a comment in + * value position that javac rejected with + * {@code incompatible types: missing return value}. Now the throw path + * mirrors {@link MarshallingStrategy#resultDispatch}: compileable body, + * loud runtime failure, actionable message. + */ + private static void emitLiftedReturn( + MethodSpec.Builder method, + BindgenType returnType, + String valueVar, + String methodName) { + final java.util.Optional lifted = + MarshallingStrategy.liftReturn(returnType, valueVar); + if (lifted.isPresent()) { + method.addStatement("return $L", lifted.get()); + return; + } + final String kind = returnType == null ? "null" : returnType.getKind().toString(); + final String name = returnType == null ? "" : " (" + returnType.getName() + ")"; + method.addStatement( + "throw new $T($S)", + UnsupportedOperationException.class, + "TODO(bindgen): lift " + kind + name + " return from " + + methodName + " not yet wired — see MarshallingStrategy#liftReturn"); + } + /** * Pick a Java local-variable name that starts with {@code desired} and does * not appear in {@code reserved}. Disambiguation uses a {@code $}-separated diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java index 4ffcb5b..1bc74c1 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/JavaCodeGenerator.java @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.logging.Logger; import javax.lang.model.element.Modifier; /** @@ -44,6 +45,8 @@ */ public abstract class JavaCodeGenerator { + private static final Logger LOGGER = Logger.getLogger(JavaCodeGenerator.class.getName()); + protected final BindgenConfig config; protected final TypeMappingRegistry typeRegistry; @@ -73,19 +76,39 @@ public List generate(final BindgenModel model) throws BindgenEx sources.add(generateType(type)); } + // Filter out interfaces whose Java class-name would collide with a + // nested type's Java class-name (same package = same simple name). + // WIT permits `interface X { record X { ... } }` — the two land in the + // same Java package as one `X.java`, and the record class overwrites + // the interface, breaking `implements X` in the generated Impl with + // "interface expected here" (ProviderManifestImpl in wasmos:runtime). + // See {@link #shouldSkipInterface}. + final List emittedInterfaces = new ArrayList<>(); + for (BindgenInterface iface : model.getInterfaces()) { + if (shouldSkipInterface(iface)) { + continue; + } + emittedInterfaces.add(iface); + } + // Generate interfaces for (BindgenInterface iface : model.getInterfaces()) { - sources.add(generateInterface(iface)); - // Generate types defined within interfaces + final boolean skipInterface = shouldSkipInterface(iface); + if (!skipInterface) { + sources.add(generateInterface(iface)); + } + // Generate types defined within interfaces regardless — the record + // that caused the collision is still the intended output; only the + // now-empty interface + Impl scaffolding is dropped. for (BindgenType type : iface.getTypes()) { sources.add(generateType(type)); } } // Generate implementation classes if enabled - if (config.isGenerateImplementations() && !model.getInterfaces().isEmpty()) { + if (config.isGenerateImplementations() && !emittedInterfaces.isEmpty()) { ImplementationCodeGenerator implGen = new ImplementationCodeGenerator(config); - sources.addAll(implGen.generate(model.getInterfaces())); + sources.addAll(implGen.generate(emittedInterfaces)); } // Bindgen 2.0: emit runtime-provider SPI + registry when configured. @@ -116,6 +139,77 @@ public List generate(final BindgenModel model) throws BindgenEx return sources; } + /** + * True when {@code iface} declares a nested type whose Java class-name would + * be identical to the interface's own Java class-name in the target package. + * + *

WIT allows an interface and a nested record/variant/enum to share the + * same identifier — {@code interface provider-manifest { record + * provider-manifest {...} }} in + * {@code wasmos:runtime/provider-manifest.wit} is a live example. Java + * cannot: both compile to {@code ProviderManifest.java} in the same + * package, and the second overwrites the first. The generated Impl then + * fails to compile with "interface expected here" against a class name. + * + *

When the interface has zero functions (it exists purely as a + * namespace for the nested types), skipping the interface + its Impl + + * BindingProvider is a safe, name-preserving fix — the nested types are + * still emitted as top-level classes in the package. + * + *

When the interface has functions, dropping it silently would delete + * user-visible API. This method throws {@link BindgenException} in that + * case so the caller sees a specific error directing them to rename the + * WIT identifier. A follow-on change may promote this to a real rename + * pass (append "Record" / "Variant" to the nested type's Java class); the + * current fix defers that surface change until it becomes required. + * + * @param iface the interface to check + * @return true when the interface should be skipped from code emission + * @throws BindgenException when the collision is present AND the interface + * carries functions (a rename pass, not a skip, is required) + */ + public static boolean shouldSkipInterface(final BindgenInterface iface) throws BindgenException { + final String ifaceClassName = JavaNaming.toClassName(iface.getName()); + BindgenType collidingType = null; + for (BindgenType nested : iface.getTypes()) { + final String nestedClassName = JavaNaming.toClassName(nested.getName()); + if (ifaceClassName.equals(nestedClassName)) { + collidingType = nested; + break; + } + } + if (collidingType == null) { + return false; + } + if (!iface.getFunctions().isEmpty()) { + throw new BindgenException( + "WIT interface '" + + iface.getName() + + "' contains a nested " + + collidingType.getKind().name().toLowerCase() + + " with the same identifier — both would map to '" + + ifaceClassName + + "' in the same Java package, and the interface carries " + + iface.getFunctions().size() + + " function(s) that cannot be silently dropped. Rename either the" + + " interface or the nested type in the WIT source (recommended:" + + " rename the nested type, since it is the source of the collision)."); + } + LOGGER.warning( + "WIT interface '" + + iface.getName() + + "' contains a nested " + + collidingType.getKind().name().toLowerCase() + + " with the same identifier — both would map to '" + + ifaceClassName + + "' in the same Java package. The interface declares no functions," + + " so it is effectively a namespace for the nested type; skipping" + + " the empty interface + its Impl + its BindingProvider. Rename the" + + " nested type in WIT if you want the interface to keep its own" + + " Java class."); + return true; + } + /** * Generates a Java type for the given bindgen type. * @@ -416,6 +510,18 @@ protected TypeName mapType(final BindgenType type) { TypeName okType = type.getOkType().map(this::mapType).orElse(null); TypeName errorType = type.getErrorType().map(this::mapType).orElse(null); return typeRegistry.mapResult(okType, errorType); + case TUPLE: + // Recurse into every element so a nested container (e.g. the + // `list` inside `tuple>`) is mapped to a real + // Java type instead of being echoed back as raw WIT source. Before + // this branch existed, TUPLE fell through to the REFERENCE default + // and mapGeneratedType was handed the whole `tuple<...>` string, + // which JavaPoet embedded verbatim into the emitted field type. + List tupleElements = new ArrayList<>(type.getTupleElements().size()); + for (BindgenType elem : type.getTupleElements()) { + tupleElements.add(mapType(elem)); + } + return typeRegistry.mapTuple(tupleElements); case REFERENCE: return typeRegistry.mapGeneratedType(type.getReferencedTypeName().get()); default: diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java index 9a1fd2f..d4cf018 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/MarshallingStrategy.java @@ -94,18 +94,51 @@ public static CodeBlock lowerArgument(BindgenType type, String javaVar, String a } return CodeBlock.of("$L.add($L);\n", argsListVar, javaVar); - case LIST: + case LIST: { + // Pre-fix, this branch was hard-coded to `(byte[]) (Object) javaVar`. + // That works only when the interface signature is `byte[]` — but + // TypeMappingRegistry.mapList maps `list` to `List` (a + // parameterised List, not a Java array), so the emitted cast + // failed with "incompatible types: List cannot be converted + // to byte[]" on every real caller. Choose the lowering shape from + // the WIT element type. + // + // * `list` / `list`: unbox the `List` into a fresh + // byte[], allocate + write, push (ptr, len) — the on-wire + // Canonical-ABI shape for a list. + // * any other list: no in-memory lowerer wired yet. Emit a + // comment + pass the raw List through so the class compiles; + // the invoke will still fail at runtime (Function.invoke can't + // unbox a Java List into WASM operands), but the failure is + // loud and specific instead of a compile-time cast error that + // blocks the whole generated tree. + final BindgenType element = type.getElementType().orElse(null); + final boolean isByteList = + element != null + && element.getKind() == BindgenType.Kind.PRIMITIVE + && isByteWidePrimitive(element.getName()); + if (isByteList) { + return CodeBlock.builder() + .addStatement("byte[] $L_bytes = new byte[$L.size()]", javaVar, javaVar) + .beginControlFlow("for (int $L_i = 0; $L_i < $L.size(); $L_i++)", + javaVar, javaVar, javaVar, javaVar) + .addStatement("$L_bytes[$L_i] = $L.get($L_i)", javaVar, javaVar, javaVar, javaVar) + .endControlFlow() + .addStatement("int $L_ptr = marshal.allocator().allocate($L_bytes.length, 1)", + javaVar, javaVar) + .addStatement("marshal.memory().write($L_ptr, $L_bytes)", javaVar, javaVar) + .addStatement("$L.add($L_ptr)", argsListVar, javaVar) + .addStatement("$L.add($L_bytes.length)", argsListVar, javaVar) + .build(); + } + final String elementKind = + element == null ? "unknown" : element.getKind() + "(" + element.getName() + ")"; return CodeBlock.builder() - .addStatement("byte[] $L_bytes = ($L instanceof byte[]) ? (byte[]) (Object) $L : null", - javaVar, javaVar, javaVar) - .beginControlFlow("if ($L_bytes != null)", javaVar) - .addStatement("int $L_ptr = marshal.allocator().allocate($L_bytes.length, 1)", - javaVar, javaVar) - .addStatement("marshal.memory().write($L_ptr, $L_bytes)", javaVar, javaVar) - .addStatement("$L.add($L_ptr)", argsListVar, javaVar) - .addStatement("$L.add($L_bytes.length)", argsListVar, javaVar) - .endControlFlow() + .add("// TODO(bindgen): lower list<$L> — only list/list has an " + + "in-memory lowerer wired today\n", elementKind) + .addStatement("$L.add($L)", argsListVar, javaVar) .build(); + } case ENUM: return CodeBlock.of("$L.add($L.ordinal());\n", argsListVar, javaVar); @@ -126,30 +159,62 @@ public static CodeBlock lowerArgument(BindgenType type, String javaVar, String a * returned by {@code Function.invoke}; for complex types it names the * return-pointer local in linear memory. * + *

Returns {@link Optional#empty()} when the bindgen type has no + * in-memory (or in-register) lifter wired yet — references, options, + * variants, records, resources, and functions all fall through today. + * The caller is expected to fall back to a statement-level placeholder + * (an {@link UnsupportedOperationException} throw) in that case rather + * than splicing a comment into an expression position. Before this + * signature change the default arm returned {@code CodeBlock.of("/* TODO + * ... */")}, which the {@code ImplementationCodeGenerator} spliced + * into {@code return $L;} — producing {@code return /* TODO + * ... */;}, a comment where a value is required. That failed with + * {@code incompatible types: missing return value} on every wasmos + * interface returning a REFERENCE or OPTION (InfoImpl.getInfo, + * InfoImpl.provider). Mirrors the {@link #liftFromOffset} contract. + * * @param type the bindgen type * @param valueVar the variable name (result Object, or retptr for complex types) - * @return the lifting code block (an expression) + * @return the lifting-expression code block, or empty when unsupported */ - public static CodeBlock liftReturn(BindgenType type, String valueVar) { + public static Optional liftReturn(BindgenType type, String valueVar) { if (type == null) { - return CodeBlock.of("null"); + return Optional.of(CodeBlock.of("null")); } switch (type.getKind()) { case PRIMITIVE: if ("string".equals(type.getName())) { - return CodeBlock.of("marshal.reader().readString($L)", valueVar); + return Optional.of(CodeBlock.of("marshal.reader().readString($L)", valueVar)); } - return liftPrimitive(type, valueVar); + return Optional.of(liftPrimitive(type, valueVar)); case LIST: - return CodeBlock.of("marshal.reader().readBytes($L)", valueVar); + // list lifts as byte[]; other list element kinds have no + // in-memory lifter yet. Bail to the fallback throw when the element + // isn't u8-shaped so we don't silently mis-lift. + { + final BindgenType element = type.getElementType().orElse(null); + if (element == null + || (element.getKind() == BindgenType.Kind.PRIMITIVE + && isByteWidePrimitive(element.getName()))) { + return Optional.of(CodeBlock.of("marshal.reader().readBytes($L)", valueVar)); + } + return Optional.empty(); + } case ENUM: - return CodeBlock.of("/* TODO: lift enum from ordinal */"); - + case FLAGS: + case RECORD: + case VARIANT: + case OPTION: + case RESULT: + case TUPLE: + case REFERENCE: + case RESOURCE: + case FUNCTION: default: - return CodeBlock.of("/* TODO: lift $L */", type.getKind()); + return Optional.empty(); } } diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistry.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistry.java index e09f583..254b493 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistry.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistry.java @@ -22,6 +22,7 @@ import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -160,6 +161,28 @@ public TypeName mapResult(final TypeName okType, final TypeName errorType) { /** * Creates a tuple type for a WIT tuple. * + *

Mapping strategy: + * + *

    + *
  • 0 elements → {@link Void} (WIT unit / empty tuple). + *
  • 1 element → the element itself (a single-slot tuple is + * structurally equal to the value; no wrapper needed). + *
  • 2 elements → {@link java.util.Map.Entry Map.Entry<K, V>} — a JDK + * type, so the generated source has zero additional dependency and + * compiles standalone. Covers the {@code list>>} headers shape used by wasmos http/websocket. + *
  • N > 2 elements → parameterized {@code {basePackage}.tuple.TupleN} + * placeholder. bindgen does not yet emit those classes; consumers + * who need larger tuples must supply their own {@code TupleN} + * records in that package. + *
+ * + *

The prior implementation dropped generic parameters (emitting the raw + * {@code Tuple2} class name), which also failed to compile — but silently, + * because bindgen's TUPLE-in-mapType branch was missing, so the raw name + * was never even reached from a record field. Now that mapType routes + * TUPLE through here, the output must be a real parameterized Java type. + * * @param elementTypes the element types * @return the tuple type name */ @@ -171,9 +194,19 @@ public TypeName mapTuple(final List elementTypes) { if (elementTypes.size() == 1) { return elementTypes.get(0); } - // Use a generated tuple type or built-in pair for 2 elements - String tupleName = "Tuple" + elementTypes.size(); - return ClassName.get(basePackage + ".tuple", tupleName); + final TypeName[] boxed = new TypeName[elementTypes.size()]; + for (int i = 0; i < elementTypes.size(); i++) { + boxed[i] = elementTypes.get(i).box(); + } + if (elementTypes.size() == 2) { + return ParameterizedTypeName.get(ClassName.get(Map.Entry.class), boxed); + } + // Use a placeholder tuple type parameterised over the element types. Real + // support for arities > 2 needs a companion TupleN generator; see the + // Javadoc above. Emit parameterised form so at least the type parameters + // don't leak as raw class references when consumers plug in their own. + final String tupleName = "Tuple" + elementTypes.size(); + return ParameterizedTypeName.get(ClassName.get(basePackage + ".tuple", tupleName), boxed); } /** diff --git a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java index 1175d2e..8609bf9 100644 --- a/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java +++ b/webassembly4j-bindgen/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParser.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.logging.Logger; /** * Recursive-descent parser for the WebAssembly Interface Type (WIT) grammar. @@ -62,6 +63,7 @@ */ public final class WitParser { + private static final Logger LOGGER = Logger.getLogger(WitParser.class.getName()); private static final int MAX_WIT_TEXT_LENGTH = 1024 * 1024; // 1 MB private final List tokens; @@ -637,6 +639,31 @@ private WitType parseTypeExpression(final Map scope) throws Bin final WitType optionInner = parseTypeExpression(scope); expect(WitToken.Kind.GT); return WitType.option(optionInner); + case "stream": + case "future": + // WIT component-model async types (WASI 0.2+). Bindgen doesn't model + // async semantics — codegen has no runtime plumbing for stream/future + // dispatch — so we lossy-coerce the SHAPE and emit a warning: + // stream → list (sequence of T; sync collection stand-in) + // future → option (possibly-present T; sync option stand-in) + // Semantics change silently in the generated Java, but the alternative + // (crashing the whole parse — as happened before this coercion was + // added — kills every OTHER interface in the same file too). Users + // needing real async wrap the coerced types at their layer. + { + final String asyncKind = head; + advance(); + expect(WitToken.Kind.LT); + final WitType asyncInner = parseTypeExpression(scope); + expect(WitToken.Kind.GT); + LOGGER.warning( + () -> asyncKind + " coerced to " + + ("stream".equals(asyncKind) ? "list" : "option") + + " — bindgen does not model WIT component-model async types"); + return "stream".equals(asyncKind) + ? WitType.list(asyncInner) + : WitType.option(asyncInner); + } case "result": advance(); return parseResultTail(scope); diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCliTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCliTest.java index aaa4479..245ea1c 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCliTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/cli/BindgenCliTest.java @@ -562,6 +562,40 @@ void shouldAcceptMultipleWitSources(@TempDir Path tempDir) throws Exception { assertEquals(0, exitCode); } + + @Test + @DisplayName("--wit on a directory picks up nested .wit files recursively") + void shouldRecursivelyExpandDirectorySource(@TempDir Path tempDir) throws Exception { + // Mirror the wit-bindgen / wac layout: top-level `wit/foo.wit` plus a + // `wit/deps//*.wit` subdir carrying transitive package deps. A + // single `--wit wit/` invocation must pull both in so combined-model + // merging has the full type closure available for cross-package `use` + // resolution (wasmos:runtime references tegmentum:common/errors this way). + Path root = tempDir.resolve("wit"); + Path deps = root.resolve("deps").resolve("mypkg"); + Files.createDirectories(deps); + Files.writeString(root.resolve("top.wit"), VALID_WIT); + Files.writeString( + deps.resolve("nested.wit"), + "interface nested-api {\n echo: func(s: string) -> string\n}"); + Path outputDir = tempDir.resolve("output"); + + int exitCode = + new CommandLine(new BindgenCli()) + .execute( + "--wit", root.toString(), + "--package", "com.example", + "--output", outputDir.toString()); + + assertEquals(0, exitCode, "CLI must succeed with recursive directory expansion"); + // Both interfaces must have been generated. + Path pkgDir = outputDir.resolve("com").resolve("example"); + assertTrue(Files.exists(pkgDir.resolve("Test.java")), + "top-level `test` interface must generate Test.java"); + assertTrue(Files.exists(pkgDir.resolve("NestedApi.java")), + "nested `nested-api` interface must generate NestedApi.java — " + + "directory expansion was not recursive"); + } } @Nested diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java index e7b3a19..bf362c7 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/ImplementationCodeGeneratorBugsTest.java @@ -248,6 +248,240 @@ void resultReturnWithUnitOkPayload() throws Exception { compileImpl(iface, impl); } + @Test + @DisplayName( + "bug 3 (task #39): a WIT `list` parameter lowers a `List` into linear memory") + void listU8ParamLowersFromListByte() throws Exception { + // Pre-fix, the LIST branch of MarshallingStrategy.lowerArgument was + // hard-coded to `(byte[]) (Object) javaVar` — but the interface + // signature is `List` (TypeMappingRegistry.mapList wraps u8 in a + // parametrised List). javac rejected the cast with "incompatible + // types: List cannot be converted to byte[]", which blocked + // every wasmos:runtime interface that carried a bytes payload + // (http.fetch, http-streaming.fetch, run.exec, websocket.send). + // Reproducer: fetch(body: list) — matches http-streaming/http. + final BindgenInterface iface = + BindgenInterface.builder() + .name("http-body") + .addFunction( + BindgenFunction.builder() + .name("send") + .addParameter("body", BindgenType.list(BindgenType.primitive("u8"))) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + // The broken pre-fix cast must be gone. + assertFalse( + content.contains("(byte[]) (Object) body"), + "list lowering must not cast a List to byte[]:\n" + content); + assertFalse( + content.contains("body instanceof byte[]"), + "list lowering must not `instanceof byte[]` a List:\n" + content); + // The replacement path unboxes the List into a fresh byte[]. + assertTrue( + content.contains("byte[] body_bytes = new byte[body.size()]"), + "expected List → byte[] unbox, got:\n" + content); + assertTrue( + content.contains("body_bytes[body_i] = body.get(body_i)"), + "expected element-by-element unbox loop, got:\n" + content); + // (ptr, len) still lands in the args list — the on-wire shape for + // list under the Canonical ABI. + assertTrue( + content.contains("marshal.memory().write(body_ptr, body_bytes)"), + "expected byte[] write into linear memory, got:\n" + content); + assertTrue( + content.contains("args.add(body_ptr)") && content.contains("args.add(body_bytes.length)"), + "expected (ptr, len) push onto args list, got:\n" + content); + + compileImpl(iface, impl); + } + + @Test + @DisplayName( + "bug 3 (task #39): a WIT `list` for a non-byte element compiles as a TODO instead of a byte[] cast") + void listNonBytesParamDoesNotAssumeByteArray() throws Exception { + // The same LIST branch bug also blew up on non-u8 element types — + // run.exec had `caps: list` and produced + // `List` in the signature. The cast to byte[] fails the + // same way. The fix leaves an honest TODO comment for element kinds + // with no in-memory lowerer wired, and passes the raw List through + // so the class compiles. Reproducer uses `list` — a JDK + // element type keeps the compile-check harness self-contained. + final BindgenInterface iface = + BindgenInterface.builder() + .name("runner") + .addFunction( + BindgenFunction.builder() + .name("exec") + .addParameter("names", BindgenType.list(BindgenType.primitive("string"))) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + assertFalse( + content.contains("(byte[]) (Object) names"), + "list must not assume the value is byte[]:\n" + content); + assertTrue( + content.contains("TODO(bindgen): lower list<"), + "expected a TODO marker naming the unlowered element kind, got:\n" + content); + // Falls back to passing the raw list — compileable, runtime-visible. + assertTrue( + content.contains("args.add(names)"), + "expected the raw List to be pushed onto args as a fallback, got:\n" + content); + + compileImpl(iface, impl); + } + + @Test + @DisplayName( + "bug 4 (task #39): the *BindingProvider `create` override uses Class, not raw Class") + void bindingProviderCreateUsesClassOfT() throws Exception { + // Pre-fix, generateBindingProvider emitted the `create` param as raw + // `Class iface` — the SPI signature is ` T create(Class iface, …)`, + // so under erasure the override collided with the parametrised + // super-method ("name clash: same erasure, yet neither overrides the + // other") and the class was rejected as `not abstract`. The `supports` + // override had the same raw-type shape, which downstream projects with + // -Xlint:rawtypes / -Werror could not build. Every interface in + // wasmos:runtime (13 of them) tripped this — 39 of the 45 compile + // errors in the repro were variants of this one bug. + final BindgenInterface iface = + BindgenInterface.builder() + .name("calculator") + .addFunction( + BindgenFunction.builder() + .name("add") + .addParameter("a", BindgenType.primitive("i32")) + .addParameter("b", BindgenType.primitive("i32")) + .returnType(BindgenType.primitive("i32")) + .build()) + .build(); + + // The class-level `generator()` helper disables provider emission + // (mirrors most bugs-file tests, which only exercise Impl bodies). + // This test targets *BindingProvider specifically, so flip that on. + final ImplementationCodeGenerator providerGen = + new ImplementationCodeGenerator( + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(Path.of("target/test-output")) + .addWitSource(Path.of("test.wit")) + .generateImplementations(true) + .generateServiceLoader(true) + .build()); + final List sources = providerGen.generate(Collections.singletonList(iface)); + GeneratedSource providerSource = null; + for (GeneratedSource s : sources) { + if (s.getClassName().equals("CalculatorBindingProvider")) { + providerSource = s; + } + } + assertNotNull(providerSource, "CalculatorBindingProvider must be generated"); + final String content = providerSource.getContent(); + + // No raw Class parameters — either as `Class iface` on a signature + // line, or as an unparameterised import-form landing next to iface. + assertFalse( + content.contains("Class iface"), + "provider must not name a raw `Class iface` parameter:\n" + content); + // supports() must take Class to mirror the SPI verbatim. + assertTrue( + content.contains("supports(Class iface)"), + "expected `supports(Class iface)`, got:\n" + content); + // create() must take Class so the override lands under the SPI's + // parametrised super-method. + assertTrue( + content.contains(" T create(Class iface,"), + "expected ` T create(Class iface, …)`, got:\n" + content); + } + + @Test + @DisplayName( + "bug 5: a REFERENCE return with no in-memory lifter throws instead of splicing a bare TODO comment") + void referenceReturnEmitsThrowNotBareComment() throws Exception { + // Matches wasmos:runtime info.get-info — signature is `get-info: func() + // -> runtime-info` (a REFERENCE return). Pre-fix, MarshallingStrategy + // .liftReturn's default arm returned `CodeBlock.of("/* TODO: lift + // REFERENCE */")`. The caller spliced that into `return $L;`, producing + // `return /* TODO: lift REFERENCE */;` — a bare comment where javac + // demands an expression. Every wasmos interface returning an unlifted + // complex type (InfoImpl.getInfo, InfoImpl.provider) tripped this. The + // fix mirrors resultDispatch's fallback: emit an + // UnsupportedOperationException throw so the class compiles and the gap + // is explicit at runtime. + final BindgenInterface iface = + BindgenInterface.builder() + .name("info") + .addFunction( + BindgenFunction.builder() + .name("get-info") + .returnType(BindgenType.reference("runtime-info")) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + assertFalse( + content.contains("return /* TODO: lift REFERENCE */"), + "reference return must not emit a bare TODO comment in return position:\n" + content); + assertTrue( + content.contains("throw new UnsupportedOperationException"), + "expected an UnsupportedOperationException throw for the unlifted REFERENCE return:\n" + + content); + assertTrue( + content.contains("TODO(bindgen): lift REFERENCE"), + "expected a REFERENCE-tagged TODO in the throw message so post-mortems can grep:\n" + + content); + assertTrue( + content.contains("MarshallingStrategy#liftReturn"), + "expected the throw message to point at MarshallingStrategy#liftReturn:\n" + content); + + compileImpl(iface, impl); + } + + @Test + @DisplayName( + "bug 5 (companion): an OPTION return with no in-memory lifter throws instead of splicing a bare TODO comment") + void optionReturnEmitsThrowNotBareComment() throws Exception { + // Matches wasmos:runtime info.provider — signature is `provider: + // func(feature: string) -> option`. Same default-arm bug as the + // REFERENCE case: liftReturn returned a comment, the caller spliced it + // into a return statement, javac rejected the file. OPTION exercises + // the same fallback path as REFERENCE. + final BindgenInterface iface = + BindgenInterface.builder() + .name("info") + .addFunction( + BindgenFunction.builder() + .name("provider") + .addParameter("feature", BindgenType.primitive("string")) + .returnType(BindgenType.option(BindgenType.primitive("string"))) + .build()) + .build(); + + final GeneratedSource impl = generator().generate(Collections.singletonList(iface)).get(0); + final String content = impl.getContent(); + + assertFalse( + content.contains("return /* TODO: lift OPTION */"), + "option return must not emit a bare TODO comment in return position:\n" + content); + assertTrue( + content.contains("throw new UnsupportedOperationException"), + "expected an UnsupportedOperationException throw for the unlifted OPTION return:\n" + + content); + assertTrue( + content.contains("TODO(bindgen): lift OPTION"), + "expected an OPTION-tagged TODO in the throw message:\n" + content); + + compileImpl(iface, impl); + } + @Test @DisplayName( "bug 2 (companion): a result return lifts the ok arm from the payload offset") @@ -359,6 +593,23 @@ private static void compileImpl(BindgenInterface iface, GeneratedSource impl) th // Sibling record referenced by bug 2's Impl in the error-side of // WitResult<..., Error>. Idempotent for tests that don't reference it. units.add(sourceFile(PACKAGE + ".Error", "package " + PACKAGE + "; public class Error {}")); + // Stub every REFERENCE type the interface pulls in (as a param, return, + // or result-arm payload). New in bug 5 coverage — REFERENCE returns + // reach the compileImpl harness now that liftReturn no longer swallows + // them into a bare comment. + final java.util.Set referencedTypes = new java.util.HashSet<>(); + for (BindgenFunction f : iface.getFunctions()) { + for (var p : f.getParameters()) { + collectReferencedTypes(p.getType(), referencedTypes); + } + if (f.hasReturnType()) { + collectReferencedTypes(f.getReturnType().get(), referencedTypes); + } + } + referencedTypes.remove("Error"); // already added above + for (String ref : referencedTypes) { + units.add(sourceFile(PACKAGE + "." + ref, "package " + PACKAGE + "; public class " + ref + " {}")); + } units.add(sourceFile(PACKAGE + ".ApiStubs", STUB_SOURCE)); final StringWriter diagnostics = new StringWriter(); @@ -397,6 +648,7 @@ private static JavaFileObject stubInterfaceFor(BindgenInterface iface) { final StringBuilder sb = new StringBuilder(); sb.append("package ").append(PACKAGE).append(";\n"); sb.append("import java.util.List;\n"); + sb.append("import java.util.Optional;\n"); sb.append("import ai.tegmentum.webassembly4j.bindgen.wit.WitResult;\n"); sb.append("public interface ").append(pascal(iface.getName())).append(" {\n"); for (BindgenFunction f : iface.getFunctions()) { @@ -428,12 +680,40 @@ private static String returnDecl(BindgenFunction f) { return typeDecl(f.getReturnType().get()); } + /** + * Walk a bindgen type tree and add the Pascal-cased Java class-name of + * every REFERENCE leaf to {@code sink}. Used by {@link #compileImpl} to + * stub the sibling classes the generated Impl expects on the classpath. + */ + private static void collectReferencedTypes(BindgenType t, java.util.Set sink) { + if (t == null) { + return; + } + switch (t.getKind()) { + case REFERENCE: + sink.add(pascal(t.getReferencedTypeName().orElse(t.getName()))); + break; + case LIST: + case OPTION: + collectReferencedTypes(t.getElementType().orElse(null), sink); + break; + case RESULT: + collectReferencedTypes(t.getOkType().orElse(null), sink); + collectReferencedTypes(t.getErrorType().orElse(null), sink); + break; + default: + break; + } + } + private static String typeDecl(BindgenType t) { switch (t.getKind()) { case PRIMITIVE: return primitiveDecl(t.getName()); case LIST: return "List<" + boxDecl(t.getElementType().get()) + ">"; + case OPTION: + return "Optional<" + boxDecl(t.getElementType().get()) + ">"; case RESULT: String ok = t.getOkType().map(ImplementationCodeGeneratorBugsTest::boxDecl).orElse("Void"); String err = diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/InterfaceNestedTypeCollisionTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/InterfaceNestedTypeCollisionTest.java new file mode 100644 index 0000000..f70e4db --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/InterfaceNestedTypeCollisionTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.BindgenException; +import ai.tegmentum.webassembly4j.bindgen.CodeStyle; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenField; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenFunction; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenInterface; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenModel; +import ai.tegmentum.webassembly4j.bindgen.model.BindgenType; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Regression coverage for the bindgen bug where WIT permits (but Java + * forbids) an interface and a nested type sharing the same identifier. + * + *

Live example: {@code wasmos:runtime/provider-manifest.wit} declares + * {@code interface provider-manifest { record provider-manifest {...} }}. + * Both compile to {@code ProviderManifest.java} in the same target Java + * package. The generator wrote the interface, then the record overwrote + * it, and the emitted {@code ProviderManifestImpl implements + * ProviderManifest} failed to compile with "interface expected here" + * because {@code ProviderManifest} was now a class, not an interface. + * + *

Resolution — {@link JavaCodeGenerator#shouldSkipInterface}: + * + *

    + *
  • Interface has zero functions: emit the nested types, drop the + * empty interface + its Impl + its BindingProvider. The interface + * was effectively a namespace; nothing user-visible is lost. + *
  • Interface has functions: throw {@link BindgenException}. Silently + * dropping methods would delete API. The message directs the user + * to rename either identifier in the WIT source. + *
+ */ +@DisplayName("Interface / nested-type name-collision handling") +class InterfaceNestedTypeCollisionTest { + + private static final String PACKAGE = "ai.tegmentum.bindgen.generated.collision"; + + private static BindgenConfig config() { + return BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(Path.of("target/test-output")) + .codeStyle(CodeStyle.MODERN) + .addWitSource(Path.of("test.wit")) + .generateImplementations(true) + .generateServiceLoader(true) + .build(); + } + + /** Build the {@code interface provider-manifest { record provider-manifest {...} }} shape. */ + private static BindgenInterface collidingInterface(boolean withFunction) { + final BindgenType record = + BindgenType.builder() + .name("provider-manifest") + .kind(BindgenType.Kind.RECORD) + .addField(new BindgenField("priority", BindgenType.primitive("u32"))) + .build(); + final BindgenInterface.Builder builder = + BindgenInterface.builder().name("provider-manifest").addType(record); + if (withFunction) { + builder.addFunction( + BindgenFunction.builder() + .name("dummy") + .returnType(BindgenType.primitive("u32")) + .build()); + } + return builder.build(); + } + + @Test + @DisplayName( + "namespace-shaped interface (no functions) with a colliding nested record: emit the record," + + " drop the interface + Impl + BindingProvider") + void collidingNamespaceInterfaceIsSkipped() throws Exception { + final BindgenModel model = + BindgenModel.builder().addInterface(collidingInterface(/* withFunction= */ false)).build(); + + final ModernCodeGenerator generator = new ModernCodeGenerator(config()); + final List sources = generator.generate(model); + + // Bucket by class name so we can assert presence/absence. + final Map byName = new HashMap<>(); + for (final GeneratedSource source : sources) { + // Class-name collisions between separate GeneratedSources land in the + // same bucket; overwrites here mirror the on-disk overwrite pattern + // that the pre-fix generator produced. + byName.put(source.getClassName(), source); + } + + // Positive pin — the record is still emitted. + assertTrue( + byName.containsKey("ProviderManifest"), + "the nested record must still be generated: " + byName.keySet()); + assertTrue( + byName.get("ProviderManifest").getContent().contains("private final int priority"), + "expected the record class body (priority field), got:\n" + + byName.get("ProviderManifest").getContent()); + + // Negative pins — Impl + BindingProvider both drop because the parent + // interface was skipped (nothing to implement). + assertFalse( + byName.containsKey("ProviderManifestImpl"), + "the empty-namespace interface must not emit an Impl: " + byName.keySet()); + assertFalse( + byName.containsKey("ProviderManifestBindingProvider"), + "the empty-namespace interface must not emit a BindingProvider: " + byName.keySet()); + + // Only ONE source with class name ProviderManifest — the record wins + // the slot and the interface is dropped entirely (no overwrite race). + int count = 0; + for (final GeneratedSource source : sources) { + if ("ProviderManifest".equals(source.getClassName())) { + count++; + } + } + assertEquals( + 1, + count, + "expected exactly one ProviderManifest source (the record), got: " + count); + } + + @Test + @DisplayName( + "interface with functions and a colliding nested record: hard-fail (rename required)") + void collidingInterfaceWithFunctionsThrows() { + final BindgenModel model = + BindgenModel.builder().addInterface(collidingInterface(/* withFunction= */ true)).build(); + + final ModernCodeGenerator generator; + try { + generator = new ModernCodeGenerator(config()); + } catch (final Exception e) { + throw new AssertionError("generator construction should not throw", e); + } + final BindgenException e = + assertThrows(BindgenException.class, () -> generator.generate(model)); + final String msg = e.getMessage(); + assertNotNull(msg, "the exception must carry a message"); + // The message must name the interface, the collision, and steer the + // user to a fix (rename either identifier in WIT). + assertTrue( + msg.contains("provider-manifest"), + "expected the WIT identifier in the error message, got: " + msg); + assertTrue( + msg.contains("Rename") || msg.contains("rename"), + "expected the error to direct the user to rename, got: " + msg); + } + + @Test + @DisplayName("non-colliding interface + nested record: both emit normally (baseline)") + void nonCollidingInterfaceStillGenerates() throws Exception { + // Distinct identifiers — same shape as the colliding case but the + // record's name differs. Confirms the skip logic only fires on real + // collisions and doesn't regress the common case. + final BindgenType record = + BindgenType.builder() + .name("manifest-body") + .kind(BindgenType.Kind.RECORD) + .addField(new BindgenField("priority", BindgenType.primitive("u32"))) + .build(); + final BindgenInterface iface = + BindgenInterface.builder().name("provider-manifest").addType(record).build(); + final BindgenModel model = BindgenModel.builder().addInterface(iface).build(); + + final ModernCodeGenerator generator = new ModernCodeGenerator(config()); + final List sources = generator.generate(model); + + final Map byName = new HashMap<>(); + for (final GeneratedSource source : sources) { + byName.put(source.getClassName(), source); + } + assertTrue(byName.containsKey("ProviderManifest"), "interface still emits"); + assertTrue(byName.containsKey("ManifestBody"), "record still emits"); + assertTrue(byName.containsKey("ProviderManifestBindingProvider"), "provider still emits"); + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/NestedContainerFieldTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/NestedContainerFieldTest.java new file mode 100644 index 0000000..49f63dc --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/NestedContainerFieldTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.generator; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.CodeGenerator; +import ai.tegmentum.webassembly4j.bindgen.CodeStyle; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Regression: a record field with a deeply-nested container type — for + * example {@code list>>}, the shape wasmos:runtime + * uses for HTTP/WebSocket headers — must have every inner container mapped + * through the type mapper. Before the fix, TUPLE was not handled by + * {@code JavaCodeGenerator.mapType}: it fell through to the REFERENCE + * default, which handed {@code mapGeneratedType} the raw WIT text + * {@code "tuple>"} — JavaPoet embedded that verbatim as + * {@code Tuple>}, and the generated code failed to + * compile with {@code symbol: class u8} / {@code symbol: class string}. + * + *

The fix threads TUPLE through {@code TypeMappingRegistry.mapTuple}, + * which now emits parameterised {@code java.util.Map.Entry} for + * 2-tuples (JDK type, self-contained) and a parameterised {@code TupleN} + * placeholder for larger tuples. + */ +@DisplayName("bindgen record fields with deeply-nested container types") +class NestedContainerFieldTest { + + private static final String PACKAGE = "com.example.fixture"; + + private static Path tupleWit; + + @BeforeAll + static void locateFixture() throws URISyntaxException { + final URL resource = + NestedContainerFieldTest.class.getClassLoader().getResource("tuple-fixture/tuple.wit"); + assertNotNull(resource, "tuple-fixture/tuple.wit missing from test resources"); + tupleWit = Path.of(URLDecoder.decode(resource.toURI().getPath(), StandardCharsets.UTF_8)); + } + + @Test + @DisplayName( + "record field list>> generates real Java, not raw WIT text") + void nestedListTupleListRecordFieldGeneratesValidJava(@TempDir Path outputDir) throws Exception { + final BindgenConfig config = + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(outputDir) + .codeStyle(CodeStyle.MODERN) + .addWitSource(tupleWit) + .generateImplementations(false) + .generateServiceLoader(false) + .build(); + + final CodeGenerator generator = new CodeGenerator(config); + final List sources = generator.generate(); + + final Map byClass = new HashMap<>(); + for (final GeneratedSource source : sources) { + byClass.put(source.getClassName(), source.getContent()); + } + + assertTrue(byClass.containsKey("Request"), "Request record must be generated"); + final String request = byClass.get("Request"); + + // Regression pins: none of these raw WIT tokens should leak into Java + // source. Before the fix `Tuple>` appeared verbatim + // as the field type on the emitted record. + assertFalse( + request.contains("Tuple):\n" + request); + assertFalse( + request.contains("list"), + "raw WIT container text leaked into generated Java (list):\n" + request); + assertFalse( + request.contains(" string "), + "lowercase primitive `string` leaked as an identifier — should be java.lang.String:\n" + + request); + assertFalse( + request.contains(" u8 "), + "primitive `u8` leaked as an identifier — should be byte / java.lang.Byte:\n" + request); + + // Positive pin: the field's Java type must be a real parameterised + // List>>. We match on the boxed form + // because record fields box their generic params. + assertTrue( + request.contains("List>>") + || request.contains("List>>"), + "expected List>> field type, got:\n" + request); + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistryTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistryTest.java index 12d8634..61c4b24 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistryTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/generator/TypeMappingRegistryTest.java @@ -375,21 +375,32 @@ void shouldReturnElementTypeForSingleElement() { } @Test - @DisplayName("should return Tuple2 for two-element tuple") - void shouldReturnTuple2ForTwoElements() { + @DisplayName("should return parameterised Map.Entry for two-element tuple") + void shouldReturnMapEntryForTwoElements() { + // 2-tuples map to java.util.Map.Entry — a JDK type, so the + // generated source compiles with no extra runtime dependency. Covers + // the wasmos `list>>` headers shape. TypeName result = registry.mapTuple(Arrays.asList(TypeName.INT, TypeName.LONG)); - assertEquals(TEST_PACKAGE + ".tuple.Tuple2", result.toString()); + assertEquals( + "java.util.Map.Entry", result.toString()); } @Test - @DisplayName("should return TupleN for N-element tuple") + @DisplayName("should return parameterised TupleN for N-element tuple") void shouldReturnTupleNForNElements() { + // For arities > 2 we still emit the TupleN placeholder — but + // parameterised, so the type parameters aren't lost. Consumers who + // need larger tuples supply their own {basePackage}.tuple.TupleN. TypeName result = registry.mapTuple( Arrays.asList(TypeName.INT, TypeName.LONG, TypeName.FLOAT, TypeName.DOUBLE)); - assertEquals(TEST_PACKAGE + ".tuple.Tuple4", result.toString()); + assertEquals( + TEST_PACKAGE + + ".tuple.Tuple4", + result.toString()); } @Test diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosRuntimeSpiMergeTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosRuntimeSpiMergeTest.java new file mode 100644 index 0000000..4e74f71 --- /dev/null +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wasmos/WasmosRuntimeSpiMergeTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ai.tegmentum.webassembly4j.bindgen.wasmos; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.tegmentum.webassembly4j.bindgen.BindgenConfig; +import ai.tegmentum.webassembly4j.bindgen.CodeGenerator; +import ai.tegmentum.webassembly4j.bindgen.CodeStyle; +import ai.tegmentum.webassembly4j.bindgen.GeneratedSource; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Regression: when {@code --runtime-provider} is set and the WIT surface is + * spread across multiple files sharing a package, the generated SPI interface + * MUST contain dispatch methods for every resource across every file — not + * just the last file processed. + * + *

Before the fix, {@code CodeGenerator.generate()} ran the java code + * generator per-file, so each pass emitted its own {@code SplitRuntime.java} + * and the last write overwrote the earlier ones. Downstream: multi-file WIT + * packages (like {@code wasmos:runtime@0.2.0}, which is 11 files) lost every + * interface's methods except the last one's from the merged SPI. + */ +@DisplayName("bindgen 2.0 SPI merge across multi-file WIT packages") +class WasmosRuntimeSpiMergeTest { + + private static final String PACKAGE = "ai.tegmentum.wasmos.split.generated"; + private static final String SPI_NAME = "SplitRuntime"; + + private static Path alphaWit; + private static Path betaWit; + + @BeforeAll + static void locateFixtures() throws URISyntaxException { + alphaWit = resolveFixture("wasmos-runtime-split/alpha.wit"); + betaWit = resolveFixture("wasmos-runtime-split/beta.wit"); + } + + private static Path resolveFixture(final String name) throws URISyntaxException { + final URL resource = WasmosRuntimeSpiMergeTest.class.getClassLoader().getResource(name); + assertNotNull(resource, "fixture " + name + " missing from test resources"); + return Path.of(URLDecoder.decode(resource.toURI().getPath(), StandardCharsets.UTF_8)); + } + + @Test + @DisplayName("SPI interface contains methods from both alpha.wit and beta.wit") + void spiMergesResourcesAcrossFiles(@TempDir Path outputDir) throws Exception { + final BindgenConfig config = + BindgenConfig.builder() + .packageName(PACKAGE) + .outputDirectory(outputDir) + .codeStyle(CodeStyle.MODERN) + .addWitSource(alphaWit) + .addWitSource(betaWit) + .generateImplementations(false) + .generateServiceLoader(false) + .runtimeProviderName(SPI_NAME) + .build(); + + final CodeGenerator generator = new CodeGenerator(config); + final List sources = generator.generate(); + + final Map byClass = new HashMap<>(); + for (final GeneratedSource source : sources) { + byClass.put(source.getClassName(), source.getContent()); + } + + assertTrue(byClass.containsKey(SPI_NAME), SPI_NAME + " interface must be generated"); + assertTrue( + byClass.containsKey(SPI_NAME + "Registry"), + SPI_NAME + "Registry class must be generated"); + + final String iface = byClass.get(SPI_NAME); + assertTrue( + iface.contains("alphaCreate("), + "SPI missing alphaCreate — alpha.wit interface was dropped:\n" + iface); + assertTrue(iface.contains("alphaPing(long handle)"), "SPI missing alphaPing"); + assertTrue(iface.contains("void alphaClose(long handle)"), "SPI missing alphaClose"); + assertTrue( + iface.contains("betaCreate("), + "SPI missing betaCreate — beta.wit interface was dropped:\n" + iface); + assertTrue(iface.contains("betaEcho(long handle,"), "SPI missing betaEcho"); + assertTrue(iface.contains("void betaClose(long handle)"), "SPI missing betaClose"); + } +} diff --git a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java index 891807f..d3023c0 100644 --- a/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java +++ b/webassembly4j-bindgen/src/test/java/ai/tegmentum/webassembly4j/bindgen/wit/WitParserTest.java @@ -316,6 +316,47 @@ void nestedContainers() throws BindgenException { assertEquals(2, doc.getInterfaces().get(0).getFunctions().size()); } + @Test + @DisplayName("stream and future parse (coerced to list / option)") + void asyncTypesCoerced() throws BindgenException { + // WASI 0.2+ async types. Before the coercion, `stream` at a record + // field or function signature threw BindgenException at parse time and + // killed every other declaration in the same file (see the wasmos + // http-streaming.wit case). The coercion keeps the surrounding file + // parseable at the cost of documented semantic loss. + final String src = + "interface i {\n" + + " record streaming-response {\n" + + " status: u16,\n" + + " body: stream,\n" + + " }\n" + + " wait-one: func() -> future;\n" + + " stream-of-strings: func() -> stream;\n" + + "}\n"; + final WitDocument doc = WitParser.parse(src); + final WitDocument.ParsedInterface pi = doc.getInterfaces().get(0); + + // The record survived — body field's stream coerced to list. + final WitType record = pi.getTypes().get("streaming-response"); + assertNotNull(record, "record with stream field must parse"); + assertEquals(WitTypeCategory.RECORD, record.getKind().getCategory()); + final WitType bodyField = record.getKind().getRecordFields().get("body"); + assertNotNull(bodyField, "body field present"); + assertEquals(WitTypeCategory.LIST, bodyField.getKind().getCategory(), + "stream must coerce to LIST category"); + + // Both functions parsed. + assertEquals(2, pi.getFunctions().size()); + assertEquals( + WitTypeCategory.OPTION, + pi.getFunctions().get("wait-one").getReturnTypes().get(0).getKind().getCategory(), + "future must coerce to OPTION category"); + assertEquals( + WitTypeCategory.LIST, + pi.getFunctions().get("stream-of-strings").getReturnTypes().get(0).getKind().getCategory(), + "stream must coerce to LIST category"); + } + // --------------------------------------------------------------------------- // resource + type-expression interplay // --------------------------------------------------------------------------- diff --git a/webassembly4j-bindgen/src/test/resources/tuple-fixture/tuple.wit b/webassembly4j-bindgen/src/test/resources/tuple-fixture/tuple.wit new file mode 100644 index 0000000..e3a4902 --- /dev/null +++ b/webassembly4j-bindgen/src/test/resources/tuple-fixture/tuple.wit @@ -0,0 +1,14 @@ +// Regression fixture for the deeply-nested container-in-record bug. +// A record field with type `list>>` — the shape +// used by http/http-streaming/websocket in wasmos:runtime — used to +// generate `List>>` (the inner types leak +// through as raw WIT source instead of being mapped to Java types). +// This fixture pins the fix so the surface compiles. +package tegmentum:tuple-fixture@0.1.0; + +interface headers-api { + record request { + headers: list>>, + } + put: func(req: request); +} diff --git a/webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/alpha.wit b/webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/alpha.wit new file mode 100644 index 0000000..d9cb04d --- /dev/null +++ b/webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/alpha.wit @@ -0,0 +1,8 @@ +package wasmos:split-fixture@0.1.0; + +interface alpha-api { + resource alpha { + constructor(seed: u32); + ping: func() -> u32; + } +} diff --git a/webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/beta.wit b/webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/beta.wit new file mode 100644 index 0000000..dcb546a --- /dev/null +++ b/webassembly4j-bindgen/src/test/resources/wasmos-runtime-split/beta.wit @@ -0,0 +1,8 @@ +package wasmos:split-fixture@0.1.0; + +interface beta-api { + resource beta { + constructor(label: string); + echo: func(msg: string) -> string; + } +} From 2017e150e9d516b9aa714110ad0d6f1964cdc6a3 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 30 Jul 2026 16:44:11 -0400 Subject: [PATCH 82/85] chore(embedder): regenerate wasmos-embedder-generated post bindgen fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downstream effect of the bindgen SPI-merge + interface-skip fixes landed in the preceding commit. Regenerated output for wasmos:host/embedder@0.1.0 (~/git/wasm-cm) picks up: - Correct SPI dispatchers when the WIT world spans multiple files. - Split EmbedderApi / StringEncoding types that previously collided with the parent interface class name. - Additional HostProviderMemory helper surface. No manual edits — pure regeneration output. --- .../embedder/generated/EmbedderApi.java | 8 ++++ .../embedder/generated/EmbedderRuntime.java | 2 + .../embedder/generated/HostProvider.java | 4 ++ .../generated/HostProviderMemory.java | 45 +++++++++++++++++++ .../embedder/generated/StringEncoding.java | 18 ++++++++ 5 files changed, 77 insertions(+) create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderApi.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProviderMemory.java create mode 100644 wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/StringEncoding.java diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderApi.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderApi.java new file mode 100644 index 0000000..d13fb50 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderApi.java @@ -0,0 +1,8 @@ +package ai.tegmentum.wasmos.embedder.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +public interface EmbedderApi { + WitResult, Error> dispatchHostCallback(long callbackId, List argsFrame); +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java index d61f833..0b421ba 100644 --- a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/EmbedderRuntime.java @@ -15,6 +15,8 @@ public interface EmbedderRuntime { HostProvider hostProviderCreate(String interfaceName, int numFuncs); + void hostProviderAttachMemory(long handle, HostProviderMemory mem); + void hostProviderClose(long handle); WitResult runtimeInstanceInstantiate(List componentBytes, diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java index b035dc8..789ea52 100644 --- a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProvider.java @@ -24,4 +24,8 @@ public void close() { public static HostProvider create(String interfaceName, int numFuncs) { return EmbedderRuntimeRegistry.runtime().hostProviderCreate(interfaceName, numFuncs); } + + public void attachMemory(HostProviderMemory mem) { + EmbedderRuntimeRegistry.runtime().hostProviderAttachMemory(this.handle, mem); + } } diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProviderMemory.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProviderMemory.java new file mode 100644 index 0000000..4496d12 --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/HostProviderMemory.java @@ -0,0 +1,45 @@ +package ai.tegmentum.wasmos.embedder.generated; + +public final class HostProviderMemory { + private final long memory; + + private final long realloc; + + private final StringEncoding stringEncoding; + + public HostProviderMemory(long memory, long realloc, StringEncoding stringEncoding) { + this.memory = memory; + this.realloc = realloc; + this.stringEncoding = stringEncoding; + } + + public long memory() { + return this.memory; + } + + public long realloc() { + return this.realloc; + } + + public StringEncoding stringEncoding() { + return this.stringEncoding; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + HostProviderMemory that = (HostProviderMemory) obj; + return this.memory == that.memory && this.realloc == that.realloc && java.util.Objects.equals(this.stringEncoding, that.stringEncoding); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(memory, realloc, stringEncoding); + } + + @Override + public String toString() { + return "HostProviderMemory[memory=" + memory + ", realloc=" + realloc + ", stringEncoding=" + stringEncoding + "]"; + } +} diff --git a/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/StringEncoding.java b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/StringEncoding.java new file mode 100644 index 0000000..3109daa --- /dev/null +++ b/wasmos-embedder-generated/src/main/java/ai/tegmentum/wasmos/embedder/generated/StringEncoding.java @@ -0,0 +1,18 @@ +package ai.tegmentum.wasmos.embedder.generated; + +public interface StringEncoding { + final class Utf8 implements StringEncoding { + public Utf8() { + } + } + + final class Utf16 implements StringEncoding { + public Utf16() { + } + } + + final class Latin1OrUtf16 implements StringEncoding { + public Latin1OrUtf16() { + } + } +} From 027ef3fd45bb4f354d699078181cbe7d328f36e5 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 30 Jul 2026 16:46:24 -0400 Subject: [PATCH 83/85] feat(wasmos): add wasmos-runtime-generated bindings + wasmos-provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new modules land together; together they let webassembly4j consumers select the wasmos runtime as a provider alongside wasmtime4j / wamr4j / endive / graalwasm. wasmos-runtime-generated: bindgen output for wasmos:runtime@0.2.0 (the portable frontend contract wasmos WASM control-planes target). The generated Java types serve as the JNI-typed layer wasmos-provider communicates with the Rust wasmos-runtime library through. 22 sources plus a local copy of WitResult so downstream jars carry no bindgen runtime dep. wasmos-provider: implements EngineProvider (id "wasmos", priority 90) via JNI to a Rust cdylib that wraps the wasmos-runtime library (~/git/wasmos). Practical component-model surface end-to-end — primitives + String + Optional + List + Map + Set (flags) + byte[] fast path + records + variants + enums + tuples, plus typed carriers for WitResource / WitFuture / WitStream / WitErrorContext. WASI-P2 full context passthrough via LinkingContext.wasiContext, ComponentConfig honors all six limits (maxMemoryBytes, epochDeadline, fuelLimit, maxTableElements, maxInstances, maxTables, maxMemories), component serialize / deserialize for hot-reload. Sync invoke plus async slice via WasmosAsyncExtension (invokeAsync returning CompletableFuture, best-effort cancellation via epoch interruption, futures / streams / error-context all park+close+pass-in wired — host-side reading of future / stream values documented as bounded scope until wasmtime ships dynamic-payload polling upstream). Includes an E2E counter-resource test fixture built via cargo-component + wit-bindgen (checked-in source tree + rebuild README under src/test/resources/counter-fixture-src/) plus 62 tests all passing (19 Rust unit tests + 43 Java tests spanning marshalling, resource lifecycle, async, WASI plumbing, ComponentConfig). Rejections with helpful messages: LinkingContext.hostFunctions + witHostFunctions + externImports (wasmos is WIT-first — use wasmtime-provider for core-wasm host injection), Engine.loadModule (wasmos is component-only), wasi:nn + wasi:http + per-endpoint egress (needs wasmtime-wasi upstream). Build setup requires a co-checked-out ~/git/wasmos tree AND sibling ~/git/wasm-cm + ~/git/wasm-continuity because wasmos-runtime declares path deps to those siblings. Documented in wasmos-provider/README.md and wasmos-provider/native/Cargo.toml. Blocks Maven Central publish until wasmos + siblings publish versioned crates OR wasmos restructures away from `path = "..//..."`. --- pom.xml | 12 + wasmos-provider/.gitignore | 15 + wasmos-provider/README.md | 116 + wasmos-provider/native/Cargo.lock | 3932 +++++++++++++++++ wasmos-provider/native/Cargo.toml | 72 + wasmos-provider/native/src/lib.rs | 2552 +++++++++++ wasmos-provider/pom.xml | 194 + .../provider/wasmos/WasmosAsyncExecutors.java | 75 + .../wasmos/WasmosComponentAdapter.java | 380 ++ .../WasmosComponentInstanceAdapter.java | 473 ++ .../provider/wasmos/WasmosEngineAdapter.java | 201 + .../wasmos/WasmosEngineCapabilities.java | 40 + .../provider/wasmos/WasmosEngineInfo.java | 49 + .../provider/wasmos/WasmosMarshalling.java | 1408 ++++++ .../provider/wasmos/WasmosProvider.java | 112 + .../wasmos/ext/WasmosAsyncExtension.java | 255 ++ .../wasmos/ext/WitErrorContextException.java | 77 + .../provider/wasmos/jni/WasmosNative.java | 293 ++ .../wasmos/jni/WasmosNativeLoader.java | 156 + ...tegmentum.webassembly4j.spi.EngineProvider | 1 + .../wasmos/WasmosAsyncInvokeTest.java | 616 +++ .../wasmos/WasmosMarshallingTest.java | 575 +++ .../wasmos/WasmosProviderDemoTest.java | 114 + .../wasmos/WasmosTypedInvokeTest.java | 402 ++ .../src/test/resources/counter-component.wasm | Bin 0 -> 6281 bytes .../resources/counter-fixture-src/Cargo.toml | 25 + .../resources/counter-fixture-src/README.md | 51 + .../resources/counter-fixture-src/src/lib.rs | 59 + .../counter-fixture-src/wit/counter.wit | 13 + .../src/test/resources/counter.wit | 13 + .../src/test/resources/scalar-component.wasm | Bin 0 -> 147 bytes wasmos-runtime-generated/pom.xml | 82 + .../runtime/generated/Capabilities.java | 4 + .../wasmos/runtime/generated/Capability.java | 40 + .../runtime/generated/CapabilityEntry.java | 55 + .../runtime/generated/CapabilitySet.java | 40 + .../wasmos/runtime/generated/Component.java | 34 + .../runtime/generated/ComponentModel.java | 4 + .../wasmos/runtime/generated/Core.java | 4 + .../runtime/generated/CoreInstance.java | 43 + .../wasmos/runtime/generated/Error.java | 38 + .../runtime/generated/ErrorCategory.java | 19 + .../wasmos/runtime/generated/Errors.java | 4 + .../wasmos/runtime/generated/Handles.java | 4 + .../wasmos/runtime/generated/Info.java | 15 + .../wasmos/runtime/generated/Instance.java | 35 + .../wasmos/runtime/generated/Module.java | 34 + .../runtime/generated/ResourceLimits.java | 40 + .../wasmos/runtime/generated/Run.java | 8 + .../wasmos/runtime/generated/Runtime.java | 27 + .../wasmos/runtime/generated/RuntimeInfo.java | 47 + .../wasmos/runtime/generated/Types.java | 4 + .../wasmos/runtime/generated/Value.java | 82 + .../runtime/generated/WasmosRuntime.java | 50 + .../generated/WasmosRuntimeRegistry.java | 36 + .../webassembly4j/bindgen/wit/WitResult.java | 127 + 56 files changed, 13157 insertions(+) create mode 100644 wasmos-provider/.gitignore create mode 100644 wasmos-provider/README.md create mode 100644 wasmos-provider/native/Cargo.lock create mode 100644 wasmos-provider/native/Cargo.toml create mode 100644 wasmos-provider/native/src/lib.rs create mode 100644 wasmos-provider/pom.xml create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncExecutors.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentAdapter.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentInstanceAdapter.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineAdapter.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineCapabilities.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineInfo.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshalling.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProvider.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WasmosAsyncExtension.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WitErrorContextException.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNative.java create mode 100644 wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNativeLoader.java create mode 100644 wasmos-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider create mode 100644 wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncInvokeTest.java create mode 100644 wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshallingTest.java create mode 100644 wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProviderDemoTest.java create mode 100644 wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosTypedInvokeTest.java create mode 100644 wasmos-provider/src/test/resources/counter-component.wasm create mode 100644 wasmos-provider/src/test/resources/counter-fixture-src/Cargo.toml create mode 100644 wasmos-provider/src/test/resources/counter-fixture-src/README.md create mode 100644 wasmos-provider/src/test/resources/counter-fixture-src/src/lib.rs create mode 100644 wasmos-provider/src/test/resources/counter-fixture-src/wit/counter.wit create mode 100644 wasmos-provider/src/test/resources/counter.wit create mode 100644 wasmos-provider/src/test/resources/scalar-component.wasm create mode 100644 wasmos-runtime-generated/pom.xml create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capabilities.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capability.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilityEntry.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilitySet.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Component.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ComponentModel.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Core.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CoreInstance.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Error.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ErrorCategory.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Errors.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Handles.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Info.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Instance.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Module.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ResourceLimits.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Run.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Runtime.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/RuntimeInfo.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Types.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Value.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntime.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntimeRegistry.java create mode 100644 wasmos-runtime-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java diff --git a/pom.xml b/pom.xml index 85485e6..b51eac5 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,8 @@ webassembly4j-bindgen webassembly4j-component-builder wasmos-embedder-generated + wasmos-runtime-generated + wasmos-provider @@ -93,6 +95,16 @@ webassembly4j-spi ${project.version} + + ai.tegmentum.webassembly4j + wasmos-embedder-generated + ${project.version} + + + ai.tegmentum.webassembly4j + wasmos-runtime-generated + ${project.version} + ai.tegmentum wasmtime4j diff --git a/wasmos-provider/.gitignore b/wasmos-provider/.gitignore new file mode 100644 index 0000000..70ebdef --- /dev/null +++ b/wasmos-provider/.gitignore @@ -0,0 +1,15 @@ +# Cargo build tree — lives outside Maven's target/ (see pom.xml exec-maven-plugin +# CARGO_TARGET_DIR env var) so `mvn clean` doesn't blow away the Rust cache. +# Everything under here is regenerated by cargo build; never committed. +.cargo-target/ + +# Standard cargo target dir — used if cargo is invoked directly without the +# CARGO_TARGET_DIR override. +native/target/ + +# Native library outputs staged by the maven-antrun step into +# src/main/resources/natives/{platform}/ — these are regenerated by every +# build from the cargo output, so tracking them creates the stale-dylib +# landmine we hit in wasmtime4j (see local-dev profile discussion). The +# maven build's unpack-dependencies overlay is authoritative. +src/main/resources/natives/ diff --git a/wasmos-provider/README.md b/wasmos-provider/README.md new file mode 100644 index 0000000..6af97f2 --- /dev/null +++ b/wasmos-provider/README.md @@ -0,0 +1,116 @@ +# wasmos-provider + +`webassembly4j` provider backed by the Rust +[`wasmos-runtime`](https://github.com/tegmentum/wasmos) library. Wraps +wasmos-runtime's `add_all_to_linker` + `caps::wasi_p2` surface via JNI +so Java consumers can host wasmos-shaped WASM components (typically +control-plane guests composed with an adapter) through +webassembly4j's normal `Engine` / `Component` / `ComponentInstance` API. + +Selected via `EngineSelection.byId("wasmos")`; provider priority 90. + +## What works + +- Load a wasmos-composed component from bytes +- Instantiate with default caps or a `LinkingContext.wasiContext()` + ("wasi_p2" cap granted through wasmos-runtime) +- Honor `ComponentConfig` fields: `maxMemoryBytes`, `epochDeadline`, + `fuelLimit`, `maxTableElements`, `maxInstances`, `maxTables`, + `maxMemories` +- Invoke exported functions — sync path (`ComponentInstance.invoke`) + and async path (`WasmosAsyncExtension.invokeAsync` returning + `CompletableFuture`) with best-effort cancellation +- Two-way marshalling for primitives, `String`, `List`, `Map`, + `Set` (flags), `Optional`, tuples, `byte[]` (list fast path), + and the typed carriers `WitResource`, `WitFuture`, `WitStream`, + `WitErrorContext` +- Component serialize / deserialize for hot-reload +- Nested-interface function lookup (functions inside exported + interfaces resolve via `#` syntax) + +## What is intentionally not supported + +- Java-provided core-wasm host functions (`LinkingContext.hostFunctions`) + — wasmos is WIT-first; use `wasmtime4j-provider` if you need to inject + Java host functions into a core-wasm module +- `Engine.loadModule` (core-wasm loading) — wasmos-provider is + component-only +- Host-side reading of `Val::Future` / `Val::Stream` — wasmtime 47's + typed reader API doesn't support dynamic-payload polling. Parking, + closing, and passing these values back into guests works; awaiting or + iterating them host-side throws a documented "wasmtime API gap". + When wasmtime upstream ships the dynamic reader, the stub JNI + entries (`futureAwait`, `streamRead`) can be filled in without a + Java surface change. +- `wasi:nn`, `wasi:http`, per-endpoint network egress — needs + wasmtime-wasi upstream API surface + +## Build setup — REQUIRES sibling repository checkouts + +`wasmos-runtime` (this module's Rust dep) declares path deps on other +tegmentum repositories that must be co-checked-out at fixed relative +paths. **Building this crate from a fresh clone of `webassembly4j` +alone WILL fail.** You need: + +``` +~/git/webassembly4j (this repo) +~/git/wasmos (wasmos-provider/native/Cargo.toml points here) +~/git/wasm-cm (wasmos's sibling path dep) +~/git/wasm-continuity (wasmos's sibling path dep) +``` + +Once all four are checked out at the same parent, `cd wasmos-provider && +mvn install` from the webassembly4j root works. + +### Why not git-dep? + +`wasmos-runtime = { git = "..." }` fails at build time because cargo +fetches wasmos from GitHub but then cannot resolve wasmos's own +`../wasm-cm/...` and `../wasm-continuity/...` path deps in the fetched +tree. + +### Why not crates.io? + +Neither `wasmos-runtime`, `wasm-cm`, nor `wasm-continuity` publishes +versioned crates yet. Publishing wasmos-provider to Maven Central is +blocked on this. + +### Paths to unblock external / CI builds + +1. wasmos + wasm-cm + wasm-continuity publish versioned crates. +2. wasmos restructures to git-dep (or workspace-internal) its + siblings — no more `path = "..//..."`. +3. Fork wasmos into a git-dep-clean tree. + +The `wasmos-provider/native/Cargo.toml` comment tracks the same +information for cargo-only readers. + +## Layout + +``` +wasmos-provider/ +├── pom.xml # exec-maven-plugin runs cargo +├── native/ +│ ├── Cargo.toml # path dep to ~/git/wasmos +│ └── src/lib.rs # JNI extern "C" shims +└── src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ + ├── WasmosProvider.java # EngineProvider SPI impl + ├── WasmosEngineAdapter.java + ├── WasmosComponentAdapter.java + ├── WasmosComponentInstanceAdapter.java + ├── WasmosMarshalling.java # WIT-shape carriers + JSON codec + ├── ext/ + │ ├── WasmosAsyncExtension.java # invokeAsync + futures/streams + │ └── WitErrorContextException.java + └── jni/ + ├── WasmosNative.java # native method decls + └── WasmosNativeLoader.java # extracts dylib from jar +``` + +## Testing + +`mvn test -pl wasmos-provider` runs the Java suite (62 tests) plus +the Rust unit tests (19). Includes an end-to-end test that loads +`~/git/wasmos/tests/e2e-fixtures/composed_wasmtime.wasm` — assumeTrue- +skipped if the fixture path doesn't exist. The wasmos-composed component +returns 42 from its `run` export. diff --git a/wasmos-provider/native/Cargo.lock b/wasmos-provider/native/Cargo.lock new file mode 100644 index 0000000..947d0ad --- /dev/null +++ b/wasmos-provider/native/Cargo.lock @@ -0,0 +1,3932 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-fs-ext" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78e5a3368ae89b7cb68186411452b4b9fac8b41be9c19bf3f47c2d2c8e36e6b" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes 3.0.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "cap-primitives" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.19", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0435c6939d6ef218bf0a3063094f2cc5c46f7a44a88f81cf932da8d3922ff8" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aca52511cc21a2c9c927aef493d11c8087bdc5483249a3d93aee41e81b8bd4d1" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6867e99f068454b0c81c50b807af88aa80aaf383105e2be26675d81fc4278466" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf66017aed971344230199ad35ca01c955cf824a0e65fed5240452d2a302445" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "933b2e5237665bf9367a34ad2100eed70c94b2796586cb09d9ba11693e061667" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322e3ca0603683cc17f0c808559c0fb85ad52b0f20ace0478bdd4ba2fa1e9678" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287ab88211d049178dc8dda243e2db176919d8b29db605c0efc7c6d76ebea0f7" + +[[package]] +name = "cranelift-control" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11438f9becf9a1e1e061bf76a33241b941a07ed01a489e9dad5fa79dc3624e9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584ecabe1462593a9cba9efbf9022e0987421ddb548989b455ab8bdd444b743c" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34249873145213a12106d5fd8a98a9dc9e90e16525e2337b2907874d1e7e9ee7" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f1c1dc4d6b4bc4f0be12b08cee0e59f7610287827aa62590f8e9ec6f45dff4" + +[[package]] +name = "cranelift-native" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d9e4f6d9a3777b1ffa818000b4a6d7afd1b8787af71a9c58d94baef9076ced" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.134.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4155f8a1afdef96a0272c67804926eee4ef35f9708ab22ad6878d4c9543df596" + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.19", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "object_store" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354792e39fa5f0009e47623cf8b15b099bf9a652fa55c6f817fe28ac84fea50" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64", + "bytes", + "chrono", + "crc-fast", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body-util", + "humantime", + "hyper", + "itertools 0.15.0", + "md-5", + "nix", + "parking_lot", + "percent-encoding", + "quick-xml", + "rand 0.10.2", + "reqwest 0.13.4", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror 2.0.19", + "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6064e52588328c1275d690baad99f8d4b3fdd870736966f1a63cfcbb20560b6" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e832980317bfc7f8c8538ffc9c69b82912f3e33ec70f7c855d521654c70fa62" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1", + "thiserror 2.0.19", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59b710751a35d54732a63851cdfacbfb2266b7160ce174faf269d3f4e84e31b" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wat", +] + +[[package]] +name = "wasm-continuity" +version = "0.1.0" +dependencies = [ + "blake3", + "serde", + "serde_json", + "thiserror 2.0.19", + "uuid", +] + +[[package]] +name = "wasm-continuity-driver" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.19", + "wasm-continuity", + "wasm-continuity-fencing", +] + +[[package]] +name = "wasm-continuity-fencing" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.19", + "wasm-continuity", +] + +[[package]] +name = "wasm-continuity-kv" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.19", + "wasm-continuity", + "wasm-continuity-kv-backend", +] + +[[package]] +name = "wasm-continuity-kv-backend" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror 2.0.19", +] + +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" +dependencies = [ + "leb128fmt", + "wasmparser 0.254.0", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmcm-cm-host-rust" +version = "0.0.1" +dependencies = [ + "anyhow", + "wasmcm-host-rust", + "wasmtime", +] + +[[package]] +name = "wasmcm-host-rust" +version = "0.0.1" +dependencies = [ + "wasmtime", +] + +[[package]] +name = "wasmos-bootstrap-log" +version = "0.1.0" + +[[package]] +name = "wasmos-capability" +version = "0.1.0" + +[[package]] +name = "wasmos-graph" +version = "0.1.0" +dependencies = [ + "wasmos-capability", + "wasmos-machine", +] + +[[package]] +name = "wasmos-machine" +version = "0.1.0" +dependencies = [ + "wasmos-bootstrap-log", +] + +[[package]] +name = "wasmos-provider-native" +version = "0.1.0" +dependencies = [ + "anyhow", + "jni 0.21.1", + "serde", + "serde_json", + "tokio", + "wasmos-runtime", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "wasmos-resolver" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "wasmos-capability", + "wasmos-graph", + "wasmos-machine", +] + +[[package]] +name = "wasmos-runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "futures", + "futures-core", + "futures-util", + "log", + "object_store", + "reqwest 0.12.28", + "serde_json", + "tokio", + "tungstenite", + "uuid", + "wasm-continuity", + "wasm-continuity-driver", + "wasm-continuity-fencing", + "wasm-continuity-kv", + "wasm-continuity-kv-backend", + "wasmcm-cm-host-rust", + "wasmcm-host-rust", + "wasmos-resolver", + "wasmparser 0.253.0", + "wasmtime", + "wasmtime-wasi", + "wat", +] + +[[package]] +name = "wasmparser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.253.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19db11f87d2486580e1e8b6f494c54df7e0566b87d0b599db843c24019667339" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasmtime" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d9f263ccb212017dae1d109b9997f218a7675db1e9a7bedf07ae1643322bf3" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser", +] + +[[package]] +name = "wasmtime-environ" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9915e3d90c36a16e0fec8be746ee87e21cf0d4a017ecf09c49b7d1cc6f31b2b6" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7c25436e1602aad2de80bfb3f586dcdfb6fe426d5b17cba37fabf9a1714d32" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b627ea698db36fb9110aa77868467f1d23791a76a88a433cb6a43e1242073b77" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f45bf943cff1d2f1976bac6a241ca4ecaf29a6efd5f1ad89e7722439fa5c392f" + +[[package]] +name = "wasmtime-internal-core" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea33e1f9685be82f90223bebcf2ffefb6b596ea3d82a3c42384527b9c6f5b447" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1ce104719932a48dbdfec0cc292c96494b0bfe764946e45fbfd3340e3a68f9" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.19", + "wasmparser 0.252.0", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976e4b71b9c0da8a9a7bb8d5fa0abedc4da879b397111fd91ece73be524fe621" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f60ecba3497cfc26284903c2e79023fd5ea991b95b0e8dafe994fd9fca4c4e" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fae6decb025ae6d12f3391b36deb62a6838d45b18864f796d84bced722ceac0d" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0982122d9470d3d72ed7aa9ed91e69538ae44c1494cd22d78fec7846b248a940" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "363cf88a80e6ed13311d0d34d9bcd02a2e07d98113b994ff41ed386a206f498e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ff41bb1656cbeb7befc42b8a7078b30a5f8393a0c8bca36a6cec94360c1a16" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1750b8e5e5d5b1ee3c0eb602d2c49d6a37a36e9996eaf0708b48ac92b62b06" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-std", + "cfg-if", + "futures", + "io-lifetimes 3.0.1", + "rand 0.10.2", + "rustix", + "thiserror 2.0.19", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54777d3afeaf3f32cc444df661d9cb4fd57067971e183c2b1f5251f33b939e68" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "254.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.254.0", +] + +[[package]] +name = "wat" +version = "1.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c" +dependencies = [ + "wast 254.0.0", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wiggle" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a23c2dc694dfafb35beb542e172d7c256488912cd84902828f8661723ad0eb7" +dependencies = [ + "bitflags", + "thiserror 2.0.19", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fe48e5e455e827339d8e65de602cc7839d241bd7063be1fc235e35581788ad" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "47.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb520c20bed78e5a311d4c9ad883ac3b6d070617d07a19fbe5dc0b408c501b4b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-parser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.252.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/wasmos-provider/native/Cargo.toml b/wasmos-provider/native/Cargo.toml new file mode 100644 index 0000000..9e988fe --- /dev/null +++ b/wasmos-provider/native/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "wasmos-provider-native" +version = "0.1.0" +edition = "2021" +description = "JNI wrapper crate for wasmos-runtime, backing the webassembly4j wasmos-provider" +license = "Apache-2.0" + +[lib] +# webassembly4j's Java layer loads the resulting dylib via System.load() at +# ServiceLoader-discovery time; a cdylib gives us the plain shared object with +# the right #[no_mangle] extern "C" symbols on macOS / Linux / Windows. +name = "wasmos_provider" +crate-type = ["cdylib"] + +[dependencies] +# wasmos-runtime dep — REQUIRES a co-checked-out ~/git/wasmos sibling +# tree AND sibling checkouts of ~/git/wasm-cm and ~/git/wasm-continuity +# because wasmos-runtime's own Cargo.toml declares path deps to those +# siblings (../wasm-cm/hosts/rust, ../wasm-continuity/crates/...). +# +# A git-dep pinned to tegmentum/wasmos main fails to resolve at build +# time — cargo fetches wasmos but the sibling `../wasm-cm/...` and +# `../wasm-continuity/...` paths don't exist in the fetched tree. +# Same problem would hit crates.io publication. +# +# Unblocking external / CI builds needs one of: +# 1. wasmos + wasm-cm + wasm-continuity publish versioned crates. +# 2. wasmos restructures to git-dep (or workspace-internal) its +# siblings — no more `path = "..//..."`. +# 3. Fork wasmos into a git-dep-clean tree we control. +# +# Until then this crate only builds from a developer's checkout that +# mirrors the tegmentum working-tree layout: +# +# ~/git/webassembly4j (this repo) +# ~/git/wasmos (path dep target below) +# ~/git/wasm-cm (wasmos's sibling path dep) +# ~/git/wasm-continuity (wasmos's sibling path dep) +# +# See wasmos-provider/README.md for the full build-setup story. +wasmos-runtime = { path = "/Users/zacharywhitley/git/wasmos" } + +# Must match wasmos-runtime's wasmtime pin (47.0.2). Using anything else +# would compile-fail against wasmos-runtime's public wasmtime types +# (Engine / Component / Linker / Store). +wasmtime = { version = "47.0.2", features = ["component-model-bytes"] } +wasmtime-wasi = "47.0.2" + +# instantiate_async / call_async require a Tokio runtime; we drive one per +# call inside the JNI shim rather than expecting the JVM caller to. +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + +# JNI bindings. Version tracks wasmtime4j-native's pin (0.21 is Java 8+ +# compatible and matches wasmtime4j-native/Cargo.toml at the time of this +# scaffold). +jni = "0.21" + +# Better panic diagnostics when the wrapper crashes on a JVM caller. +anyhow = "1.0" + +# JSON marshalling for two-way Val <-> Java-object conversion across the +# JNI boundary. Modelled on wasmtime4j-native's concurrent_call_json module — +# the "trade compile speed for invoke correctness now, swap in binary later" +# strategy. serde + serde_json are already deep transitive deps via wasmtime +# so the compile-cost delta is zero. +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[profile.release] +opt-level = 3 +lto = false +codegen-units = 16 diff --git a/wasmos-provider/native/src/lib.rs b/wasmos-provider/native/src/lib.rs new file mode 100644 index 0000000..2aad418 --- /dev/null +++ b/wasmos-provider/native/src/lib.rs @@ -0,0 +1,2552 @@ +//! JNI wrapper around `wasmos-runtime` — the Rust crate at +//! `~/git/wasmos` that layers wasmos-shaped WIT-typed host bindings +//! (host:wasmtime, host:bootstrap, host:caps, host:grants, +//! wasmos:host/*) on top of an internal wasmtime linkage. +//! +//! # Surface +//! +//! Engine / component / instance lifecycle mirrors the wasmtime4j +//! bindings 1:1 as opaque `long` handles. The invoke path has two +//! flavours: +//! +//! * `instanceInvokeReturningI32` — nullary fast path for the demo +//! (`guest-demo-portable`'s `run() -> i32`). Skips JSON round-trip. +//! * `instanceInvokeJson` — general path: Java hands over a JSON blob +//! of typed `JsonVal` argument values, Rust converts to +//! `wasmtime::component::Val`, calls the function, converts results +//! back into JSON. The JSON strategy is the one wasmtime4j-native +//! uses for its concurrent-call path — cheap-and-cheerful, easy to +//! evolve. A binary marshaller can replace it later without touching +//! the Java-side API. +//! * `instanceInvokeJsonReturningBytes` — same as `instanceInvokeJson` +//! but returns the first `list` result as a raw jbyteArray, +//! dodging the base64 detour on the JSON path. +//! +//! Instantiation has two flavours: +//! +//! * `componentInstantiate` — default WASI ctx, no limits, no epoch. +//! Kept for the MVP demo path. +//! * `componentInstantiateWithConfig` — accepts optional WASI settings +//! (args / env / preopens / stdio inheritance / http toggle) plus +//! optional `maxMemoryBytes` and `epochDeadline`. Handles the +//! `LinkingContext.wasiContext()` + `ComponentConfig` plumbing. +//! +//! # Concurrency +//! +//! wasmos-runtime's `add_all_to_linker` uses async wasmtime imports +//! (see the "Async linker" doc comment on `caps::wasi_p2` in wasmos +//! ~/git/wasmos/src/lib.rs), so instantiation and invocation must +//! run inside a Tokio runtime. A multi-thread runtime is created +//! once per engine and reused for every `block_on` on that engine's +//! components / instances. +//! +//! # Error handling +//! +//! Every entry point catches `anyhow::Error`s and throws a Java +//! `WebAssemblyException`. Rust panics in this crate would be a bug +//! (we take input from Java-side validated shims), not a normal +//! path — we do NOT `catch_unwind` on the JNI boundary because +//! `&mut JNIEnv` isn't `UnwindSafe`. A panic would abort the JVM; +//! any code path that could plausibly panic should return a Result +//! instead. + +#![allow(non_snake_case)] +#![allow(clippy::missing_safety_doc)] + +use std::collections::HashMap; +use std::sync::Mutex; + +use jni::objects::{JByteArray, JClass, JObjectArray, JString}; +use jni::sys::{jboolean, jbyteArray, jint, jlong, jstring}; +use jni::JNIEnv; + +use wasmtime::component::{Component, FutureAny, Linker, ResourceAny, StreamAny, Val}; +use wasmtime::{AsContextMut, Config, Engine, Store}; +use wasmos_runtime::{caps, HostState}; +use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder}; + +use serde::{Deserialize, Serialize}; + +// ----------------------------------------------------------------------------- +// Handle wrappers +// ----------------------------------------------------------------------------- +// Each Java `long` handle is a `Box::into_raw(Box::new(_))` pointer. We hand +// the JVM the pointer as an opaque `jlong`; ownership returns to Rust via the +// matching `*Close` entry point which reconstructs the Box and drops it. + +/// Owned engine + persistent Tokio runtime. The runtime is created eagerly at +/// engine creation time so subsequent instantiate / invoke calls can `block_on` +/// with no per-call setup cost — and, more importantly, so async wasi:p2 +/// timers/clocks see a live runtime for their whole lifetime. +/// +/// The engine has `epoch_interruption(true)` set so +/// `componentInstantiateWithConfig` can wire an epoch deadline without needing +/// a second engine — the flag is a no-op for callers that don't set a deadline. +struct EngineHandle { + engine: Engine, + runtime: tokio::runtime::Runtime, +} + +/// Owned component + a back-pointer to the engine (needed at instantiate +/// time to build a fresh Linker + Store). +struct ComponentHandle { + engine_ptr: *mut EngineHandle, + component: Component, +} + +/// Owned component instance + its Store. The store lives here so +/// subsequent invoke calls have the store to thread through wasmtime's +/// call_async. Also carries the engine back-pointer so invoke can +/// grab the same Tokio runtime that instantiated it. +/// +/// `resources` is a per-instance registry that parks `ResourceAny` values +/// returned from guest calls. Java receives an opaque `WitResource{ tableId, +/// typeName, owned }` — the wasm ResourceAny stays in Rust; only its slot id +/// crosses the JNI boundary. On the way back in, we look up the parked value +/// and hand it back to wasmtime. `ResourceAny` is `Copy`, so lookups are +/// cheap; no reference-counting needed. +struct InstanceHandle { + engine_ptr: *mut EngineHandle, + store: Mutex>, + instance: wasmtime::component::Instance, + resources: Mutex, + /// Per-instance registry for `FutureAny` values returned by guest calls. + /// Slot ids are handed to Java as `long`s (see `JsonVal::Future.table_id`); + /// monotonic, never reused (same policy as `ResourceRegistry`). See + /// `FutureRegistry` docs for lifecycle details, and the module-level + /// "wasmtime Val::Future API gap" note for what awaiting looks like + /// today (spoiler: type-erased polling isn't in the public API). + futures: Mutex, + /// Per-instance registry for `StreamAny` values returned by guest calls. + /// Mirrors `FutureRegistry` exactly — `StreamAny`'s public API is + /// structurally identical to `FutureAny`'s in wasmtime 47 (Clone, Debug, + /// `try_into_stream_reader::` requires compile-time `T`, and `close(store)` + /// for disposal). Park + close + pass-in work; host-side dynamic reading + /// is the same API gap as futures. See `StreamRegistry` for docs. + streams: Mutex, + /// Per-instance registry for `ErrorContextAny` values returned by guest + /// calls or received on `result<_, error-context>` err branches. Uses the + /// same parking-lot shape as the other registries so pass-back into a + /// guest is possible even though wasmtime 47 exposes no `close`/`drop` on + /// `ErrorContextAny` (see the FIXME on + /// `wasmtime::component::ErrorContextAny`). The parked value is only ever + /// released when the instance handle is dropped, or when Java explicitly + /// evicts it via `errorContextClose`. + error_contexts: Mutex, +} + +/// Per-instance parking lot for `ResourceAny` values. Slot ids are handed to +/// Java as `long`s (see `JsonVal::Resource.table_id`). Ids are monotonically +/// increasing; we never reuse a freed slot so the "stale handle after +/// take-owned" case surfaces as a clean "unknown resource id" error rather +/// than an aliased hit on a newly parked resource. +/// +/// Ownership semantics — driven by the `owned` bit on the way IN from Java: +/// * `owned=true` → the Java caller is transferring the resource to the +/// guest (matches WIT `own`). We remove the entry; subsequent uses of +/// that WitResource fail. +/// * `owned=false` → the Java caller is lending the resource for the length +/// of this call (matches WIT `borrow`). The entry stays. +/// +/// On the way OUT from a guest return, we park with the ResourceAny's own +/// `owned()` flag reported back to Java verbatim. +#[derive(Default)] +struct ResourceRegistry { + next_id: u64, + entries: HashMap, +} + +impl ResourceRegistry { + /// Park a `ResourceAny` and hand out a fresh slot id. Ids monotonically + /// increase; freed slots are never reused (see struct docs). + fn park(&mut self, any: ResourceAny) -> u64 { + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .expect("wasmos-provider: resource id counter overflow"); + self.entries.insert(id, any); + id + } + + /// Fetch (borrow-shaped) — returns a copy but keeps the entry so the same + /// WitResource can be lent multiple times. + fn peek(&self, id: u64) -> Option { + self.entries.get(&id).copied() + } + + /// Fetch and remove (own-shaped) — the entry is consumed; subsequent + /// lookups against this id will miss. + fn take(&mut self, id: u64) -> Option { + self.entries.remove(&id) + } +} + +/// Per-instance parking lot for `FutureAny` values. Mirrors `ResourceRegistry` +/// exactly — monotonic u64 ids, no reuse, take-vs-peek semantics driven by +/// the Java caller's intent. See the module-level docs for how this interacts +/// with the wasmtime 47 `FutureAny` public API (short version: return-direction +/// parking works; pass-in direction works; dynamic *awaiting* a `FutureAny` +/// with a runtime-known payload type is a wasmtime public-API gap — the +/// `FutureAny::try_into_future_reader::` path requires `T` at compile time +/// and `FutureConsumer::Item` is an associated type, so there's no +/// type-erased await surface today). +#[derive(Default)] +struct FutureRegistry { + next_id: u64, + entries: HashMap, +} + +impl FutureRegistry { + fn park(&mut self, any: FutureAny) -> u64 { + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .expect("wasmos-provider: future id counter overflow"); + self.entries.insert(id, any); + id + } + + /// Borrow-shaped fetch. `FutureAny: Clone` (wasmtime derives it) so we + /// can hand a copy to a guest call while keeping the entry parked, which + /// matches the "read end" semantics — the same `FutureAny` may in + /// principle be lowered into more than one call before being closed. + /// + /// Currently unused by production code (there's no borrow-shaped Future + /// arm in `to_val` — the JsonVal::Future decode always takes; see the + /// enum docs for why); kept for parity with `ResourceRegistry::peek` and + /// exercised by the test suite. Suppress dead-code warning accordingly. + #[allow(dead_code)] + fn peek(&self, id: u64) -> Option { + self.entries.get(&id).cloned() + } + + /// Own-shaped fetch. Removes the entry; subsequent lookups miss. + fn take(&mut self, id: u64) -> Option { + self.entries.remove(&id) + } +} + +/// Per-instance parking lot for `StreamAny` values. Mirrors `FutureRegistry` +/// exactly — monotonic u64 ids, no reuse, take-vs-peek semantics driven by +/// the Java caller's intent. See the module-level docs for how this interacts +/// with the wasmtime 47 `StreamAny` public API (short version: same story as +/// `FutureAny` — park and close and pass-in are wired; dynamic host-side +/// *reading* of a `StreamAny` with a runtime-known payload type is a wasmtime +/// public-API gap because `StreamAny::try_into_stream_reader::` requires +/// `T` at compile time). +#[derive(Default)] +struct StreamRegistry { + next_id: u64, + entries: HashMap, +} + +impl StreamRegistry { + fn park(&mut self, any: StreamAny) -> u64 { + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .expect("wasmos-provider: stream id counter overflow"); + self.entries.insert(id, any); + id + } + + /// Borrow-shaped fetch. `StreamAny: Clone`; see `FutureRegistry::peek` + /// docs for symmetry rationale. Currently unused by production code + /// (there's no borrow-shaped Stream arm in `to_val`), kept for parity + /// with `FutureRegistry` and exercised by tests. + #[allow(dead_code)] + fn peek(&self, id: u64) -> Option { + self.entries.get(&id).cloned() + } + + /// Own-shaped fetch. Removes the entry; subsequent lookups miss. + fn take(&mut self, id: u64) -> Option { + self.entries.remove(&id) + } +} + +/// Per-instance parking lot for `error-context` handles. Mirrors the shape +/// of the other registries but differs on lifecycle: wasmtime 47's +/// `ErrorContextAny` is a placeholder (see `FIXME(#11161)` on the wasmtime +/// side) with `pub(crate) u32` internals and no publicly-defined `close` or +/// `drop`. It is also not publicly re-exported at the `wasmtime::component` +/// module boundary — its `pub` visibility only reaches as far as being usable +/// through the `Val::ErrorContext(...)` enum variant. To sidestep the naming +/// problem we park the whole `Val::ErrorContext(any)` value; `Val: Clone` +/// preserves the parking-lot copy semantics without needing to name the +/// inner type. +/// +/// Eviction is Java-driven only (`errorContextClose`) or automatic on +/// instance handle drop. On pass-in, the parked `Val` is cloned back +/// verbatim, so wasmtime sees the original ErrorContextAny handle. +#[derive(Default)] +struct ErrorContextRegistry { + next_id: u64, + entries: HashMap, +} + +impl ErrorContextRegistry { + /// Park a `Val::ErrorContext(any)` value. The debug caller-passed value + /// MUST be an `ErrorContext`-shaped `Val`; other variants would still + /// park but round-trip incoherently on pass-in — the type is asserted + /// upstream in `from_val`. + fn park(&mut self, ec: Val) -> u64 { + debug_assert!( + matches!(&ec, Val::ErrorContext(_)), + "ErrorContextRegistry::park requires a Val::ErrorContext, got {ec:?}" + ); + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .expect("wasmos-provider: error-context id counter overflow"); + self.entries.insert(id, ec); + id + } + + #[allow(dead_code)] + fn peek(&self, id: u64) -> Option { + self.entries.get(&id).cloned() + } + + fn take(&mut self, id: u64) -> Option { + self.entries.remove(&id) + } +} + +// ----------------------------------------------------------------------------- +// Error helpers +// ----------------------------------------------------------------------------- + +const EXCEPTION_CLASS: &str = "ai/tegmentum/webassembly4j/api/exception/WebAssemblyException"; + +/// Throw a `WebAssemblyException` with the given message. Ignores JNI errors +/// (the JVM will already be in an exception-pending state on any failure here). +fn throw(env: &mut JNIEnv, msg: &str) { + let _ = env.throw_new(EXCEPTION_CLASS, msg); +} + +/// Ergonomic helper — translate an `anyhow::Result` into a JNI return +/// value: on `Ok`, return the value; on `Err`, throw +/// `WebAssemblyException` and return the caller-supplied sentinel. +fn or_throw(env: &mut JNIEnv, result: anyhow::Result, sentinel: T) -> T { + match result { + Ok(v) => v, + Err(e) => { + throw(env, &format!("wasmos-provider: {:#}", e)); + sentinel + } + } +} + +/// Read a jstring into a `String`, translating any JNI error into `anyhow`. +fn jstring_to_string(env: &mut JNIEnv, s: &JString) -> anyhow::Result { + env.get_string(s) + .map(|jni_str| jni_str.into()) + .map_err(|e| anyhow::anyhow!("get_string: {e}")) +} + +/// Read a JObjectArray of jstrings into `Vec`. Skips null entries +/// (returns them as empty strings — callers pass tightly-packed arrays). +fn jstringarray_to_vec(env: &mut JNIEnv, arr: &JObjectArray) -> anyhow::Result> { + let len = env + .get_array_length(arr) + .map_err(|e| anyhow::anyhow!("get_array_length: {e}"))?; + let mut out = Vec::with_capacity(len as usize); + for i in 0..len { + let elem = env + .get_object_array_element(arr, i) + .map_err(|e| anyhow::anyhow!("get_object_array_element[{i}]: {e}"))?; + if elem.is_null() { + out.push(String::new()); + continue; + } + let js: JString = elem.into(); + out.push(jstring_to_string(env, &js)?); + } + Ok(out) +} + +// ----------------------------------------------------------------------------- +// Exported-function lookup helpers +// ----------------------------------------------------------------------------- + +/// Resolve a callable `Func` from an `Instance` by name, supporting both +/// top-level exports and nested-in-interface exports. +/// +/// * If `name` contains a `#`, split on the FIRST `#` — the left half is +/// the exported interface name (e.g. `"tegmentum:counter/counter-api@0.1.0"`) +/// and the right half is the item name within that interface (e.g. +/// `"[constructor]counter"` or `"[method]counter.get"`). +/// Wasmtime's `Instance::get_func` doesn't descend into exported instances +/// on its own; we chain two `get_export_index` calls to reach the func. +/// * Otherwise, look up `name` directly at the top level — the existing +/// plain-function path. +/// +/// Returns `None` if `name` resolves to something that isn't a lifted +/// component function (e.g. a nested instance, a type export, or unknown). +fn resolve_func( + instance: &wasmtime::component::Instance, + store: &mut Store, + name: &str, +) -> Option { + if let Some(hash_at) = name.find('#') { + let iface_name = &name[..hash_at]; + let item_name = &name[hash_at + 1..]; + let iface_idx = instance.get_export_index(&mut *store, None, iface_name)?; + let func_idx = instance.get_export_index(&mut *store, Some(&iface_idx), item_name)?; + instance.get_func(&mut *store, &func_idx) + } else { + instance.get_func(&mut *store, name) + } +} + +// ----------------------------------------------------------------------------- +// JSON Val marshalling +// ----------------------------------------------------------------------------- +// Design mirrors wasmtime4j-native's `concurrent_call_json::JsonVal`. Keeps +// tag names short (`t` / `v`) to trim payload size — the Java side has a +// matching Jackson-free hand-rolled parser/writer so JVM adds no runtime dep. +// +// `Bytes` is a fast-path variant that base64-encodes a `list` payload — +// avoids the O(n) JsonVal-per-byte round-trip for large binary blobs. On the +// return path we detect `Val::List` whose elements are all `Val::U8` and +// emit the `Bytes` variant automatically. + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "v")] +enum JsonVal { + Bool(bool), + S8(i8), + S16(i16), + S32(i32), + S64(i64), + U8(u8), + U16(u16), + U32(u32), + U64(u64), + F32(f32), + F64(f64), + Char(char), + String(String), + /// `list` fast path — base64-encoded bytes. Reduces both wire size + /// and per-element boxing on both sides. + Bytes(String), + List(Vec), + Option(Option>), + /// Result-shaped value. `is_ok` picks the branch; the corresponding side + /// carries the payload (or `None` for `result<_,_>` with a unit branch). + Result { + is_ok: bool, + #[serde(skip_serializing_if = "Option::is_none", default)] + ok: Option>, + #[serde(skip_serializing_if = "Option::is_none", default)] + err: Option>, + }, + Record(Vec<(String, JsonVal)>), + Tuple(Vec), + Variant { + discriminant: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + value: Option>, + }, + Enum(String), + Flags(Vec), + /// Distinct from `Record`: keys can be arbitrary `Val`s, not just + /// field-name strings. Wire form is a flat array of `[key_json, + /// value_json]` two-tuples — identical layout to what wasmtime's + /// `Val::Map(Vec<(Val, Val)>)` implies. The Java side uses a + /// `WitMap` explicit wrapper so callers opt in intentionally rather + /// than have every `Map` guessed one way or another. + Map(Vec<(JsonVal, JsonVal)>), + /// Component-model `own` / `borrow` handle. The `ResourceAny` lives + /// Rust-side in `InstanceHandle::resources`; Java only ever sees + /// `table_id` (opaque slot number), `type_name` (opaque + /// `ResourceType`-derived identity, useful only for equality), and + /// `owned` (drives take-vs-peek on the way back in — see + /// `ResourceRegistry` docs). This is the wasmos-provider's `Val::Resource` + /// story; no other JsonVal shape crosses a "handle" over the wire. + Resource { + table_id: u64, + type_name: String, + owned: bool, + }, + /// Component-model `future` handle. Same parking-lot pattern as + /// `Resource`: the `FutureAny` lives Rust-side in + /// `InstanceHandle::futures`; Java only ever sees `table_id` (opaque + /// slot number) and `type_name` (opaque Debug rendering of the + /// `FutureAny`, useful only for equality-adjacent introspection). + /// + /// There is no `owned` bit here — wasmtime 47's `FutureAny: Clone` and + /// the read/write ends are already separated at the type level (this is + /// the read end); "borrow" as a distinct concept doesn't apply. The + /// Java carrier consumes the slot on pass-in by default (parity with + /// how a future handle typically flows exactly once through a guest + /// import); explicit disposal is via `futureClose`. + /// + /// See the `FutureRegistry` docs for the wasmtime API gap that + /// currently blocks a dynamic host-side *await* on this handle. + Future { + table_id: u64, + type_name: String, + }, + /// Component-model `stream` handle. Same parking-lot pattern as + /// `Future`: the `StreamAny` lives Rust-side in + /// `InstanceHandle::streams`; Java only ever sees `table_id` (opaque + /// slot number) and `type_name` (opaque Debug rendering of the + /// `StreamAny`, useful only for equality-adjacent introspection). + /// + /// Same lifecycle contract as `Future`: the decode arm always takes + /// (a stream handle's read end flows through a guest import exactly + /// once). Explicit disposal is via `streamClose`. Host-side dynamic + /// reading of the stream's items has the identical wasmtime API gap + /// as `Future` — `StreamAny::try_into_stream_reader::` is + /// compile-time typed. + Stream { + table_id: u64, + type_name: String, + }, + /// Component-model `error-context` handle. Parked in + /// `InstanceHandle::error_contexts` and surfaced to Java as an opaque + /// slot id plus the wasmtime-derived numeric `rep` (parsed out of + /// `ErrorContextAny`'s Debug rendering — the underlying `u32` is + /// `pub(crate)` so Debug is the only public window). The `rep` value + /// is useful for equality-adjacent identification; it has no other + /// user-facing meaning today because wasmtime 47's `ErrorContextAny` + /// is a placeholder (see wasmtime's `FIXME(#11161)`). + /// + /// There is no wasmtime-defined dispose on `ErrorContextAny`, so + /// `errorContextClose` on the Java side is a pure Rust-side registry + /// eviction — it drops the parked handle so the slot id becomes + /// unknown for later pass-in attempts. + ErrorContext { + table_id: u64, + rep: u32, + }, +} + +/// Base64 alphabet — hand-rolled to avoid pulling in the `base64` crate for a +/// couple of `encode` / `decode` calls. Standard alphabet with `=` padding. +const B64_ALPHABET: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +fn b64_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4); + let mut i = 0; + while i + 3 <= bytes.len() { + let b0 = bytes[i]; + let b1 = bytes[i + 1]; + let b2 = bytes[i + 2]; + out.push(B64_ALPHABET[(b0 >> 2) as usize] as char); + out.push(B64_ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char); + out.push(B64_ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char); + out.push(B64_ALPHABET[(b2 & 0x3f) as usize] as char); + i += 3; + } + let rem = bytes.len() - i; + if rem == 1 { + let b0 = bytes[i]; + out.push(B64_ALPHABET[(b0 >> 2) as usize] as char); + out.push(B64_ALPHABET[((b0 & 0x03) << 4) as usize] as char); + out.push('='); + out.push('='); + } else if rem == 2 { + let b0 = bytes[i]; + let b1 = bytes[i + 1]; + out.push(B64_ALPHABET[(b0 >> 2) as usize] as char); + out.push(B64_ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char); + out.push(B64_ALPHABET[((b1 & 0x0f) << 2) as usize] as char); + out.push('='); + } + out +} + +fn b64_decode(s: &str) -> anyhow::Result> { + // Build a reverse-lookup table lazily. Cheap; done once per decode. + let mut rev = [255u8; 256]; + for (i, &c) in B64_ALPHABET.iter().enumerate() { + rev[c as usize] = i as u8; + } + let bytes = s.as_bytes(); + if bytes.len() % 4 != 0 { + anyhow::bail!("base64: length not a multiple of 4"); + } + let mut out = Vec::with_capacity(bytes.len() / 4 * 3); + let mut i = 0; + while i < bytes.len() { + let c0 = bytes[i]; + let c1 = bytes[i + 1]; + let c2 = bytes[i + 2]; + let c3 = bytes[i + 3]; + let v0 = rev[c0 as usize]; + let v1 = rev[c1 as usize]; + if v0 == 255 || v1 == 255 { + anyhow::bail!("base64: invalid char at offset {i}"); + } + out.push((v0 << 2) | (v1 >> 4)); + if c2 != b'=' { + let v2 = rev[c2 as usize]; + if v2 == 255 { + anyhow::bail!("base64: invalid char at offset {}", i + 2); + } + out.push(((v1 & 0x0f) << 4) | (v2 >> 2)); + if c3 != b'=' { + let v3 = rev[c3 as usize]; + if v3 == 255 { + anyhow::bail!("base64: invalid char at offset {}", i + 3); + } + out.push(((v2 & 0x03) << 6) | v3); + } + } + i += 4; + } + Ok(out) +} + +impl JsonVal { + /// Decode a JsonVal into a wasmtime `Val`, using `resources`, + /// `futures`, `streams`, and `error_contexts` to resolve any parked + /// slot ids back into `ResourceAny` / `FutureAny` / `StreamAny` / + /// `ErrorContextAny` handles. All registries are passed as `&mut` + /// because the pass-in arms consume (take) entries — Resource by way + /// of the caller-supplied `owned` bit, Future/Stream unconditionally + /// (a future/stream read end typically flows through a guest import + /// exactly once), ErrorContext unconditionally (Clone would let us + /// peek but no known WIT shape wants to hold the same handle twice). + fn to_val( + &self, + resources: &mut ResourceRegistry, + futures: &mut FutureRegistry, + streams: &mut StreamRegistry, + error_contexts: &mut ErrorContextRegistry, + ) -> anyhow::Result { + Ok(match self { + JsonVal::Bool(v) => Val::Bool(*v), + JsonVal::S8(v) => Val::S8(*v), + JsonVal::S16(v) => Val::S16(*v), + JsonVal::S32(v) => Val::S32(*v), + JsonVal::S64(v) => Val::S64(*v), + JsonVal::U8(v) => Val::U8(*v), + JsonVal::U16(v) => Val::U16(*v), + JsonVal::U32(v) => Val::U32(*v), + JsonVal::U64(v) => Val::U64(*v), + JsonVal::F32(v) => Val::Float32(*v), + JsonVal::F64(v) => Val::Float64(*v), + JsonVal::Char(v) => Val::Char(*v), + JsonVal::String(v) => Val::String(v.clone()), + JsonVal::Bytes(b64) => { + let bytes = b64_decode(b64)?; + Val::List(bytes.into_iter().map(Val::U8).collect()) + } + JsonVal::List(items) => Val::List( + items + .iter() + .map(|v| v.to_val(resources, futures, streams, error_contexts)) + .collect::>>()?, + ), + JsonVal::Option(opt) => Val::Option( + opt.as_ref() + .map(|v| v.to_val(resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?, + ), + JsonVal::Result { is_ok, ok, err } => { + if *is_ok { + Val::Result(Ok(ok + .as_ref() + .map(|v| v.to_val(resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?)) + } else { + Val::Result(Err(err + .as_ref() + .map(|v| v.to_val(resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?)) + } + } + JsonVal::Record(fields) => Val::Record( + fields + .iter() + .map(|(name, jv)| jv.to_val(resources, futures, streams, error_contexts).map(|v| (name.clone(), v))) + .collect::>>()?, + ), + JsonVal::Tuple(items) => Val::Tuple( + items + .iter() + .map(|v| v.to_val(resources, futures, streams, error_contexts)) + .collect::>>()?, + ), + JsonVal::Variant { discriminant, value } => Val::Variant( + discriminant.clone(), + value + .as_ref() + .map(|v| v.to_val(resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?, + ), + JsonVal::Enum(v) => Val::Enum(v.clone()), + JsonVal::Flags(v) => Val::Flags(v.clone()), + JsonVal::Map(pairs) => Val::Map( + pairs + .iter() + .map(|(k, v)| Ok((k.to_val(resources, futures, streams, error_contexts)?, + v.to_val(resources, futures, streams, error_contexts)?))) + .collect::>>()?, + ), + JsonVal::Resource { table_id, owned, .. } => { + // Java-side `owned` bit drives the take-vs-peek choice — the + // WIT signature the guest expects is what the caller is + // targeting, and it dictates whether we transfer ownership + // (own) or lend a copy (borrow). `type_name` is not + // consulted here; it's Java-side introspection only. + let any = if *owned { + resources.take(*table_id).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown resource id {} (already consumed \ + as own or never parked)", + table_id + ) + })? + } else { + resources.peek(*table_id).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown resource id {} (never parked or \ + already consumed by prior own transfer)", + table_id + ) + })? + }; + Val::Resource(any) + } + JsonVal::Future { table_id, .. } => { + // Futures unconditionally take: a `future` handle + // typically flows through a guest import exactly once, and + // the read end (which is what we're holding) is single-shot + // by nature. Peek-style repeated pass-in isn't a shape the + // component model surfaces, so we don't try to model it. + // Callers that need to keep the handle after passing it in + // shouldn't; there's no "borrow>" in the wire + // model. + let any = futures.take(*table_id).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown future id {} (already consumed or \ + never parked)", + table_id + ) + })?; + Val::Future(any) + } + JsonVal::Stream { table_id, .. } => { + // Streams unconditionally take, same rationale as Future: + // the read end of a `stream` is single-shot at the + // wire-model level. + let any = streams.take(*table_id).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown stream id {} (already consumed or \ + never parked)", + table_id + ) + })?; + Val::Stream(any) + } + JsonVal::ErrorContext { table_id, .. } => { + // ErrorContext unconditionally takes on pass-in. There is no + // borrow shape for `error-context` in the WIT wire model — + // an error-context handle is terminal by convention. If a + // caller needs to re-park after peeking they can round-trip + // via `from_val` on the returned Val, but pass-in through + // JSON always consumes to keep parity with Future/Stream. + // + // The registry stores the whole `Val::ErrorContext(any)` + // (see ErrorContextRegistry docs for why); pass-in hands + // that Val straight back to wasmtime. + error_contexts.take(*table_id).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown error-context id {} (already consumed \ + or never parked)", + table_id + ) + })? + } + }) + } + + /// Encode a wasmtime `Val` into a JsonVal, parking any `Resource(any)` / + /// `Future(any)` / `Stream(any)` / `ErrorContext(any)` values in their + /// respective registries and emitting a slot id in the JSON. + fn from_val( + val: &Val, + resources: &mut ResourceRegistry, + futures: &mut FutureRegistry, + streams: &mut StreamRegistry, + error_contexts: &mut ErrorContextRegistry, + ) -> anyhow::Result { + Ok(match val { + Val::Bool(v) => JsonVal::Bool(*v), + Val::S8(v) => JsonVal::S8(*v), + Val::S16(v) => JsonVal::S16(*v), + Val::S32(v) => JsonVal::S32(*v), + Val::S64(v) => JsonVal::S64(*v), + Val::U8(v) => JsonVal::U8(*v), + Val::U16(v) => JsonVal::U16(*v), + Val::U32(v) => JsonVal::U32(*v), + Val::U64(v) => JsonVal::U64(*v), + Val::Float32(v) => JsonVal::F32(*v), + Val::Float64(v) => JsonVal::F64(*v), + Val::Char(v) => JsonVal::Char(*v), + Val::String(v) => JsonVal::String(v.clone()), + Val::List(items) => { + // Fast-path: if all items are U8, encode as `Bytes(base64)`. + if !items.is_empty() && items.iter().all(|v| matches!(v, Val::U8(_))) { + let raw: Vec = items + .iter() + .map(|v| match v { + Val::U8(b) => *b, + _ => unreachable!("guarded by matches! above"), + }) + .collect(); + JsonVal::Bytes(b64_encode(&raw)) + } else { + JsonVal::List( + items + .iter() + .map(|v| JsonVal::from_val(v, resources, futures, streams, error_contexts)) + .collect::>>()?, + ) + } + } + Val::Option(opt) => JsonVal::Option( + opt.as_ref() + .map(|v| JsonVal::from_val(v, resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?, + ), + Val::Result(res) => match res { + Ok(ok_val) => JsonVal::Result { + is_ok: true, + ok: ok_val + .as_ref() + .map(|v| JsonVal::from_val(v, resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?, + err: None, + }, + Err(err_val) => JsonVal::Result { + is_ok: false, + ok: None, + err: err_val + .as_ref() + .map(|v| JsonVal::from_val(v, resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?, + }, + }, + Val::Record(fields) => JsonVal::Record( + fields + .iter() + .map(|(name, v)| { + JsonVal::from_val(v, resources, futures, streams, error_contexts).map(|jv| (name.clone(), jv)) + }) + .collect::>>()?, + ), + Val::Tuple(items) => JsonVal::Tuple( + items + .iter() + .map(|v| JsonVal::from_val(v, resources, futures, streams, error_contexts)) + .collect::>>()?, + ), + Val::Variant(discriminant, payload) => JsonVal::Variant { + discriminant: discriminant.clone(), + value: payload + .as_ref() + .map(|v| JsonVal::from_val(v, resources, futures, streams, error_contexts).map(Box::new)) + .transpose()?, + }, + Val::Enum(discriminant) => JsonVal::Enum(discriminant.clone()), + Val::Flags(v) => JsonVal::Flags(v.clone()), + Val::Map(pairs) => JsonVal::Map( + pairs + .iter() + .map(|(k, v)| { + Ok(( + JsonVal::from_val(k, resources, futures, streams, error_contexts)?, + JsonVal::from_val(v, resources, futures, streams, error_contexts)?, + )) + }) + .collect::>>()?, + ), + Val::Resource(any) => { + // Park the ResourceAny; hand Java a stable u64 slot id and + // the ResourceType's Debug rendering as opaque type identity. + // The Debug format for a guest ResourceType includes the + // component's internal resource index; that's stable within + // an instance's lifetime, which is exactly the window a + // WitResource is valid for. + let owned = any.owned(); + let type_name = format!("{:?}", any.ty()); + let table_id = resources.park(*any); + JsonVal::Resource { + table_id, + type_name, + owned, + } + } + Val::Future(any) => { + // Park the FutureAny; hand Java an opaque u64 slot id + the + // FutureAny's Debug rendering as a type-identity hint (the + // payload type is buried in a private `PayloadType` + // field, so Debug is the widest window wasmtime 47's public + // API gives us). See the module-level "wasmtime API gap" + // note on why Java cannot yet host-side *await* this handle. + let type_name = format!("{:?}", any); + // `FutureAny: Clone` — safe to clone into the registry + // rather than consuming the borrow. Java has to explicitly + // dispose via `futureClose`; the registry keeps the parked + // copy alive until then (or until the instance handle + // itself is dropped, which drops the whole registry). + let table_id = futures.park(any.clone()); + JsonVal::Future { + table_id, + type_name, + } + } + Val::Stream(any) => { + // Same pattern as Val::Future. `StreamAny: Clone` in + // wasmtime 47, so we clone into the registry and keep the + // borrow intact. Debug format is the only public window on + // the payload type — same "opaque hint" role as Future. + let type_name = format!("{:?}", any); + let table_id = streams.park(any.clone()); + JsonVal::Stream { + table_id, + type_name, + } + } + Val::ErrorContext(any) => { + // Park the ErrorContext-shaped Val. Its Debug form is + // `ErrorContextAny(N)` where N is the pub(crate) u32 rep; + // parse the number out so Java gets a plain numeric hint + // instead of the debug string wrapper. If the format ever + // changes (unlikely — it's a plain tuple struct) fall back + // to 0. The rep is only meant for equality-adjacent + // introspection; wasmtime's `FIXME(#11161)` documents that + // the type has no other operations at 47.0.2. + // + // We store the whole `Val` in the registry (not the + // `ErrorContextAny`) because `ErrorContextAny` isn't + // re-exported at the `wasmtime::component` module boundary + // and can't be named as a HashMap value type from outside + // the wasmtime crate. Cloning the enclosing Val preserves + // the underlying handle. + let dbg = format!("{:?}", any); + let rep = dbg + .strip_prefix("ErrorContextAny(") + .and_then(|s| s.strip_suffix(')')) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + let table_id = error_contexts.park(val.clone()); + JsonVal::ErrorContext { table_id, rep } + } + }) + } +} + +// ----------------------------------------------------------------------------- +// Engine lifecycle +// ----------------------------------------------------------------------------- + +/// Create a wasmos-friendly wasmtime engine. `wasm_component_model = true` is +/// mandatory to load composed components; `epoch_interruption = true` is +/// mandatory so per-instance epoch deadlines are cheap to opt into via +/// `componentInstantiateWithConfig` (the flag is a no-op for callers who +/// don't set a deadline). +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_engineCreate( + mut env: JNIEnv, + _class: JClass, +) -> jlong { + let result: anyhow::Result = (|| { + let mut config = Config::new(); + // NB: wasmtime 47 removed `Config::async_support` — async is now + // available on any engine that turned on `component-model` (which + // it does by default). Explicit call intentionally omitted; leaving + // this comment as a breadcrumb for any future wasmtime downgrade. + config.wasm_component_model(true); + config.epoch_interruption(true); + // Enable fuel bookkeeping globally so per-instance fuel limits are + // cheap to opt into via `componentInstantiateWithConfig`. No-op + // performance hit for callers that never set fuel — wasmtime only + // decrements on backedges when the store has a nonzero fuel budget. + config.consume_fuel(true); + let engine = Engine::new(&config)?; + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + let handle = Box::new(EngineHandle { engine, runtime }); + Ok(Box::into_raw(handle) as jlong) + })(); + or_throw(&mut env, result, 0) +} + +/// Drop the engine and its Tokio runtime. Safe to call once with a +/// nonzero handle produced by `engineCreate`; no-op on zero. +/// +/// # Safety +/// The handle MUST originate from `engineCreate`. Passing anything else is UB. +#[no_mangle] +pub unsafe extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_engineClose( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) { + if handle == 0 { + return; + } + let _ = Box::from_raw(handle as *mut EngineHandle); +} + +/// Increment the engine's epoch counter — call from a Java timer thread when +/// an instance was instantiated with `epochDeadline`. Callers that never set +/// a deadline can ignore this. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_engineIncrementEpoch( + mut env: JNIEnv, + _class: JClass, + handle: jlong, +) { + if handle == 0 { + throw(&mut env, "wasmos-provider: engine handle is null"); + return; + } + let engine = unsafe { &(*(handle as *mut EngineHandle)).engine }; + engine.increment_epoch(); +} + +// ----------------------------------------------------------------------------- +// Component lifecycle +// ----------------------------------------------------------------------------- + +/// Compile a Component from raw bytes. Returns a handle usable by +/// `componentInstantiate`. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentLoad( + mut env: JNIEnv, + _class: JClass, + engine_handle: jlong, + bytes: JByteArray, +) -> jlong { + // Convert the byte array up-front so the fallible-body closure below + // doesn't need to hold `&mut env`. + let raw_result: anyhow::Result> = env + .convert_byte_array(&bytes) + .map_err(|e| anyhow::anyhow!("convert_byte_array: {e}")); + let raw = match raw_result { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return 0; + } + }; + + let result: anyhow::Result = (|| { + if engine_handle == 0 { + anyhow::bail!("engine handle is null"); + } + let engine_ptr = engine_handle as *mut EngineHandle; + // SAFETY: We never move the Box; we only borrow through the raw pointer. + // Java is responsible for keeping the engine alive as long as any component + // handle derived from it is live (documented on WasmosEngineAdapter). + let engine = unsafe { &(*engine_ptr).engine }; + let component = Component::from_binary(engine, &raw)?; + let handle = Box::new(ComponentHandle { + engine_ptr, + component, + }); + Ok(Box::into_raw(handle) as jlong) + })(); + or_throw(&mut env, result, 0) +} + +/// Drop a component handle. See `engineClose` safety note. +#[no_mangle] +pub unsafe extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentClose( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) { + if handle == 0 { + return; + } + let _ = Box::from_raw(handle as *mut ComponentHandle); +} + +/// Serialize a compiled Component into its cached AOT byte representation. +/// The returned blob can later be handed to `componentDeserialize` on an +/// engine with a compatible wasmtime config to skip the compile step — +/// the intended use case is warm restarts / hot-reload where the same +/// component gets loaded repeatedly across process launches. +/// +/// Byte compatibility is tied to the wasmtime version, engine config, and +/// target ISA; a deserialize on a different setup will fail cleanly rather +/// than silently produce wrong code. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentSerialize( + mut env: JNIEnv, + _class: JClass, + component_handle: jlong, +) -> jbyteArray { + let result: anyhow::Result> = (|| { + if component_handle == 0 { + anyhow::bail!("component handle is null"); + } + let ch = component_handle as *mut ComponentHandle; + let component = unsafe { &(*ch).component }; + component + .serialize() + .map_err(|e| anyhow::anyhow!("Component::serialize: {e}")) + })(); + match result { + Ok(bytes) => match env.byte_array_from_slice(&bytes) { + Ok(a) => a.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: byte_array_from_slice: {e}")); + std::ptr::null_mut() + } + }, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + std::ptr::null_mut() + } + } +} + +/// Deserialize a component previously produced by `componentSerialize`. +/// Returns a fresh ComponentHandle bound to the caller's engine. +/// +/// # Safety +/// +/// `Component::deserialize` is `unsafe` in wasmtime because it trusts the +/// input bytes to be a well-formed AOT image that the current engine +/// produced (a hostile / corrupted image can cause UB when its +/// pre-computed function pointers are called). The trust boundary belongs +/// to the JVM caller: they're responsible for only feeding this bytes +/// they got from a prior `componentSerialize` call on this or a +/// compatible engine. We don't try to validate here — the wasmtime API +/// puts the safety contract in the caller's hands and we relay it. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentDeserialize( + mut env: JNIEnv, + _class: JClass, + engine_handle: jlong, + bytes: JByteArray, +) -> jlong { + let raw_result: anyhow::Result> = env + .convert_byte_array(&bytes) + .map_err(|e| anyhow::anyhow!("convert_byte_array: {e}")); + let raw = match raw_result { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return 0; + } + }; + + let result: anyhow::Result = (|| { + if engine_handle == 0 { + anyhow::bail!("engine handle is null"); + } + let engine_ptr = engine_handle as *mut EngineHandle; + let engine = unsafe { &(*engine_ptr).engine }; + // SAFETY: The JVM caller warranted the input bytes came from a + // prior componentSerialize call against a compatible engine. Any + // failure will surface as an anyhow error caught below. + let component = unsafe { + Component::deserialize(engine, &raw) + .map_err(|e| anyhow::anyhow!("Component::deserialize: {e}"))? + }; + let handle = Box::new(ComponentHandle { + engine_ptr, + component, + }); + Ok(Box::into_raw(handle) as jlong) + })(); + or_throw(&mut env, result, 0) +} + +// ----------------------------------------------------------------------------- +// Instance lifecycle + invoke +// ----------------------------------------------------------------------------- + +/// Instantiate a component with the full wasmos-runtime host surface +/// (`add_all_to_linker`) plus `caps::wasi_p2` for the composed +/// adapter-wasmtime's wasi imports. +/// +/// A fresh `Store` is created per instantiation with a default +/// `HostState::new()` — empty preview-2 WasiCtx, no memory limits, no epoch +/// deadline. For a configured WASI ctx / memory-limits / epoch-deadline path, +/// use `componentInstantiateWithConfig`. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentInstantiate( + mut env: JNIEnv, + _class: JClass, + component_handle: jlong, +) -> jlong { + let result: anyhow::Result = (|| { + instantiate_with(component_handle, HostState::new(), LimitsBag::none()) + })(); + or_throw(&mut env, result, 0) +} + +/// Same as `componentInstantiate` but with an optional configured WASI ctx +/// (built from the argsArr / envKeysArr / envValsArr / preopen arrays) plus +/// a `LimitsBag` struct of outer-store limits. Any `-1` numeric arg means +/// "unset". +/// +/// Bit-flags in `flags`: 0x01=inheritStdin, 0x02=inheritStdout, +/// 0x04=inheritStderr, 0x08=hasWasiContext (if 0, all wasi args are ignored +/// and default HostState::new() is used). +/// +/// Limit args (all `-1` = unset): +/// * `max_memory_bytes` — per-linear-memory upper bound in bytes +/// * `epoch_deadline` — trap when epoch counter passes this value +/// * `fuel_limit` — set the store's initial fuel budget (requires +/// `Config::consume_fuel(true)`, which the engine enables globally) +/// * `max_table_elements` — per-table element cap +/// * `max_instances` — max component-model instances per store +/// * `max_tables` — max tables per store +/// * `max_memories` — max linear memories per store +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentInstantiateWithConfig( + mut env: JNIEnv, + _class: JClass, + component_handle: jlong, + flags: jint, + args_arr: JObjectArray, + env_keys_arr: JObjectArray, + env_vals_arr: JObjectArray, + preopen_hosts_arr: JObjectArray, + preopen_guests_arr: JObjectArray, + preopen_writable_arr: JByteArray, + max_memory_bytes: jlong, + epoch_deadline: jlong, + fuel_limit: jlong, + max_table_elements: jlong, + max_instances: jlong, + max_tables: jlong, + max_memories: jlong, +) -> jlong { + let has_wasi = (flags & 0x08) != 0; + + // Pull all jni-scoped inputs up front so the fallible body doesn't hold + // multiple simultaneous borrows of &mut env. + let host_state_result: anyhow::Result = (|| { + if !has_wasi { + return Ok(HostState::new()); + } + let inherit_stdin = (flags & 0x01) != 0; + let inherit_stdout = (flags & 0x02) != 0; + let inherit_stderr = (flags & 0x04) != 0; + let args = if args_arr.is_null() { + Vec::new() + } else { + jstringarray_to_vec(&mut env, &args_arr)? + }; + let env_keys = if env_keys_arr.is_null() { + Vec::new() + } else { + jstringarray_to_vec(&mut env, &env_keys_arr)? + }; + let env_vals = if env_vals_arr.is_null() { + Vec::new() + } else { + jstringarray_to_vec(&mut env, &env_vals_arr)? + }; + if env_keys.len() != env_vals.len() { + anyhow::bail!( + "envKeys / envVals length mismatch: {} vs {}", + env_keys.len(), + env_vals.len() + ); + } + let preopen_hosts = if preopen_hosts_arr.is_null() { + Vec::new() + } else { + jstringarray_to_vec(&mut env, &preopen_hosts_arr)? + }; + let preopen_guests = if preopen_guests_arr.is_null() { + Vec::new() + } else { + jstringarray_to_vec(&mut env, &preopen_guests_arr)? + }; + let preopen_writable: Vec = if preopen_writable_arr.is_null() { + Vec::new() + } else { + env.convert_byte_array(&preopen_writable_arr) + .map_err(|e| anyhow::anyhow!("convert_byte_array(preopen_writable): {e}"))? + }; + if preopen_hosts.len() != preopen_guests.len() + || preopen_hosts.len() != preopen_writable.len() + { + anyhow::bail!( + "preopen array length mismatch: hosts={} guests={} writable={}", + preopen_hosts.len(), + preopen_guests.len(), + preopen_writable.len() + ); + } + + let mut b = WasiCtxBuilder::new(); + if inherit_stdin { + b.inherit_stdin(); + } + if inherit_stdout { + b.inherit_stdout(); + } + if inherit_stderr { + b.inherit_stderr(); + } + for arg in &args { + b.arg(arg); + } + for (k, v) in env_keys.iter().zip(env_vals.iter()) { + b.env(k, v); + } + for i in 0..preopen_hosts.len() { + let host_path = &preopen_hosts[i]; + let guest_path = &preopen_guests[i]; + let writable = preopen_writable[i] != 0; + let (dp, fp) = if writable { + (DirPerms::all(), FilePerms::all()) + } else { + (DirPerms::READ, FilePerms::READ) + }; + b.preopened_dir(host_path, guest_path, dp, fp) + .map_err(|e| anyhow::anyhow!("preopened_dir({host_path} -> {guest_path}): {e}"))?; + } + Ok(HostState::with_wasi(b.build())) + })(); + let host_state = match host_state_result { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return 0; + } + }; + + let limits = LimitsBag { + max_memory_bytes: neg_to_none(max_memory_bytes).map(|v| v as usize), + epoch_deadline: neg_to_none(epoch_deadline).map(|v| v as u64), + fuel_limit: neg_to_none(fuel_limit).map(|v| v as u64), + max_table_elements: neg_to_none(max_table_elements).map(|v| v as usize), + max_instances: neg_to_none(max_instances).map(|v| v as usize), + max_tables: neg_to_none(max_tables).map(|v| v as usize), + max_memories: neg_to_none(max_memories).map(|v| v as usize), + }; + + let result: anyhow::Result = (|| { + instantiate_with(component_handle, host_state, limits) + })(); + or_throw(&mut env, result, 0) +} + +/// `-1` (or any negative) → None; otherwise Some(v). Centralised so the +/// "-1 means unset" convention is threaded through one place, not every +/// call site. +fn neg_to_none(v: jlong) -> Option { + if v < 0 { + None + } else { + Some(v) + } +} + +/// Outer-store limits bundle. Every field is optional — unset means "use +/// wasmtime's default" for that dimension. Fields map 1:1 onto +/// `StoreLimitsBuilder` setters (except `fuel_limit`, which goes through +/// `Store::set_fuel`, and `epoch_deadline`, which goes through +/// `Store::set_epoch_deadline` + `epoch_deadline_trap`). +struct LimitsBag { + max_memory_bytes: Option, + epoch_deadline: Option, + fuel_limit: Option, + max_table_elements: Option, + max_instances: Option, + max_tables: Option, + max_memories: Option, +} + +impl LimitsBag { + fn none() -> Self { + Self { + max_memory_bytes: None, + epoch_deadline: None, + fuel_limit: None, + max_table_elements: None, + max_instances: None, + max_tables: None, + max_memories: None, + } + } + + /// True if any of the StoreLimits fields (memory / tables / memories / + /// instances / table-elements) is set — used to decide whether to + /// install `Store::limiter`. `fuel_limit` and `epoch_deadline` go + /// through separate Store APIs so they don't count. + fn wants_limiter(&self) -> bool { + self.max_memory_bytes.is_some() + || self.max_table_elements.is_some() + || self.max_instances.is_some() + || self.max_tables.is_some() + || self.max_memories.is_some() + } +} + +/// Shared instantiate path. `host_state` supplies any WASI context; +/// `limits` bundles the optional outer-store limits (memory / fuel / +/// epoch / instances / tables / memories / table-elements). +fn instantiate_with( + component_handle: jlong, + mut host_state: HostState, + limits: LimitsBag, +) -> anyhow::Result { + if component_handle == 0 { + anyhow::bail!("component handle is null"); + } + let ch = component_handle as *mut ComponentHandle; + let engine_ptr = unsafe { (*ch).engine_ptr }; + let engine = unsafe { &(*engine_ptr).engine }; + let runtime = unsafe { &(*engine_ptr).runtime }; + let component_ref = unsafe { &(*ch).component }; + + // Configure outer_limits BEFORE building the store — the limiter closure + // borrows the field mutably. Only build a fresh StoreLimits when at + // least one dimension was requested; otherwise leave HostState's + // default (wide-open, matches pre-limits behaviour). + if limits.wants_limiter() { + let mut b = wasmtime::StoreLimitsBuilder::new(); + if let Some(v) = limits.max_memory_bytes { + b = b.memory_size(v); + } + if let Some(v) = limits.max_table_elements { + b = b.table_elements(v); + } + if let Some(v) = limits.max_instances { + b = b.instances(v); + } + if let Some(v) = limits.max_tables { + b = b.tables(v); + } + if let Some(v) = limits.max_memories { + b = b.memories(v); + } + host_state.outer_limits = b.build(); + } + + let mut linker: Linker = Linker::new(engine); + wasmos_runtime::add_all_to_linker(&mut linker, |s| s) + .map_err(|e| anyhow::anyhow!("add_all_to_linker: {e}"))?; + caps::wasi_p2(&mut linker).map_err(|e| anyhow::anyhow!("caps::wasi_p2: {e}"))?; + + let mut store = Store::new(engine, host_state); + if limits.wants_limiter() { + store.limiter(|state| &mut state.outer_limits); + } + // The engine has epoch_interruption(true) globally so per-instance + // deadlines are cheap to opt into. Without this, the store's default + // deadline of 0 traps immediately on the first backedge check — even + // for callers that never asked for epoch interruption. Setting to + // u64::MAX effectively disables the trap for unopted-in callers. + if let Some(deadline) = limits.epoch_deadline { + // Trap when the deadline is exceeded — the simplest sane default; + // yield-based deadlines can be added later behind a separate flag. + store.epoch_deadline_trap(); + store.set_epoch_deadline(deadline); + } else { + store.set_epoch_deadline(u64::MAX); + } + // Fuel: engine has `consume_fuel(true)` globally so `set_fuel` is a + // cheap opt-in per store. Wasmtime starts fuel-enabled stores at 0 + // fuel — which would trap the very first backedge for callers who + // never asked for a fuel budget. Setting to u64::MAX by default + // makes the "no fuel limit" case work identically to a fuel-disabled + // engine from the caller's perspective; the small per-backedge + // instrumentation cost is the price for lazy opt-in. + let fuel = limits.fuel_limit.unwrap_or(u64::MAX); + store + .set_fuel(fuel) + .map_err(|e| anyhow::anyhow!("set_fuel({fuel}): {e}"))?; + + let instance = + runtime.block_on(async { linker.instantiate_async(&mut store, component_ref).await })?; + + let handle = Box::new(InstanceHandle { + engine_ptr, + store: Mutex::new(store), + instance, + resources: Mutex::new(ResourceRegistry::default()), + futures: Mutex::new(FutureRegistry::default()), + streams: Mutex::new(StreamRegistry::default()), + error_contexts: Mutex::new(ErrorContextRegistry::default()), + }); + Ok(Box::into_raw(handle) as jlong) +} + +/// Drop an instance handle (which drops its Store). +#[no_mangle] +pub unsafe extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_instanceClose( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) { + if handle == 0 { + return; + } + let _ = Box::from_raw(handle as *mut InstanceHandle); +} + +// ----------------------------------------------------------------------------- +// Future handle lifecycle +// ----------------------------------------------------------------------------- +// See the `FutureRegistry` docs for the parking-lot design; these two entry +// points are the Java-visible surface for disposing of a parked `FutureAny` +// and (attempting to) await its value. Await is a wasmtime-47 public-API gap +// (see below); close is fully functional. + +/// Evict a parked `FutureAny` from the instance's future registry and call +/// `FutureAny::close(store)` on it so the write end sees the read end as +/// dropped. Idempotent-adjacent: passing an unknown id is an error (matches +/// the Resource contract), passing a null instance handle throws. +/// +/// Safe to call on a WitFuture that was already consumed by a guest pass-in +/// only in the sense that the entry will have been removed — the error path +/// will surface as "unknown future id". Java-side callers should keep a +/// close call in a `finally` block when they own a WitFuture but never pass +/// it back to a guest. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_futureClose( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + future_id: jlong, +) { + let result: anyhow::Result<()> = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let store_mutex = unsafe { &(*ih).store }; + let futures_mutex = unsafe { &(*ih).futures }; + let mut store = store_mutex.lock().unwrap(); + let mut futures = futures_mutex.lock().unwrap(); + let mut any = futures.take(future_id as u64).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown future id {} (already closed, transferred \ + to guest, or never parked)", + future_id + ) + })?; + // FutureAny::close signals the writer that the read end is gone. + // Swallow a "future already closed" error only implicitly (via the + // take path above); a live registry hit that fails to close is a + // real error worth surfacing. + any.close(store.as_context_mut()) + .map_err(|e| anyhow::anyhow!("FutureAny::close: {e}"))?; + Ok(()) + })(); + or_throw(&mut env, result, ()); +} + +/// Attempt to await a parked `FutureAny` and hand back the resolved value +/// as a JSON blob (same schema as `instanceInvokeJson` returns). +/// +/// # wasmtime 47 API gap +/// +/// This method currently ALWAYS throws `WebAssemblyException` with a clear +/// "wasmtime API gap" message. The `FutureAny` public API in wasmtime 47 +/// only exposes: +/// +/// * `try_into_future_reader::()` — requires `T: ComponentType` at +/// COMPILE time. Not usable for our runtime-typed marshalling layer. +/// * `try_from_future_reader::(store, reader)` — same static-T constraint, +/// only useful on the way into a guest. +/// * `close(&mut self, store)` — plain disposal, no value extraction. +/// +/// The Rust-side lift path (`FutureReader::pipe`) also needs +/// `T: Lift + 'static` compile-time. There is no `FutureAny::poll_dynamic` / +/// `FutureAny::await_val` shape in the public surface today. When one +/// lands upstream (see wasmtime issues around dynamically-typed +/// component-model concurrency), this method's body swaps in the real +/// implementation without touching the JNI signature. +/// +/// The Java caller receives a `CompletableFuture` that completes +/// exceptionally — the async layer handles that shape naturally. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_futureAwait( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + future_id: jlong, +) -> jstring { + let result: anyhow::Result = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let futures_mutex = unsafe { &(*ih).futures }; + let futures = futures_mutex.lock().unwrap(); + if !futures.entries.contains_key(&(future_id as u64)) { + anyhow::bail!( + "wasmos-provider: unknown future id {} (already awaited/closed, \ + transferred to guest, or never parked)", + future_id + ); + } + // Registry hit — the handle is real. But we can't resolve it: the + // wasmtime 47 public API has no dynamic-payload-type await surface. + // See this function's doc comment for the API gap details. + anyhow::bail!( + "wasmos-provider: FutureAny host-side await is not supported by wasmtime 47's \ + public API (FutureAny::try_into_future_reader requires a compile-time T, \ + and FutureConsumer::Item is an associated type — there is no type-erased \ + await path). The WitFuture can still be closed via WasmosAsyncExtension.closeFuture \ + or passed back into a guest import that consumes it. This is tracked as slice-1's \ + known limitation; a real await lands when wasmtime exposes a dynamic API." + ) + })(); + match result { + Ok(s) => match env.new_string(s) { + Ok(js) => js.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: new_string: {e}")); + std::ptr::null_mut() + } + }, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + std::ptr::null_mut() + } + } +} + +// ----------------------------------------------------------------------------- +// Stream handle lifecycle +// ----------------------------------------------------------------------------- +// Structural mirror of the Future entries above. Same wasmtime 47 API gap — +// there is no dynamic-payload-type read surface on `StreamAny` (only the +// compile-time-typed `try_into_stream_reader::`), so `streamRead` is +// intentionally a stub-that-throws matching `futureAwait`. Close and pass-in +// are fully wired. + +/// Evict a parked `StreamAny` from the instance's stream registry and call +/// `StreamAny::close(store)` on it so the write end sees the read end as +/// dropped. Symmetric with `futureClose`. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_streamClose( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + stream_id: jlong, +) { + let result: anyhow::Result<()> = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let store_mutex = unsafe { &(*ih).store }; + let streams_mutex = unsafe { &(*ih).streams }; + let mut store = store_mutex.lock().unwrap(); + let mut streams = streams_mutex.lock().unwrap(); + let mut any = streams.take(stream_id as u64).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown stream id {} (already closed, transferred \ + to guest, or never parked)", + stream_id + ) + })?; + any.close(store.as_context_mut()) + .map_err(|e| anyhow::anyhow!("StreamAny::close: {e}"))?; + Ok(()) + })(); + or_throw(&mut env, result, ()); +} + +/// Attempt to read from a parked `StreamAny`. Currently ALWAYS throws +/// with a clear "wasmtime API gap" message — the same reason `futureAwait` +/// does. Kept as a stable JNI entry so the Java surface is future-proof: +/// when wasmtime exposes a dynamic-payload-type read surface for +/// `StreamAny`, only the Rust body swaps in. +/// +/// # wasmtime 47 API gap +/// +/// `StreamAny`'s public API in wasmtime 47 only exposes +/// `try_into_stream_reader::()` (requires a compile-time `T`) and +/// `close(store)`. There is no `try_read_dynamic` / `poll_next_dynamic` +/// surface, so runtime-typed marshalling cannot lift stream items to +/// `Val`s here. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_streamRead( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + stream_id: jlong, +) -> jstring { + let result: anyhow::Result = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let streams_mutex = unsafe { &(*ih).streams }; + let streams = streams_mutex.lock().unwrap(); + if !streams.entries.contains_key(&(stream_id as u64)) { + anyhow::bail!( + "wasmos-provider: unknown stream id {} (already read/closed, \ + transferred to guest, or never parked)", + stream_id + ); + } + // Registry hit — same API-gap story as `futureAwait`. + anyhow::bail!( + "wasmos-provider: StreamAny host-side read is not supported by wasmtime 47's \ + public API (StreamAny::try_into_stream_reader requires a compile-time T, \ + and there is no type-erased next/poll surface). The WitStream can still be \ + closed via WasmosAsyncExtension.closeStream or passed back into a guest \ + import that consumes it. This is tracked as slice-2's known limitation; a \ + real read lands when wasmtime exposes a dynamic API." + ) + })(); + match result { + Ok(s) => match env.new_string(s) { + Ok(js) => js.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: new_string: {e}")); + std::ptr::null_mut() + } + }, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + std::ptr::null_mut() + } + } +} + +// ----------------------------------------------------------------------------- +// ErrorContext handle lifecycle +// ----------------------------------------------------------------------------- +// wasmtime 47's `ErrorContextAny` is a placeholder (see `FIXME(#11161)` on the +// wasmtime side) — it has no publicly-defined `close`, `drop`, or any accessor +// beyond `Debug`. All we can do is evict from our registry. + +/// Evict a parked `ErrorContextAny` from the instance's registry. Pure +/// Rust-side eviction — wasmtime has no dispose surface on +/// `ErrorContextAny`, so a Java caller who is done with a WitErrorContext +/// simply drops the slot to keep the registry from growing. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_errorContextClose( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + error_context_id: jlong, +) { + let result: anyhow::Result<()> = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let ec_mutex = unsafe { &(*ih).error_contexts }; + let mut ec = ec_mutex.lock().unwrap(); + ec.take(error_context_id as u64).ok_or_else(|| { + anyhow::anyhow!( + "wasmos-provider: unknown error-context id {} (already closed or \ + never parked)", + error_context_id + ) + })?; + Ok(()) + })(); + or_throw(&mut env, result, ()); +} + +/// Invoke an exported function with signature `func() -> i32`. Kept as a +/// nullary fast path for the demo (`guest-demo-portable`'s `run` export) — +/// skips the JSON round-trip that `instanceInvokeJson` incurs. +/// +/// Returns the i32 result; on any error, throws `WebAssemblyException` and +/// returns 0. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_instanceInvokeReturningI32( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + name: JString, +) -> jint { + let fn_name = match jstring_to_string(&mut env, &name) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return 0; + } + }; + + let result: anyhow::Result = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let engine_ptr = unsafe { (*ih).engine_ptr }; + let runtime = unsafe { &(*engine_ptr).runtime }; + + let store_mutex = unsafe { &(*ih).store }; + let instance = unsafe { &(*ih).instance }; + let mut store = store_mutex.lock().unwrap(); + + let typed = instance + .get_typed_func::<(), (i32,)>(&mut *store, &fn_name) + .map_err(|e| anyhow::anyhow!("get_typed_func('{}'): {}", fn_name, e))?; + + let (out,) = runtime.block_on(async { typed.call_async(&mut *store, ()).await })?; + + Ok(out) + })(); + or_throw(&mut env, result, 0) +} + +/// General invoke path — hand over a JSON array of typed argument `JsonVal`s, +/// receive a JSON array of typed result `JsonVal`s. See the `JsonVal` +/// definition above for the schema; the Java-side `WasmosMarshalling` +/// mirrors it. +/// +/// For a `func() -> ()` return, the Rust side returns `"[]"`. On error, +/// throws `WebAssemblyException` and returns null. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_instanceInvokeJson( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + name: JString, + args_json: JString, +) -> jstring { + let fn_name = match jstring_to_string(&mut env, &name) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + let args_raw = match jstring_to_string(&mut env, &args_json) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + + let result: anyhow::Result = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let json_args: Vec = serde_json::from_str(&args_raw) + .map_err(|e| anyhow::anyhow!("deserialize args JSON: {e}"))?; + + let ih = instance_handle as *mut InstanceHandle; + let engine_ptr = unsafe { (*ih).engine_ptr }; + let runtime = unsafe { &(*engine_ptr).runtime }; + let store_mutex = unsafe { &(*ih).store }; + let instance = unsafe { &(*ih).instance }; + let resources_mutex = unsafe { &(*ih).resources }; + let futures_mutex = unsafe { &(*ih).futures }; + let streams_mutex = unsafe { &(*ih).streams }; + let error_contexts_mutex = unsafe { &(*ih).error_contexts }; + let mut store = store_mutex.lock().unwrap(); + let mut resources = resources_mutex.lock().unwrap(); + let mut futures = futures_mutex.lock().unwrap(); + let mut streams = streams_mutex.lock().unwrap(); + let mut error_contexts = error_contexts_mutex.lock().unwrap(); + + // Decode args AFTER acquiring the registry locks — the Resource / + // Future / Stream / ErrorContext arms call take/peek against those + // registries. + let mut params: Vec = Vec::with_capacity(json_args.len()); + for jv in &json_args { + params.push(jv.to_val( + &mut resources, + &mut futures, + &mut streams, + &mut error_contexts, + )?); + } + + let func = resolve_func(instance, &mut store, &fn_name) + .ok_or_else(|| anyhow::anyhow!("no exported function '{}'", fn_name))?; + + // Preallocate results — wasmtime `Func::call_async` needs a slice + // sized to the return arity. Grab that from the func's declared type. + let ty = func.ty(&*store); + let result_arity = ty.results().len(); + let mut results: Vec = vec![Val::Bool(false); result_arity]; + + runtime.block_on(async { + func.call_async(&mut *store, ¶ms, &mut results).await + })?; + // NB: post_return_async is a no-op in wasmtime 47 (per the crate's + // #[deprecated] annotation) — omitted intentionally. + + // Encoding the return path parks any Resource(any) / Future(any) / + // Stream(any) / ErrorContext(any) values in their registries — Java + // sees the fresh slot ids in the JSON blob. + let json_results: Vec = results + .iter() + .map(|v| JsonVal::from_val( + v, + &mut resources, + &mut futures, + &mut streams, + &mut error_contexts, + )) + .collect::>>()?; + Ok(serde_json::to_string(&json_results) + .map_err(|e| anyhow::anyhow!("serialize results JSON: {e}"))?) + })(); + + match result { + Ok(s) => match env.new_string(s) { + Ok(js) => js.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: new_string: {e}")); + std::ptr::null_mut() + } + }, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + std::ptr::null_mut() + } + } +} + +/// Byte-array-returning variant of `instanceInvokeJson`. Callers who know +/// the exported function returns exactly one `list` can skip the JSON +/// detour on the return path — we hand back a raw jbyteArray of the +/// element bytes. Args still go through JSON (rare enough that we don't +/// bother with a fast-path array-in surface). +/// +/// If the return arity isn't 1, or the sole result isn't a `list`, +/// throws `WebAssemblyException`. Returns null on error. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_instanceInvokeJsonReturningBytes( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + name: JString, + args_json: JString, +) -> jbyteArray { + let fn_name = match jstring_to_string(&mut env, &name) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + let args_raw = match jstring_to_string(&mut env, &args_json) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + + let result: anyhow::Result> = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let json_args: Vec = serde_json::from_str(&args_raw) + .map_err(|e| anyhow::anyhow!("deserialize args JSON: {e}"))?; + + let ih = instance_handle as *mut InstanceHandle; + let engine_ptr = unsafe { (*ih).engine_ptr }; + let runtime = unsafe { &(*engine_ptr).runtime }; + let store_mutex = unsafe { &(*ih).store }; + let instance = unsafe { &(*ih).instance }; + let resources_mutex = unsafe { &(*ih).resources }; + let futures_mutex = unsafe { &(*ih).futures }; + let streams_mutex = unsafe { &(*ih).streams }; + let error_contexts_mutex = unsafe { &(*ih).error_contexts }; + let mut store = store_mutex.lock().unwrap(); + let mut resources = resources_mutex.lock().unwrap(); + let mut futures = futures_mutex.lock().unwrap(); + let mut streams = streams_mutex.lock().unwrap(); + let mut error_contexts = error_contexts_mutex.lock().unwrap(); + + // Resolve any Resource / Future / Stream / ErrorContext args after + // grabbing the registry locks. + let mut params: Vec = Vec::with_capacity(json_args.len()); + for jv in &json_args { + params.push(jv.to_val( + &mut resources, + &mut futures, + &mut streams, + &mut error_contexts, + )?); + } + // Result path here is byte-array-only — no need to hold the registry + // borrows after params decode, but keeping them live is cheap and + // future-proofs any list-adjacent variants that might carry + // resource / future / stream / error-context handles alongside the + // bytes. + let _ = &mut *resources; + let _ = &mut *futures; + let _ = &mut *streams; + let _ = &mut *error_contexts; + + let func = resolve_func(instance, &mut store, &fn_name) + .ok_or_else(|| anyhow::anyhow!("no exported function '{}'", fn_name))?; + let ty = func.ty(&*store); + let result_arity = ty.results().len(); + if result_arity != 1 { + anyhow::bail!( + "invokeBytes expects exactly one return value, got {}", + result_arity + ); + } + let mut results: Vec = vec![Val::Bool(false); 1]; + runtime.block_on(async { + func.call_async(&mut *store, ¶ms, &mut results).await + })?; + // post_return_async is a no-op in wasmtime 47 (deprecated). + + match &results[0] { + Val::List(items) => { + let mut out = Vec::with_capacity(items.len()); + for v in items { + match v { + Val::U8(b) => out.push(*b), + other => anyhow::bail!( + "invokeBytes expects list, got list element {:?}", + other + ), + } + } + Ok(out) + } + Val::String(s) => Ok(s.as_bytes().to_vec()), + other => anyhow::bail!( + "invokeBytes expects list or string result, got {:?}", + other + ), + } + })(); + + match result { + Ok(bytes) => match env.byte_array_from_slice(&bytes) { + Ok(a) => a.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: byte_array_from_slice: {e}")); + std::ptr::null_mut() + } + }, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + std::ptr::null_mut() + } + } +} + +/// Return list of exported function names, as a null-separated UTF-8 blob in a +/// jbyteArray. Java splits on 0x00. Keeping it a byte blob avoids per-call +/// jstring allocation loops for what's an occasional metadata read. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_componentExportedFunctionNames( + mut env: JNIEnv, + _class: JClass, + component_handle: jlong, +) -> jbyteArray { + let names_result: anyhow::Result> = (|| { + if component_handle == 0 { + anyhow::bail!("component handle is null"); + } + let ch = component_handle as *mut ComponentHandle; + let engine_ptr = unsafe { (*ch).engine_ptr }; + let engine = unsafe { &(*engine_ptr).engine }; + let component = unsafe { &(*ch).component }; + let component_type = component.component_type(); + let mut names: Vec = Vec::new(); + for (name, item) in component_type.exports(engine) { + if let wasmtime::component::types::ComponentItem::ComponentFunc(_) = item.ty { + names.push(name.to_string()); + } + } + Ok(names) + })(); + let names = match names_result { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + let joined = names.join("\0"); + match env.byte_array_from_slice(joined.as_bytes()) { + Ok(a) => a.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: byte_array_from_slice: {e}")); + std::ptr::null_mut() + } + } +} + +/// Introspect an instance's function signature — returns "argCount:resultCount" +/// as an ASCII byte blob. Used by the Java-side marshalling helper when it +/// needs to size a param list without a prior WIT descriptor. Both counts are +/// jint-sized; negative values would indicate an error but we always throw +/// instead. Returns null on error; jboolean sentinel isn't in scope. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_instanceFunctionArity( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + name: JString, +) -> jbyteArray { + let fn_name = match jstring_to_string(&mut env, &name) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + let result: anyhow::Result<(usize, usize)> = (|| { + if instance_handle == 0 { + anyhow::bail!("instance handle is null"); + } + let ih = instance_handle as *mut InstanceHandle; + let store_mutex = unsafe { &(*ih).store }; + let instance = unsafe { &(*ih).instance }; + let mut store = store_mutex.lock().unwrap(); + let func = resolve_func(instance, &mut store, &fn_name) + .ok_or_else(|| anyhow::anyhow!("no exported function '{}'", fn_name))?; + let ty = func.ty(&*store); + let arity = (ty.params().len(), ty.results().len()); + Ok(arity) + })(); + let (args, rets) = match result { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return std::ptr::null_mut(); + } + }; + let s = format!("{args}:{rets}"); + match env.byte_array_from_slice(s.as_bytes()) { + Ok(a) => a.into_raw(), + Err(e) => { + throw(&mut env, &format!("wasmos-provider: byte_array_from_slice: {e}")); + std::ptr::null_mut() + } + } +} + +/// Report whether an instance exports a given function name. Cheaper than +/// pulling the whole export list when a Java caller only wants a hasFunction +/// probe. Returns JNI_TRUE / JNI_FALSE. Throws on invalid handle. +#[no_mangle] +pub extern "system" fn Java_ai_tegmentum_webassembly4j_provider_wasmos_jni_WasmosNative_instanceHasFunction( + mut env: JNIEnv, + _class: JClass, + instance_handle: jlong, + name: JString, +) -> jboolean { + let fn_name = match jstring_to_string(&mut env, &name) { + Ok(v) => v, + Err(e) => { + throw(&mut env, &format!("wasmos-provider: {:#}", e)); + return 0; + } + }; + if instance_handle == 0 { + throw(&mut env, "wasmos-provider: instance handle is null"); + return 0; + } + let ih = instance_handle as *mut InstanceHandle; + let store_mutex = unsafe { &(*ih).store }; + let instance = unsafe { &(*ih).instance }; + let mut store = store_mutex.lock().unwrap(); + if resolve_func(instance, &mut store, &fn_name).is_some() { + 1 + } else { + 0 + } +} + +// ----------------------------------------------------------------------------- +// Tests — Rust-side smoke tests for the JsonVal marshalling helpers. Confirm +// the on-the-wire schema is stable and round-trips cleanly for every variant +// the Java side is expected to send. +// ----------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn roundtrip(v: Val) { + let mut reg = ResourceRegistry::default(); + let mut futs = FutureRegistry::default(); + let mut strs = StreamRegistry::default(); + let mut ecs = ErrorContextRegistry::default(); + let jv = JsonVal::from_val(&v, &mut reg, &mut futs, &mut strs, &mut ecs) + .expect("from_val"); + let json = serde_json::to_string(&jv).expect("serialize"); + let back: JsonVal = serde_json::from_str(&json).expect("deserialize"); + let v2 = back + .to_val(&mut reg, &mut futs, &mut strs, &mut ecs) + .expect("to_val"); + // Val doesn't implement PartialEq; compare Debug reprs. + assert_eq!(format!("{:?}", v), format!("{:?}", v2)); + } + + #[test] + fn primitives_roundtrip() { + roundtrip(Val::Bool(true)); + roundtrip(Val::S8(-1)); + roundtrip(Val::U8(200)); + roundtrip(Val::S16(-1234)); + roundtrip(Val::U16(60000)); + roundtrip(Val::S32(-42)); + roundtrip(Val::U32(4_000_000_000)); + roundtrip(Val::S64(-1_000_000_000_000)); + roundtrip(Val::U64(u64::MAX)); + roundtrip(Val::Float32(1.5)); + roundtrip(Val::Float64(-2.25)); + roundtrip(Val::Char('Z')); + roundtrip(Val::String("hello".to_string())); + } + + #[test] + fn list_and_bytes_roundtrip() { + // Empty list stays empty list (not Bytes fast path). + roundtrip(Val::List(vec![])); + // All-U8 list picks up the base64 fast path — check the wire form is + // Bytes and it decodes back to Val::List(U8). + let bytes = Val::List(vec![Val::U8(0), Val::U8(1), Val::U8(2), Val::U8(255)]); + let mut reg = ResourceRegistry::default(); + let mut futs = FutureRegistry::default(); + let mut strs = StreamRegistry::default(); + let mut ecs = ErrorContextRegistry::default(); + let jv = JsonVal::from_val(&bytes, &mut reg, &mut futs, &mut strs, &mut ecs).unwrap(); + assert!(matches!(jv, JsonVal::Bytes(_))); + roundtrip(bytes); + // Non-U8 list stays a List. + roundtrip(Val::List(vec![Val::S32(1), Val::S32(2), Val::S32(3)])); + } + + #[test] + fn option_and_result_roundtrip() { + roundtrip(Val::Option(None)); + roundtrip(Val::Option(Some(Box::new(Val::S32(7))))); + roundtrip(Val::Result(Ok(Some(Box::new(Val::String("ok".into())))))); + roundtrip(Val::Result(Err(Some(Box::new(Val::S32(-1)))))); + roundtrip(Val::Result(Ok(None))); + roundtrip(Val::Result(Err(None))); + } + + #[test] + fn record_and_tuple_roundtrip() { + roundtrip(Val::Tuple(vec![Val::S32(1), Val::String("x".into())])); + roundtrip(Val::Record(vec![ + ("name".into(), Val::String("alice".into())), + ("age".into(), Val::U32(30)), + ])); + } + + #[test] + fn variant_and_enum_and_flags_roundtrip() { + roundtrip(Val::Variant("some".into(), Some(Box::new(Val::S32(1))))); + roundtrip(Val::Variant("none".into(), None)); + roundtrip(Val::Enum("red".into())); + roundtrip(Val::Flags(vec!["read".into(), "write".into()])); + } + + #[test] + fn map_roundtrip() { + // Map — keys are non-string so this MUST land as + // Val::Map, not Val::Record. + roundtrip(Val::Map(vec![ + (Val::S32(1), Val::String("one".into())), + (Val::S32(2), Val::String("two".into())), + ])); + // Empty map. + roundtrip(Val::Map(vec![])); + // Map> — checks that inner Bytes fast path + // survives when nested inside a Map value slot. + roundtrip(Val::Map(vec![ + ( + Val::String("a".into()), + Val::List(vec![Val::U8(1), Val::U8(2), Val::U8(3)]), + ), + ( + Val::String("b".into()), + Val::List(vec![Val::U8(255), Val::U8(0)]), + ), + ])); + } + + #[test] + fn resource_json_wire_format_is_stable() { + // We can't easily fabricate a ResourceAny here (its constructor is + // pub(crate)), but the JsonVal::Resource wire schema is what the + // Java side depends on. Round-trip via serde to lock in field + // names — a rename would silently break Java parsing otherwise. + let jv = JsonVal::Resource { + table_id: 42, + type_name: "TestType".into(), + owned: true, + }; + let json = serde_json::to_string(&jv).unwrap(); + assert!( + json.contains("\"Resource\"") && json.contains("\"table_id\":42") + && json.contains("\"type_name\":\"TestType\"") + && json.contains("\"owned\":true"), + "wire format regression: {json}" + ); + let back: JsonVal = serde_json::from_str(&json).unwrap(); + match back { + JsonVal::Resource { + table_id, + type_name, + owned, + } => { + assert_eq!(table_id, 42); + assert_eq!(type_name, "TestType"); + assert!(owned); + } + other => panic!("expected Resource, got {other:?}"), + } + } + + #[test] + fn resource_registry_starts_empty() { + // Baseline invariants for the ResourceRegistry — full park/take + // exercise happens through the JNI path once a live ResourceAny is + // available (needs a real component-model resource). We can't + // fabricate a ResourceAny here because its constructor is + // pub(crate); a zeroed ResourceAny would be UB (invalid + // HostResourceIndex). The full lifecycle is covered end-to-end via + // Java-side round-trip tests plus the E2E-with-component path. + let reg = ResourceRegistry::default(); + assert_eq!(reg.next_id, 0); + assert!(reg.entries.is_empty()); + assert!(reg.peek(0).is_none()); + } + + #[test] + fn resource_to_val_errors_on_unknown_id() { + // The Resource decode arm must fail cleanly when Java sends a + // slot id that isn't parked. Two failure modes: owned=true takes + // (miss on second use); owned=false peeks (miss when never parked). + let mut reg = ResourceRegistry::default(); + let mut futs = FutureRegistry::default(); + let mut strs = StreamRegistry::default(); + let mut ecs = ErrorContextRegistry::default(); + let jv_take = JsonVal::Resource { + table_id: 12345, + type_name: "T".into(), + owned: true, + }; + let err = jv_take + .to_val(&mut reg, &mut futs, &mut strs, &mut ecs) + .unwrap_err() + .to_string(); + assert!( + err.contains("12345") && err.contains("unknown resource id"), + "unexpected error: {err}" + ); + let jv_peek = JsonVal::Resource { + table_id: 999, + type_name: "T".into(), + owned: false, + }; + let err = jv_peek + .to_val(&mut reg, &mut futs, &mut strs, &mut ecs) + .unwrap_err() + .to_string(); + assert!( + err.contains("999") && err.contains("unknown resource id"), + "unexpected error: {err}" + ); + } + + #[test] + fn future_json_wire_format_is_stable() { + // JsonVal::Future wire schema — the Java side keys on the exact + // field names, so a rename here would silently break parsing. + let jv = JsonVal::Future { + table_id: 17, + type_name: "FutureAny { .. }".into(), + }; + let json = serde_json::to_string(&jv).unwrap(); + assert!( + json.contains("\"Future\"") + && json.contains("\"table_id\":17") + && json.contains("\"type_name\":\"FutureAny { .. }\""), + "wire format regression: {json}" + ); + let back: JsonVal = serde_json::from_str(&json).unwrap(); + match back { + JsonVal::Future { table_id, type_name } => { + assert_eq!(table_id, 17); + assert_eq!(type_name, "FutureAny { .. }"); + } + other => panic!("expected Future, got {other:?}"), + } + } + + #[test] + fn future_registry_starts_empty() { + // Same coverage shape as `resource_registry_starts_empty`. + // Fabricating a live FutureAny requires a real component-model + // future value on a live store, so the full park/take exercise + // lives on the Java-side integration path (once a fixture with a + // `future` export exists). + let reg = FutureRegistry::default(); + assert_eq!(reg.next_id, 0); + assert!(reg.entries.is_empty()); + assert!(reg.peek(0).is_none()); + } + + #[test] + fn future_to_val_errors_on_unknown_id() { + // The Future decode arm always takes (there's no borrow shape for + // futures — see the JsonVal::Future doc comment) so a single failure + // mode is enough here. + let mut reg = ResourceRegistry::default(); + let mut futs = FutureRegistry::default(); + let mut strs = StreamRegistry::default(); + let mut ecs = ErrorContextRegistry::default(); + let jv = JsonVal::Future { + table_id: 54321, + type_name: "T".into(), + }; + let err = jv + .to_val(&mut reg, &mut futs, &mut strs, &mut ecs) + .unwrap_err() + .to_string(); + assert!( + err.contains("54321") && err.contains("unknown future id"), + "unexpected error: {err}" + ); + } + + #[test] + fn stream_json_wire_format_is_stable() { + // Mirrors `future_json_wire_format_is_stable` — the Java side keys on + // the exact field names, so a rename here would silently break parsing. + let jv = JsonVal::Stream { + table_id: 21, + type_name: "StreamAny { .. }".into(), + }; + let json = serde_json::to_string(&jv).unwrap(); + assert!( + json.contains("\"Stream\"") + && json.contains("\"table_id\":21") + && json.contains("\"type_name\":\"StreamAny { .. }\""), + "wire format regression: {json}" + ); + let back: JsonVal = serde_json::from_str(&json).unwrap(); + match back { + JsonVal::Stream { table_id, type_name } => { + assert_eq!(table_id, 21); + assert_eq!(type_name, "StreamAny { .. }"); + } + other => panic!("expected Stream, got {other:?}"), + } + } + + #[test] + fn stream_registry_starts_empty() { + // Same coverage shape as `future_registry_starts_empty`; a full + // park/take exercise needs a live StreamAny value, which requires a + // real component-model stream on a live store. + let reg = StreamRegistry::default(); + assert_eq!(reg.next_id, 0); + assert!(reg.entries.is_empty()); + assert!(reg.peek(0).is_none()); + } + + #[test] + fn stream_to_val_errors_on_unknown_id() { + // Stream decode always takes — mirrors the Future contract. + let mut reg = ResourceRegistry::default(); + let mut futs = FutureRegistry::default(); + let mut strs = StreamRegistry::default(); + let mut ecs = ErrorContextRegistry::default(); + let jv = JsonVal::Stream { + table_id: 7777, + type_name: "T".into(), + }; + let err = jv + .to_val(&mut reg, &mut futs, &mut strs, &mut ecs) + .unwrap_err() + .to_string(); + assert!( + err.contains("7777") && err.contains("unknown stream id"), + "unexpected error: {err}" + ); + } + + #[test] + fn error_context_json_wire_format_is_stable() { + // Wire schema lock — the Java side keys on the exact field names + // ("table_id", "rep"). A rename would silently break parsing. + let jv = JsonVal::ErrorContext { + table_id: 3, + rep: 42, + }; + let json = serde_json::to_string(&jv).unwrap(); + assert!( + json.contains("\"ErrorContext\"") + && json.contains("\"table_id\":3") + && json.contains("\"rep\":42"), + "wire format regression: {json}" + ); + let back: JsonVal = serde_json::from_str(&json).unwrap(); + match back { + JsonVal::ErrorContext { table_id, rep } => { + assert_eq!(table_id, 3); + assert_eq!(rep, 42); + } + other => panic!("expected ErrorContext, got {other:?}"), + } + } + + #[test] + fn error_context_registry_starts_empty() { + let reg = ErrorContextRegistry::default(); + assert_eq!(reg.next_id, 0); + assert!(reg.entries.is_empty()); + assert!(reg.peek(0).is_none()); + } + + #[test] + fn error_context_to_val_errors_on_unknown_id() { + let mut reg = ResourceRegistry::default(); + let mut futs = FutureRegistry::default(); + let mut strs = StreamRegistry::default(); + let mut ecs = ErrorContextRegistry::default(); + let jv = JsonVal::ErrorContext { + table_id: 88888, + rep: 0, + }; + let err = jv + .to_val(&mut reg, &mut futs, &mut strs, &mut ecs) + .unwrap_err() + .to_string(); + assert!( + err.contains("88888") && err.contains("unknown error-context id"), + "unexpected error: {err}" + ); + } + + #[test] + fn base64_edge_cases() { + assert_eq!(b64_encode(&[]), ""); + assert_eq!(b64_encode(&[0]), "AA=="); + assert_eq!(b64_encode(&[0, 1]), "AAE="); + assert_eq!(b64_encode(&[0, 1, 2]), "AAEC"); + assert_eq!(b64_decode("AAECAw==").unwrap(), vec![0, 1, 2, 3]); + // Reject non-multiple-of-4 lengths. + assert!(b64_decode("AAA").is_err()); + } +} diff --git a/wasmos-provider/pom.xml b/wasmos-provider/pom.xml new file mode 100644 index 0000000..8f5b656 --- /dev/null +++ b/wasmos-provider/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + + ai.tegmentum.webassembly4j + webassembly4j-parent + 2.5.2-SNAPSHOT + + + wasmos-provider + jar + + WebAssembly4J wasmos Provider + + wasmos engine provider — a wasmtime-backed component runtime that + pre-links the wasmos-runtime host surface (host:wasmtime, + host:bootstrap, host:caps, host:grants, wasmos:host/*) plus + wasi:p2 onto every component instance. Backed by a JNI wrapper + cdylib that links wasmos-runtime (Rust crate at ~/git/wasmos). + + + + + ${project.basedir}/.cargo-target + ${project.basedir}/native + release + + + false + + + + + ai.tegmentum.webassembly4j + webassembly4j-spi + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + + cargo-build-wasmos-provider-native + generate-resources + + exec + + + cargo + ${native.crate.dir} + + build + --${cargo.build.mode} + + + ${native.cargo.target.dir} + 1 + + ${skip.native.build} + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + + stage-native-library + process-resources + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unknown OS/arch combination for wasmos-provider native + staging. Set -Dskip.native.build=true to skip, or add a + matching <condition> in wasmos-provider/pom.xml. + + + + + + + + + + + + + + + + + + + + diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncExecutors.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncExecutors.java new file mode 100644 index 0000000..15acedf --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncExecutors.java @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Factory for the {@code wasmos-provider-async} worker pool used by + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension} + * to schedule blocking JNI invokes off the caller's thread. + * + *

Rationale for a dedicated pool over {@link java.util.concurrent.ForkJoinPool#commonPool}: + * the commonPool is shared with parallel streams and any other JVM async + * code; blocking JNI calls on it starve unrelated work and can deadlock + * fork/join-based algorithms. A named pool also makes stacks / thread + * dumps immediately readable. + * + *

Sizing: 0 core threads (so an idle engine doesn't hold worker threads + * indefinitely), max {@value #MAX_WORKERS} workers, 60s keep-alive. Chosen + * to match the "cached thread pool with a bounded max" recommendation in + * the slice-1 brief. Callers submitting more than {@value #MAX_WORKERS} + * concurrent invocations will queue — safer than unbounded expansion which + * would fight wasmtime's own worker threads for OS-level scheduling + * resources. + * + *

Threads are daemons so a stray reference to an unclosed engine doesn't + * block JVM shutdown. + */ +final class WasmosAsyncExecutors { + + static final int MAX_WORKERS = 32; + private static final long KEEP_ALIVE_SECONDS = 60L; + + private WasmosAsyncExecutors() {} + + /** + * Create a fresh bounded cached pool. One per {@link WasmosEngineAdapter}; + * shut down when the engine closes. + */ + static ThreadPoolExecutor newEnginePool() { + return new ThreadPoolExecutor( + 0, + MAX_WORKERS, + KEEP_ALIVE_SECONDS, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + new AsyncThreadFactory()); + } + + /** Named daemon threads for cleaner stack dumps. */ + private static final class AsyncThreadFactory implements ThreadFactory { + private static final AtomicInteger POOL_ID = new AtomicInteger(); + private final AtomicInteger threadId = new AtomicInteger(); + private final int poolIndex = POOL_ID.incrementAndGet(); + + @Override + public Thread newThread(Runnable r) { + final Thread t = new Thread(r, + "wasmos-provider-async-" + poolIndex + "-" + threadId.incrementAndGet()); + t.setDaemon(true); + return t; + } + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentAdapter.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentAdapter.java new file mode 100644 index 0000000..705ee92 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentAdapter.java @@ -0,0 +1,380 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.WasiContext; +import ai.tegmentum.webassembly4j.api.config.ComponentConfig; +import ai.tegmentum.webassembly4j.api.exception.InstantiationException; +import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; +import ai.tegmentum.webassembly4j.api.exception.WebAssemblyException; +import ai.tegmentum.webassembly4j.provider.wasmos.jni.WasmosNative; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Adapter over a native wasmos component handle. Instantiation defers to + * the JNI shim, which wires {@code wasmos_runtime::add_all_to_linker} + + * {@code caps::wasi_p2} on a fresh Linker<HostState> per instance — + * matching the reference Rust flow in {@code wasmos/tests/e2e_smoke.rs}. + * + *

Supported linking-context features: + *

    + *
  • {@link LinkingContext#wasiContext()} — args, env, preopens + * (read-only + read-write), stdio inheritance are all plumbed + * through to {@code wasmtime_wasi::WasiCtxBuilder} on the Rust + * side.
  • + *
+ * + *

Rejected linking-context features: + *

    + *
  • {@link LinkingContext#hostFunctions()} + {@link + * LinkingContext#witHostFunctions()} — wasmos's philosophy is + * WIT-first; guest imports are satisfied through wasmos's own host + * surfaces (host:wasmtime, host:bootstrap, host:caps, host:grants, + * wasmos:host/*), not by Java-provided closures. Callers who want + * Java-hosted imports should use the {@code "wasmtime"} provider + * instead.
  • + *
  • {@link LinkingContext#wasiNnConfig()} — wasi:nn isn't wired into + * wasmos-runtime yet.
  • + *
+ * + *

Supported {@link ComponentConfig} features (all optional, all + * routed through wasmtime's outer-store surface): + *

    + *
  • {@link ComponentConfig#maxMemoryBytes()} — surfaced as + * {@code wasmtime::StoreLimitsBuilder::memory_size} on the outer + * store.
  • + *
  • {@link ComponentConfig#epochDeadline()} — surfaced as a + * {@code Store::set_epoch_deadline} + {@code epoch_deadline_trap} + * on the fresh store. Callers must drive the epoch counter via + * {@link WasmosNative#engineIncrementEpoch(long)} from an + * out-of-band scheduler; the provider doesn't spawn a background + * ticker.
  • + *
  • {@link ComponentConfig#fuelLimit()} — surfaced as + * {@code Store::set_fuel} on the fresh store. The engine has + * {@code Config::consume_fuel(true)} enabled globally so this is + * a cheap per-store opt-in.
  • + *
  • {@link ComponentConfig#maxTableElements()} — surfaced as + * {@code StoreLimitsBuilder::table_elements}.
  • + *
  • {@link ComponentConfig#maxInstances()} — surfaced as + * {@code StoreLimitsBuilder::instances}.
  • + *
  • {@link ComponentConfig#maxTables()} — surfaced as + * {@code StoreLimitsBuilder::tables}.
  • + *
  • {@link ComponentConfig#maxMemories()} — surfaced as + * {@code StoreLimitsBuilder::memories}.
  • + *
+ * + *

{@link ComponentConfig#trapOnGrowFailure()} is silently accepted — + * wasmtime's {@code StoreLimits} always traps on limit exceed; there's + * no separate switch to gate. + */ +final class WasmosComponentAdapter implements Component { + + private final WasmosEngineAdapter engine; + private final long handle; + private volatile boolean closed = false; + + WasmosComponentAdapter(final WasmosEngineAdapter engine, final long handle) { + this.engine = engine; + this.handle = handle; + } + + private long nativeHandle() { + if (closed) { + throw new IllegalStateException("wasmos component has been closed"); + } + return handle; + } + + @Override + public ComponentInstance instantiate() { + return instantiateInternal(null, null); + } + + @Override + public ComponentInstance instantiate(LinkingContext linkingContext) { + rejectUnsupportedLinkingContext(linkingContext); + return instantiateInternal(linkingContext, null); + } + + @Override + public ComponentInstance instantiate(ComponentConfig config) { + rejectUnsupportedComponentConfig(config); + return instantiateInternal(null, config); + } + + @Override + public ComponentInstance instantiate(LinkingContext linkingContext, ComponentConfig config) { + rejectUnsupportedLinkingContext(linkingContext); + rejectUnsupportedComponentConfig(config); + return instantiateInternal(linkingContext, config); + } + + private void rejectUnsupportedLinkingContext(LinkingContext ctx) { + if (ctx == null) return; + if (!ctx.hostFunctions().isEmpty() || !ctx.witHostFunctions().isEmpty()) { + throw new UnsupportedFeatureException( + "wasmos-provider does not accept Java-hosted host functions — " + + "wasmos's design is WIT-first, guest imports must be satisfied via " + + "wasmos's own host surfaces (host:wasmtime, wasmos:host/*). " + + "For Java-hosted imports, use the \"wasmtime\" provider instead."); + } + if (ctx.wasiNnConfig() != null) { + throw new UnsupportedFeatureException( + "wasmos-provider does not surface wasi:nn (WASI Neural Networks)"); + } + if (!ctx.externImports().isEmpty()) { + throw new UnsupportedFeatureException( + "wasmos-provider does not accept externImports — the wasmos host " + + "linker resolves imports through its own registered surfaces"); + } + } + + private void rejectUnsupportedComponentConfig(ComponentConfig config) { + if (config == null) return; + // As of the deferred-items pass every ComponentConfig field is + // plumbed through to the outer store — nothing to reject here + // beyond a sanity check that the caller didn't ask for a negative + // number (which would collide with the `-1 = unset` sentinel on + // the JNI boundary). + rejectNegative("fuelLimit", config.fuelLimit()); + rejectNegative("maxMemoryBytes", config.maxMemoryBytes()); + rejectNegative("epochDeadline", config.epochDeadline()); + rejectNegative("maxTableElements", config.maxTableElements()); + rejectNegative("maxInstances", config.maxInstances()); + rejectNegative("maxTables", config.maxTables()); + rejectNegative("maxMemories", config.maxMemories()); + // trapOnGrowFailure is implied by any positive limit — wasmtime's + // StoreLimits always traps on limit exceed; no separate switch. So we + // silently accept it. + } + + private static void rejectNegative(String name, java.util.OptionalLong opt) { + if (opt.isPresent() && opt.getAsLong() < 0) { + throw new IllegalArgumentException( + "ComponentConfig." + name + " must be >= 0; got " + opt.getAsLong() + + " (the wasmos-provider JNI uses -1 as the 'unset' sentinel)"); + } + } + + private ComponentInstance instantiateInternal(LinkingContext ctx, ComponentConfig config) { + final long inst; + try { + if (ctx == null && config == null) { + inst = WasmosNative.componentInstantiate(nativeHandle()); + } else { + inst = instantiateWithConfig(ctx, config); + } + if (inst == 0) { + throw new InstantiationException( + "wasmos componentInstantiate returned 0 handle without throwing"); + } + return new WasmosComponentInstanceAdapter(this, inst); + } catch (WebAssemblyException e) { + // Preserve upstream messages (which include the wasmos-runtime + // error) but classify as an instantiation-time failure so + // downstream error handling can distinguish it from invoke-time + // failures. + throw new InstantiationException(e.getMessage(), e); + } + } + + /** + * Marshal a WasiContext + ComponentConfig into the flat argument set the + * native {@code componentInstantiateWithConfig} shim expects, then call + * it. See {@link WasmosNative#componentInstantiateWithConfig} for the + * flag-bit and array-shape contract. + */ + private long instantiateWithConfig(LinkingContext ctx, ComponentConfig config) { + final WasiContext wasi = ctx == null ? null : ctx.wasiContext(); + int flags = 0; + String[] args = new String[0]; + String[] envKeys = new String[0]; + String[] envVals = new String[0]; + String[] preopenHosts = new String[0]; + String[] preopenGuests = new String[0]; + byte[] preopenWritable = new byte[0]; + + if (wasi != null) { + flags |= 0x08; + if (wasi.inheritStdin()) flags |= 0x01; + if (wasi.inheritStdout()) flags |= 0x02; + if (wasi.inheritStderr()) flags |= 0x04; + + final List argsList = wasi.args(); + args = argsList == null ? new String[0] : argsList.toArray(new String[0]); + + final Map envMap = wasi.env(); + if (envMap != null && !envMap.isEmpty()) { + envKeys = new String[envMap.size()]; + envVals = new String[envMap.size()]; + int i = 0; + for (Map.Entry e : envMap.entrySet()) { + envKeys[i] = e.getKey(); + envVals[i] = e.getValue(); + i++; + } + } + + final List preopens = wasi.preopenDirs(); + if (preopens != null && !preopens.isEmpty()) { + final Map guestMap = wasi.preopenGuestPaths(); + final List readOnly = wasi.readOnlyPreopenDirs(); + final int n = preopens.size(); + preopenHosts = new String[n]; + preopenGuests = new String[n]; + preopenWritable = new byte[n]; + for (int i = 0; i < n; i++) { + final String host = preopens.get(i); + preopenHosts[i] = host; + final String guest = guestMap == null ? null : guestMap.get(host); + // Convention when no remap is supplied: guest sees the + // host path unchanged. + preopenGuests[i] = guest != null ? guest : host; + preopenWritable[i] = (readOnly != null && readOnly.contains(host)) + ? (byte) 0 + : (byte) 1; + } + } + + if (wasi.wasiHttpEnabled()) { + throw new UnsupportedFeatureException( + "wasmos-provider does not surface wasi:http yet"); + } + if (wasi.allowNetwork() && !wasi.egressRules().isEmpty()) { + // wasmtime-wasi 47 sockets are all-or-nothing at the ctx + // level; we don't yet plumb per-endpoint allow-lists. + throw new UnsupportedFeatureException( + "wasmos-provider does not enforce network egress allow-lists yet"); + } + } + + return WasmosNative.componentInstantiateWithConfig( + nativeHandle(), + flags, + args, + envKeys, + envVals, + preopenHosts, + preopenGuests, + preopenWritable, + optLong(config == null ? null : config.maxMemoryBytes()), + optLong(config == null ? null : config.epochDeadline()), + optLong(config == null ? null : config.fuelLimit()), + optLong(config == null ? null : config.maxTableElements()), + optLong(config == null ? null : config.maxInstances()), + optLong(config == null ? null : config.maxTables()), + optLong(config == null ? null : config.maxMemories())); + } + + /** {@code -1} sentinel for absent OptionalLongs — matches the JNI + * convention documented on + * {@link WasmosNative#componentInstantiateWithConfig}. */ + private static long optLong(java.util.OptionalLong opt) { + return (opt != null && opt.isPresent()) ? opt.getAsLong() : -1L; + } + + @Override + public List exportedInterfaces() { + // The MVP JNI surface only lists exported functions, not the + // component's exported interfaces (which requires walking the + // component-type's nested Instance items). Return empty rather + // than pretending; callers doing introspection should probe + // exports directly against the instance. + return Collections.emptyList(); + } + + @Override + public List importedInterfaces() { + return Collections.emptyList(); + } + + @Override + public boolean exportsInterface(String name) { + return false; + } + + @Override + public boolean importsInterface(String name) { + return false; + } + + /** + * Serialize the compiled component into wasmtime's cached AOT byte + * representation. Bytes can be fed to + * {@link WasmosEngineAdapter#deserializeComponent(byte[])} on a + * compatible engine to skip re-compiling — the primary use case is + * warm-restart / hot-reload where the same component gets loaded + * repeatedly across process launches. + * + *

Byte-compat scope: tied to wasmtime version, engine + * {@code Config}, and target ISA. A mismatched engine surfaces a + * clean deserialize error rather than silently misbehaving. + */ + @Override + public byte[] serialize() { + final byte[] bytes = WasmosNative.componentSerialize(nativeHandle()); + if (bytes == null) { + // The JNI shim throws WebAssemblyException on failure; null + // here would be a contract violation on the Rust side — + // convert to something diagnosable rather than a nondescript + // NPE downstream. + throw new WebAssemblyException( + "wasmos componentSerialize returned null without throwing"); + } + return bytes; + } + + /** + * Returns the component's exported function names. Not part of the + * {@link Component} API but useful for tests / debugging — callers can + * grab it via {@code component.exportedFunctionNames()} from a cast + * reference. + */ + List exportedFunctionNames() { + final byte[] blob = WasmosNative.componentExportedFunctionNames(nativeHandle()); + if (blob == null || blob.length == 0) { + return Collections.emptyList(); + } + final String joined = new String(blob, StandardCharsets.UTF_8); + // Rust side joins on NUL (0x00); splitting on it recovers the names. + // Previous code split on space, which silently coalesced multi-name + // exports into one — invisible only when a component happens to + // export exactly one function. + final String[] parts = joined.split("\0"); + final List out = new ArrayList<>(parts.length); + for (String p : parts) { + if (!p.isEmpty()) { + out.add(p); + } + } + return out; + } + + WasmosEngineAdapter engine() { + return engine; + } + + @Override + public synchronized void close() { + if (closed) { + return; + } + closed = true; + WasmosNative.componentClose(handle); + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentInstanceAdapter.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentInstanceAdapter.java new file mode 100644 index 0000000..8cb6094 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosComponentInstanceAdapter.java @@ -0,0 +1,473 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.Function; +import ai.tegmentum.webassembly4j.api.Global; +import ai.tegmentum.webassembly4j.api.Memory; +import ai.tegmentum.webassembly4j.api.Table; +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; +import ai.tegmentum.webassembly4j.api.exception.WebAssemblyException; +import ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension; +import ai.tegmentum.webassembly4j.provider.wasmos.ext.WitErrorContextException; +import ai.tegmentum.webassembly4j.provider.wasmos.jni.WasmosNative; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * Adapter over a native wasmos component instance handle. + * + *

Invoke path: + *

    + *
  • {@link #invoke(String, Object...)} — general path via JSON + * marshalling ({@link WasmosMarshalling}). Handles primitives (bool, + * s8/s16/s32/s64, u8/u16/u32/u64 via wrappers, f32/f64, char), + * string, list, record, tuple, option, result, variant, enum, flags, + * plus a byte[] fast path for {@code list}. Wasmtime type-checks + * against the exported function's WIT signature on the Rust side.
  • + *
  • {@link #invokeBytes(String, Object...)} — byte[] fast path for a + * single {@code list} (or {@code string}) return. Skips the + * JSON detour on the return path.
  • + *
  • {@link #invokeWit(String, Object...)} — currently identical to + * {@link #invoke(String, Object...)}. A future WIT-value overload + * would branch here.
  • + *
+ * + *

The old nullary-i32 fast path is preserved internally: any + * {@code invoke(name)} whose exported function has arity {@code 0:1} and + * whose result parses cleanly as an {@code S32} takes it, saving the JSON + * round-trip on the demo path. + */ +final class WasmosComponentInstanceAdapter implements ComponentInstance { + + private final WasmosComponentAdapter component; + private final long handle; + private volatile boolean closed = false; + + WasmosComponentInstanceAdapter(final WasmosComponentAdapter component, final long handle) { + this.component = component; + this.handle = handle; + } + + private long nativeHandle() { + if (closed) { + throw new IllegalStateException("wasmos component instance has been closed"); + } + return handle; + } + + @Override + public Object invoke(String functionName, Object... args) { + if (functionName == null || functionName.isEmpty()) { + throw new IllegalArgumentException("functionName must be non-empty"); + } + try { + // Fast path: preserve the pre-marshalling `run() -> i32` shape for + // the demo test. Only applies when caller passes no args AND the + // exported function's arity is exactly (0 args, 1 result). + if (args == null || args.length == 0) { + final ArityInfo arity = readArity(functionName); + if (arity != null && arity.args == 0 && arity.results == 1) { + try { + return WasmosNative.instanceInvokeReturningI32(nativeHandle(), functionName); + } catch (WebAssemblyException wex) { + // Signature mismatch (not i32-returning) — fall back + // to the general JSON path. Don't leak the fast-path + // failure to the caller: it's an implementation detail. + } + } + } + final String argsJson = WasmosMarshalling.marshalArgs(args); + final String resultJson = WasmosNative.instanceInvokeJson( + nativeHandle(), functionName, argsJson); + final List results = WasmosMarshalling.unmarshalResults(resultJson); + return unwrapResults(results); + } catch (WebAssemblyException e) { + throw new ExecutionException( + "wasmos invoke failed for '" + functionName + "': " + e.getMessage(), e); + } + } + + @Override + public byte[] invokeBytes(String functionName, Object... args) { + if (functionName == null || functionName.isEmpty()) { + throw new IllegalArgumentException("functionName must be non-empty"); + } + try { + final String argsJson = WasmosMarshalling.marshalArgs(args); + return WasmosNative.instanceInvokeJsonReturningBytes( + nativeHandle(), functionName, argsJson); + } catch (WebAssemblyException e) { + throw new ExecutionException( + "wasmos invokeBytes failed for '" + functionName + "': " + e.getMessage(), e); + } + } + + // invokeWit currently mirrors invoke. Kept as an override rather than + // inheriting the default so future WIT-shape returns (records / variants + // with typed carriers) can diverge here without touching invoke's + // simpler natural-shape contract. + @Override + public Object invokeWit(String functionName, Object... args) { + return invoke(functionName, args); + } + + /** + * Result flattener — the shape callers expect from {@link ComponentInstance#invoke} + * for the return arity varies: + *
    + *
  • arity 0 → null
  • + *
  • arity 1 → the sole value directly
  • + *
  • arity 2+ → a {@link List} of values
  • + *
+ */ + private static Object unwrapResults(List results) { + if (results.isEmpty()) return null; + if (results.size() == 1) return results.get(0); + return results; + } + + /** Package-private for tests + `invoke` fast-path arity probe. */ + ArityInfo readArity(String name) { + try { + final byte[] blob = WasmosNative.instanceFunctionArity(nativeHandle(), name); + if (blob == null) return null; + final String s = new String(blob, StandardCharsets.UTF_8); + final int idx = s.indexOf(':'); + if (idx < 0) return null; + return new ArityInfo( + Integer.parseInt(s.substring(0, idx)), + Integer.parseInt(s.substring(idx + 1))); + } catch (Throwable t) { + return null; + } + } + + static final class ArityInfo { + final int args; + final int results; + + ArityInfo(int args, int results) { + this.args = args; + this.results = results; + } + } + + @Override + public boolean hasFunction(String name) { + return WasmosNative.instanceHasFunction(nativeHandle(), name); + } + + @Override + public List exportedFunctions() { + return component.exportedFunctionNames(); + } + + @Override + public List exportedInterfaces() { + return component.exportedInterfaces(); + } + + @Override + public boolean exportsInterface(String name) { + return component.exportsInterface(name); + } + + // Component instances don't expose core module exports; empty is the + // documented convention (see webassembly4j api ComponentInstance javadoc). + + @Override + public Optional function(String name) { return Optional.empty(); } + + @Override + public Optional memory(String name) { return Optional.empty(); } + + @Override + public Optional

table(String name) { return Optional.empty(); } + + @Override + public Optional global(String name) { return Optional.empty(); } + + @Override + public Optional unwrap(Class nativeType) { + // Bare-long handle only; no Java-object native representation to expose. + return Optional.empty(); + } + + /** + * Provider-specific extension surface. Currently exposes + * {@link WasmosAsyncExtension} for the slice-1 async invoke path; + * later slices will add stream / error-context extensions here. + */ + @Override + @SuppressWarnings("unchecked") + public Optional extension(Class extensionType) { + if (extensionType == WasmosAsyncExtension.class) { + return Optional.of((T) new AsyncExtensionImpl()); + } + return Optional.empty(); + } + + // ---- Async invoke ------------------------------------------------------ + + /** + * Package-private async invoke entry — schedules the blocking JNI + * {@link #invoke} on the engine's shared {@code wasmos-provider-async} + * pool and returns a cancellation-aware {@link CompletableFuture}. + * Exposed publicly through {@link WasmosAsyncExtension}. + */ + CompletableFuture invokeAsync(String functionName, Object... args) { + if (functionName == null || functionName.isEmpty()) { + throw new IllegalArgumentException("functionName must be non-empty"); + } + final ExecutorService pool = component.engine().asyncExecutor(); + final CancelAwareFuture cf = new CancelAwareFuture<>(component.engine()); + pool.execute(() -> { + if (cf.isCancelled()) return; // dropped before the worker picked it up + try { + cf.complete(invoke(functionName, args)); + } catch (Throwable t) { + cf.completeExceptionally(t); + } + }); + return cf; + } + + /** Byte-array-returning counterpart, mirroring {@link #invokeBytes}. */ + CompletableFuture invokeBytesAsync(String functionName, Object... args) { + if (functionName == null || functionName.isEmpty()) { + throw new IllegalArgumentException("functionName must be non-empty"); + } + final ExecutorService pool = component.engine().asyncExecutor(); + final CancelAwareFuture cf = new CancelAwareFuture<>(component.engine()); + pool.execute(() -> { + if (cf.isCancelled()) return; + try { + cf.complete(invokeBytes(functionName, args)); + } catch (Throwable t) { + cf.completeExceptionally(t); + } + }); + return cf; + } + + /** + * CompletableFuture subclass that fires a best-effort + * {@code engineIncrementEpoch} nudge on cancellation. See the + * {@link WasmosAsyncExtension} class-level javadoc for the exact + * semantics — the nudge only reliably traps guests instantiated with + * an epoch deadline; guests without a deadline finish their WASM call + * regardless. The {@link CompletableFuture} promise is always marked + * cancelled so downstream chains stop propagating. + */ + private static final class CancelAwareFuture extends CompletableFuture { + private final WasmosEngineAdapter engine; + + CancelAwareFuture(WasmosEngineAdapter engine) { + this.engine = engine; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + final boolean wasCancelled = super.cancel(mayInterruptIfRunning); + if (wasCancelled && mayInterruptIfRunning) { + // Best-effort nudge — safe on a shared engine (only bumps a + // counter, doesn't touch store state). Wrapped in try/catch + // so a failure here (e.g. engine closed racing with cancel) + // never leaks past the Future contract. + try { + WasmosNative.engineIncrementEpoch(engine.nativeHandle()); + } catch (Throwable ignored) { + // Swallow — cancellation is best-effort by contract. + } + } + return wasCancelled; + } + } + + /** + * Trivial delegate to the enclosing adapter — separate class so the + * public {@link WasmosAsyncExtension} interface doesn't require the + * adapter itself to be public. + */ + private final class AsyncExtensionImpl implements WasmosAsyncExtension { + @Override + public CompletableFuture invokeAsync(String functionName, Object... args) { + return WasmosComponentInstanceAdapter.this.invokeAsync(functionName, args); + } + + @Override + public CompletableFuture invokeBytesAsync(String functionName, Object... args) { + return WasmosComponentInstanceAdapter.this.invokeBytesAsync(functionName, args); + } + + @Override + public CompletableFuture awaitFuture(Object witFuture) { + // Route through the async pool for shape parity with the other + // await-flavoured entry points — callers get a CompletableFuture + // that they can chain regardless of underlying blocking semantics. + // The JNI call is expected to throw (wasmtime API gap); we + // surface that as an exceptional completion rather than a + // synchronous throw so `thenApply` / `handle` chains behave + // uniformly and the failure isn't observed off the calling + // thread's stack. + final CompletableFuture cf = new CompletableFuture<>(); + if (!(witFuture instanceof WasmosMarshalling.WitFuture)) { + cf.completeExceptionally(new IllegalArgumentException( + "awaitFuture expects a WitFuture; got " + + (witFuture == null ? "null" : witFuture.getClass().getName()))); + return cf; + } + final WasmosMarshalling.WitFuture wf = (WasmosMarshalling.WitFuture) witFuture; + component.engine().asyncExecutor().execute(() -> { + try { + final String json = WasmosNative.futureAwait(nativeHandle(), wf.tableId()); + // If wasmtime ever grows a dynamic await, the JNI method + // returns the JSON blob for the resolved value; wrap it + // through the same unmarshaller as invoke() so callers + // get natural Java types. + final List results = WasmosMarshalling.unmarshalResults(json); + cf.complete(unwrapResults(results)); + } catch (Throwable t) { + cf.completeExceptionally(new ExecutionException( + "wasmos awaitFuture failed for tableId=" + wf.tableId() + + ": " + t.getMessage(), t)); + } + }); + return cf; + } + + @Override + public void closeFuture(Object witFuture) { + if (!(witFuture instanceof WasmosMarshalling.WitFuture)) { + throw new IllegalArgumentException( + "closeFuture expects a WitFuture; got " + + (witFuture == null ? "null" : witFuture.getClass().getName())); + } + final WasmosMarshalling.WitFuture wf = (WasmosMarshalling.WitFuture) witFuture; + try { + WasmosNative.futureClose(nativeHandle(), wf.tableId()); + } catch (WebAssemblyException e) { + throw new ExecutionException( + "wasmos closeFuture failed for tableId=" + wf.tableId() + + ": " + e.getMessage(), e); + } + } + + @Override + public CompletableFuture readStream(Object witStream) { + // Same shape as awaitFuture — route through the async pool so + // callers see uniform CompletableFuture semantics. The JNI is + // expected to throw (wasmtime API gap) and we surface that as + // an exceptional completion. + final CompletableFuture cf = new CompletableFuture<>(); + if (!(witStream instanceof WasmosMarshalling.WitStream)) { + cf.completeExceptionally(new IllegalArgumentException( + "readStream expects a WitStream; got " + + (witStream == null ? "null" : witStream.getClass().getName()))); + return cf; + } + final WasmosMarshalling.WitStream ws = (WasmosMarshalling.WitStream) witStream; + component.engine().asyncExecutor().execute(() -> { + try { + final String json = WasmosNative.streamRead(nativeHandle(), ws.tableId()); + // Future-proofing: when wasmtime exposes a dynamic read + // surface, the JNI is expected to hand back the + // resolved value(s) as a JSON blob using the same + // marshalling schema as invokeAsync's results. Callers + // get the natural Java-typed value(s). + final List results = WasmosMarshalling.unmarshalResults(json); + cf.complete(unwrapResults(results)); + } catch (Throwable t) { + cf.completeExceptionally(new ExecutionException( + "wasmos readStream failed for tableId=" + ws.tableId() + + ": " + t.getMessage(), t)); + } + }); + return cf; + } + + @Override + public void closeStream(Object witStream) { + if (!(witStream instanceof WasmosMarshalling.WitStream)) { + throw new IllegalArgumentException( + "closeStream expects a WitStream; got " + + (witStream == null ? "null" : witStream.getClass().getName())); + } + final WasmosMarshalling.WitStream ws = (WasmosMarshalling.WitStream) witStream; + try { + WasmosNative.streamClose(nativeHandle(), ws.tableId()); + } catch (WebAssemblyException e) { + throw new ExecutionException( + "wasmos closeStream failed for tableId=" + ws.tableId() + + ": " + e.getMessage(), e); + } + } + + @Override + public void closeErrorContext(Object witErrorContext) { + if (!(witErrorContext instanceof WasmosMarshalling.WitErrorContext)) { + throw new IllegalArgumentException( + "closeErrorContext expects a WitErrorContext; got " + + (witErrorContext == null ? "null" + : witErrorContext.getClass().getName())); + } + final WasmosMarshalling.WitErrorContext we = + (WasmosMarshalling.WitErrorContext) witErrorContext; + try { + WasmosNative.errorContextClose(nativeHandle(), we.tableId()); + } catch (WebAssemblyException e) { + throw new ExecutionException( + "wasmos closeErrorContext failed for tableId=" + we.tableId() + + ": " + e.getMessage(), e); + } + } + + @Override + public WitErrorContextException wrapErrorContext(Object witErrorContext) { + if (!(witErrorContext instanceof WasmosMarshalling.WitErrorContext)) { + throw new IllegalArgumentException( + "wrapErrorContext expects a WitErrorContext; got " + + (witErrorContext == null ? "null" + : witErrorContext.getClass().getName())); + } + final WasmosMarshalling.WitErrorContext we = + (WasmosMarshalling.WitErrorContext) witErrorContext; + // Message includes tableId + rep so the exception's message alone + // is diagnosable without pulling the carrier out via + // errorContext() — matches the "cheap first look" ergonomics of + // ExecutionException elsewhere in the provider. + return new WitErrorContextException( + "wasmos guest returned error-context (tableId=" + we.tableId() + + ", rep=" + we.rep() + ')', + we); + } + } + + /** + * Instances are dropped implicitly with the component (the wasmos + * runtime keeps them together). The API doesn't declare + * {@code close()} here, so we free the native handle via a + * package-private cleanup hook driven from + * {@link WasmosComponentAdapter#close()}. + */ + synchronized void closeInternal() { + if (closed) { + return; + } + closed = true; + WasmosNative.instanceClose(handle); + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineAdapter.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineAdapter.java new file mode 100644 index 0000000..25a7ab3 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineAdapter.java @@ -0,0 +1,201 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.EngineCapabilities; +import ai.tegmentum.webassembly4j.api.EngineInfo; +import ai.tegmentum.webassembly4j.api.Module; +import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; +import ai.tegmentum.webassembly4j.api.exception.WebAssemblyException; +import ai.tegmentum.webassembly4j.provider.wasmos.jni.WasmosNative; + +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * {@link Engine} adapter — thin Java-side owner of a native wasmos engine + * handle. Lifecycle model: + * + *
    + *
  • {@link #create()} allocates the handle (which itself allocates a + * wasmtime {@code Engine} + a persistent Tokio runtime).
  • + *
  • {@link #loadComponent(byte[])} produces a + * {@link WasmosComponentAdapter} sharing this engine.
  • + *
  • {@link #close()} releases the native handle. Any components / + * instances still alive at that point produce undefined behaviour on + * subsequent use — mirroring wasmtime4j-provider's contract.
  • + *
+ * + *

Core-module loading is intentionally unsupported: wasmos-runtime is a + * component-model host. Callers that just want core wasm should pick the + * {@code "wasmtime"} provider instead. + */ +final class WasmosEngineAdapter implements Engine { + + private final long handle; + private volatile boolean closed = false; + /** + * Lazily-created worker pool for + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension#invokeAsync}. + * Null until first use; shut down on {@link #close()}. See + * {@link WasmosAsyncExecutors} for sizing rationale. + */ + private volatile ThreadPoolExecutor asyncPool; + + private WasmosEngineAdapter(final long handle) { + this.handle = handle; + } + + static WasmosEngineAdapter create() { + final long h = WasmosNative.engineCreate(); + if (h == 0) { + throw new WebAssemblyException( + "wasmos engineCreate returned 0 handle without throwing"); + } + return new WasmosEngineAdapter(h); + } + + long nativeHandle() { + if (closed) { + throw new IllegalStateException("wasmos engine has been closed"); + } + return handle; + } + + @Override + public EngineInfo info() { + return new WasmosEngineInfo(); + } + + @Override + public EngineCapabilities capabilities() { + return new WasmosEngineCapabilities(); + } + + /** + * Core modules are intentionally unsupported — wasmos-runtime exposes + * only the component-model surface. Callers that need core-wasm + * hosting should select the {@code "wasmtime"} provider instead. + */ + @Override + public Module loadModule(byte[] bytes) { + throw new UnsupportedFeatureException( + "wasmos-provider does not host core wasm modules; " + + "use the \"wasmtime\" provider for core-module hosting"); + } + + @Override + public Component loadComponent(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("component bytes must be non-empty"); + } + final long compHandle = WasmosNative.componentLoad(nativeHandle(), bytes); + if (compHandle == 0) { + throw new WebAssemblyException( + "wasmos componentLoad returned 0 handle without throwing"); + } + return new WasmosComponentAdapter(this, compHandle); + } + + /** + * Deserialize a component from bytes previously produced by + * {@link WasmosComponentAdapter#serialize()} on a compatible engine. + * Skips the compile step — useful for warm-restart / hot-reload + * scenarios. + * + *

Trust boundary: the underlying wasmtime + * {@code Component::deserialize} is {@code unsafe} — it assumes + * the bytes are a well-formed AOT image from a compatible engine. + * Passing arbitrary or tampered bytes is undefined behaviour on + * the Rust side. Callers must trust the byte provenance. + */ + Component deserializeComponent(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("serialized bytes must be non-empty"); + } + final long compHandle = WasmosNative.componentDeserialize(nativeHandle(), bytes); + if (compHandle == 0) { + throw new WebAssemblyException( + "wasmos componentDeserialize returned 0 handle without throwing"); + } + return new WasmosComponentAdapter(this, compHandle); + } + + @Override + public Optional extension(Class extensionType) { + return Optional.empty(); + } + + @Override + public Optional unwrap(Class nativeType) { + // The native handle is a plain long; there's no Java-side object + // to hand out. A future refactor could expose HostState / caps + // configuration through this hook. + return Optional.empty(); + } + + /** + * Package-private accessor for the shared async pool. Instantiated on + * first call (double-checked locking) so engines that never dispatch + * async invokes pay no thread-pool overhead. Returns the underlying + * {@link ExecutorService} — callers should NOT shut it down; the engine + * owns its lifecycle and stops it in {@link #close()}. + */ + ExecutorService asyncExecutor() { + ThreadPoolExecutor local = asyncPool; + if (local == null) { + synchronized (this) { + local = asyncPool; + if (local == null) { + if (closed) { + throw new IllegalStateException( + "wasmos engine has been closed; cannot start async executor"); + } + local = WasmosAsyncExecutors.newEnginePool(); + asyncPool = local; + } + } + } + return local; + } + + @Override + public synchronized void close() { + if (closed) { + return; + } + closed = true; + // Stop accepting new async work first. Existing tasks finish on + // their worker threads — we don't interrupt because a blocking JNI + // invoke ignores Thread.interrupt() and interrupting would just + // leave stale interrupt state on the worker. + final ThreadPoolExecutor local = asyncPool; + if (local != null) { + asyncPool = null; + local.shutdown(); + try { + // Bounded wait so close() doesn't hang on a runaway invoke; + // 2s is enough for well-behaved workloads to drain and short + // enough that a hung guest doesn't block engine teardown. + if (!local.awaitTermination(2, TimeUnit.SECONDS)) { + local.shutdownNow(); + } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + local.shutdownNow(); + } + } + WasmosNative.engineClose(handle); + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineCapabilities.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineCapabilities.java new file mode 100644 index 0000000..938e815 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineCapabilities.java @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import ai.tegmentum.webassembly4j.api.EngineCapabilities; + +/** + * Capability matrix for wasmos. Reflects the MVP surface — component-model + * hosting + WASI preview 2 are on; core-module hosting, GC, fuel, epoch + * interruption, threads are off (either because wasmos-runtime doesn't + * expose them yet or because the wasmos-provider MVP hasn't wired them + * through). A future pass can flip these once we plumb configuration + * through the JNI shim. + */ +final class WasmosEngineCapabilities implements EngineCapabilities { + + @Override public boolean supportsCoreModules() { return false; } + @Override public boolean supportsComponents() { return true; } + @Override public boolean supportsWasi() { return true; } + @Override public boolean supportsFuel() { return false; } + @Override public boolean supportsEpochInterruption(){ return false; } + @Override public boolean supportsThreads() { return false; } + @Override public boolean supportsGc() { return false; } + @Override public boolean supportsReferenceTypes() { return false; } + @Override public boolean supportsMultiMemory() { return false; } + @Override public boolean supportsNativeInterop() { return true; } + @Override public boolean supportsWasiHttp() { return false; } + + /** wasmos-runtime uses async wasmtime under the hood — every instance + * goes through {@code instantiate_async} + {@code call_async} on the + * provider's persistent Tokio runtime. */ + @Override public boolean supportsAsyncComponents() { return true; } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineInfo.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineInfo.java new file mode 100644 index 0000000..d2e2dc4 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosEngineInfo.java @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import ai.tegmentum.webassembly4j.api.EngineInfo; + +/** + * Static {@link EngineInfo} for the wasmos provider. The engine version + * tracks wasmos-runtime, not wasmtime — a wasmos-runtime version bump + * reshuffles what's on the linker, whereas the underlying wasmtime bump + * is a transitive detail. + */ +final class WasmosEngineInfo implements EngineInfo { + + @Override + public String engineId() { + return "wasmos"; + } + + @Override + public String providerId() { + return "wasmos"; + } + + @Override + public String providerVersion() { + return "1.0.0-SNAPSHOT"; + } + + @Override + public String engineVersion() { + // wasmos-runtime = 0.1.0 as of the initial provider scaffold; when we + // move to a versioned crate release we should read this from a + // manifest header instead of hardcoding. + return "0.1.0"; + } + + @Override + public int minimumJavaVersion() { + return 11; + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshalling.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshalling.java new file mode 100644 index 0000000..229ca3e --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshalling.java @@ -0,0 +1,1408 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Cheap-and-cheerful JSON marshalling between Java-natural objects and the + * on-the-wire {@code JsonVal} schema that + * {@code wasmos-provider/native/src/lib.rs} defines. + * + *

Design principles: + *

    + *
  • No third-party JSON dep — vanilla JVM, hand-rolled writer/reader. + * Kept minimal because the schema is closed (a fixed set of + * {@code JsonVal} variants).
  • + *
  • Symmetric with the Rust {@code JsonVal} enum. Tag key is {@code "t"}, + * payload key is {@code "v"} — matches the {@code #[serde(tag = "t", + * content = "v")]} annotation.
  • + *
  • Fast path for {@code byte[]}: encoded as {@code {"t":"Bytes", + * "v":"...base64..."}}; the Rust side decodes to {@code Val::List} + * and detects the same pattern on the return path to hand it back as + * {@code Bytes} instead of an O(n) list of tagged u8s.
  • + *
+ * + *

Java type mapping — mirrors the natural-shape convention documented on + * {@code ComponentInstance.invoke} for symmetry with other providers: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
BooleanBool
ByteS8
ShortS16
IntegerS32
LongS64
FloatF32
DoubleF64
CharacterChar
StringString
byte[]Bytes(base64)
List<?>List
Optional<?>/nullOption
Map<String,?>Record (field iteration order matters)
{@link WitMap}Map (arbitrary key types)
Set<String>Flags
{@link WitResult}Result
{@link WitVariant}Variant
{@link WitEnum}Enum
{@link WitResource}Resource (opaque host-parked handle)
{@link WitFuture}Future (opaque host-parked handle; awaiting is a wasmtime API gap)
{@link WitStream}Stream (opaque host-parked handle; reading is a wasmtime API gap)
{@link WitErrorContext}ErrorContext (opaque host-parked handle; rep is a wasmtime numeric hint)
+ * + *

Unsigned integers: the WIT spec has u8/u16/u32/u64 but Java doesn't; + * callers who need those specific WIT widths must use {@link WitU8} / {@link + * WitU16} / {@link WitU32} / {@link WitU64} wrappers (rare enough that we + * don't try to guess). Plain Byte/Short/Integer/Long default to signed + * widths — that's what the widest range of Java code produces naturally. + */ +final class WasmosMarshalling { + + private WasmosMarshalling() {} + + // Tag names — must match the Rust JsonVal enum exactly (serde tag = "t"). + private static final String TAG = "t"; + private static final String VAL = "v"; + + // ---- Explicit-typed wrappers for unsigned / typed-result shapes ------- + + /** Wrap a Java int as a WIT u8. Overflow at u8 boundary throws. */ + static final class WitU8 { + final short value; + + WitU8(int v) { + if (v < 0 || v > 0xFF) { + throw new IllegalArgumentException("u8 out of range: " + v); + } + this.value = (short) v; + } + } + + static final class WitU16 { + final int value; + + WitU16(int v) { + if (v < 0 || v > 0xFFFF) { + throw new IllegalArgumentException("u16 out of range: " + v); + } + this.value = v; + } + } + + static final class WitU32 { + final long value; + + WitU32(long v) { + if (v < 0 || v > 0xFFFFFFFFL) { + throw new IllegalArgumentException("u32 out of range: " + v); + } + this.value = v; + } + } + + /** u64 via {@link java.math.BigInteger}-free Long — bit-pattern is + * reinterpreted on the wire. */ + static final class WitU64 { + final long bits; + + WitU64(long bits) { this.bits = bits; } + } + + /** WIT result carrier. Exactly one of {@link #ok} / {@link #err} is + * meaningful, selected by {@link #isOk}. */ + static final class WitResult { + final boolean isOk; + final Object ok; + final Object err; + + private WitResult(boolean isOk, Object ok, Object err) { + this.isOk = isOk; + this.ok = ok; + this.err = err; + } + + static WitResult ok(Object v) { return new WitResult(true, v, null); } + + static WitResult err(Object v) { return new WitResult(false, null, v); } + } + + /** WIT variant carrier — a named discriminant plus an optional payload. */ + static final class WitVariant { + final String discriminant; + final Object value; + + WitVariant(String discriminant, Object value) { + this.discriminant = discriminant; + this.value = value; + } + } + + /** WIT enum carrier — a named discriminant, no payload. Distinguishable + * from a plain String on the marshalling path. */ + static final class WitEnum { + final String discriminant; + + WitEnum(String discriminant) { this.discriminant = discriminant; } + } + + /** + * WIT component-model resource handle. The actual guest resource lives + * Rust-side in the wasmos-provider native crate's per-instance + * {@code ResourceRegistry}; a {@code WitResource} is just a lightweight + * carrier for the slot id + opaque type identity + ownership flag that + * lets Java-side callers pass the handle back into another invoke. + * + *

Fields: + *

    + *
  • {@code tableId} — the Rust-side slot number. Monotonically + * increasing per instance; never reused, so a stale WitResource + * (e.g. after an own-transfer) surfaces as a clean error on + * reuse rather than an aliased hit.
  • + *
  • {@code typeIdentifier} — the wasmtime {@code ResourceType}'s + * {@code Debug} rendering. Opaque to Java; useful only for + * equality checks between two WitResources.
  • + *
  • {@code owned} — on values RECEIVED from a guest return, this + * reflects the ResourceAny's own ownership status. On values + * PASSED IN to a call, this is the caller's intent: {@code true} + * transfers ownership (matches WIT {@code own}) and consumes + * the WitResource; {@code false} lends a borrow view (matches + * WIT {@code borrow}) and leaves the WitResource reusable. + * The caller is responsible for setting this to match the + * function's WIT signature.
  • + *
+ * + *

Immutable — the fields are final and there are no setters. Same + * {@code WitResource} can be threaded through multiple borrow-shaped + * calls before a final own-shaped call consumes it. + */ + static final class WitResource { + private final long tableId; + private final String typeIdentifier; + private final boolean owned; + + WitResource(long tableId, String typeIdentifier, boolean owned) { + this.tableId = tableId; + this.typeIdentifier = typeIdentifier == null ? "" : typeIdentifier; + this.owned = owned; + } + + public long tableId() { return tableId; } + public String typeIdentifier() { return typeIdentifier; } + public boolean owned() { return owned; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WitResource)) return false; + final WitResource other = (WitResource) o; + return tableId == other.tableId + && owned == other.owned + && typeIdentifier.equals(other.typeIdentifier); + } + + @Override + public int hashCode() { + int h = Long.hashCode(tableId); + h = 31 * h + typeIdentifier.hashCode(); + h = 31 * h + Boolean.hashCode(owned); + return h; + } + + @Override + public String toString() { + return "WitResource{tableId=" + tableId + + ", typeIdentifier='" + typeIdentifier + + "', owned=" + owned + '}'; + } + } + + /** + * WIT component-model {@code future} handle. Same lightweight-carrier + * shape as {@link WitResource}: the {@code FutureAny} lives Rust-side in + * the wasmos-provider native crate's per-instance {@code FutureRegistry}, + * and Java only ever sees a stable slot id + an opaque Debug rendering + * of the {@code FutureAny} for equality-adjacent introspection. + * + *

Unlike {@link WitResource}, {@code WitFuture} has no {@code owned} + * bit. Wasmtime 47's {@code FutureAny} is already the "read end" of a + * split future, and there is no borrow-shaped pass-in variant in the + * component model — a future handle flows through a guest import + * exactly once. The Rust-side decode arm always consumes (takes) the + * parked entry. + * + *

Awaiting is currently unsupported. Wasmtime 47's public + * {@code FutureAny} API only exposes + * {@code try_into_future_reader::()} (compile-time typed) and + * {@code close(store)}. There is no dynamic-payload-type await surface, + * which means our JSON-based marshalling can't lift the resolved value + * to a {@link Object}. See + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension#awaitFuture} + * for how the extension exposes this gap (returned + * {@link java.util.concurrent.CompletableFuture} completes exceptionally + * with a clear message). A parked {@code WitFuture} can still be: + *

    + *
  • passed back into a guest import that consumes it (the pass-in + * path IS wired end-to-end and simply hands the parked + * {@code FutureAny} back to wasmtime);
  • + *
  • explicitly disposed of via + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension#closeFuture}.
  • + *
+ * + *

Immutable — fields are final and there are no setters. + */ + static final class WitFuture { + private final long tableId; + private final String typeIdentifier; + + WitFuture(long tableId, String typeIdentifier) { + this.tableId = tableId; + this.typeIdentifier = typeIdentifier == null ? "" : typeIdentifier; + } + + public long tableId() { return tableId; } + public String typeIdentifier() { return typeIdentifier; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WitFuture)) return false; + final WitFuture other = (WitFuture) o; + return tableId == other.tableId + && typeIdentifier.equals(other.typeIdentifier); + } + + @Override + public int hashCode() { + int h = Long.hashCode(tableId); + h = 31 * h + typeIdentifier.hashCode(); + return h; + } + + @Override + public String toString() { + return "WitFuture{tableId=" + tableId + + ", typeIdentifier='" + typeIdentifier + "'}"; + } + } + + /** + * WIT component-model {@code stream} handle. Structural mirror of + * {@link WitFuture} — the {@code StreamAny} lives Rust-side in the + * wasmos-provider native crate's per-instance {@code StreamRegistry}, + * and Java only ever sees a stable slot id + an opaque Debug rendering + * of the {@code StreamAny} for equality-adjacent introspection. + * + *

Same lifecycle as {@link WitFuture}: no {@code owned} bit + * (streams have a read-vs-write split at the type level in wasmtime), + * pass-in consumes on the Rust decode path, and explicit disposal is + * via + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension#closeStream}. + * + *

Reading is currently unsupported. Wasmtime 47's public + * {@code StreamAny} API only exposes + * {@code try_into_stream_reader::()} (compile-time typed) and + * {@code close(store)}. There is no dynamic-payload-type read/poll + * surface, which means our JSON-based marshalling can't lift stream + * items to Java objects. See + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension#readStream} + * for how the extension exposes this gap (returned + * {@link java.util.concurrent.CompletableFuture} completes exceptionally + * with a clear message). + * + *

Immutable — fields are final and there are no setters. + */ + static final class WitStream { + private final long tableId; + private final String typeIdentifier; + + WitStream(long tableId, String typeIdentifier) { + this.tableId = tableId; + this.typeIdentifier = typeIdentifier == null ? "" : typeIdentifier; + } + + public long tableId() { return tableId; } + public String typeIdentifier() { return typeIdentifier; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WitStream)) return false; + final WitStream other = (WitStream) o; + return tableId == other.tableId + && typeIdentifier.equals(other.typeIdentifier); + } + + @Override + public int hashCode() { + int h = Long.hashCode(tableId); + h = 31 * h + typeIdentifier.hashCode(); + return h; + } + + @Override + public String toString() { + return "WitStream{tableId=" + tableId + + ", typeIdentifier='" + typeIdentifier + "'}"; + } + } + + /** + * WIT component-model {@code error-context} handle. The underlying + * wasmtime 47 {@code ErrorContextAny} is a placeholder type (see + * wasmtime's {@code FIXME(#11161)}) that carries only an opaque + * {@code pub(crate) u32} rep — no debug message, no backtrace, no + * dispose surface. The Rust side still parks it in a per-instance + * registry so a Java caller can pass it back into a guest import + * that accepts an {@code error-context} argument; Java-visible + * disposal is a pure Rust-side registry eviction via + * {@link ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension#closeErrorContext}. + * + *

Fields: + *

    + *
  • {@code tableId} — Rust-side slot number, monotonically + * increasing, never reused. Used to look up the parked handle + * on pass-in and to target close.
  • + *
  • {@code rep} — parsed out of {@code ErrorContextAny}'s Debug + * rendering. Numeric identity hint only; not meaningful beyond + * equality-adjacent introspection.
  • + *
+ * + *

Immutable. Structural equality; parking multiple WitErrorContexts + * in a Set is safe. + */ + static final class WitErrorContext { + private final long tableId; + private final long rep; + + WitErrorContext(long tableId, long rep) { + this.tableId = tableId; + this.rep = rep; + } + + public long tableId() { return tableId; } + public long rep() { return rep; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WitErrorContext)) return false; + final WitErrorContext other = (WitErrorContext) o; + return tableId == other.tableId && rep == other.rep; + } + + @Override + public int hashCode() { + return 31 * Long.hashCode(tableId) + Long.hashCode(rep); + } + + @Override + public String toString() { + return "WitErrorContext{tableId=" + tableId + ", rep=" + rep + '}'; + } + } + + /** + * WIT map carrier. Distinguishes {@code map} from + * {@code record { ... }} at the marshalling boundary: a plain + * {@code Map} routes to WIT record (field-name lookup); + * a {@code WitMap} routes to WIT map (arbitrary key type). + * + *

Keys and values are marshalled by the same rules as any other + * WitVal — nesting other typed carriers is supported. Iteration + * order is preserved on the wire; the underlying map is treated as + * a {@link LinkedHashMap} even if the caller hands in another + * implementation. + */ + static final class WitMap { + final Map pairs; + + WitMap(Map pairs) { + // Copy into a fresh LinkedHashMap so downstream mutations to + // the caller's map don't leak into the on-wire form. + this.pairs = new LinkedHashMap<>(); + for (Map.Entry e : pairs.entrySet()) { + this.pairs.put((Object) e.getKey(), (Object) e.getValue()); + } + } + } + + // ---- Public API -------------------------------------------------------- + + /** + * Marshal a Java args array to the JSON-array wire form. The empty-args + * case ({@code args == null || args.length == 0}) returns the literal + * {@code "[]"} without any allocation. + */ + static String marshalArgs(Object[] args) { + if (args == null || args.length == 0) { + return "[]"; + } + final StringBuilder sb = new StringBuilder(64); + sb.append('['); + for (int i = 0; i < args.length; i++) { + if (i > 0) sb.append(','); + writeJsonVal(sb, args[i]); + } + sb.append(']'); + return sb.toString(); + } + + /** + * Unmarshal a JSON-array of {@code JsonVal}s into a list of Java-natural + * objects. Empty results ({@code "[]"}) returns an empty list. + */ + static List unmarshalResults(String json) { + if (json == null) { + return new ArrayList<>(); + } + final Parser p = new Parser(json); + p.skipWs(); + p.expect('['); + p.skipWs(); + final List out = new ArrayList<>(); + if (p.peek() == ']') { + p.consume(); + return out; + } + while (true) { + p.skipWs(); + out.add(readJsonVal(p)); + p.skipWs(); + final char c = p.peek(); + if (c == ',') { + p.consume(); + continue; + } + if (c == ']') { + p.consume(); + return out; + } + throw new IllegalArgumentException( + "expected ',' or ']' at pos " + p.pos + " in: " + json); + } + } + + // ---- Writer ------------------------------------------------------------ + + private static void writeJsonVal(StringBuilder sb, Object v) { + if (v == null) { + sb.append("{\"").append(TAG).append("\":\"Option\",\"").append(VAL).append("\":null}"); + return; + } + if (v instanceof Boolean) { + sb.append("{\"").append(TAG).append("\":\"Bool\",\"").append(VAL).append("\":"); + sb.append(((Boolean) v) ? "true" : "false"); + sb.append('}'); + return; + } + if (v instanceof Byte) { + writeTagged(sb, "S8", Byte.toString((Byte) v)); + return; + } + if (v instanceof Short) { + writeTagged(sb, "S16", Short.toString((Short) v)); + return; + } + if (v instanceof Integer) { + writeTagged(sb, "S32", Integer.toString((Integer) v)); + return; + } + if (v instanceof Long) { + writeTagged(sb, "S64", Long.toString((Long) v)); + return; + } + if (v instanceof Float) { + writeTagged(sb, "F32", Float.toString((Float) v)); + return; + } + if (v instanceof Double) { + writeTagged(sb, "F64", Double.toString((Double) v)); + return; + } + if (v instanceof Character) { + // Char is encoded as an int (u32 codepoint) — matches serde's + // representation of Rust's `char` (single Unicode scalar value). + writeTagged(sb, "Char", Integer.toString(((Character) v).charValue())); + return; + } + if (v instanceof String) { + sb.append("{\"").append(TAG).append("\":\"String\",\"").append(VAL).append("\":"); + writeJsonString(sb, (String) v); + sb.append('}'); + return; + } + if (v instanceof byte[]) { + final String b64 = Base64.getEncoder().encodeToString((byte[]) v); + sb.append("{\"").append(TAG).append("\":\"Bytes\",\"").append(VAL).append("\":"); + writeJsonString(sb, b64); + sb.append('}'); + return; + } + if (v instanceof WitU8) { + writeTagged(sb, "U8", Short.toString(((WitU8) v).value)); + return; + } + if (v instanceof WitU16) { + writeTagged(sb, "U16", Integer.toString(((WitU16) v).value)); + return; + } + if (v instanceof WitU32) { + writeTagged(sb, "U32", Long.toString(((WitU32) v).value)); + return; + } + if (v instanceof WitU64) { + // u64 is encoded as an unsigned integer literal. Java Long can't + // represent the full u64 range with `Long.toString`; use the + // unsigned formatter. + writeTagged(sb, "U64", Long.toUnsignedString(((WitU64) v).bits)); + return; + } + if (v instanceof Optional) { + final Optional opt = (Optional) v; + sb.append("{\"").append(TAG).append("\":\"Option\",\"").append(VAL).append("\":"); + if (opt.isPresent()) { + writeJsonVal(sb, opt.get()); + } else { + sb.append("null"); + } + sb.append('}'); + return; + } + if (v instanceof WitResult) { + final WitResult r = (WitResult) v; + sb.append("{\"").append(TAG).append("\":\"Result\",\"").append(VAL).append("\":{"); + sb.append("\"is_ok\":").append(r.isOk); + if (r.isOk && r.ok != null) { + sb.append(",\"ok\":"); + writeJsonVal(sb, r.ok); + } else if (!r.isOk && r.err != null) { + sb.append(",\"err\":"); + writeJsonVal(sb, r.err); + } + sb.append("}}"); + return; + } + if (v instanceof WitVariant) { + final WitVariant wv = (WitVariant) v; + sb.append("{\"").append(TAG).append("\":\"Variant\",\"").append(VAL).append("\":{"); + sb.append("\"discriminant\":"); + writeJsonString(sb, wv.discriminant); + if (wv.value != null) { + sb.append(",\"value\":"); + writeJsonVal(sb, wv.value); + } + sb.append("}}"); + return; + } + if (v instanceof WitEnum) { + sb.append("{\"").append(TAG).append("\":\"Enum\",\"").append(VAL).append("\":"); + writeJsonString(sb, ((WitEnum) v).discriminant); + sb.append('}'); + return; + } + if (v instanceof Set) { + final Set set = (Set) v; + sb.append("{\"").append(TAG).append("\":\"Flags\",\"").append(VAL).append("\":["); + boolean first = true; + for (Object flag : set) { + if (!first) sb.append(','); + first = false; + writeJsonString(sb, String.valueOf(flag)); + } + sb.append("]}"); + return; + } + if (v instanceof WitResource) { + // Wire form: { "t":"Resource", "v": { "table_id": , + // "type_name": "", "owned": } }. Field names + // MUST match the JsonVal::Resource variant in Rust + // (wasmos-provider/native/src/lib.rs) exactly — serde + // deserialization is field-name-driven. + final WitResource r = (WitResource) v; + sb.append("{\"").append(TAG).append("\":\"Resource\",\"").append(VAL).append("\":{"); + sb.append("\"table_id\":").append(r.tableId); + sb.append(",\"type_name\":"); + writeJsonString(sb, r.typeIdentifier); + sb.append(",\"owned\":").append(r.owned); + sb.append("}}"); + return; + } + if (v instanceof WitFuture) { + // Wire form: { "t":"Future", "v": { "table_id": , + // "type_name": "" } }. Field names MUST match the + // JsonVal::Future variant in Rust (wasmos-provider/native/src/lib.rs). + // No `owned` bit here — see WitFuture javadoc; futures always + // take on the Rust decode path. + final WitFuture wf = (WitFuture) v; + sb.append("{\"").append(TAG).append("\":\"Future\",\"").append(VAL).append("\":{"); + sb.append("\"table_id\":").append(wf.tableId); + sb.append(",\"type_name\":"); + writeJsonString(sb, wf.typeIdentifier); + sb.append("}}"); + return; + } + if (v instanceof WitStream) { + // Wire form: { "t":"Stream", "v": { "table_id": , + // "type_name": "" } }. Structural mirror of WitFuture. + final WitStream ws = (WitStream) v; + sb.append("{\"").append(TAG).append("\":\"Stream\",\"").append(VAL).append("\":{"); + sb.append("\"table_id\":").append(ws.tableId); + sb.append(",\"type_name\":"); + writeJsonString(sb, ws.typeIdentifier); + sb.append("}}"); + return; + } + if (v instanceof WitErrorContext) { + // Wire form: { "t":"ErrorContext", "v": { "table_id": , + // "rep": } }. Field names MUST match the + // JsonVal::ErrorContext variant in Rust — the wasmtime rep is + // a u32, we widen to Java long so it survives the wire form. + final WitErrorContext we = (WitErrorContext) v; + sb.append("{\"").append(TAG).append("\":\"ErrorContext\",\"").append(VAL).append("\":{"); + sb.append("\"table_id\":").append(we.tableId); + sb.append(",\"rep\":").append(we.rep); + sb.append("}}"); + return; + } + if (v instanceof WitMap) { + // WIT map: keys are typed JsonVals, so `[key_json, + // value_json]` pairs — same layout as Val::Map(Vec<(Val, Val)>) + // on the Rust side. Distinct from Record because Record's + // wire layout uses raw JSON strings for the key half. + final WitMap wm = (WitMap) v; + sb.append("{\"").append(TAG).append("\":\"Map\",\"").append(VAL).append("\":["); + boolean first = true; + for (Map.Entry e : wm.pairs.entrySet()) { + if (!first) sb.append(','); + first = false; + sb.append('['); + writeJsonVal(sb, e.getKey()); + sb.append(','); + writeJsonVal(sb, e.getValue()); + sb.append(']'); + } + sb.append("]}"); + return; + } + if (v instanceof Map) { + final Map m = (Map) v; + sb.append("{\"").append(TAG).append("\":\"Record\",\"").append(VAL).append("\":["); + boolean first = true; + for (Map.Entry e : m.entrySet()) { + if (!first) sb.append(','); + first = false; + sb.append('['); + writeJsonString(sb, String.valueOf(e.getKey())); + sb.append(','); + writeJsonVal(sb, e.getValue()); + sb.append(']'); + } + sb.append("]}"); + return; + } + if (v instanceof Collection) { + final Collection c = (Collection) v; + sb.append("{\"").append(TAG).append("\":\"List\",\"").append(VAL).append("\":["); + boolean first = true; + for (Object it : c) { + if (!first) sb.append(','); + first = false; + writeJsonVal(sb, it); + } + sb.append("]}"); + return; + } + throw new IllegalArgumentException( + "wasmos-provider marshalling: unsupported Java type " + + v.getClass().getName()); + } + + private static void writeTagged(StringBuilder sb, String tag, String val) { + sb.append("{\"").append(TAG).append("\":\"").append(tag).append("\",\"") + .append(VAL).append("\":").append(val).append('}'); + } + + private static void writeJsonString(StringBuilder sb, String s) { + sb.append('"'); + final int n = s.length(); + for (int i = 0; i < n; i++) { + final char c = s.charAt(i); + switch (c) { + case '"': sb.append("\\\""); break; + case '\\': sb.append("\\\\"); break; + case '\b': sb.append("\\b"); break; + case '\f': sb.append("\\f"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: + if (c < 0x20) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + sb.append('"'); + } + + // ---- Reader ------------------------------------------------------------ + + /** + * Read a single tagged JsonVal object from the parser at its current + * position (after any leading whitespace). Returns a Java-natural object. + */ + private static Object readJsonVal(Parser p) { + p.expect('{'); + p.skipWs(); + String tag = null; + String rawValue = null; + int rawValueStart = -1; + int rawValueEnd = -1; + // Two-field object: {"t":"...", "v":...}. Values can be null. + boolean sawT = false; + boolean sawV = false; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if (TAG.equals(key)) { + tag = p.readString(); + sawT = true; + } else if (VAL.equals(key)) { + rawValueStart = p.pos; + p.skipJsonValue(); + rawValueEnd = p.pos; + rawValue = p.source.substring(rawValueStart, rawValueEnd); + sawV = true; + } else { + // Unknown field — skip its value to stay forward-compatible. + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + if (!sawT) { + throw new IllegalArgumentException("missing '" + TAG + "' at pos " + p.pos); + } + if (!sawV) { + // No `v` field. Only meaningful for unit-payload variants — treat + // as null. + rawValue = "null"; + } + return decodeTag(tag, rawValue); + } + + private static Object decodeTag(String tag, String rawValue) { + switch (tag) { + case "Bool": + return Boolean.valueOf("true".equalsIgnoreCase(rawValue.trim())); + case "S8": + return Byte.parseByte(rawValue.trim()); + case "S16": + return Short.parseShort(rawValue.trim()); + case "S32": + return Integer.parseInt(rawValue.trim()); + case "S64": + return Long.parseLong(rawValue.trim()); + case "U8": + return (short) Integer.parseInt(rawValue.trim()); + case "U16": + return Integer.parseInt(rawValue.trim()); + case "U32": + return Long.parseLong(rawValue.trim()); + case "U64": + return Long.parseUnsignedLong(rawValue.trim()); + case "F32": + return Float.parseFloat(rawValue.trim()); + case "F64": + return Double.parseDouble(rawValue.trim()); + case "Char": { + // Encoded as an integer codepoint. + final int cp = Integer.parseInt(rawValue.trim()); + return (char) cp; + } + case "String": { + final Parser p = new Parser(rawValue); + p.skipWs(); + return p.readString(); + } + case "Bytes": { + final Parser p = new Parser(rawValue); + p.skipWs(); + final String b64 = p.readString(); + return Base64.getDecoder().decode(b64); + } + case "List": { + final Parser p = new Parser(rawValue); + p.skipWs(); + return readJsonArray(p); + } + case "Option": { + final String trimmed = rawValue.trim(); + if ("null".equals(trimmed)) { + return Optional.empty(); + } + final Parser p = new Parser(rawValue); + p.skipWs(); + return Optional.of(readJsonVal(p)); + } + case "Result": { + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('{'); + boolean isOk = false; + Object ok = null; + Object err = null; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if ("is_ok".equals(key)) { + final String bool = p.readBareLiteral(); + isOk = "true".equalsIgnoreCase(bool); + } else if ("ok".equals(key)) { + if (p.peek() == 'n') { + p.readBareLiteral(); // "null" + } else { + ok = readJsonVal(p); + } + } else if ("err".equals(key)) { + if (p.peek() == 'n') { + p.readBareLiteral(); + } else { + err = readJsonVal(p); + } + } else { + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + return isOk ? WitResult.ok(ok) : WitResult.err(err); + } + case "Record": { + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('['); + final Map out = new LinkedHashMap<>(); + p.skipWs(); + if (p.peek() == ']') { + p.consume(); + return out; + } + while (true) { + p.skipWs(); + p.expect('['); + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(','); + p.skipWs(); + final Object value = readJsonVal(p); + p.skipWs(); + p.expect(']'); + out.put(key, value); + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect(']'); + return out; + } + } + case "Tuple": { + final Parser p = new Parser(rawValue); + p.skipWs(); + return readJsonArray(p); + } + case "Variant": { + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('{'); + String disc = null; + Object value = null; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if ("discriminant".equals(key)) { + disc = p.readString(); + } else if ("value".equals(key)) { + if (p.peek() == 'n') { + p.readBareLiteral(); + } else { + value = readJsonVal(p); + } + } else { + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + return new WitVariant(disc, value); + } + case "Enum": { + final Parser p = new Parser(rawValue); + p.skipWs(); + return new WitEnum(p.readString()); + } + case "Map": { + // Wire form: [[key_json, value_json], ...]. Keys are decoded + // as full JsonVals rather than raw strings; we surface the + // result as a plain LinkedHashMap so callers get natural + // Java iteration semantics (the WitMap wrapper is only + // needed on the outgoing side to disambiguate from Record). + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('['); + final Map out = new LinkedHashMap<>(); + p.skipWs(); + if (p.peek() == ']') { + p.consume(); + return out; + } + while (true) { + p.skipWs(); + p.expect('['); + p.skipWs(); + final Object key = readJsonVal(p); + p.skipWs(); + p.expect(','); + p.skipWs(); + final Object value = readJsonVal(p); + p.skipWs(); + p.expect(']'); + out.put(key, value); + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect(']'); + return out; + } + } + case "Future": { + // Object payload: { "table_id": , "type_name": }. + // Fields may arrive in any order; unknown fields are skipped + // for forward-compat. See the JsonVal::Future doc comment + // for why there's no `owned` bit here. + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('{'); + long tid = 0L; + String typeName = ""; + boolean sawTableId = false; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if ("table_id".equals(key)) { + tid = Long.parseLong(p.readBareLiteral().trim()); + sawTableId = true; + } else if ("type_name".equals(key)) { + typeName = p.readString(); + } else { + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + if (!sawTableId) { + throw new IllegalArgumentException( + "Future JsonVal missing required 'table_id' field"); + } + return new WitFuture(tid, typeName); + } + case "Stream": { + // Structural mirror of the Future arm — same field names, + // same forward-compat unknown-key skip, same required-field + // set. Kept as a separate case (rather than sharing a helper) + // so failure messages name the WIT shape the caller was + // handed. + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('{'); + long tid = 0L; + String typeName = ""; + boolean sawTableId = false; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if ("table_id".equals(key)) { + tid = Long.parseLong(p.readBareLiteral().trim()); + sawTableId = true; + } else if ("type_name".equals(key)) { + typeName = p.readString(); + } else { + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + if (!sawTableId) { + throw new IllegalArgumentException( + "Stream JsonVal missing required 'table_id' field"); + } + return new WitStream(tid, typeName); + } + case "ErrorContext": { + // Object payload: { "table_id": , "rep": }. + // The `rep` field is a u32 on the Rust side; Java carries it + // as long so the whole u32 range survives without silent + // truncation. + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('{'); + long tid = 0L; + long rep = 0L; + boolean sawTableId = false; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if ("table_id".equals(key)) { + tid = Long.parseLong(p.readBareLiteral().trim()); + sawTableId = true; + } else if ("rep".equals(key)) { + rep = Long.parseLong(p.readBareLiteral().trim()); + } else { + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + if (!sawTableId) { + throw new IllegalArgumentException( + "ErrorContext JsonVal missing required 'table_id' field"); + } + return new WitErrorContext(tid, rep); + } + case "Resource": { + // Object payload: { "table_id": , "type_name": , + // "owned": }. Fields may arrive in any order (serde + // doesn't guarantee ordering across versions). Unknown fields + // are skipped for forward-compat with future wire additions. + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('{'); + long tid = 0L; + String typeName = ""; + boolean owned = false; + boolean sawTableId = false; + while (true) { + p.skipWs(); + final String key = p.readString(); + p.skipWs(); + p.expect(':'); + p.skipWs(); + if ("table_id".equals(key)) { + tid = Long.parseLong(p.readBareLiteral().trim()); + sawTableId = true; + } else if ("type_name".equals(key)) { + typeName = p.readString(); + } else if ("owned".equals(key)) { + owned = "true".equalsIgnoreCase(p.readBareLiteral().trim()); + } else { + p.skipJsonValue(); + } + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect('}'); + break; + } + if (!sawTableId) { + throw new IllegalArgumentException( + "Resource JsonVal missing required 'table_id' field"); + } + return new WitResource(tid, typeName, owned); + } + case "Flags": { + final Parser p = new Parser(rawValue); + p.skipWs(); + p.expect('['); + final List out = new ArrayList<>(); + p.skipWs(); + if (p.peek() == ']') { + p.consume(); + return new java.util.LinkedHashSet<>(out); + } + while (true) { + p.skipWs(); + out.add(p.readString()); + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect(']'); + return new java.util.LinkedHashSet<>(out); + } + } + default: + throw new IllegalArgumentException( + "wasmos-provider marshalling: unknown JsonVal tag '" + tag + "'"); + } + } + + private static List readJsonArray(Parser p) { + p.expect('['); + p.skipWs(); + final List out = new ArrayList<>(); + if (p.peek() == ']') { + p.consume(); + return out; + } + while (true) { + p.skipWs(); + out.add(readJsonVal(p)); + p.skipWs(); + if (p.peek() == ',') { + p.consume(); + continue; + } + p.expect(']'); + return out; + } + } + + // ---- Minimal JSON parser ---------------------------------------------- + + /** + * Hand-rolled JSON parser tuned for the tightly-scoped JsonVal schema. + * Not a general-purpose JSON reader; skips validation that would be + * dead code on Rust-produced input (we trust the sibling serde emitter). + */ + private static final class Parser { + + final String source; + int pos; + + Parser(String source) { + this.source = source; + this.pos = 0; + } + + char peek() { + if (pos >= source.length()) { + throw new IllegalArgumentException("unexpected end of JSON at pos " + pos); + } + return source.charAt(pos); + } + + void consume() { pos++; } + + void expect(char c) { + if (pos >= source.length()) { + throw new IllegalArgumentException( + "expected '" + c + "' at pos " + pos + " but hit end of input"); + } + if (source.charAt(pos) != c) { + throw new IllegalArgumentException( + "expected '" + c + "' at pos " + pos + " but got '" + + source.charAt(pos) + "'"); + } + pos++; + } + + void skipWs() { + while (pos < source.length()) { + final char c = source.charAt(pos); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + pos++; + } else { + return; + } + } + } + + String readString() { + if (source.charAt(pos) != '"') { + throw new IllegalArgumentException( + "expected '\"' at pos " + pos + " but got '" + source.charAt(pos) + "'"); + } + pos++; + final StringBuilder sb = new StringBuilder(); + while (pos < source.length()) { + final char c = source.charAt(pos); + if (c == '"') { + pos++; + return sb.toString(); + } + if (c == '\\') { + pos++; + final char esc = source.charAt(pos); + pos++; + switch (esc) { + case '"': sb.append('"'); break; + case '\\': sb.append('\\'); break; + case '/': sb.append('/'); break; + case 'b': sb.append('\b'); break; + case 'f': sb.append('\f'); break; + case 'n': sb.append('\n'); break; + case 'r': sb.append('\r'); break; + case 't': sb.append('\t'); break; + case 'u': { + final String hex = source.substring(pos, pos + 4); + pos += 4; + sb.append((char) Integer.parseInt(hex, 16)); + break; + } + default: + throw new IllegalArgumentException( + "invalid escape \\" + esc + " at pos " + pos); + } + continue; + } + sb.append(c); + pos++; + } + throw new IllegalArgumentException("unterminated string at pos " + pos); + } + + /** Read a bareword literal (true/false/null/number) up to a + * delimiter (comma / brace / bracket / whitespace / EOF). */ + String readBareLiteral() { + final int start = pos; + while (pos < source.length()) { + final char c = source.charAt(pos); + if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' + || c == '\n' || c == '\r') { + break; + } + pos++; + } + return source.substring(start, pos); + } + + /** + * Advance past a JSON value (string / number / object / array / + * true / false / null) without decoding it. Used to slice out the + * raw substring of a `"v"` field. + */ + void skipJsonValue() { + skipWs(); + if (pos >= source.length()) return; + final char c = source.charAt(pos); + if (c == '"') { + readString(); + return; + } + if (c == '{') { + pos++; + int depth = 1; + boolean inString = false; + boolean esc = false; + while (pos < source.length() && depth > 0) { + final char ch = source.charAt(pos); + if (inString) { + if (esc) { + esc = false; + } else if (ch == '\\') { + esc = true; + } else if (ch == '"') { + inString = false; + } + } else { + if (ch == '"') inString = true; + else if (ch == '{') depth++; + else if (ch == '}') depth--; + } + pos++; + } + return; + } + if (c == '[') { + pos++; + int depth = 1; + boolean inString = false; + boolean esc = false; + while (pos < source.length() && depth > 0) { + final char ch = source.charAt(pos); + if (inString) { + if (esc) { + esc = false; + } else if (ch == '\\') { + esc = true; + } else if (ch == '"') { + inString = false; + } + } else { + if (ch == '"') inString = true; + else if (ch == '[') depth++; + else if (ch == ']') depth--; + } + pos++; + } + return; + } + readBareLiteral(); + } + } + + // Kept for symmetry with the Rust side's exposure of raw bytes — unused + // by the marshalling helpers themselves but handy for tests that want to + // reconstruct the wire form byte-for-byte. + static byte[] asUtf8(String s) { + return s == null ? new byte[0] : s.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProvider.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProvider.java new file mode 100644 index 0000000..ca2ebe7 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProvider.java @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.config.WebAssemblyConfig; +import ai.tegmentum.webassembly4j.provider.wasmos.jni.WasmosNative; +import ai.tegmentum.webassembly4j.spi.EngineProvider; +import ai.tegmentum.webassembly4j.spi.ProviderAvailability; +import ai.tegmentum.webassembly4j.spi.ProviderDescriptor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * webassembly4j {@link EngineProvider} for wasmos — a wasmtime-backed + * component runtime that pre-links the wasmos-runtime host surface + * (host:wasmtime, host:bootstrap, host:caps, host:grants, wasmos:host/*) + * and wasi:p2 onto every component instance. + * + *

Discovered via {@link java.util.ServiceLoader}. Selection: pass + * provider id {@code "wasmos"} to + * {@link ai.tegmentum.webassembly4j.api.EngineSelection}, or let the + * default priority ordering pick it up (priority 90 — deliberately + * below the plain-wasmtime provider so a caller that just wants + * component-model support doesn't accidentally instantiate a + * wasmos-shaped host on a component that doesn't need it). + * + *

The underlying runtime is wasmos-runtime (Rust crate at + * {@code ~/git/wasmos}) accessed through a JNI wrapper cdylib bundled + * in this jar's {@code /natives/} directory. + */ +public final class WasmosProvider implements EngineProvider { + + private static final String ENGINE_ID = "wasmos"; + private static final String PROVIDER_ID = "wasmos"; + private static final String VERSION = "1.0.0-SNAPSHOT"; + private static final int MIN_JAVA = 11; + /** Priority sits between wasmtime (200) and graalwasm (150): a caller who + * wants any-provider-will-do lands on wasmtime; a caller who wants + * wasmos-shaped hosting asks for "wasmos" by id explicitly. */ + private static final int PRIORITY = 90; + + @Override + public ProviderDescriptor descriptor() { + return new ProviderDescriptor() { + @Override public String engineId() { return ENGINE_ID; } + @Override public String providerId() { return PROVIDER_ID; } + @Override public String version() { return VERSION; } + @Override public int minimumJavaVersion() { return MIN_JAVA; } + @Override public Set tags() { + final Set tags = new HashSet<>(); + tags.add("native"); + tags.add("component-model"); + tags.add("wasmos"); + return Collections.unmodifiableSet(tags); + } + @Override public int priority() { return PRIORITY; } + }; + } + + /** + * Probes availability by trying to create + immediately close a + * wasmos engine handle. This exercises the whole native surface + * (dylib extraction, JNI symbol resolution, wasmtime + Tokio + * initialization) so a false "available" answer is very unlikely. + */ + @Override + public ProviderAvailability availability() { + try { + final long h = WasmosNative.engineCreate(); + if (h == 0) { + return unavailable("engineCreate returned 0 handle without throwing"); + } + WasmosNative.engineClose(h); + return available(); + } catch (Throwable t) { + return unavailable(t.getClass().getName() + ": " + t.getMessage()); + } + } + + @Override + public Engine create(WebAssemblyConfig config) { + // WebAssemblyConfig is accepted for SPI symmetry but the MVP + // provider has no engine-specific knobs; the wasmos-runtime side + // is opinionated (async on, component-model on, wasi_p2 on) and + // isn't tunable through the current webassembly4j API. + return WasmosEngineAdapter.create(); + } + + private static ProviderAvailability available() { + return new ProviderAvailability() { + @Override public boolean available() { return true; } + @Override public String message() { return "wasmos runtime available"; } + }; + } + + private static ProviderAvailability unavailable(final String reason) { + return new ProviderAvailability() { + @Override public boolean available() { return false; } + @Override public String message() { return "wasmos not available: " + reason; } + }; + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WasmosAsyncExtension.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WasmosAsyncExtension.java new file mode 100644 index 0000000..8236593 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WasmosAsyncExtension.java @@ -0,0 +1,255 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos.ext; + +import java.util.concurrent.CompletableFuture; + +/** + * Provider-specific async extension exposed by the {@code wasmos} engine + * provider. Obtain from a wasmos-backed {@link ai.tegmentum.webassembly4j.api.ComponentInstance} + * via + *

+ *     instance.extension(WasmosAsyncExtension.class)
+ *             .ifPresent(async -> async.invokeAsync("run").thenAccept(System.out::println));
+ * 
+ * + *

This is the slice-1 (Option C hybrid) async surface: {@code Val::Future}, + * {@code Val::Stream}, and {@code Val::ErrorContext} marshalling live in + * later slices. What ships here is a Java-side {@link CompletableFuture} + * wrapper around the blocking JNI invoke path — Java consumers preferring + * async ergonomics can chain component invocations non-blockingly while the + * work runs on a dedicated worker pool. + * + *

Threading: Each invocation is scheduled onto the engine's + * shared {@code wasmos-provider-async} pool (bounded cached executor, daemon + * threads). The store is Rust-side {@code Mutex}-protected, so concurrent + * invocations against the same {@link ai.tegmentum.webassembly4j.api.ComponentInstance} + * still serialize; only work targeting different instances runs in + * parallel. Callers doing sustained parallel workloads should instantiate + * one component per parallel stream of calls. + * + *

Cancellation: {@code future.cancel(true)} on the returned + * future is best-effort. Semantics: + *

    + *
  • If the future has not yet started running on the worker pool + * (still queued), it is dropped and the WASM invoke never runs.
  • + *
  • If the future has started, the Java-side promise is marked + * cancelled and downstream {@code thenApply} / {@code thenCompose} + * chains stop propagating. The provider additionally invokes + * {@link ai.tegmentum.webassembly4j.provider.wasmos.jni.WasmosNative#engineIncrementEpoch} + * on the underlying engine as a nudge — this reliably traps the + * running invoke only when the instance was instantiated with an + * epoch deadline (via {@link ai.tegmentum.webassembly4j.api.config.ComponentConfig#epochDeadline}). + * Instances without an epoch deadline will finish their current + * WASM call to completion; the {@link CompletableFuture} contract + * still returns {@code true} from {@code cancel} to signal that the + * downstream chain will not observe the result.
  • + *
+ * + *

Callers that need authoritative cancellation should always instantiate + * with a non-negative {@code epochDeadline}. The nudge above is safe on a + * shared engine — {@code engineIncrementEpoch} only bumps the counter, it + * doesn't touch any store state. + * + * @since 2.5.2 + */ +public interface WasmosAsyncExtension { + + /** + * Async counterpart of {@link ai.tegmentum.webassembly4j.api.ComponentInstance#invoke}. + * Returns a {@link CompletableFuture} that completes with the invoke + * result on the {@code wasmos-provider-async} worker pool, or + * completes exceptionally with the wrapping + * {@link ai.tegmentum.webassembly4j.api.exception.ExecutionException} + * that the sync path would have thrown. + * + *

Argument / return marshalling is identical to the sync path — see + * {@link ai.tegmentum.webassembly4j.api.ComponentInstance#invoke} for + * the type-mapping table. + * + *

See the class-level javadoc for cancellation semantics; the + * returned future's {@code cancel(true)} is best-effort and its + * authoritative-ness depends on whether the instance was instantiated + * with an epoch deadline. + */ + CompletableFuture invokeAsync(String functionName, Object... args); + + /** + * Async counterpart of + * {@link ai.tegmentum.webassembly4j.api.ComponentInstance#invokeBytes}. + * Skips the JSON detour on the return path for functions returning + * {@code list} or {@code string}. + * + *

Cancellation semantics mirror {@link #invokeAsync}. + */ + CompletableFuture invokeBytesAsync(String functionName, Object... args); + + /** + * Await a parked {@code WitFuture} (a {@code future} handle returned + * by an earlier guest call) and complete with its resolved value. + * + *

Currently unsupported due to a wasmtime 47 public-API gap. + * The returned {@link CompletableFuture} always completes exceptionally + * with an + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * explaining the gap. + * + *

The wasmtime 47 {@code FutureAny} API only exposes: + *

    + *
  • {@code try_into_future_reader::()} — requires a compile-time + * {@code T}. Incompatible with our runtime-typed JSON marshalling.
  • + *
  • {@code close(store)} — plain disposal, no value extraction.
  • + *
+ * {@code FutureConsumer::Item} is likewise a compile-time associated + * type, so there's no type-erased await surface upstream today. + * + *

The wrapping method is stable in shape: when wasmtime exposes a + * dynamic await API upstream, only the JNI body and this method's + * implementation change; callers using + * {@code future.awaitFuture(handle).thenApply(...)} chains keep working + * without recompilation. + * + *

In the meantime, callers can still round-trip a {@code WitFuture} + * through a guest import that consumes it (that path IS wired + * end-to-end via {@link #invokeAsync}), or dispose of it via + * {@link #closeFuture(Object)}. + * + * @param witFuture a {@code WitFuture} produced by the marshalling + * layer as a return value of an earlier invoke call; passed as + * {@link Object} to avoid leaking the package-private carrier type + * into the public API. + */ + CompletableFuture awaitFuture(Object witFuture); + + /** + * Close a parked {@code WitFuture} — evicts it from the Rust-side + * future registry and calls wasmtime's {@code FutureAny::close(store)}. + * Should always be called in a {@code finally} block when a caller + * receives a {@code WitFuture} but doesn't pass it back to a guest + * import (which would consume the parked entry naturally). + * + *

Throws + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * if the slot id is unknown (already closed, or transferred to a guest). + * + * @param witFuture a {@code WitFuture} produced by the marshalling + * layer, passed as {@link Object} to avoid leaking the carrier type. + */ + void closeFuture(Object witFuture); + + /** + * Read a parked {@code WitStream} and complete with a materialised + * value. + * + *

Currently unsupported due to a wasmtime 47 public-API gap. + * Structurally identical to {@link #awaitFuture} — the returned + * {@link CompletableFuture} always completes exceptionally with a + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * explaining the gap. + * + *

Wasmtime 47's {@code StreamAny} API only exposes + * {@code try_into_stream_reader::()} (compile-time typed) and + * {@code close(store)}. There is no dynamic-payload-type read/poll + * surface, so runtime-typed marshalling can't lift stream items to + * Java. We deliberately expose this as a single-value + * {@code CompletableFuture} (rather than a {@link java.util.concurrent.Flow.Publisher}) + * because the underlying gap means only the "hit the wall" signal is + * actionable today — a multi-value publisher over a + * gap-that-throws-immediately would be over-engineering. When + * wasmtime lands a dynamic read surface upstream, this shape can be + * revisited (either evolved to return a publisher, or paired with a + * new streaming-shaped method) without breaking the current no-op + * contract. + * + *

In the meantime, callers can still round-trip a {@code WitStream} + * through a guest import that consumes it (that path IS wired + * end-to-end via {@link #invokeAsync}), or dispose of it via + * {@link #closeStream(Object)}. + * + * @param witStream a {@code WitStream} produced by the marshalling + * layer as a return value of an earlier invoke call; passed as + * {@link Object} to avoid leaking the package-private carrier type. + */ + CompletableFuture readStream(Object witStream); + + /** + * Close a parked {@code WitStream} — evicts it from the Rust-side + * stream registry and calls wasmtime's {@code StreamAny::close(store)}. + * Mirrors {@link #closeFuture}. + * + *

Throws + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * if the slot id is unknown (already closed or transferred to a guest). + * + * @param witStream a {@code WitStream} produced by the marshalling + * layer, passed as {@link Object} to avoid leaking the carrier type. + */ + void closeStream(Object witStream); + + /** + * Close a parked {@code WitErrorContext} — evicts it from the + * Rust-side error-context registry. Wasmtime 47's + * {@code ErrorContextAny} has no publicly-defined dispose surface + * (see wasmtime's {@code FIXME(#11161)}), so this is a pure + * eviction — no wasmtime-side call is made. Callers who received a + * {@code WitErrorContext} but don't pass it back to a guest should + * call this in a {@code finally} block to keep the registry from + * growing unbounded. + * + *

Throws + * {@link ai.tegmentum.webassembly4j.api.exception.ExecutionException} + * if the slot id is unknown (already closed or transferred to a guest). + * + * @param witErrorContext a {@code WitErrorContext} produced by the + * marshalling layer, passed as {@link Object} to avoid leaking + * the carrier type. + */ + void closeErrorContext(Object witErrorContext); + + /** + * Convenience wrapper that transforms a Java-visible + * {@code WitErrorContext} into a + * {@link WitErrorContextException} so callers can complete a + * downstream {@link CompletableFuture} exceptionally with an + * inspectable exception type. + * + *

Intended usage — inside a {@code handle(...)} or {@code compose(...)} + * chain, when the caller has parsed a {@code result} + * return value and wants to lift its {@code err} branch into a + * standard exception path: + * + *

+     * invokeAsync("get-thing")
+     *     .thenApply(v -> {
+     *         if (v instanceof WasmosMarshalling.WitResult) {
+     *             final var r = (WasmosMarshalling.WitResult) v;
+     *             if (!r.isOk && r.err instanceof WasmosMarshalling.WitErrorContext) {
+     *                 throw async.wrapErrorContext((WasmosMarshalling.WitErrorContext) r.err);
+     *             }
+     *             return r.ok;
+     *         }
+     *         return v;
+     *     });
+     * 
+ * + *

The parameter is typed as {@link Object} because the concrete + * {@code WasmosMarshalling.WitErrorContext} type is package-private + * on purpose. + * + * @param witErrorContext an opaque {@code WitErrorContext} produced + * by the marshalling layer + * @return a {@link WitErrorContextException} carrying the handle; + * ready to be thrown from a {@code CompletableFuture} + * transformer to trigger exceptional completion + * @throws IllegalArgumentException if {@code witErrorContext} isn't a + * recognised WitErrorContext carrier + */ + WitErrorContextException wrapErrorContext(Object witErrorContext); +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WitErrorContextException.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WitErrorContextException.java new file mode 100644 index 0000000..47454e4 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/ext/WitErrorContextException.java @@ -0,0 +1,77 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos.ext; + +import ai.tegmentum.webassembly4j.api.exception.ExecutionException; + +/** + * Java-side exception wrapper for a wasmos-provider WIT + * {@code error-context} handle. Carries the opaque handle (as an + * {@link Object} — the underlying carrier type is package-private in + * {@code wasmos-provider}) so consumers can inspect it via the + * exceptional {@link java.util.concurrent.CompletableFuture} chain. + * + *

Wasmtime 47's {@code ErrorContextAny} is a placeholder type (see + * wasmtime's {@code FIXME(#11161)}) — the only publicly-accessible + * information is a numeric rep. That's carried on the wrapped + * {@code WitErrorContext}. Downstream code that wants to introspect can + * do so via the pass-in path (route the handle back into a guest import + * that accepts an error-context argument) or via + * {@link WasmosAsyncExtension#closeErrorContext} for disposal. + * + *

Extends {@link ExecutionException} so + * {@code CompletableFuture.get()} rethrows it wrapped in + * {@link java.util.concurrent.ExecutionException} with a consistent + * exception-hierarchy story shared with other invoke-time failures. + * Callers doing {@code .handle((v, t) -> …)} can pattern-match on + * {@code WitErrorContextException} to detect the specific case and + * pull the underlying handle out via {@link #errorContext()}. + * + * @since 2.5.2 + */ +public final class WitErrorContextException extends ExecutionException { + + private static final long serialVersionUID = 1L; + + /** + * The opaque {@code WitErrorContext} carrier. Passed through as + * {@link Object} because the concrete + * {@code WasmosMarshalling.WitErrorContext} type is package-private + * on purpose (the wasmos-provider marshalling layer keeps its + * carrier types out of the public API surface). Callers who + * absolutely need the numeric rep can call {@code toString()} — the + * carrier's toString includes {@code rep=} — or route the + * handle back into a guest via + * {@link WasmosAsyncExtension#invokeAsync}. + */ + private final Object errorContext; + + public WitErrorContextException(String message, Object errorContext) { + super(message); + this.errorContext = errorContext; + } + + public WitErrorContextException(String message, Object errorContext, Throwable cause) { + super(message, cause); + this.errorContext = errorContext; + } + + /** + * The wrapped {@code WitErrorContext} — an opaque + * {@code WasmosMarshalling.WitErrorContext} carrier. Passed as + * {@link Object} to avoid leaking the package-private carrier type; + * downstream code that wants to introspect can call {@code toString()} + * on it (which includes the numeric rep) or pass it back into a guest + * import via {@link WasmosAsyncExtension#invokeAsync}. + */ + public Object errorContext() { + return errorContext; + } +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNative.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNative.java new file mode 100644 index 0000000..e976800 --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNative.java @@ -0,0 +1,293 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos.jni; + +/** + * Raw JNI surface into the wasmos-provider Rust cdylib (see + * {@code wasmos-provider/native/src/lib.rs}). Every method here maps 1:1 to + * an {@code extern "C"} fn on the Rust side; the Rust side throws + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} on + * failure and returns a sentinel value (0 for {@code long}, {@code null} for + * arrays / strings). + * + *

All handles are opaque {@code long} pointers to Rust-heap-owned + * structs. Every {@code *Create} / {@code *Load} / {@code *Instantiate} + * call must be paired with the corresponding {@code *Close} call — + * failure to do so leaks a wasmtime Engine or Store, and (in the case of + * the engine handle) a Tokio runtime with a worker-thread pool. + * + *

Package-private on purpose: this is not part of the provider's public + * API. Callers should use the framework-neutral webassembly4j + * {@link ai.tegmentum.webassembly4j.api.Engine} / + * {@link ai.tegmentum.webassembly4j.api.Component} / + * {@link ai.tegmentum.webassembly4j.api.ComponentInstance} surface, which the + * wasmos-provider adapters implement on top of this class. + */ +public final class WasmosNative { + + static { + WasmosNativeLoader.load(); + } + + private WasmosNative() {} + + // ---- Engine ------------------------------------------------------------ + + /** + * Create a wasmtime Engine wired for the wasmos-runtime host surface, + * plus a persistent Tokio runtime for driving async instantiation / + * invocation. Returns an opaque handle. + */ + public static native long engineCreate(); + + /** Drop the engine + its Tokio runtime. */ + public static native void engineClose(long engineHandle); + + /** + * Increment the engine's epoch counter. Only meaningful for instances + * instantiated with a non-negative {@code epochDeadline} via + * {@link #componentInstantiateWithConfig}. Callers running without a + * deadline can ignore this method. + */ + public static native void engineIncrementEpoch(long engineHandle); + + // ---- Component --------------------------------------------------------- + + /** + * Compile a Component from raw bytes. The engine handle must outlive + * the returned component handle. + */ + public static native long componentLoad(long engineHandle, byte[] bytes); + + /** Drop a component. */ + public static native void componentClose(long componentHandle); + + /** + * Serialize the component into wasmtime's cached AOT byte + * representation. Bytes can be fed back to + * {@link #componentDeserialize(long, byte[])} on a compatible engine + * to skip re-compiling. Intended for warm-restart / hot-reload + * scenarios; compatibility is tied to wasmtime version + Config + + * target ISA (a mismatched engine will fail to deserialize rather + * than silently misbehave). + */ + public static native byte[] componentSerialize(long componentHandle); + + /** + * Deserialize bytes produced by {@link #componentSerialize(long)} + * against a compatible engine. Returns a fresh component handle. + * + *

Note: wasmtime's underlying {@code Component::deserialize} is + * {@code unsafe} — it trusts the input to be a well-formed AOT + * image from a compatible engine. Passing arbitrary or tampered + * bytes is undefined behaviour. Callers are responsible for the + * provenance of the bytes. + */ + public static native long componentDeserialize(long engineHandle, byte[] bytes); + + /** + * List the component's exported function names as a null-separated + * UTF-8 blob. Cheap-and-cheerful serialization to avoid a jstring + * allocation loop for what's a metadata query. + */ + public static native byte[] componentExportedFunctionNames(long componentHandle); + + // ---- Instance lifecycle ------------------------------------------------ + + /** + * Instantiate a component with the full wasmos-runtime host surface + * (bootstrap / caps / grants / host:wasmtime) plus wasi:p2. The engine + * associated with the component must still be alive. Uses a default + * {@code HostState::new()} — empty WASI ctx, no limits, no epoch. + */ + public static native long componentInstantiate(long componentHandle); + + /** + * Instantiate with optional WASI ctx + optional resource / epoch limits. + * + *

Flag bits: + *

    + *
  • {@code 0x01} — inheritStdin
  • + *
  • {@code 0x02} — inheritStdout
  • + *
  • {@code 0x04} — inheritStderr
  • + *
  • {@code 0x08} — hasWasiContext (when unset, all WASI args are + * ignored and a default empty WasiCtx is used — equivalent to + * calling {@link #componentInstantiate})
  • + *
+ * + *

String arrays are index-aligned: + * {@code envKeys[i]} pairs with {@code envVals[i]}; + * {@code preopenHosts[i]} maps to {@code preopenGuests[i]} with + * writability decided by the i-th byte of {@code preopenWritable} + * ({@code 0}=read-only, non-zero=read-write). + * + *

Numeric args use {@code -1} to mean "unset": + * {@code maxMemoryBytes<0} skips the memory limiter, + * {@code epochDeadline<0} skips epoch wiring, and so on. Each + * outer-store limit is independent — a caller can set only fuel or + * only tables without dragging the others in. + * + * @param componentHandle from {@link #componentLoad} + * @param flags see the bit-flag doc above + * @param args wasi arguments (empty array if not needed) + * @param envKeys wasi env keys + * @param envVals wasi env vals (same length as envKeys) + * @param preopenHosts host paths to preopen + * @param preopenGuests guest paths for each preopen (same length) + * @param preopenWritable per-preopen writability flag (same length) + * @param maxMemoryBytes memory-size limit in bytes, or {@code -1} for none + * @param epochDeadline epoch deadline (ticks beyond current), or {@code -1} + * @param fuelLimit initial store fuel budget (engine has + * {@code Config::consume_fuel(true)} on globally so this is a cheap + * opt-in), or {@code -1} for unlimited + * @param maxTableElements per-table element cap, or {@code -1} + * @param maxInstances max component-model instances per store, or {@code -1} + * @param maxTables max tables per store, or {@code -1} + * @param maxMemories max linear memories per store, or {@code -1} + */ + public static native long componentInstantiateWithConfig( + long componentHandle, + int flags, + String[] args, + String[] envKeys, + String[] envVals, + String[] preopenHosts, + String[] preopenGuests, + byte[] preopenWritable, + long maxMemoryBytes, + long epochDeadline, + long fuelLimit, + long maxTableElements, + long maxInstances, + long maxTables, + long maxMemories); + + /** Drop an instance + its Store. */ + public static native void instanceClose(long instanceHandle); + + // ---- Instance invoke --------------------------------------------------- + + /** + * Invoke {@code name: func() -> i32} on the instance. Fast path — no + * marshalling, no JSON. Throws + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} on + * any wasmtime error, including a signature mismatch (Rust side calls + * {@code get_typed_func::<(), (i32,)>}). + */ + public static native int instanceInvokeReturningI32(long instanceHandle, String name); + + /** + * General invoke path. {@code argsJson} is a JSON array of typed + * {@code JsonVal} objects (see the schema on the Rust side or the + * {@code WasmosMarshalling} Java helper). Returns a JSON array of typed + * {@code JsonVal} result objects. + */ + public static native String instanceInvokeJson( + long instanceHandle, String name, String argsJson); + + /** + * Byte-array-returning variant. Callers who know the exported function + * returns exactly one {@code list} (or {@code string}) can skip the + * JSON detour on the return path — the Rust side hands back a raw + * {@code byte[]}. Args still go through JSON. + */ + public static native byte[] instanceInvokeJsonReturningBytes( + long instanceHandle, String name, String argsJson); + + /** + * Introspect an exported function's arity as an ASCII byte blob of + * the form {@code "argCount:resultCount"} (e.g. {@code "2:1"} for a + * function taking two args and returning one result). Used by the + * Java marshalling helper when it needs to size a param list without + * a WIT descriptor in hand. + */ + public static native byte[] instanceFunctionArity(long instanceHandle, String name); + + /** + * Cheap {@code exports.contains(name)} probe against a live instance. + * Avoids the whole export list when a caller only needs a boolean. + */ + public static native boolean instanceHasFunction(long instanceHandle, String name); + + // ---- Future handle lifecycle ------------------------------------------ + // + // Val::Future marshalling (slice-1 async, r.2): FutureAny values returned + // from a guest call are parked in a per-instance FutureRegistry on the + // Rust side; Java receives a {@code WitFuture} carrying just the u64 slot + // id + a Debug-formatted type-name hint. These two natives are the + // Java-visible lifecycle surface for those parked handles. + + /** + * Close a parked {@code FutureAny} — evicts it from the instance's + * future registry and calls wasmtime's {@code FutureAny::close(store)} + * so the writer end sees the read end as dropped. Throws + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * if the slot id is unknown (already closed, transferred to a guest via + * pass-in, or never parked) or the instance handle is null. + */ + public static native void futureClose(long instanceHandle, long futureId); + + /** + * Attempt to await a parked {@code FutureAny}. Currently ALWAYS throws + * {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * with a clear "wasmtime 47 API gap" message. See the Rust-side + * {@code futureAwait} doc for the underlying reason — the public + * {@code FutureAny} surface has no dynamic-payload-type await/poll + * method, so runtime-typed marshalling can't lift the resolved value. + * + *

Kept as a JNI entry point so the Java surface is future-proof: + * when wasmtime exposes a dynamic await API, only the Rust body needs + * to swap in — the JNI signature and the Java-side wrapper stay put. + */ + public static native String futureAwait(long instanceHandle, long futureId); + + // ---- Stream handle lifecycle ------------------------------------------ + // + // Val::Stream marshalling (slice-2 async): structurally identical to + // futures — StreamAny values returned from a guest call are parked in a + // per-instance StreamRegistry on the Rust side; Java receives a {@code + // WitStream} carrying just the u64 slot id + a Debug-formatted type-name + // hint. Same wasmtime 47 "typed-reader only" API gap applies to reading. + + /** + * Close a parked {@code StreamAny} — evicts it from the instance's + * stream registry and calls wasmtime's {@code StreamAny::close(store)} + * so the writer end sees the read end as dropped. Symmetric with + * {@link #futureClose}. + */ + public static native void streamClose(long instanceHandle, long streamId); + + /** + * Attempt to read from a parked {@code StreamAny}. Currently ALWAYS + * throws {@link ai.tegmentum.webassembly4j.api.exception.WebAssemblyException} + * with a "wasmtime 47 API gap" message — same story as + * {@link #futureAwait}, since {@code StreamAny::try_into_stream_reader} + * requires a compile-time {@code T}. Kept as a stable JNI entry so the + * Java surface is future-proof; only the Rust body swaps in when + * wasmtime exposes a dynamic API. + */ + public static native String streamRead(long instanceHandle, long streamId); + + // ---- ErrorContext handle lifecycle ------------------------------------ + // + // Val::ErrorContext marshalling (slice-3): wasmtime 47's ErrorContextAny + // is a placeholder (see FIXME(#11161) upstream) with `pub(crate) u32` + // internals and no publicly-defined dispose. We park the Val on the Rust + // side purely to allow round-trip pass-back; Java sees a slot id + the + // parsed numeric rep as an equality-adjacent hint. + + /** + * Evict a parked error-context handle from the instance's registry. + * Pure Rust-side eviction — wasmtime 47 has no dispose surface on + * error-context, so this simply drops the parked handle so subsequent + * pass-in attempts against this id fail cleanly. + */ + public static native void errorContextClose(long instanceHandle, long errorContextId); +} diff --git a/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNativeLoader.java b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNativeLoader.java new file mode 100644 index 0000000..1122eae --- /dev/null +++ b/wasmos-provider/src/main/java/ai/tegmentum/webassembly4j/provider/wasmos/jni/WasmosNativeLoader.java @@ -0,0 +1,156 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos.jni; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Loads the {@code libwasmos_provider.{so,dylib,dll}} native library out of + * the packaged jar's {@code /natives/{os}-{arch}/} directory. + * + *

Deliberately self-contained (no dependency on wasmtime4j's + * {@code wasmtime4j-native-loader}) so wasmos-provider stands alone with + * no wasmtime4j coupling — per the F-Webassembly4j-Wasmos-Provider r.1 + * architectural constraint. + * + *

The layout matches wasmtime4j-native's convention: + *

+ *   /natives/darwin-aarch64/libwasmos_provider.dylib
+ *   /natives/darwin-x86_64/libwasmos_provider.dylib
+ *   /natives/linux-x86_64/libwasmos_provider.so
+ *   /natives/linux-aarch64/libwasmos_provider.so
+ *   /natives/windows-x86_64/wasmos_provider.dll
+ * 
+ * + *

System properties for troubleshooting: + *

    + *
  • {@code webassembly4j.wasmos.native.path} — absolute path to a prebuilt + * dylib. Bypasses jar extraction. Useful during development when + * cargo builds outside the Maven cycle. + *
  • {@code webassembly4j.wasmos.native.debug} — set to {@code true} to + * log resolution attempts to {@code System.err}. + *
+ */ +final class WasmosNativeLoader { + + private static final String PROP_OVERRIDE = "webassembly4j.wasmos.native.path"; + private static final String PROP_DEBUG = "webassembly4j.wasmos.native.debug"; + private static final String LIB_BASENAME = "wasmos_provider"; + + // Loaded exactly once per classloader; a second call is a no-op (System.load + // would throw UnsatisfiedLinkError on double-load of the same absolute path, + // and the singleton fingerprint gives idempotency in the face of static-init + // races across parallel threads). + private static volatile boolean loaded = false; + + private WasmosNativeLoader() {} + + static synchronized void load() { + if (loaded) { + return; + } + final String override = System.getProperty(PROP_OVERRIDE); + if (override != null && !override.isEmpty()) { + debug("using override path: " + override); + System.load(override); + loaded = true; + return; + } + + final String platform = detectPlatform(); + final String fileName = libraryFileName(platform); + final String resourcePath = "/natives/" + platform + "/" + fileName; + debug("looking up " + resourcePath); + + try (InputStream in = WasmosNativeLoader.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new UnsatisfiedLinkError( + "wasmos-provider native library not found on classpath at " + resourcePath + + " — either the wasmos-provider jar was built without the platform's" + + " native library, or the current platform (" + platform + ") is not" + + " supported. Override with -D" + PROP_OVERRIDE + "=/absolute/path."); + } + final Path tmp = Files.createTempFile("wasmos-provider-", fileName); + tmp.toFile().deleteOnExit(); + // TRUNCATE_EXISTING: createTempFile made an empty file we're + // about to fill; the option keeps us defensive if the file + // ever gets prefixed by unrelated content between calls. + try (OutputStream out = Files.newOutputStream( + tmp, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING)) { + copy(in, out); + } + debug("extracted to " + tmp); + System.load(tmp.toAbsolutePath().toString()); + loaded = true; + } catch (IOException e) { + throw new UnsatisfiedLinkError( + "failed to extract wasmos-provider native library: " + e.getMessage()); + } + } + + private static String detectPlatform() { + final String os = System.getProperty("os.name", "").toLowerCase(java.util.Locale.ROOT); + final String archRaw = System.getProperty("os.arch", "").toLowerCase(java.util.Locale.ROOT); + final String arch; + switch (archRaw) { + case "aarch64": + case "arm64": + arch = "aarch64"; + break; + case "x86_64": + case "amd64": + arch = "x86_64"; + break; + default: + arch = archRaw; + } + final String osFamily; + if (os.contains("mac") || os.contains("darwin")) { + osFamily = "darwin"; + } else if (os.contains("linux")) { + osFamily = "linux"; + } else if (os.contains("win")) { + osFamily = "windows"; + } else { + osFamily = os; + } + return osFamily + "-" + arch; + } + + private static String libraryFileName(String platform) { + if (platform.startsWith("windows-")) { + return LIB_BASENAME + ".dll"; + } + if (platform.startsWith("darwin-")) { + return "lib" + LIB_BASENAME + ".dylib"; + } + // linux + everything else — .so + return "lib" + LIB_BASENAME + ".so"; + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + final byte[] buf = new byte[64 * 1024]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + } + + private static void debug(String msg) { + if (Boolean.getBoolean(PROP_DEBUG)) { + System.err.println("[wasmos-provider-loader] " + msg); + } + } + +} diff --git a/wasmos-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider b/wasmos-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider new file mode 100644 index 0000000..b805d91 --- /dev/null +++ b/wasmos-provider/src/main/resources/META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider @@ -0,0 +1 @@ +ai.tegmentum.webassembly4j.provider.wasmos.WasmosProvider diff --git a/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncInvokeTest.java b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncInvokeTest.java new file mode 100644 index 0000000..b39dc30 --- /dev/null +++ b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosAsyncInvokeTest.java @@ -0,0 +1,616 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.provider.wasmos.ext.WasmosAsyncExtension; +import ai.tegmentum.webassembly4j.provider.wasmos.ext.WitErrorContextException; +import ai.tegmentum.webassembly4j.spi.EngineProvider; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ServiceLoader; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Slice-1 async coverage for wasmos-provider. The MVP promise is: + * {@code instance.extension(WasmosAsyncExtension.class).invokeAsync(...)} + * returns a {@link CompletableFuture} that behaves like a well-formed + * async wrapper around the sync invoke path — the chain composes, errors + * propagate, and cancel() at least severs the downstream promise. + * + *

These tests reuse the {@code composed_wasmtime.wasm} demo fixture as + * the async surface's north-star acceptance case (mirroring the sync-side + * {@link WasmosProviderDemoTest#composedRunReturns42}). If the fixture + * isn't present the async tests skip via {@link org.junit.jupiter.api.Assumptions} + * — the local wasmos checkout is a soft dependency, not a hard one. + */ +class WasmosAsyncInvokeTest { + + private static final Path COMPOSED_FIXTURE = Path.of( + System.getProperty("user.home"), + "git", "wasmos", "tests", "e2e-fixtures", "composed_wasmtime.wasm"); + + private static EngineProvider wasmosProvider() { + return ServiceLoader.load(EngineProvider.class) + .stream() + .map(ServiceLoader.Provider::get) + .filter(p -> "wasmos".equals(p.descriptor().providerId())) + .findFirst() + .orElseThrow(() -> new AssertionError( + "wasmos provider not on classpath — see WasmosProviderDemoTest")); + } + + @Test + @DisplayName("extension(WasmosAsyncExtension.class) is present on wasmos component instance") + void extensionIsPresent() { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(Files.readAllBytes(COMPOSED_FIXTURE))) { + final ComponentInstance instance = component.instantiate(); + try { + assertTrue(instance.extension(WasmosAsyncExtension.class).isPresent(), + "wasmos component instance must surface WasmosAsyncExtension"); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + @DisplayName("invokeAsync(\"run\").get() returns 42 through the demo fixture") + void invokeAsyncRunReturns42() throws Exception { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + final CompletableFuture future = async.invokeAsync("run"); + assertNotNull(future, "invokeAsync must return a non-null future"); + + // 30s cap is generous — the sync path returns in ms; timing + // out here would indicate the worker pool never dispatched. + final Object result = future.get(30, TimeUnit.SECONDS); + assertEquals(42, ((Number) result).intValue(), + "invokeAsync('run') must return 42, matching sync semantics"); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("thenApply chain composes: invokeAsync('run').thenApply(+1).get() returns 43") + void thenApplyChainComposes() throws Exception { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + final Integer result = async.invokeAsync("run") + .thenApply(x -> ((Number) x).intValue() + 1) + .get(30, TimeUnit.SECONDS); + assertEquals(43, result.intValue(), + "thenApply on invokeAsync must propagate the (+1) transform"); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("invokeAsync on a missing export completes exceptionally with ExecutionException") + void invokeAsyncMissingExportCompletesExceptionally() throws Exception { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + final CompletableFuture future = + async.invokeAsync("this-export-does-not-exist"); + + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> future.get(30, TimeUnit.SECONDS)); + assertNotNull(ex.getCause(), + "ExecutionException must wrap the underlying invoke failure"); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("cancel(true) marks the Java future cancelled — WASM interrupt is best-effort") + void cancelIsBestEffort() throws Exception { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + // Kick off a call and immediately cancel it. On this fast + // demo the worker may have already finished — either way, + // the semantics we're pinning down are: + // * cancel(true) never throws + // * post-cancel, the future either reports cancelled OR + // already-completed (well-defined race; we accept both) + // * the promise chain sees CancellationException on + // downstream get() if cancel won the race + final CompletableFuture future = async.invokeAsync("run"); + final boolean cancelResult = future.cancel(true); + + if (cancelResult) { + assertTrue(future.isCancelled(), + "cancel(true) that returned true must leave isCancelled() true"); + assertThrows(CancellationException.class, + () -> future.get(5, TimeUnit.SECONDS), + "cancelled future must throw CancellationException on get()"); + } else { + // Lost the race — the future had already completed + // normally before cancel could take effect. Confirm the + // result is still the expected 42 so we know we're not + // silently masking a broken invoke path. + assertTrue(future.isDone(), + "cancel(true)==false must mean the future had already completed"); + assertFalse(future.isCancelled(), + "cancel(true)==false must leave isCancelled() false"); + assertEquals(42, ((Number) future.get()).intValue(), + "if cancel lost the race, the result must still be the sync 42"); + } + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("awaitFuture on unknown WitFuture id completes exceptionally with API-gap message") + void awaitFutureUnknownIdSurfacesGap() throws Exception { + // Slice-1 r.2 boundary test — the parked-slot lookup on the Rust + // side happens BEFORE the "wasmtime API gap" bail-out, so hitting + // an unknown id gets the standard "unknown future id" error path, + // NOT the API-gap message. The API-gap message only surfaces once + // the slot lookup succeeds. Both paths must complete the Java + // future exceptionally (never throw synchronously from awaitFuture) + // and both must wrap the JNI failure so downstream error handling + // has a stable exception shape. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + // No component in-tree returns a `future`, so we can't + // exercise a real parked handle end-to-end here — but the + // "unknown id" path is what the API-gap swap-in will + // preserve, so it's the right shape to lock down. + final WasmosMarshalling.WitFuture bogus = + new WasmosMarshalling.WitFuture(999_999L, "no-such-future"); + + final CompletableFuture future = async.awaitFuture(bogus); + assertNotNull(future, "awaitFuture must never return null"); + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> future.get(10, TimeUnit.SECONDS)); + assertNotNull(ex.getCause(), + "awaitFuture failure must wrap the JNI exception in ExecutionException"); + final String msg = ex.getCause().getMessage(); + assertTrue(msg.contains("999999") || msg.contains("unknown future id"), + "expected 'unknown future id' or the numeric id in the error; got: " + msg); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("awaitFuture(null) completes exceptionally with IllegalArgumentException") + void awaitFutureRejectsNullArg() throws Exception { + // Type-safety guard on the Object-typed carrier: passing null (or a + // non-WitFuture) must fail cleanly rather than NPE'ing inside the + // JNI shim. Completes-exceptionally rather than sync-throws so + // callers using thenApply/handle chains see a uniform failure shape. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + final CompletableFuture future = async.awaitFuture(null); + assertNotNull(future); + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + assertTrue(ex.getCause() instanceof IllegalArgumentException, + "awaitFuture(null) should complete exceptionally with IAE; got " + + ex.getCause()); + + // Same shape for a wrongly-typed argument. + final CompletableFuture f2 = async.awaitFuture("not-a-witfuture"); + final ExecutionException ex2 = assertThrows(ExecutionException.class, + () -> f2.get(5, TimeUnit.SECONDS)); + assertTrue(ex2.getCause() instanceof IllegalArgumentException); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("closeFuture on unknown WitFuture id throws ExecutionException wrapping the JNI failure") + void closeFutureUnknownIdThrows() throws Exception { + // Synchronous by design (no async pool detour) — closeFuture is a + // registry eviction + wasmtime FutureAny::close(store), fast enough + // that offloading to a worker adds latency without value. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + // Null / wrong type — IllegalArgumentException, synchronous. + assertThrows(IllegalArgumentException.class, + () -> async.closeFuture(null)); + assertThrows(IllegalArgumentException.class, + () -> async.closeFuture(new Object())); + + // Unknown id — the JNI throws WebAssemblyException; the + // extension wraps it in ExecutionException to keep the + // caller-visible exception type consistent with invoke + // failures. + final WasmosMarshalling.WitFuture bogus = + new WasmosMarshalling.WitFuture(424242L, "gone"); + final ai.tegmentum.webassembly4j.api.exception.ExecutionException ex = + assertThrows( + ai.tegmentum.webassembly4j.api.exception.ExecutionException.class, + () -> async.closeFuture(bogus)); + assertTrue(ex.getMessage().contains("424242") + || ex.getMessage().contains("unknown future id"), + "expected 'unknown future id' or the numeric id in the error; got: " + + ex.getMessage()); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("readStream on unknown WitStream id completes exceptionally with wrapping ExecutionException") + void readStreamUnknownIdSurfacesGap() throws Exception { + // Structural mirror of awaitFutureUnknownIdSurfacesGap. The parked + // slot lookup happens BEFORE the wasmtime API-gap bail-out, so an + // unknown id gets the "unknown stream id" error path. Both paths + // must complete the Java future exceptionally. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + final WasmosMarshalling.WitStream bogus = + new WasmosMarshalling.WitStream(777_777L, "no-such-stream"); + + final CompletableFuture future = async.readStream(bogus); + assertNotNull(future, "readStream must never return null"); + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> future.get(10, TimeUnit.SECONDS)); + assertNotNull(ex.getCause(), + "readStream failure must wrap the JNI exception in ExecutionException"); + final String msg = ex.getCause().getMessage(); + assertTrue(msg.contains("777777") || msg.contains("unknown stream id"), + "expected 'unknown stream id' or the numeric id in the error; got: " + msg); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("readStream(null) completes exceptionally with IllegalArgumentException") + void readStreamRejectsNullArg() throws Exception { + // Type-safety guard on the Object-typed carrier — passing null or + // a non-WitStream must fail cleanly rather than NPE'ing inside the + // JNI shim. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + final CompletableFuture future = async.readStream(null); + assertNotNull(future); + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> future.get(5, TimeUnit.SECONDS)); + assertTrue(ex.getCause() instanceof IllegalArgumentException); + + final CompletableFuture f2 = async.readStream("not-a-witstream"); + final ExecutionException ex2 = assertThrows(ExecutionException.class, + () -> f2.get(5, TimeUnit.SECONDS)); + assertTrue(ex2.getCause() instanceof IllegalArgumentException); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("closeStream on unknown WitStream id throws ExecutionException wrapping the JNI failure") + void closeStreamUnknownIdThrows() throws Exception { + // Synchronous — mirrors closeFutureUnknownIdThrows. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + assertThrows(IllegalArgumentException.class, + () -> async.closeStream(null)); + assertThrows(IllegalArgumentException.class, + () -> async.closeStream(new Object())); + + final WasmosMarshalling.WitStream bogus = + new WasmosMarshalling.WitStream(313131L, "gone"); + final ai.tegmentum.webassembly4j.api.exception.ExecutionException ex = + assertThrows( + ai.tegmentum.webassembly4j.api.exception.ExecutionException.class, + () -> async.closeStream(bogus)); + assertTrue(ex.getMessage().contains("313131") + || ex.getMessage().contains("unknown stream id"), + "expected 'unknown stream id' or the numeric id in the error; got: " + + ex.getMessage()); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("closeErrorContext on unknown WitErrorContext id throws ExecutionException wrapping the JNI failure") + void closeErrorContextUnknownIdThrows() throws Exception { + // Synchronous like the other close* methods. The Rust side has no + // wasmtime dispose to call — it's pure registry eviction — but the + // "unknown id" error surface is the same shape. + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + assertThrows(IllegalArgumentException.class, + () -> async.closeErrorContext(null)); + assertThrows(IllegalArgumentException.class, + () -> async.closeErrorContext(new Object())); + + final WasmosMarshalling.WitErrorContext bogus = + new WasmosMarshalling.WitErrorContext(212121L, 5L); + final ai.tegmentum.webassembly4j.api.exception.ExecutionException ex = + assertThrows( + ai.tegmentum.webassembly4j.api.exception.ExecutionException.class, + () -> async.closeErrorContext(bogus)); + assertTrue(ex.getMessage().contains("212121") + || ex.getMessage().contains("unknown error-context id"), + "expected 'unknown error-context id' or the numeric id in the error; got: " + + ex.getMessage()); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("wrapErrorContext produces a WitErrorContextException that completes a CF exceptionally") + void wrapErrorContextCompletesFutureExceptionally() throws Exception { + // The integration story: a caller receives a WitErrorContext (via an + // invoke result), transforms it into a WitErrorContextException in a + // thenApply / handle chain, and downstream get() surfaces the + // exception with the carrier still inspectable via .errorContext(). + // Uses a synthetic WitErrorContext so no fixture-returning-error-context + // is required (none exists in-tree). + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + try (Engine engine = wasmosProvider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + // Type-safety guards first — null / wrong type. + assertThrows(IllegalArgumentException.class, + () -> async.wrapErrorContext(null)); + assertThrows(IllegalArgumentException.class, + () -> async.wrapErrorContext(new Object())); + + final WasmosMarshalling.WitErrorContext we = + new WasmosMarshalling.WitErrorContext(101L, 202L); + final WitErrorContextException wrapped = async.wrapErrorContext(we); + assertNotNull(wrapped, "wrapErrorContext must never return null"); + assertNotNull(wrapped.errorContext(), + "wrapped exception must carry the underlying handle"); + assertTrue(wrapped.getMessage().contains("101") + && wrapped.getMessage().contains("202"), + "wrapped exception message should include tableId + rep; got: " + + wrapped.getMessage()); + + // The lift-through-CompletableFuture story. + final CompletableFuture src = CompletableFuture.completedFuture(we); + final CompletableFuture composed = src.thenApply(v -> { + throw async.wrapErrorContext(v); + }); + final ExecutionException ex = assertThrows(ExecutionException.class, + () -> composed.get(5, TimeUnit.SECONDS)); + assertTrue(ex.getCause() instanceof WitErrorContextException, + "cause should be WitErrorContextException; got " + + (ex.getCause() == null ? "null" : ex.getCause().getClass())); + final WitErrorContextException cause = (WitErrorContextException) ex.getCause(); + assertEquals(we, cause.errorContext(), + "wrapped handle should equal the source WitErrorContext"); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } + + @Test + @DisplayName("engine close() shuts down the async pool cleanly") + void engineCloseShutsDownAsyncPool() throws Exception { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE); + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + + final Engine engine = wasmosProvider().create(null); + try { + final Component component = engine.loadComponent(bytes); + final ComponentInstance instance = component.instantiate(); + try { + final WasmosAsyncExtension async = instance + .extension(WasmosAsyncExtension.class) + .orElseThrow(() -> new AssertionError("async extension missing")); + + // Prime the pool by dispatching at least one invoke — this + // forces asyncExecutor() to create the ThreadPoolExecutor + // whose shutdown we want to exercise. + async.invokeAsync("run").get(30, TimeUnit.SECONDS); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + component.close(); + } + } finally { + // The main assertion: close() returns in bounded time with the + // pool torn down. If pool shutdown hangs, the JVM-level test + // timeout will fail this. Bounded via an explicit future in + // case someone regresses the internal awaitTermination. + final CompletableFuture closed = CompletableFuture.runAsync(engine::close); + try { + closed.get(10, TimeUnit.SECONDS); + } catch (TimeoutException te) { + throw new AssertionError("engine.close() did not return within 10s — " + + "async pool shutdown likely hanging"); + } + } + } +} diff --git a/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshallingTest.java b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshallingTest.java new file mode 100644 index 0000000..90024e9 --- /dev/null +++ b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosMarshallingTest.java @@ -0,0 +1,575 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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 static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Round-trip tests for {@link WasmosMarshalling} — Java object -> JSON -> Java + * object. + * + *

The full end-to-end path is Java -> JSON -> Rust {@code JsonVal} -> {@code Val} + * -> wasmtime -> {@code Val} -> {@code JsonVal} -> JSON -> Java. This test + * covers the Java-side halves; the Rust-side round-trip is validated by the + * {@code #[cfg(test)]} module in {@code wasmos-provider/native/src/lib.rs}. + */ +final class WasmosMarshallingTest { + + /** + * Round-trip a single Java value through marshal(args) + unmarshal(results), + * treating results as if it were the args JSON blob (they share the exact + * same JsonVal-array schema). Returns the round-tripped Java object. + */ + private static Object roundTrip(Object v) { + final String json = WasmosMarshalling.marshalArgs(new Object[] { v }); + final List back = WasmosMarshalling.unmarshalResults(json); + assertEquals(1, back.size(), () -> "expected exactly one result for " + json); + return back.get(0); + } + + @Test + @DisplayName("primitive round-trips preserve type + value") + void primitives() { + assertEquals(Boolean.TRUE, roundTrip(Boolean.TRUE)); + assertEquals(Boolean.FALSE, roundTrip(Boolean.FALSE)); + assertEquals((byte) -7, roundTrip((byte) -7)); + assertEquals((short) 1234, roundTrip((short) 1234)); + assertEquals(-42, roundTrip(-42)); + assertEquals(1_000_000_000_000L, roundTrip(1_000_000_000_000L)); + assertEquals(1.5f, roundTrip(1.5f)); + assertEquals(-2.25, roundTrip(-2.25)); + assertEquals('Z', roundTrip('Z')); + assertEquals("hello", roundTrip("hello")); + } + + @Test + @DisplayName("string round-trip handles quotes, backslashes, newlines, unicode BMP") + void stringEscapes() { + assertEquals("he said \"hi\"", roundTrip("he said \"hi\"")); + assertEquals("a\\b", roundTrip("a\\b")); + assertEquals("line1\nline2", roundTrip("line1\nline2")); + assertEquals("tab\there", roundTrip("tab\there")); + assertEquals("Ω hello", roundTrip("Ω hello")); + } + + @Test + @DisplayName("byte[] round-trip preserves bytes (base64 wire form)") + void bytesFastPath() { + final byte[] input = { 0, 1, 2, 3, 127, -1, -128 }; + final Object round = roundTrip(input); + assertInstanceOf(byte[].class, round); + assertArrayEquals(input, (byte[]) round); + } + + @Test + @DisplayName("empty byte[] round-trips as empty byte[]") + void bytesEmpty() { + final Object round = roundTrip(new byte[0]); + assertInstanceOf(byte[].class, round); + assertEquals(0, ((byte[]) round).length); + } + + @Test + @DisplayName("unsigned wrapper types round-trip through their tagged forms") + void unsignedWrappers() { + assertEquals((short) 200, roundTrip(new WasmosMarshalling.WitU8(200))); + assertEquals(60_000, roundTrip(new WasmosMarshalling.WitU16(60_000))); + assertEquals(4_000_000_000L, roundTrip(new WasmosMarshalling.WitU32(4_000_000_000L))); + // u64 uses unsigned string encoding — round-trip preserves the exact bit pattern. + assertEquals(-1L, roundTrip(new WasmosMarshalling.WitU64(-1L))); // 0xFFFF...FFFF + } + + @Test + @DisplayName("Optional.of + Optional.empty round-trip") + void optionRoundtrip() { + final Object present = roundTrip(Optional.of(42)); + assertInstanceOf(Optional.class, present); + assertEquals(Optional.of(42), present); + + final Object empty = roundTrip(Optional.empty()); + assertInstanceOf(Optional.class, empty); + assertFalse(((Optional) empty).isPresent()); + } + + @Test + @DisplayName("null argument round-trips as Optional.empty") + void nullBecomesEmptyOption() { + final Object round = roundTrip(null); + assertInstanceOf(Optional.class, round); + assertFalse(((Optional) round).isPresent()); + } + + @Test + @DisplayName("List round-trip preserves element order + types") + void listRoundtrip() { + final List input = Arrays.asList("alpha", "beta", "gamma"); + final Object round = roundTrip(input); + assertInstanceOf(List.class, round); + assertEquals(input, round); + } + + @Test + @DisplayName("Record (Map) round-trip preserves insertion order") + void recordRoundtrip() { + final Map input = new LinkedHashMap<>(); + input.put("name", "alice"); + input.put("age", 30); + input.put("greeting", "hi"); + final Object round = roundTrip(input); + assertInstanceOf(Map.class, round); + assertEquals(new ArrayList<>(input.keySet()), + new ArrayList<>(((Map) round).keySet()), + "record field iteration order should be preserved for wasmtime lookup"); + assertEquals(input, round); + } + + @Test + @DisplayName("Set round-trip as WIT flags") + void flagsRoundtrip() { + final Set input = new LinkedHashSet<>(); + input.add("read"); + input.add("write"); + final Object round = roundTrip(input); + assertInstanceOf(Set.class, round); + assertEquals(input, round); + } + + @Test + @DisplayName("Result carriers round-trip both branches") + void resultRoundtrip() { + final Object ok = roundTrip(WasmosMarshalling.WitResult.ok(7)); + assertInstanceOf(WasmosMarshalling.WitResult.class, ok); + assertTrue(((WasmosMarshalling.WitResult) ok).isOk); + assertEquals(7, ((WasmosMarshalling.WitResult) ok).ok); + + final Object err = roundTrip(WasmosMarshalling.WitResult.err("bad")); + assertInstanceOf(WasmosMarshalling.WitResult.class, err); + assertFalse(((WasmosMarshalling.WitResult) err).isOk); + assertEquals("bad", ((WasmosMarshalling.WitResult) err).err); + + // Unit-payload branches + final Object okUnit = roundTrip(WasmosMarshalling.WitResult.ok(null)); + assertNull(((WasmosMarshalling.WitResult) okUnit).ok); + assertTrue(((WasmosMarshalling.WitResult) okUnit).isOk); + } + + @Test + @DisplayName("Variant + enum carriers survive round-trip") + void variantEnumRoundtrip() { + final Object variant = roundTrip(new WasmosMarshalling.WitVariant("some", 42)); + assertInstanceOf(WasmosMarshalling.WitVariant.class, variant); + assertEquals("some", ((WasmosMarshalling.WitVariant) variant).discriminant); + assertEquals(42, ((WasmosMarshalling.WitVariant) variant).value); + + final Object bareVariant = roundTrip(new WasmosMarshalling.WitVariant("none", null)); + assertNull(((WasmosMarshalling.WitVariant) bareVariant).value); + + final Object enumVal = roundTrip(new WasmosMarshalling.WitEnum("red")); + assertInstanceOf(WasmosMarshalling.WitEnum.class, enumVal); + assertEquals("red", ((WasmosMarshalling.WitEnum) enumVal).discriminant); + } + + @Test + @DisplayName("empty args JSON is the literal '[]'") + void emptyArgs() { + assertEquals("[]", WasmosMarshalling.marshalArgs(null)); + assertEquals("[]", WasmosMarshalling.marshalArgs(new Object[0])); + } + + @Test + @DisplayName("empty results JSON parses to empty list") + void emptyResults() { + assertEquals(0, WasmosMarshalling.unmarshalResults("[]").size()); + } + + @Test + @DisplayName("nested list-of-lists round-trip") + void nestedList() { + final List nested = Arrays.asList( + Arrays.asList(1, 2, 3), + Arrays.asList("a", "b")); + final Object round = roundTrip(nested); + assertInstanceOf(List.class, round); + assertEquals(nested, round); + } + + @Test + @DisplayName("WitMap round-trip preserves typed keys + order") + void witMapWithIntegerKeys() { + // Distinct from Record: keys are Integer (not String) so plain + // Map can't be used. WitMap forces the Val::Map branch. + final Map input = new LinkedHashMap<>(); + input.put(1, "one"); + input.put(2, "two"); + input.put(3, "three"); + final Object round = roundTrip(new WasmosMarshalling.WitMap(input)); + assertInstanceOf(Map.class, round); + final Map back = (Map) round; + assertEquals(input.size(), back.size()); + assertEquals("one", back.get(1)); + assertEquals("two", back.get(2)); + assertEquals("three", back.get(3)); + // Order preservation matters for anyone using WitMap as a + // deterministic ordered container. + assertEquals(new java.util.ArrayList<>(input.keySet()), + new java.util.ArrayList<>(back.keySet())); + } + + @Test + @DisplayName("WitMap round-trips byte[] values through Bytes fast path") + void witMapWithByteArrayValues() { + final Map input = new LinkedHashMap<>(); + input.put("first", new byte[] { 1, 2, 3 }); + input.put("second", new byte[] { -1, 0, 127 }); + final Object round = roundTrip(new WasmosMarshalling.WitMap(input)); + assertInstanceOf(Map.class, round); + final Map back = (Map) round; + assertArrayEquals(new byte[] { 1, 2, 3 }, (byte[]) back.get("first")); + assertArrayEquals(new byte[] { -1, 0, 127 }, (byte[]) back.get("second")); + } + + @Test + @DisplayName("WitMap with empty pairs round-trips as empty Map") + void witMapEmpty() { + final Object round = roundTrip(new WasmosMarshalling.WitMap(new LinkedHashMap<>())); + assertInstanceOf(Map.class, round); + assertTrue(((Map) round).isEmpty()); + } + + @Test + @DisplayName("WitResource round-trips tableId + typeIdentifier + owned") + void witResourceRoundtrip() { + // Owned handle — matches what a guest return of `own` produces + // on the Rust side. Round-trip must preserve every field byte-for-byte + // because Java uses tableId to re-find the parked ResourceAny. + final WasmosMarshalling.WitResource owned = new WasmosMarshalling.WitResource( + 42L, "ResourceType { kind: Guest { component: 0, resource: 3 } }", true); + final Object roundOwned = roundTrip(owned); + assertInstanceOf(WasmosMarshalling.WitResource.class, roundOwned); + final WasmosMarshalling.WitResource ownedBack = + (WasmosMarshalling.WitResource) roundOwned; + assertEquals(42L, ownedBack.tableId()); + assertEquals("ResourceType { kind: Guest { component: 0, resource: 3 } }", + ownedBack.typeIdentifier()); + assertTrue(ownedBack.owned()); + // Equals / hashCode consistency — WitResource is used as a value + // handle so structural equality matters for anyone parking it in a + // Set / Map. + assertEquals(owned, ownedBack); + assertEquals(owned.hashCode(), ownedBack.hashCode()); + } + + @Test + @DisplayName("WitResource borrow-flavored round-trip preserves owned=false") + void witResourceBorrowRoundtrip() { + // Same shape as the owned case but with owned=false — matches a + // WIT `borrow` on the way OUT. Round-trip must not silently + // upgrade to owned; the Rust side keys on this to decide + // take-vs-peek on the way back IN. + final WasmosMarshalling.WitResource borrow = new WasmosMarshalling.WitResource( + 7L, "my-type", false); + final Object round = roundTrip(borrow); + assertInstanceOf(WasmosMarshalling.WitResource.class, round); + final WasmosMarshalling.WitResource back = + (WasmosMarshalling.WitResource) round; + assertEquals(7L, back.tableId()); + assertEquals("my-type", back.typeIdentifier()); + assertFalse(back.owned()); + } + + @Test + @DisplayName("WitResource with large tableId (u64-adjacent) round-trips") + void witResourceLargeTableId() { + // Rust-side ids are u64; Java carries them as long. The wire form + // must survive a u64-ish value without overflow — pick something + // > Integer.MAX_VALUE. + final long largeId = 9_999_999_999L; + final WasmosMarshalling.WitResource wr = new WasmosMarshalling.WitResource( + largeId, "T", true); + final Object round = roundTrip(wr); + assertEquals(largeId, + ((WasmosMarshalling.WitResource) round).tableId()); + } + + @Test + @DisplayName("WitResource in a nested List round-trips through container marshalling") + void witResourceNestedInList() { + // Resources can appear as elements of collections just like any + // other WitVal — verify the Resource writer + reader arms compose + // with List. This is the shape a `list>` return would + // produce. + final List items = Arrays.asList( + new WasmosMarshalling.WitResource(1L, "T", true), + new WasmosMarshalling.WitResource(2L, "T", false), + new WasmosMarshalling.WitResource(3L, "T", true)); + final Object round = roundTrip(items); + assertInstanceOf(List.class, round); + final List back = (List) round; + assertEquals(3, back.size()); + assertEquals(new WasmosMarshalling.WitResource(1L, "T", true), back.get(0)); + assertEquals(new WasmosMarshalling.WitResource(2L, "T", false), back.get(1)); + assertEquals(new WasmosMarshalling.WitResource(3L, "T", true), back.get(2)); + } + + @Test + @DisplayName("WitResource typeIdentifier with special JSON chars round-trips") + void witResourceEscapedTypeIdentifier() { + // Debug output for ResourceType contains braces and colons — those + // don't need escaping in JSON strings, but quotes and backslashes + // do. The Debug format is unlikely to contain quotes, but be + // defensive so a future wasmtime change doesn't silently corrupt + // the type identifier field on the wire. + final WasmosMarshalling.WitResource wr = new WasmosMarshalling.WitResource( + 1L, "quoted \"value\" with \\ backslash", false); + final Object round = roundTrip(wr); + assertEquals("quoted \"value\" with \\ backslash", + ((WasmosMarshalling.WitResource) round).typeIdentifier()); + } + + @Test + @DisplayName("WitFuture round-trips tableId + typeIdentifier") + void witFutureRoundtrip() { + // Return-direction shape: a WitFuture parked by the Rust decoder, + // handed to Java, then round-tripped through the marshalling layer. + // Every field must survive byte-for-byte — tableId is what Java uses + // to re-find the parked FutureAny (or call futureClose against it). + final WasmosMarshalling.WitFuture wf = new WasmosMarshalling.WitFuture( + 7L, "FutureAny { id: TableId(3), ty: Guest(Future(u32)) }"); + final Object round = roundTrip(wf); + assertInstanceOf(WasmosMarshalling.WitFuture.class, round); + final WasmosMarshalling.WitFuture back = (WasmosMarshalling.WitFuture) round; + assertEquals(7L, back.tableId()); + assertEquals("FutureAny { id: TableId(3), ty: Guest(Future(u32)) }", + back.typeIdentifier()); + // Value-shape equality — a WitFuture might be parked in a Set / Map + // by callers doing bookkeeping across multiple futures, so equals / + // hashCode consistency matters. + assertEquals(wf, back); + assertEquals(wf.hashCode(), back.hashCode()); + } + + @Test + @DisplayName("WitFuture with large tableId (u64-adjacent) round-trips") + void witFutureLargeTableId() { + // Rust-side ids are u64 monotonic; Java carries them as long. The + // wire form must survive the >Integer.MAX_VALUE range without any + // silent truncation. + final long largeId = 12_345_678_901L; + final WasmosMarshalling.WitFuture wf = new WasmosMarshalling.WitFuture( + largeId, "F"); + final Object round = roundTrip(wf); + assertEquals(largeId, + ((WasmosMarshalling.WitFuture) round).tableId()); + } + + @Test + @DisplayName("WitFuture in a nested List round-trips through container marshalling") + void witFutureNestedInList() { + // A guest returning `list>` produces exactly this shape; + // verify the Future writer + reader arms compose with List. Same + // regression guarantee as the WitResource nested-list test — proves + // the marshalling arm plays with the container arms cleanly. + final List items = Arrays.asList( + new WasmosMarshalling.WitFuture(1L, "F"), + new WasmosMarshalling.WitFuture(2L, "F"), + new WasmosMarshalling.WitFuture(3L, "F")); + final Object round = roundTrip(items); + assertInstanceOf(List.class, round); + final List back = (List) round; + assertEquals(3, back.size()); + assertEquals(new WasmosMarshalling.WitFuture(1L, "F"), back.get(0)); + assertEquals(new WasmosMarshalling.WitFuture(2L, "F"), back.get(1)); + assertEquals(new WasmosMarshalling.WitFuture(3L, "F"), back.get(2)); + } + + @Test + @DisplayName("WitFuture wire form uses \"Future\" tag (matches Rust JsonVal::Future)") + void witFutureWireFormat() { + // Explicit wire-format check — the Rust JsonVal enum is + // #[serde(tag = "t", content = "v")] and the tag string must match + // byte-for-byte. A tag rename here would silently break the JNI + // round-trip; guard against it. + final WasmosMarshalling.WitFuture wf = new WasmosMarshalling.WitFuture( + 42L, "type-hint"); + final String wire = WasmosMarshalling.marshalArgs(new Object[] { wf }); + assertTrue(wire.contains("\"Future\""), + "WitFuture wire form must use tag=\"Future\"; wire=" + wire); + assertTrue(wire.contains("\"table_id\":42"), + "wire form must expose table_id verbatim; wire=" + wire); + assertTrue(wire.contains("\"type_name\":\"type-hint\""), + "wire form must expose type_name verbatim; wire=" + wire); + } + + @Test + @DisplayName("WitStream round-trips tableId + typeIdentifier") + void witStreamRoundtrip() { + // Return-direction shape: WitStream parked by the Rust decoder, + // handed to Java, then round-tripped through the marshalling layer. + // tableId must survive byte-for-byte — Java uses it to re-find the + // parked StreamAny or to close it. + final WasmosMarshalling.WitStream ws = new WasmosMarshalling.WitStream( + 9L, "StreamAny { id: TableId(5), ty: Guest(Stream(u32)) }"); + final Object round = roundTrip(ws); + assertInstanceOf(WasmosMarshalling.WitStream.class, round); + final WasmosMarshalling.WitStream back = (WasmosMarshalling.WitStream) round; + assertEquals(9L, back.tableId()); + assertEquals("StreamAny { id: TableId(5), ty: Guest(Stream(u32)) }", + back.typeIdentifier()); + assertEquals(ws, back); + assertEquals(ws.hashCode(), back.hashCode()); + } + + @Test + @DisplayName("WitStream with large tableId (u64-adjacent) round-trips") + void witStreamLargeTableId() { + final long largeId = 8_888_888_888L; + final WasmosMarshalling.WitStream ws = new WasmosMarshalling.WitStream(largeId, "S"); + final Object round = roundTrip(ws); + assertEquals(largeId, + ((WasmosMarshalling.WitStream) round).tableId()); + } + + @Test + @DisplayName("WitStream in a nested List round-trips through container marshalling") + void witStreamNestedInList() { + // list> return shape — validates the Stream writer + + // reader arms compose with container arms. + final List items = Arrays.asList( + new WasmosMarshalling.WitStream(1L, "S"), + new WasmosMarshalling.WitStream(2L, "S"), + new WasmosMarshalling.WitStream(3L, "S")); + final Object round = roundTrip(items); + assertInstanceOf(List.class, round); + final List back = (List) round; + assertEquals(3, back.size()); + assertEquals(new WasmosMarshalling.WitStream(1L, "S"), back.get(0)); + assertEquals(new WasmosMarshalling.WitStream(2L, "S"), back.get(1)); + assertEquals(new WasmosMarshalling.WitStream(3L, "S"), back.get(2)); + } + + @Test + @DisplayName("WitStream wire form uses \"Stream\" tag (matches Rust JsonVal::Stream)") + void witStreamWireFormat() { + // Explicit wire-format check — the Rust JsonVal enum is + // #[serde(tag = "t", content = "v")] and the tag string must match + // byte-for-byte. A tag rename here would silently break the JNI + // round-trip. + final WasmosMarshalling.WitStream ws = new WasmosMarshalling.WitStream( + 55L, "type-hint"); + final String wire = WasmosMarshalling.marshalArgs(new Object[] { ws }); + assertTrue(wire.contains("\"Stream\""), + "WitStream wire form must use tag=\"Stream\"; wire=" + wire); + assertTrue(wire.contains("\"table_id\":55"), + "wire form must expose table_id verbatim; wire=" + wire); + assertTrue(wire.contains("\"type_name\":\"type-hint\""), + "wire form must expose type_name verbatim; wire=" + wire); + } + + @Test + @DisplayName("WitErrorContext round-trips tableId + rep") + void witErrorContextRoundtrip() { + final WasmosMarshalling.WitErrorContext we = new WasmosMarshalling.WitErrorContext( + 4L, 12345L); + final Object round = roundTrip(we); + assertInstanceOf(WasmosMarshalling.WitErrorContext.class, round); + final WasmosMarshalling.WitErrorContext back = + (WasmosMarshalling.WitErrorContext) round; + assertEquals(4L, back.tableId()); + assertEquals(12345L, back.rep()); + assertEquals(we, back); + assertEquals(we.hashCode(), back.hashCode()); + } + + @Test + @DisplayName("WitErrorContext with u32-max rep round-trips (long-widened)") + void witErrorContextLargeRep() { + // wasmtime's rep is a u32; we widen to Java long on the wire. + // The 0xFFFFFFFF max must survive without silent truncation. + final long u32Max = 4_294_967_295L; + final WasmosMarshalling.WitErrorContext we = new WasmosMarshalling.WitErrorContext( + 1L, u32Max); + final Object round = roundTrip(we); + assertEquals(u32Max, + ((WasmosMarshalling.WitErrorContext) round).rep()); + } + + @Test + @DisplayName("WitErrorContext in a nested List round-trips through container marshalling") + void witErrorContextNestedInList() { + // list is an unusual but wire-valid shape — verify + // the ErrorContext writer + reader arms compose with List. Same + // regression guarantee as the WitFuture nested-list test. + final List items = Arrays.asList( + new WasmosMarshalling.WitErrorContext(1L, 1L), + new WasmosMarshalling.WitErrorContext(2L, 2L), + new WasmosMarshalling.WitErrorContext(3L, 3L)); + final Object round = roundTrip(items); + assertInstanceOf(List.class, round); + final List back = (List) round; + assertEquals(3, back.size()); + assertEquals(new WasmosMarshalling.WitErrorContext(1L, 1L), back.get(0)); + assertEquals(new WasmosMarshalling.WitErrorContext(2L, 2L), back.get(1)); + assertEquals(new WasmosMarshalling.WitErrorContext(3L, 3L), back.get(2)); + } + + @Test + @DisplayName("WitErrorContext wire form uses \"ErrorContext\" tag (matches Rust JsonVal::ErrorContext)") + void witErrorContextWireFormat() { + // Explicit wire-format check — the Rust JsonVal enum is + // #[serde(tag = "t", content = "v")] and the tag string must match + // byte-for-byte. A tag rename here would silently break the JNI + // round-trip. + final WasmosMarshalling.WitErrorContext we = new WasmosMarshalling.WitErrorContext( + 7L, 99L); + final String wire = WasmosMarshalling.marshalArgs(new Object[] { we }); + assertTrue(wire.contains("\"ErrorContext\""), + "WitErrorContext wire form must use tag=\"ErrorContext\"; wire=" + wire); + assertTrue(wire.contains("\"table_id\":7"), + "wire form must expose table_id verbatim; wire=" + wire); + assertTrue(wire.contains("\"rep\":99"), + "wire form must expose rep verbatim; wire=" + wire); + } + + @Test + @DisplayName("plain Map still routes to Record (not Map)") + void plainMapStillRoutesToRecord() { + // Regression guard for the tie-breaking rule: only WitMap should + // route to Map; a bare Map stays Record so existing + // record-shaped callers aren't disturbed. + final Map input = new LinkedHashMap<>(); + input.put("k", "v"); + final Object round = roundTrip(input); + assertInstanceOf(Map.class, round); + // The unmarshalled shape is a LinkedHashMap regardless; the + // distinction shows up on the wire as tag="Record" (unfortunately + // opaque from unmarshalResults). Sanity-check the actual wire + // string for the record tag. + final String wire = WasmosMarshalling.marshalArgs(new Object[] { input }); + assertTrue(wire.contains("\"Record\""), + "plain Map must marshal as Record, not Map; wire=" + wire); + } +} diff --git a/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProviderDemoTest.java b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProviderDemoTest.java new file mode 100644 index 0000000..ae20fe8 --- /dev/null +++ b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosProviderDemoTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +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 ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.spi.EngineProvider; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.ServiceLoader; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * End-to-end demo — load the wasmos {@code composed_wasmtime.wasm} + * fixture through the wasmos-provider, call the {@code run} export, assert + * 42. This is the wasmos-provider MVP's north-star acceptance test: + * passing it means a caller using ONLY webassembly4j's public API + + * selecting {@code "wasmos"} as the provider can drive a full + * {@code guest-demo-portable + adapter-wasmtime} composition through the + * bundled Rust wasmos-runtime. + * + *

The reference Rust equivalent lives at + * {@code ~/git/wasmos/tests/e2e_smoke.rs} and is the shape we mirror + * end-to-end. + * + *

Fixture location: {@code ~/git/wasmos/tests/e2e-fixtures/composed_wasmtime.wasm}. + * If absent, {@link org.junit.jupiter.api.Assumptions#assumeTrue} skips the + * test — regenerate via the wasmos build instructions. + */ +class WasmosProviderDemoTest { + + private static final Path COMPOSED_FIXTURE = Path.of( + System.getProperty("user.home"), + "git", "wasmos", "tests", "e2e-fixtures", "composed_wasmtime.wasm"); + + @Test + @DisplayName("wasmos-provider is registered via ServiceLoader with id=\"wasmos\"") + void serviceLoaderPicksUpWasmosProvider() { + boolean found = false; + for (EngineProvider p : ServiceLoader.load(EngineProvider.class)) { + if ("wasmos".equals(p.descriptor().providerId())) { + found = true; + assertEquals("wasmos", p.descriptor().engineId()); + assertTrue(p.descriptor().tags().contains("wasmos")); + break; + } + } + assertTrue(found, + "expected an EngineProvider with providerId=\"wasmos\" on the classpath — " + + "check META-INF/services/ai.tegmentum.webassembly4j.spi.EngineProvider"); + } + + @Test + @DisplayName("composed guest.run() returns 42 through wasmos-provider + JNI wasmos-runtime") + void composedRunReturns42() throws Exception { + assumeTrue(Files.exists(COMPOSED_FIXTURE), + "composed_wasmtime.wasm fixture not present at " + COMPOSED_FIXTURE + + " — rebuild via wasmos e2e_smoke instructions"); + + final byte[] bytes = Files.readAllBytes(COMPOSED_FIXTURE); + assertTrue(bytes.length > 0, "fixture must be non-empty"); + + final EngineProvider provider = ServiceLoader.load(EngineProvider.class) + .stream() + .map(ServiceLoader.Provider::get) + .filter(p -> "wasmos".equals(p.descriptor().providerId())) + .findFirst() + .orElseThrow(() -> new AssertionError( + "wasmos provider not on classpath — see the ServiceLoader test above")); + + try (Engine engine = provider.create(null); + Component component = engine.loadComponent(bytes)) { + assertNotNull(component, "wasmos-provider should compile the composed component"); + + // Sanity check: the exported functions should include "run" (the + // guest-demo-portable's entry point). If this fails, the fixture + // was rebuilt with a different shape and the whole test needs + // updating. + final List exports = ((WasmosComponentAdapter) component) + .exportedFunctionNames(); + assertTrue(exports.contains("run"), + "composed component should export 'run' — got: " + exports); + + final ComponentInstance instance = component.instantiate(); + try { + final Object result = instance.invoke("run"); + assertEquals(42, ((Number) result).intValue(), + "composed guest-demo-portable.run() must return 42 " + + "(same as wasmos/tests/e2e_smoke.rs)"); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + } +} diff --git a/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosTypedInvokeTest.java b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosTypedInvokeTest.java new file mode 100644 index 0000000..26fb88a --- /dev/null +++ b/wasmos-provider/src/test/java/ai/tegmentum/webassembly4j/provider/wasmos/WasmosTypedInvokeTest.java @@ -0,0 +1,402 @@ +/* + * Copyright 2026 Tegmentum AI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +package ai.tegmentum.webassembly4j.provider.wasmos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import ai.tegmentum.webassembly4j.api.Component; +import ai.tegmentum.webassembly4j.api.ComponentInstance; +import ai.tegmentum.webassembly4j.api.DefaultLinkingContext; +import ai.tegmentum.webassembly4j.api.DefaultWasiContext; +import ai.tegmentum.webassembly4j.api.Engine; +import ai.tegmentum.webassembly4j.api.HostFunctionDefinition; +import ai.tegmentum.webassembly4j.api.LinkingContext; +import ai.tegmentum.webassembly4j.api.ValueType; +import ai.tegmentum.webassembly4j.api.WasiContext; +import ai.tegmentum.webassembly4j.api.config.ComponentConfig; +import ai.tegmentum.webassembly4j.api.exception.UnsupportedFeatureException; +import ai.tegmentum.webassembly4j.spi.EngineProvider; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ServiceLoader; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * End-to-end typed-invoke tests — proves the Java args flow into Rust, + * wasmtime executes with them, and the Rust return path decodes back + * into a Java-natural object. + * + *

Uses {@code scalar-component.wasm} (mirrored into wasmos-provider's + * test resources from the endive fixture) — a minimal Component Model + * binary that exports {@code add: func(u32, u32) -> u32}. + * + *

Also verifies the linking-context + component-config plumbing — + * WASI-P2 opt-in, memory-limit passthrough, and the philosophical + * rejection of Java-hosted host functions with a helpful message. + */ +final class WasmosTypedInvokeTest { + + private static EngineProvider provider() { + return ServiceLoader.load(EngineProvider.class) + .stream() + .map(ServiceLoader.Provider::get) + .filter(p -> "wasmos".equals(p.descriptor().providerId())) + .findFirst() + .orElseThrow(() -> new AssertionError("wasmos provider not on classpath")); + } + + private static byte[] loadFixture(String name) throws IOException { + try (InputStream is = WasmosTypedInvokeTest.class + .getClassLoader() + .getResourceAsStream(name)) { + if (is == null) return null; + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final byte[] buf = new byte[8192]; + int n; + while ((n = is.read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + } + + private static void withInstance( + ComponentInstance instance, + InstanceRunnable body) throws Exception { + try { + body.run(instance); + } finally { + if (instance instanceof WasmosComponentInstanceAdapter) { + ((WasmosComponentInstanceAdapter) instance).closeInternal(); + } + } + } + + @FunctionalInterface + private interface InstanceRunnable { + void run(ComponentInstance instance) throws Exception; + } + + @Test + @DisplayName("scalar-component add(2, 3) returns 5 through typed JSON invoke path") + void scalarComponentAdd() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + assertNotNull(component, "component compile should succeed"); + withInstance(component.instantiate(), instance -> { + assertTrue(instance.hasFunction("add"), + "scalar-component should export 'add'; got " + + instance.exportedFunctions()); + // u32 args go via WitU32 wrappers so the JSON schema sends + // proper U32-tagged JsonVals that wasmtime type-checks + // against the func signature. + final Object result = instance.invoke( + "add", + new WasmosMarshalling.WitU32(2L), + new WasmosMarshalling.WitU32(3L)); + assertEquals(5L, ((Number) result).longValue(), + "add(2, 3) should be 5; got " + result); + }); + } + } + + @Test + @DisplayName("componentInstantiate honors ComponentConfig.maxMemoryBytes") + void componentConfigMemoryPasses() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + // A generous limit that the scalar guest can't come close to + // touching — we're validating the plumbing goes through, not + // stressing the limiter. + final ComponentConfig cfg = ComponentConfig.builder() + .maxMemoryBytes(64L * 1024 * 1024) // 64 MiB + .build(); + withInstance(component.instantiate(cfg), instance -> { + final Object result = instance.invoke( + "add", + new WasmosMarshalling.WitU32(10L), + new WasmosMarshalling.WitU32(20L)); + assertEquals(30L, ((Number) result).longValue()); + }); + } + } + + @Test + @DisplayName("LinkingContext with wasiContext instantiates cleanly (WASI-P2 plumbing)") + void linkingContextWithWasi() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + final WasiContext wasi = DefaultWasiContext.builder() + .addArg("wasmos-provider-test") + .env("HELLO", "world") + .build(); + final LinkingContext ctx = DefaultLinkingContext.builder() + .wasiContext(wasi) + .build(); + withInstance(component.instantiate(ctx), instance -> { + // scalar-component doesn't touch WASI — we're validating the + // instantiation plumbing itself doesn't throw when a WasiCtx + // is configured. + final Object result = instance.invoke( + "add", + new WasmosMarshalling.WitU32(7L), + new WasmosMarshalling.WitU32(35L)); + assertEquals(42L, ((Number) result).longValue()); + }); + } + } + + @Test + @DisplayName("linking-context with Java-hosted host functions is rejected with a useful message") + void hostFunctionsRejected() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + final LinkingContext ctx = DefaultLinkingContext.builder() + .addHostFunction(new HostFunctionDefinition( + "mod", + "fn", + new ValueType[0], + new ValueType[0], + args -> new Object[0])) + .build(); + final UnsupportedFeatureException ex = assertThrows( + UnsupportedFeatureException.class, + () -> component.instantiate(ctx)); + // The error message must guide the user to the wasmtime provider — + // that's the whole point of the philosophical rejection. + assertTrue(ex.getMessage().contains("wasmtime"), + "error should mention the \"wasmtime\" fallback provider; got: " + + ex.getMessage()); + } + } + + @Test + @DisplayName("ComponentConfig.fuelLimit is honored; instantiation succeeds and function runs") + void fuelLimitAccepted() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + // Generous fuel budget — add(u32, u32) is a trivial op so any + // reasonable amount lets it complete. Focus is validating the + // set_fuel plumbing succeeds and doesn't blow up mid-invoke, + // not measuring exact fuel consumption. + final ComponentConfig cfg = ComponentConfig.builder() + .fuelLimit(1_000_000_000L) + .build(); + withInstance(component.instantiate(cfg), instance -> { + final Object result = instance.invoke( + "add", + new WasmosMarshalling.WitU32(11L), + new WasmosMarshalling.WitU32(22L)); + assertEquals(33L, ((Number) result).longValue()); + }); + } + } + + @Test + @DisplayName("Extended ComponentConfig fields (tables/instances/memories/table-elements) instantiate cleanly") + void extendedComponentConfigFields() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + // All limits set to generous values that scalar-component + // won't come close to hitting. Validates the JNI signature + + // wire-through, not the limiter's fault behaviour (those + // would need a purpose-built component to exercise). + final ComponentConfig cfg = ComponentConfig.builder() + .maxTableElements(1024L) + .maxInstances(32L) + .maxTables(8L) + .maxMemories(4L) + .build(); + withInstance(component.instantiate(cfg), instance -> { + final Object result = instance.invoke( + "add", + new WasmosMarshalling.WitU32(1L), + new WasmosMarshalling.WitU32(2L)); + assertEquals(3L, ((Number) result).longValue()); + }); + } + } + + @Test + @DisplayName("Component.serialize() + Engine.deserializeComponent round-trip preserves behaviour") + void serializeRoundTrip() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + final byte[] cached; + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + cached = component.serialize(); + assertNotNull(cached, "serialize() must return non-null AOT bytes"); + assertTrue(cached.length > 0, "serialized bytes must be non-empty"); + } + + // Fresh engine, deserialize the cached bytes rather than compile + // from the raw component source. Ensures the byte cache is + // engine-independent within a matching wasmtime Config. + try (Engine engine = provider().create(null)) { + final Component reloaded = + ((WasmosEngineAdapter) engine).deserializeComponent(cached); + try (Component c = reloaded) { + withInstance(c.instantiate(), instance -> { + final Object result = instance.invoke( + "add", + new WasmosMarshalling.WitU32(40L), + new WasmosMarshalling.WitU32(2L)); + assertEquals(42L, ((Number) result).longValue(), + "deserialized component must produce identical results"); + }); + } + } + } + + // --------------------------------------------------------------------- + // Val::Resource end-to-end: constructor -> increment (x2) -> get. The + // fixture is `counter-component.wasm`, built from + // `counter-fixture-src/` (see the README there for the rebuild recipe). + // Exercises the full round-trip: guest returns own, Java + // parks it in the Rust ResourceRegistry as a WitResource, then passes + // that WitResource back in as borrow for the method calls. + // --------------------------------------------------------------------- + + /** Fully-qualified export names — the resource lives inside an + * exported interface, so top-level `Instance::get_func` can't see it. + * The wasmos-provider native resolver splits on the first `#` and + * walks `get_export_index` twice. See + * `wasmos-provider/native/src/lib.rs::resolve_func`. */ + private static final String CTR_IFACE = "tegmentum:counter/counter-api@0.1.0"; + private static final String CTR_CONSTRUCTOR = CTR_IFACE + "#[constructor]counter"; + private static final String CTR_INCREMENT = CTR_IFACE + "#[method]counter.increment"; + private static final String CTR_GET = CTR_IFACE + "#[method]counter.get"; + + @Test + @DisplayName("counter-component: constructor -> increment x2 -> get returns 12 (Val::Resource E2E)") + void counterResourceRoundTrip() throws Exception { + final byte[] bytes = loadFixture("counter-component.wasm"); + assumeTrue(bytes != null, "counter-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + assertNotNull(component, "counter-component should compile"); + withInstance(component.instantiate(), instance -> { + // Constructor returns an owned counter resource; wasmos-provider + // parks the ResourceAny in the per-instance ResourceRegistry and + // hands Java back a WitResource carrying { tableId, typeName, + // owned=true }. + final Object created = instance.invoke( + CTR_CONSTRUCTOR, + new WasmosMarshalling.WitU32(10L)); + assertNotNull(created, "constructor must return the counter handle"); + assertTrue(created instanceof WasmosMarshalling.WitResource, + "constructor should return a WitResource; got " + + created.getClass().getName() + " = " + created); + final WasmosMarshalling.WitResource owned = + (WasmosMarshalling.WitResource) created; + assertTrue(owned.owned(), + "guest return of own must be owned=true; got " + owned); + assertTrue(owned.tableId() >= 0, + "tableId must be a valid slot id; got " + owned.tableId()); + + // Increment twice — WIT `increment: func()` takes an implicit + // borrow of `self`, i.e. `borrow` in the ABI. We pass + // the same WitResource with owned=false so the Rust registry + // does `peek` (leaves it parked) rather than `take` (removes + // it), which lets us reuse the handle across calls. + final WasmosMarshalling.WitResource borrow = + new WasmosMarshalling.WitResource( + owned.tableId(), owned.typeIdentifier(), false); + + final Object r1 = instance.invoke(CTR_INCREMENT, borrow); + // increment returns () — the JSON path unwraps arity-0 to null. + assertEquals(null, r1, "increment returns unit; got " + r1); + + final Object r2 = instance.invoke(CTR_INCREMENT, borrow); + assertEquals(null, r2, "increment returns unit; got " + r2); + + // get — same borrow-shaped call, but returns u32. + final Object got = instance.invoke(CTR_GET, borrow); + assertNotNull(got, "get must return a value"); + // WIT u32 comes back as Long (WitU32 domain) — accept both + // Number widths defensively. + assertEquals(12L, ((Number) got).longValue(), + "counter started at 10, +1 +1 -> 12; got " + got); + }); + } + } + + @Test + @DisplayName("counter-component: WitResource handles remain valid across instantiation close") + void counterResourceDroppedOnInstanceClose() throws Exception { + // Closing the instance drops the ResourceRegistry alongside the + // Store — no leaks even if the caller never explicitly hands the + // resource back. Sanity-check that the close path doesn't throw + // when live resources are still parked. + final byte[] bytes = loadFixture("counter-component.wasm"); + assumeTrue(bytes != null, "counter-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + withInstance(component.instantiate(), instance -> { + // Create a resource and immediately walk away — the close + // hook driven by withInstance should tear it down cleanly. + final Object created = instance.invoke( + CTR_CONSTRUCTOR, + new WasmosMarshalling.WitU32(0L)); + assertTrue(created instanceof WasmosMarshalling.WitResource, + "constructor should still return a WitResource: " + created); + }); + } + } + + @Test + @DisplayName("negative ComponentConfig field values are rejected with a helpful message") + void negativeConfigValuesRejected() throws Exception { + final byte[] bytes = loadFixture("scalar-component.wasm"); + assumeTrue(bytes != null, "scalar-component.wasm missing from test resources"); + + try (Engine engine = provider().create(null); + Component component = engine.loadComponent(bytes)) { + final ComponentConfig cfg = ComponentConfig.builder() + .fuelLimit(-5L) // negative collides with the -1 sentinel on the JNI boundary + .build(); + final IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> component.instantiate(cfg)); + assertTrue(ex.getMessage().contains("fuelLimit"), + "error should identify the offending field; got: " + ex.getMessage()); + } + } +} diff --git a/wasmos-provider/src/test/resources/counter-component.wasm b/wasmos-provider/src/test/resources/counter-component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5ba6dfde1bae7b4942b46edc24d691da200dd50c GIT binary patch literal 6281 zcmcIp+i%;}8UM~9DN>?T%Sn@UOSh701|%@#ii&S3P%Lbg1Vz%cYlUox?O8aZK|2mPHdKD8-CB); zIBfNL!*;M0rNL@6=ybzD&Zq3W;HD%fAmrtEC_(`?yS3+jvQ2>%h!E=dbRIwwEf zQQykG^mWXX*YU8{{a-4HN^bQN9=p;fN1T)8rHn;ha^#f?InseF-I2anlK56t*0~Z= zCYXmEqC)U!pYBWLXDBbH3|pM}u&xv1w9u;-klnt{iXEc?POK#g3l2}ks5 zNUkcx?NqOysj=xgO!tNVk+S<^e&%)y$8dCIH^FAXHjCG388$->3ryF28N+y=+=A_3 zmHi>7!2amF_S!mD>`x8`qmWq&xmW}gnqyS|z`AQD)D!?Drg%x8#q18L2lSXu;heO5 zah0iHTg`N&^3sA3lNBG5=bARe?P6!zMXG+r63M*9!{1~U@mHNK{C)QQ&NnGq3HQZ1g2XC*o2kI)ciBh-BzxZm zP!0PuUrW)9#ClCr5wCHmjaZG&$u>fo`n&FNfsuUy3E?k zP8k95wN0mpJlB~j6Xq5bhbx%3$fOX+DFISu=c-WDZfQygyGmGAVnD+qK#Y;xWPaPR znP1ft@N;aIV7mrSPu&EK@{{MMMzgK9Q`~T@H!6_F=BUciXI0}sXm=R3b*?Deu{IoY zvnJkTgQ3n9PQfW|Ryag#c5XGXd#+GOx!ih?_zVk5Ks5l2l~)w0Xvk3g$?EygUt4Sp^W7=2h7|ei!A!A zhYhvXXBb1(SOqXdC$$lE`yU6JkUXp4;V7`Z2nsUvfh!b@9#=jzcpc?e7^k4061Dj@Ycanc*||a zK#r7=Fwnj;`7{!J4qw>*6g^MzP=nR=Vq{$*Vh6zCiR-P0mnPJQ44%zR}VQG1PoNr z#!)ojBXy$vOu>~tDMSm6AP9QVU?XUclXTb*lCYgd!$HsqlXe^d$!-w!`Q0SZpZS1q zjM=CRyUWR9=X!Gc+SaWbYd3?7TVWWebF|k?qW8m~-D@RDn85eNpxL;Esmna$^HKZl zVJ~WLE0jEM-%PWK5C!cJ7_1HBU@eBsUX-MT-nXjOyU>pYnOZj8e1)|g>)x{RlHV5Y zBfZw5Ue8*XhAP~;ePiR|yBlvVZv~5rE_-?__gjNbFGnZ{<9HYspZ;B`^sKK+wsPqv zlBu)NNxfFf?|SbB3+-VXE)7SVCfFzDvXvN)dUt(v|Pm<~QDo6;kDn*0HI-Bnff=ae1 zV$=6&7AG@$We-nr06%dwHZ+$V1x}tg0Nty}{irWy1eN*r2au{;G4bIc!z6B0*qnX# zjD!$H6K4*x1wFkvFHTP;2hG9xWIRfwE=W)T((xf0rXhYb5o8%sl14I=LLPHVCyu*$ zlaTZ@w}2-^;Vq~VGQWN!0sYfhK$d*^Z9={Pal10uFLp-I2@SE1=#ns_L6k`HS= ziXM<(Op1dJmVO}UyGOI2(Fnt?p!?7yOdg|p{%S-O6U{!s K;Y, +} + +impl GuestCounter for Counter { + fn new(initial: u32) -> Self { + Counter { + value: Cell::new(initial), + } + } + + fn increment(&self) { + self.value.set(self.value.get().wrapping_add(1)); + } + + fn get(&self) -> u32 { + self.value.get() + } +} + +impl Guest for Component { + type Counter = Counter; +} + +bindings::export!(Component with_types_in bindings); + +// Provide a minimal panic handler — we're `no_std` so libcore's default +// panic handler (which lives in `std`) isn't available. Aborting is fine +// for a test fixture: the counter methods are infallible and no path +// panics under normal use. +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + core::arch::wasm32::unreachable() +} diff --git a/wasmos-provider/src/test/resources/counter-fixture-src/wit/counter.wit b/wasmos-provider/src/test/resources/counter-fixture-src/wit/counter.wit new file mode 100644 index 0000000..14432df --- /dev/null +++ b/wasmos-provider/src/test/resources/counter-fixture-src/wit/counter.wit @@ -0,0 +1,13 @@ +package tegmentum:counter@0.1.0; + +interface counter-api { + resource counter { + constructor(initial: u32); + increment: func(); + get: func() -> u32; + } +} + +world counter-world { + export counter-api; +} diff --git a/wasmos-provider/src/test/resources/counter.wit b/wasmos-provider/src/test/resources/counter.wit new file mode 100644 index 0000000..14432df --- /dev/null +++ b/wasmos-provider/src/test/resources/counter.wit @@ -0,0 +1,13 @@ +package tegmentum:counter@0.1.0; + +interface counter-api { + resource counter { + constructor(initial: u32); + increment: func(); + get: func() -> u32; + } +} + +world counter-world { + export counter-api; +} diff --git a/wasmos-provider/src/test/resources/scalar-component.wasm b/wasmos-provider/src/test/resources/scalar-component.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a67fa30c2fc73f40d5a2e3cf8f1df7a309dba5f2 GIT binary patch literal 147 zcmWlSF%H5o6hrObD4!641ShE2**E}4AF=2VSHgYEw*LX?K^8Fp literal 0 HcmV?d00001 diff --git a/wasmos-runtime-generated/pom.xml b/wasmos-runtime-generated/pom.xml new file mode 100644 index 0000000..5d925d8 --- /dev/null +++ b/wasmos-runtime-generated/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + ai.tegmentum.webassembly4j + webassembly4j-parent + 2.5.2-SNAPSHOT + + + wasmos-runtime-generated + jar + + WebAssembly4J :: wasmos-runtime generated bindings + + Java bindings generated by webassembly4j-bindgen from + wasmos:runtime@0.2.0 — the portable, backend-agnostic runtime API + that wasmos adapters (adapter-wasmtime, adapter-wamr, adapter-js) + implement (see ~/git/wasmos). The initial slice binds the six + interfaces that make up the core runtime surface: + + * core — core-Wasm compile / instantiate / invoke + * component-model— rich CM management API + * run — one-shot fire-and-forget execution + * capabilities — cap-set definitions + * info — supports() / version() introspection + * types — portable value / capability / error types + + Plus the transitive tegmentum:common/errors dep that types.wit + pulls in. + + The remaining wasmos:runtime interfaces (http, http-streaming, + websocket, timers, provider-manifest) are excluded from this + slice until bindgen learns to map deeply-nested container + record fields cleanly — see the parent's task ledger. + + The content of this module is the generated source tree under + ai.tegmentum.wasmos.runtime.generated plus a local copy of + WitResult so the jar has no compile-time or runtime dependency + on webassembly4j-bindgen (which is a maven-plugin-packaged + build-time-only artifact). Providers that implement the + wasmos:runtime SPI depend on this module for the generated + surface + WitResult. + + Regenerate with: + + java -jar webassembly4j-bindgen/target/webassembly4j-bindgen-2.5.2-SNAPSHOT-cli.jar \ + -w ~/git/wasmos/wit/deps/tegmentum-common \ + -w ~/git/wasmos/wit/deps/wasmos-runtime/types.wit \ + -w ~/git/wasmos/wit/deps/wasmos-runtime/capabilities.wit \ + -w ~/git/wasmos/wit/deps/wasmos-runtime/core.wit \ + -w ~/git/wasmos/wit/deps/wasmos-runtime/component-model.wit \ + -w ~/git/wasmos/wit/deps/wasmos-runtime/run.wit \ + -w ~/git/wasmos/wit/deps/wasmos-runtime/info.wit \ + --package ai.tegmentum.wasmos.runtime.generated \ + --output wasmos-runtime-generated/src/main/java \ + --no-impl \ + --runtime-provider WasmosRuntime + + + + + ai.tegmentum.webassembly4j + webassembly4j-api + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + + + + + diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capabilities.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capabilities.java new file mode 100644 index 0000000..bf54893 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capabilities.java @@ -0,0 +1,4 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public interface Capabilities { +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capability.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capability.java new file mode 100644 index 0000000..94145f5 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Capability.java @@ -0,0 +1,40 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.Optional; + +public final class Capability { + private final String name; + + private final Optional scope; + + public Capability(String name, Optional scope) { + this.name = name; + this.scope = scope; + } + + public String name() { + return this.name; + } + + public Optional scope() { + return this.scope; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Capability that = (Capability) obj; + return java.util.Objects.equals(this.name, that.name) && java.util.Objects.equals(this.scope, that.scope); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(name, scope); + } + + @Override + public String toString() { + return "Capability[name=" + name + ", scope=" + scope + "]"; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilityEntry.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilityEntry.java new file mode 100644 index 0000000..bfbed5a --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilityEntry.java @@ -0,0 +1,55 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.Optional; + +public final class CapabilityEntry { + private final String name; + + private final boolean native_; + + private final boolean available; + + private final Optional provider; + + public CapabilityEntry(String name, boolean native_, boolean available, + Optional provider) { + this.name = name; + this.native_ = native_; + this.available = available; + this.provider = provider; + } + + public String name() { + return this.name; + } + + public boolean native_() { + return this.native_; + } + + public boolean available() { + return this.available; + } + + public Optional provider() { + return this.provider; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + CapabilityEntry that = (CapabilityEntry) obj; + return java.util.Objects.equals(this.name, that.name) && this.native_ == that.native_ && this.available == that.available && java.util.Objects.equals(this.provider, that.provider); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(name, native_, available, provider); + } + + @Override + public String toString() { + return "CapabilityEntry[name=" + name + ", native_=" + native_ + ", available=" + available + ", provider=" + provider + "]"; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilitySet.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilitySet.java new file mode 100644 index 0000000..21e696d --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CapabilitySet.java @@ -0,0 +1,40 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.List; + +public final class CapabilitySet { + private final List entries; + + private final String source; + + public CapabilitySet(List entries, String source) { + this.entries = entries; + this.source = source; + } + + public List entries() { + return this.entries; + } + + public String source() { + return this.source; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + CapabilitySet that = (CapabilitySet) obj; + return java.util.Objects.equals(this.entries, that.entries) && java.util.Objects.equals(this.source, that.source); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(entries, source); + } + + @Override + public String toString() { + return "CapabilitySet[entries=" + entries + ", source=" + source + "]"; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Component.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Component.java new file mode 100644 index 0000000..48bdd99 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Component.java @@ -0,0 +1,34 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Resource type: component + * + *

This resource should be closed after use. + */ +public class Component implements AutoCloseable { + private final long handle; + + public Component(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + WasmosRuntimeRegistry.runtime().componentClose(this.handle); + } + + public static WitResult fromBytes(Runtime runtime, List bytes) { + return WasmosRuntimeRegistry.runtime().componentFromBytes(runtime, bytes); + } + + public List exportNames() { + return WasmosRuntimeRegistry.runtime().componentExportNames(this.handle); + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ComponentModel.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ComponentModel.java new file mode 100644 index 0000000..2b4cb05 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ComponentModel.java @@ -0,0 +1,4 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public interface ComponentModel { +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Core.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Core.java new file mode 100644 index 0000000..6776333 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Core.java @@ -0,0 +1,4 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public interface Core { +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CoreInstance.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CoreInstance.java new file mode 100644 index 0000000..4d9aa8f --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/CoreInstance.java @@ -0,0 +1,43 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Resource type: core-instance + * + *

This resource should be closed after use. + */ +public class CoreInstance implements AutoCloseable { + private final long handle; + + public CoreInstance(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + WasmosRuntimeRegistry.runtime().coreInstanceClose(this.handle); + } + + public static WitResult fromModule(Runtime runtime, Module module, + List caps) { + return WasmosRuntimeRegistry.runtime().coreInstanceFromModule(runtime, module, caps); + } + + public WitResult, Error> invoke(String exportName, List args) { + return WasmosRuntimeRegistry.runtime().coreInstanceInvoke(this.handle, exportName, args); + } + + public WitResult, Error> readMemory(int offset, int len) { + return WasmosRuntimeRegistry.runtime().coreInstanceReadMemory(this.handle, offset, len); + } + + public WitResult writeMemory(int offset, List bytes) { + return WasmosRuntimeRegistry.runtime().coreInstanceWriteMemory(this.handle, offset, bytes); + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Error.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Error.java new file mode 100644 index 0000000..51315dc --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Error.java @@ -0,0 +1,38 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public final class Error { + private final ErrorCategory category; + + private final String message; + + public Error(ErrorCategory category, String message) { + this.category = category; + this.message = message; + } + + public ErrorCategory category() { + return this.category; + } + + public String message() { + return this.message; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Error that = (Error) obj; + return java.util.Objects.equals(this.category, that.category) && java.util.Objects.equals(this.message, that.message); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(category, message); + } + + @Override + public String toString() { + return "Error[category=" + category + ", message=" + message + "]"; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ErrorCategory.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ErrorCategory.java new file mode 100644 index 0000000..fe131c2 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ErrorCategory.java @@ -0,0 +1,19 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public enum ErrorCategory { + NOT_FOUND, + + ALREADY_EXISTS, + + PERMISSION_DENIED, + + INVALID_ARGUMENT, + + RESOURCE_EXHAUSTED, + + IO, + + UNSUPPORTED, + + INTERNAL +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Errors.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Errors.java new file mode 100644 index 0000000..13822cb --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Errors.java @@ -0,0 +1,4 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public interface Errors { +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Handles.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Handles.java new file mode 100644 index 0000000..c265b1e --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Handles.java @@ -0,0 +1,4 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public interface Handles { +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Info.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Info.java new file mode 100644 index 0000000..bad3d20 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Info.java @@ -0,0 +1,15 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.Optional; + +public interface Info { + RuntimeInfo getInfo(); + + boolean supports(String feature); + + boolean native_(String feature); + + boolean available(String feature); + + Optional provider(String feature); +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Instance.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Instance.java new file mode 100644 index 0000000..59c5f9d --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Instance.java @@ -0,0 +1,35 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Resource type: instance + * + *

This resource should be closed after use. + */ +public class Instance implements AutoCloseable { + private final long handle; + + public Instance(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + WasmosRuntimeRegistry.runtime().instanceClose(this.handle); + } + + public static WitResult fromComponent(Runtime runtime, Component component, + List caps) { + return WasmosRuntimeRegistry.runtime().instanceFromComponent(runtime, component, caps); + } + + public WitResult, Error> invoke(String exportName, List args) { + return WasmosRuntimeRegistry.runtime().instanceInvoke(this.handle, exportName, args); + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Module.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Module.java new file mode 100644 index 0000000..864b238 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Module.java @@ -0,0 +1,34 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Resource type: module + * + *

This resource should be closed after use. + */ +public class Module implements AutoCloseable { + private final long handle; + + public Module(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + WasmosRuntimeRegistry.runtime().moduleClose(this.handle); + } + + public static WitResult fromBytes(Runtime runtime, List bytes) { + return WasmosRuntimeRegistry.runtime().moduleFromBytes(runtime, bytes); + } + + public List exportNames() { + return WasmosRuntimeRegistry.runtime().moduleExportNames(this.handle); + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ResourceLimits.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ResourceLimits.java new file mode 100644 index 0000000..1562c29 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/ResourceLimits.java @@ -0,0 +1,40 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.Optional; + +public final class ResourceLimits { + private final Optional maxMemoryBytes; + + private final Optional cycleBudget; + + public ResourceLimits(Optional maxMemoryBytes, Optional cycleBudget) { + this.maxMemoryBytes = maxMemoryBytes; + this.cycleBudget = cycleBudget; + } + + public Optional maxMemoryBytes() { + return this.maxMemoryBytes; + } + + public Optional cycleBudget() { + return this.cycleBudget; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + ResourceLimits that = (ResourceLimits) obj; + return java.util.Objects.equals(this.maxMemoryBytes, that.maxMemoryBytes) && java.util.Objects.equals(this.cycleBudget, that.cycleBudget); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(maxMemoryBytes, cycleBudget); + } + + @Override + public String toString() { + return "ResourceLimits[maxMemoryBytes=" + maxMemoryBytes + ", cycleBudget=" + cycleBudget + "]"; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Run.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Run.java new file mode 100644 index 0000000..2f0969b --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Run.java @@ -0,0 +1,8 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +public interface Run { + WitResult exec(List componentBytes, List caps); +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Runtime.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Runtime.java new file mode 100644 index 0000000..44c6826 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Runtime.java @@ -0,0 +1,27 @@ +package ai.tegmentum.wasmos.runtime.generated; + +/** + * Resource type: runtime + * + *

This resource should be closed after use. + */ +public class Runtime implements AutoCloseable { + private final long handle; + + public Runtime(long handle) { + this.handle = handle; + } + + public long handle() { + return this.handle; + } + + @Override + public void close() { + WasmosRuntimeRegistry.runtime().runtimeClose(this.handle); + } + + public static Runtime create(ResourceLimits limits) { + return WasmosRuntimeRegistry.runtime().runtimeCreate(limits); + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/RuntimeInfo.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/RuntimeInfo.java new file mode 100644 index 0000000..d2d0dd2 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/RuntimeInfo.java @@ -0,0 +1,47 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.List; + +public final class RuntimeInfo { + private final String name; + + private final String version; + + private final List features; + + public RuntimeInfo(String name, String version, List features) { + this.name = name; + this.version = version; + this.features = features; + } + + public String name() { + return this.name; + } + + public String version() { + return this.version; + } + + public List features() { + return this.features; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + RuntimeInfo that = (RuntimeInfo) obj; + return java.util.Objects.equals(this.name, that.name) && java.util.Objects.equals(this.version, that.version) && java.util.Objects.equals(this.features, that.features); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(name, version, features); + } + + @Override + public String toString() { + return "RuntimeInfo[name=" + name + ", version=" + version + ", features=" + features + "]"; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Types.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Types.java new file mode 100644 index 0000000..4de8c68 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Types.java @@ -0,0 +1,4 @@ +package ai.tegmentum.wasmos.runtime.generated; + +public interface Types { +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Value.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Value.java new file mode 100644 index 0000000..849119c --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/Value.java @@ -0,0 +1,82 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.List; + +public interface Value { + final class VI32 implements Value { + private final int value; + + public VI32(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + } + + final class VI64 implements Value { + private final long value; + + public VI64(long value) { + this.value = value; + } + + public long value() { + return this.value; + } + } + + final class VF32 implements Value { + private final float value; + + public VF32(float value) { + this.value = value; + } + + public float value() { + return this.value; + } + } + + final class VF64 implements Value { + private final double value; + + public VF64(double value) { + this.value = value; + } + + public double value() { + return this.value; + } + } + + final class VString implements Value { + private final String value; + + public VString(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + } + + final class VBytes implements Value { + private final List value; + + public VBytes(List value) { + this.value = value; + } + + public List value() { + return this.value; + } + } + + final class VNull implements Value { + public VNull() { + } + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntime.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntime.java new file mode 100644 index 0000000..90b35a7 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntime.java @@ -0,0 +1,50 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import ai.tegmentum.webassembly4j.bindgen.wit.WitResult; +import java.util.List; + +/** + * Runtime-provider SPI for the generated bindings. + * + *

The embedder implements this interface once and installs the implementation + * via {@link WasmosRuntimeRegistry#install(WasmosRuntime)}. Generated resource method bodies dispatch + * through {@link WasmosRuntimeRegistry#runtime()} to reach the installed provider. + * + *

Emitted by webassembly4j-bindgen 2.0 (runtime-provider dispatch mode). + */ +public interface WasmosRuntime { + Runtime runtimeCreate(ResourceLimits limits); + + void runtimeClose(long handle); + + WitResult moduleFromBytes(Runtime runtime, List bytes); + + List moduleExportNames(long handle); + + void moduleClose(long handle); + + WitResult coreInstanceFromModule(Runtime runtime, Module module, + List caps); + + WitResult, Error> coreInstanceInvoke(long handle, String exportName, + List args); + + WitResult, Error> coreInstanceReadMemory(long handle, int offset, int len); + + WitResult coreInstanceWriteMemory(long handle, int offset, List bytes); + + void coreInstanceClose(long handle); + + WitResult componentFromBytes(Runtime runtime, List bytes); + + List componentExportNames(long handle); + + void componentClose(long handle); + + WitResult instanceFromComponent(Runtime runtime, Component component, + List caps); + + WitResult, Error> instanceInvoke(long handle, String exportName, List args); + + void instanceClose(long handle); +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntimeRegistry.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntimeRegistry.java new file mode 100644 index 0000000..5bb59fa --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/wasmos/runtime/generated/WasmosRuntimeRegistry.java @@ -0,0 +1,36 @@ +package ai.tegmentum.wasmos.runtime.generated; + +import java.util.Objects; + +/** + * Single-slot registry for the {@link WasmosRuntime} runtime-provider SPI. + * + *

Call {@link #install(WasmosRuntime)} once from the embedder's startup path, before + * invoking any generated resource method. Generated bodies reach the installed + * provider via {@link #runtime()}, which throws {@link IllegalStateException} if + * nothing was installed. + * + *

Emitted by webassembly4j-bindgen 2.0. + */ +public final class WasmosRuntimeRegistry { + private static volatile WasmosRuntime provider; + + private WasmosRuntimeRegistry() { + } + + public static void install(WasmosRuntime provider) { + WasmosRuntimeRegistry.provider = Objects.requireNonNull(provider, "provider"); + } + + public static void uninstall() { + WasmosRuntimeRegistry.provider = null; + } + + public static WasmosRuntime runtime() { + WasmosRuntime r = provider; + if (r == null) { + throw new IllegalStateException("WasmosRuntime not installed; call WasmosRuntimeRegistry.install(...) before invoking generated bindings"); + } + return r; + } +} diff --git a/wasmos-runtime-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java b/wasmos-runtime-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java new file mode 100644 index 0000000..ba37ff4 --- /dev/null +++ b/wasmos-runtime-generated/src/main/java/ai/tegmentum/webassembly4j/bindgen/wit/WitResult.java @@ -0,0 +1,127 @@ +package ai.tegmentum.webassembly4j.bindgen.wit; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +/** + * Runtime representation of the WIT {@code result} type. + * + *

The bindgen code generator references this class by its fully-qualified + * name (see {@link + * ai.tegmentum.webassembly4j.bindgen.generator.TypeMappingRegistry}); it must + * therefore exist as a real class on the consumer's classpath. The two + * factory methods {@link #ok(Object)} and {@link #err(Object)} construct + * either side; the resulting instance is immutable. + * + *

Generated code typically follows this pattern: + * + *

{@code
+ * WitResult result = parser.parse(language, source);
+ * if (result.isOk()) {
+ *     ParseResult tree = result.unwrap();
+ * } else {
+ *     String msg = result.unwrapErr();
+ * }
+ * }
+ * + * @param the success-payload type ({@link Void} when the WIT signature is + * {@code result<_, err>}) + * @param the error-payload type ({@link Void} when the WIT signature is + * {@code result} or {@code result}) + */ +public final class WitResult { + + private final T ok; + private final E err; + private final boolean isOk; + + private WitResult(final T ok, final E err, final boolean isOk) { + this.ok = ok; + this.err = err; + this.isOk = isOk; + } + + /** Construct a successful result. */ + public static WitResult ok(final T value) { + return new WitResult<>(value, null, true); + } + + /** Construct an error result. */ + public static WitResult err(final E value) { + return new WitResult<>(null, value, false); + } + + /** True if this is a success result. */ + public boolean isOk() { + return isOk; + } + + /** True if this is an error result. */ + public boolean isErr() { + return !isOk; + } + + /** Returns the success payload or throws when {@link #isErr()}. */ + public T unwrap() { + if (!isOk) { + throw new NoSuchElementException("WitResult is err: " + err); + } + return ok; + } + + /** Returns the error payload or throws when {@link #isOk()}. */ + public E unwrapErr() { + if (isOk) { + throw new NoSuchElementException("WitResult is ok: " + ok); + } + return err; + } + + /** Optional view of the success side. */ + public Optional ok() { + return isOk ? Optional.ofNullable(ok) : Optional.empty(); + } + + /** Optional view of the error side. */ + public Optional err() { + return isOk ? Optional.empty() : Optional.ofNullable(err); + } + + /** Map the success payload through {@code f}, leaving an error unchanged. */ + public WitResult map(final Function f) { + Objects.requireNonNull(f, "f"); + return isOk ? WitResult.ok(f.apply(ok)) : WitResult.err(err); + } + + /** Map the error payload through {@code f}, leaving a success unchanged. */ + public WitResult mapErr(final Function f) { + Objects.requireNonNull(f, "f"); + return isOk ? WitResult.ok(ok) : WitResult.err(f.apply(err)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof WitResult)) { + return false; + } + final WitResult other = (WitResult) o; + return isOk == other.isOk + && Objects.equals(ok, other.ok) + && Objects.equals(err, other.err); + } + + @Override + public int hashCode() { + return Objects.hash(ok, err, isOk); + } + + @Override + public String toString() { + return isOk ? "Ok(" + ok + ")" : "Err(" + err + ")"; + } +} From e46b17f7fcacb92a3922e48f5932e8e99506fb01 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 30 Jul 2026 17:32:37 -0400 Subject: [PATCH 84/85] chore(wasmos-provider): use relative ../../../wasmos path dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the absolute path dep (`/Users/zacharywhitley/git/wasmos` — only works on the original author's machine) for a relative path (`../../../wasmos` from wasmos-provider/native/Cargo.toml). Anyone with the standard sibling-checkout layout (~/git/webassembly4j + ~/git/wasmos + ~/git/wasm-cm + ~/git/wasm-continuity, or any other common-parent arrangement) now gets a working build. The path dep itself is unchanged in kind — still blocks external / CI builds and Central publish until wasmos + wasm-cm + wasm-continuity publish versioned crates or wasmos restructures away from `path = "..//..."` for its own deps. wasmos-provider/README.md and Cargo.toml comment both spell out the tree-layout requirement. --- wasmos-provider/README.md | 21 ++++++++++++--------- wasmos-provider/native/Cargo.toml | 29 ++++++++++++++--------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/wasmos-provider/README.md b/wasmos-provider/README.md index 6af97f2..ebbfeb8 100644 --- a/wasmos-provider/README.md +++ b/wasmos-provider/README.md @@ -47,20 +47,23 @@ Selected via `EngineSelection.byId("wasmos")`; provider priority 90. ## Build setup — REQUIRES sibling repository checkouts -`wasmos-runtime` (this module's Rust dep) declares path deps on other -tegmentum repositories that must be co-checked-out at fixed relative -paths. **Building this crate from a fresh clone of `webassembly4j` -alone WILL fail.** You need: +`wasmos-runtime` (this module's Rust dep) is declared via a RELATIVE +path (`../../../wasmos` from `wasmos-provider/native/Cargo.toml`), +which itself declares path deps on other tegmentum repositories that +must be co-checked-out at fixed relative paths. **Building this crate +from a fresh clone of `webassembly4j` alone WILL fail.** You need +four sibling checkouts: ``` ~/git/webassembly4j (this repo) -~/git/wasmos (wasmos-provider/native/Cargo.toml points here) -~/git/wasm-cm (wasmos's sibling path dep) -~/git/wasm-continuity (wasmos's sibling path dep) +~/git/wasmos (wasmos-provider/native/Cargo.toml points here via ../../../wasmos) +~/git/wasm-cm (wasmos's sibling path dep, ../wasm-cm/...) +~/git/wasm-continuity (wasmos's sibling path dep, ../wasm-continuity/...) ``` -Once all four are checked out at the same parent, `cd wasmos-provider && -mvn install` from the webassembly4j root works. +Once all four are checked out at the same parent (any parent — the +paths are all relative), `cd wasmos-provider && mvn install` from the +webassembly4j root works. ### Why not git-dep? diff --git a/wasmos-provider/native/Cargo.toml b/wasmos-provider/native/Cargo.toml index 9e988fe..2b8890d 100644 --- a/wasmos-provider/native/Cargo.toml +++ b/wasmos-provider/native/Cargo.toml @@ -13,15 +13,22 @@ name = "wasmos_provider" crate-type = ["cdylib"] [dependencies] -# wasmos-runtime dep — REQUIRES a co-checked-out ~/git/wasmos sibling -# tree AND sibling checkouts of ~/git/wasm-cm and ~/git/wasm-continuity -# because wasmos-runtime's own Cargo.toml declares path deps to those -# siblings (../wasm-cm/hosts/rust, ../wasm-continuity/crates/...). +# wasmos-runtime dep — RELATIVE path (../../../wasmos) resolves against +# a co-checked-out sibling tegmentum working tree. Same tree also needs +# ~/git/wasm-cm and ~/git/wasm-continuity because wasmos-runtime's own +# Cargo.toml declares path deps to those siblings. +# +# Standard developer layout expected: +# +# ~/git/webassembly4j (this repo) +# ~/git/wasmos (../../../wasmos from wasmos-provider/native/) +# ~/git/wasm-cm (wasmos's sibling path dep) +# ~/git/wasm-continuity (wasmos's sibling path dep) # # A git-dep pinned to tegmentum/wasmos main fails to resolve at build # time — cargo fetches wasmos but the sibling `../wasm-cm/...` and -# `../wasm-continuity/...` paths don't exist in the fetched tree. -# Same problem would hit crates.io publication. +# `../wasm-continuity/...` paths don't exist in the fetched tree. Same +# problem would hit crates.io publication. # # Unblocking external / CI builds needs one of: # 1. wasmos + wasm-cm + wasm-continuity publish versioned crates. @@ -29,16 +36,8 @@ crate-type = ["cdylib"] # siblings — no more `path = "..//..."`. # 3. Fork wasmos into a git-dep-clean tree we control. # -# Until then this crate only builds from a developer's checkout that -# mirrors the tegmentum working-tree layout: -# -# ~/git/webassembly4j (this repo) -# ~/git/wasmos (path dep target below) -# ~/git/wasm-cm (wasmos's sibling path dep) -# ~/git/wasm-continuity (wasmos's sibling path dep) -# # See wasmos-provider/README.md for the full build-setup story. -wasmos-runtime = { path = "/Users/zacharywhitley/git/wasmos" } +wasmos-runtime = { path = "../../../wasmos" } # Must match wasmos-runtime's wasmtime pin (47.0.2). Using anything else # would compile-fail against wasmos-runtime's public wasmtime types From f8da0608c0c2be37734d71a92e38f31176117741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:16:03 +0000 Subject: [PATCH 85/85] chore(deps): bump the maven-minor-patch group across 1 directory with 16 updates Bumps the maven-minor-patch group with 16 updates in the / directory: | Package | From | To | | --- | --- | --- | | [run.endive:runtime](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [run.endive:wasm](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [run.endive:wasi](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [run.endive:compiler](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [run.endive:wasm](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [run.endive:wasi](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [run.endive:compiler](https://github.com/bytecodealliance/endive) | `1.0.1` | `1.1.0` | | [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson-databind) | `2.22.1` | `2.22.2` | | [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) | `3.15.0` | `3.16.0` | | [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.5.6` | `3.6.0` | | [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) | `3.5.6` | `3.6.0` | | [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) | `3.5.0` | `3.5.1` | | [org.codehaus.mojo:flatten-maven-plugin](https://github.com/mojohaus/flatten-maven-plugin) | `1.7.3` | `1.8.0` | | [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) | `3.6.3` | `3.6.4` | | [org.apache.maven.plugin-tools:maven-plugin-annotations](https://github.com/apache/maven-plugin-tools) | `3.15.2` | `3.16.0` | | [org.apache.maven.plugins:maven-plugin-plugin](https://github.com/apache/maven-plugin-tools) | `3.15.2` | `3.16.0` | | [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) | `3.5.6` | `3.6.0` | | [org.apache.maven.plugins:maven-antrun-plugin](https://github.com/apache/maven-antrun-plugin) | `3.1.0` | `3.2.0` | | org.apache.maven:apache-maven | `3.9.9` | `3.9.16` | | [org.apache.maven.wrapper:maven-wrapper](https://github.com/apache/maven-wrapper) | `3.2.0` | `3.3.4` | Updates `run.endive:runtime` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `run.endive:wasm` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `run.endive:wasi` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `run.endive:compiler` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `run.endive:wasm` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `run.endive:wasi` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `run.endive:compiler` from 1.0.1 to 1.1.0 - [Release notes](https://github.com/bytecodealliance/endive/releases) - [Commits](https://github.com/bytecodealliance/endive/compare/1.0.1...1.1.0) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.22.1 to 2.22.2 - [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.1...jackson-databind-2.22.2) Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.15.0 to 3.16.0 - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.15.0...maven-compiler-plugin-3.16.0) Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.6 to 3.6.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.6...surefire-3.6.0) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.6 to 3.6.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.6...surefire-3.6.0) Updates `org.apache.maven.plugins:maven-jar-plugin` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.5.0...maven-jar-plugin-3.5.1) Updates `org.codehaus.mojo:flatten-maven-plugin` from 1.7.3 to 1.8.0 - [Release notes](https://github.com/mojohaus/flatten-maven-plugin/releases) - [Commits](https://github.com/mojohaus/flatten-maven-plugin/compare/1.7.3...1.8.0) Updates `org.codehaus.mojo:exec-maven-plugin` from 3.6.3 to 3.6.4 - [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases) - [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.6.3...3.6.4) Updates `org.apache.maven.plugin-tools:maven-plugin-annotations` from 3.15.2 to 3.16.0 - [Release notes](https://github.com/apache/maven-plugin-tools/releases) - [Commits](https://github.com/apache/maven-plugin-tools/compare/maven-plugin-tools-3.15.2...maven-plugin-tools-3.16.0) Updates `org.apache.maven.plugins:maven-plugin-plugin` from 3.15.2 to 3.16.0 - [Release notes](https://github.com/apache/maven-plugin-tools/releases) - [Commits](https://github.com/apache/maven-plugin-tools/compare/maven-plugin-tools-3.15.2...maven-plugin-tools-3.16.0) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.6 to 3.6.0 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.6...surefire-3.6.0) Updates `org.apache.maven.plugins:maven-antrun-plugin` from 3.1.0 to 3.2.0 - [Release notes](https://github.com/apache/maven-antrun-plugin/releases) - [Commits](https://github.com/apache/maven-antrun-plugin/compare/maven-antrun-plugin-3.1.0...maven-antrun-plugin-3.2.0) Updates `org.apache.maven:apache-maven` from 3.9.9 to 3.9.16 Updates `org.apache.maven.wrapper:maven-wrapper` from 3.2.0 to 3.3.4 - [Release notes](https://github.com/apache/maven-wrapper/releases) - [Commits](https://github.com/apache/maven-wrapper/compare/maven-wrapper-3.2.0...maven-wrapper-3.3.4) --- updated-dependencies: - dependency-name: run.endive:runtime dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: run.endive:wasm dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: run.endive:wasi dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: run.endive:compiler dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: run.endive:wasm dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: run.endive:wasi dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: run.endive:compiler dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.22.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-version: 3.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.codehaus.mojo:flatten-maven-plugin dependency-version: 1.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.codehaus.mojo:exec-maven-plugin dependency-version: 3.6.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugin-tools:maven-plugin-annotations dependency-version: 3.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-plugin-plugin dependency-version: 3.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven.plugins:maven-antrun-plugin dependency-version: 3.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: maven-minor-patch - dependency-name: org.apache.maven:apache-maven dependency-version: 3.9.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: maven-minor-patch - dependency-name: org.apache.maven.wrapper:maven-wrapper dependency-version: 3.3.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: maven-minor-patch ... Signed-off-by: dependabot[bot] --- .mvn/wrapper/maven-wrapper.jar | Bin 62547 -> 63093 bytes .mvn/wrapper/maven-wrapper.properties | 22 +- mvnw | 262 +++++++++++++----------- mvnw.cmd | 21 +- pom.xml | 12 +- wasmos-provider/pom.xml | 4 +- wasmtime4j-provider/pom.xml | 2 +- webassembly4j-benchmarks/pom.xml | 2 +- webassembly4j-bindgen/pom.xml | 4 +- webassembly4j-component-builder/pom.xml | 4 +- 10 files changed, 175 insertions(+), 158 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar index cb28b0e37c7d206feb564310fdeec0927af4123a..eebc3c18b4e0bce4a2a9e66be24c4a410f17a1fa 100644 GIT binary patch delta 19846 zcmZ6zb8sfz+C3cGzGK_AZQGgHPA0gci7~Nl+xEn^F|n;lzB$ixs=oL9s(M$i>OZ=A z*IpO)x~|;|C*Y44;7H1H;1F;iATS^xMK;uW@knH_|GDcCcH@@D^AZDNCg28`F~pw( zKL-B}9?w7^?aBLg zssfQz)n=g_!G=qH&tUi9r3{6ZbC;H2s;=9hnxlPLH$@yv-OjaCt{HL~#X)Q3OL)=k zgd~tOFw>sOw>NfdQS`_GV3!KnjPQtD_t+!l9~h676l9iN?p|0egza5qyV;srV7do> zmv#8xStKHZ5<>iE8i_2R6#oKUP`ZCX1Ss3TpbwPmU+@k}^Dm$V6a5!BfGPhA2Eg+E z0gV>msmO@`dE;OzXLtYz2uM{T96BD586{v(K*_lfsib*-pHdXMe=O*JE=wWrFm|H3 zzf}uDs}%V9;@GAd8dd;9qpz~{<9D%jB$Pp?&_zcJ-vA2ibkG5nr@yWQ#fkSAD1)=d zG$IQo%I~^pD4Mu?v@+q>HDS6bdJK@MKe_#s8Dr(vHr0f^lZ}c~%bi^R%oyYE!omEr zaE&$SJkY5B`NGYxhgxYc5D+m45D?bHz;^t^fCB(9S>0X z@~|&eiDT$U7u{43ufiUIXmXzMGX0!!&5*?Rp6V2O*lHj7MhakAE3Hf%DCgi|;gI8P zVQG22a(yiX%0J8oFNUB~yurBhdn(qc(^pB_)98N7Ab+c&9v8fD(vK$=N0L68w~l$F z&X*BbN9^Gx1Osnl1QppDqFBok9ZJxfYDl4fy5EesB}owD`#=Vq#Z+cb05*fIk-jzyeUW!K7vR);&-J{?ae`QW#9L+dV}9-V{c-S2{! z^R$MaF3d4_2ocm`edZh01P`Kf6d5frKQ_&P+P{(%eG!|PHrg+&S8yCYDawy*n@7&4 zf?_t>%=T)6bteDFp_$-NivD(Qo3@=o#xYI5NU-gd!obyeF!&~`yo$D!{%_8=v$!|3J(`Nk3disG_8;$lOgR|Tc zAU@mTuf~}lCW5YuWB15$3F{n7D+;&sT*<|XsI_SbO^pS1P!&=FxQoVtlFqmLk9=OH zM-Cn3hp{_Xwzt6g>ChxmAJAl)?`ef}rVGDKBSPBAixB7dBG{{urS(deye@`|nQ#pG zhhC1UEdmdzamKK{q**z_MWJ+v=jAyoEVCX^;oj4T3n=IlCyM+eQb3jd2!9ydopI^Q zP5*p})C+5SK20N$F$s{8hm_BgUbvF5n+EY6;*6F;HHBoXW~tg|`9m*{NI$S}5B(qN z{r}9nf@h~<{>!(WzkE~shi|(FyufNTTU&Gy3;`~MPUlQPeUADb-3{GD?Zy~Q6;e?d z)DU&}rJ#oec7nv@Z(Ng#Z!cPrvrv%003q3~MxFOOR6HkakE>0d=d4Cg3;)lzC-6Sj zoONmAU!trrCebL%U>cqhdW*R8_7MiBoj$Tn8>;P zvINc*q|GtL2>b`e2+RRu^~P|EiZ79@xfxg@7Gvq&E}RK#ffZcu)ssA$kU@U78BSuT~t@&C`!@9u;pz9@0t*;-({C?e{<0Yz%Kg4hGLRxB^LlXPK8q7Sy zmMSG!g~N#7|27~3_WZ(!M$u~;*n3I{@*ID_62TAWe2Z&2rTfHpfvngO<$PR`c)I^a z;y}1}b!^5PHW46Jq9ibY)A7s6S2X)lMF}?^n&r`z5WSx1?3!)?e5gmhDLhO?hh?Bs zJF*DCUyc0T)ki*xYYu;}GtOQ6tG1cS}+UIF@l-GSLGCY@CEb8As+K%SjY4D`k|$JYh_2S5cv2s)i1pL>wOf0cjqv zejhfAk#?iJo?`a`RFW#yJnJ^lO2~E1!^V}gWq2LSTKDxy-S6vlkPe5koQd<<(dJ0! zuA#zSPVolw^=wD-ao_4B2X$ox^qq|d>h66*+VJP<6oKR4)y7#%+1MaAaGI;x?zf74gQWEOMk5vbU`#dsx!rOIXt09>fT@r&9oBHN)hrZh^M-ZA* zI7k3h8B0TBsRrw6VS>^BC`)l%XGmBO@kjo)O+xj2FC6#*`Os3LchzwQ3023h$;L_d?>fhgANW4Hj`emt{)ZrkA(2Kx#%& z!lpyUlE$JGd&s}&7TnXJHRwmYPqy?>xzbg`Wxdh)KRz|?Yos-pF9ItMj8wz@X zt)1C`xW)2hVJF#QZdt@><|rG18UDPOq&S=>92{Ng3p0qL!d$Ueje|Zre#67exoe$K z1l;+?sJ)^}W+#d>3JQsiOX*z4i@Sw54~WBhpnudh0)!i06?ALhqj?<)=lGa)04CDB z$%gq+%p{rHgB2rqH0dwN$R(vmHxU4G3&S;U9F z7(x{%^+A>{h20~}J#MS1$zS|9+|?ULK(s))bTiST>H}O=X|=5ceee>q)smdz89LIR zlo#dZ{xaWi7H}!b6EwsZ0_rK7hU=c7v5I%ASiSCM-HY+ z+d~#Y^o3ZkXm=3TAH`Cqanmov1IQ^N&PMpHMkacOm8S@rLDyHFYXM6xd%w1RHnr<3 zv{c_hsA`Ywr7XY(*D^~)4vMxj><@`Aey*~U;$YmaR7Aamcej9TxQ66vv~`i(ZFi43 zFs3VSUoE<@jV(1%^0g^+M!K#n_II_Mk+0(5@h#+;F`hS_Pb8qk+&ohQ8u(Lu(G|BR zoJmEp!=-N$6Q7XTZM~@c8!TbDwz2GIg!vEixa7=dRUnx^oKW@d1n%^KccAuTPm$Qd zsmFI`A$yi(A>P%_(l70={=mbRz8f(xJ*b@%?YPM&qq!T6F5&kgZl)V$IR)<>`Q=(- z`s|gMC|s-?QkfZ@`2sZ#Tj1!UYRzSk+zS9Ql|w(CK))yyO5VP_Dwzwzx4NnSksj4L zdpXV$;H`D*itA6<+m{k-O{4ScH%w?(KZsq;kmzejrf-L{!r;UU#hal0x3y*j`R8}Y z-tv;3>acGkZyljV{k7w`dg=90lh#qOdI|3~q;D%U#qt?d6OPXt3xD}P?v!XP@A6`g z*h{8IbIENxm)-d`m6Xi+?d#JA*PjR|{DEHS)9^hfL9(KvoKGgRCrC*3W)$}m0yysB zJJu2A{_H+`y;?X??j?uuJoCo|KNUc-eH@7~z;%la_o+H)v{wu0pEop7LvL0w9=Ue$ z8Q4|g*xz_Dg8VAo<9n&;V=z}Qv2f3HcVOZ0GQ;iI>w5_v1+Kn3;On=n?HqdO)mqbY z3=FF7bdm85O)=0BSqp5IhQC$3oC_tgZvN7g2+lp79-5gnKNw}R;O)`!bh8w+tYghJ zg)LF|F_1SlmlSJx(Gy+1aUW?_R28K@3U@(q6b-ju2@#v5>$4WaaJ_L?zW}gYwqGzc z#L?iQsFxSC0O~5z&kPx>q_{=9-fQ;9UgqLNhK0~N>@tY`^0_*1g$STJR9 zEMxF7tffYd3a4Bi8>hevRqaq7hEi-pDH64g!m@eVU{h>sHP*0uINID6Uy&0A4r{P_crWk&zwze)k?$jic4| zLf*_#vYQ#zZ8`SY{L-T{ge9FTiSXz@H+EWYICg76MsB4qeRzxSFjZSD3~%dOPG<7M zG{Oh-!6|FPs5KsOGP!CqiIZcJ87@LV@5qif1G4T;TEINVp0_KrTCy#wQiVNaDM>JJ zTsCn<0b}X8Cjm>Zxt|J+h!nHky1g};I{9DYR#dPYW+`XeZyAu;44VDZjC&lb2T!1s z>Fl+O&iB&!i~33@YeBYfW{job-O0(GMf8XiavJQ*Hu?G|%k(8Dsx2FBE)5hF-TJ!g zm0n_l*5IwyEd9>v~_CX%!~dW?h&QCuk>}Qkwso0IOx3xWP_?aDul3gaI?Oz4{qJJ)kC!%&hps(PRv={qbFms);pr`r| zB#^zuy(<8Z2k!p*?YLc?%HiNz65=(Jcx^-4luE`u{_t`&z`&GB%ajl>N8u3mv2fUc zd6oH1fAniv?kM~I zCEUP;NjzjIb9~6scYkBx68@wsbZT#76L<`jx;BxZvx3pQp)qU+j{D+6#Na7}=>sy= zC22P-47p&xB;T5_MIgN(m~g72C}Z8pd&YE71G#w*3z2i}g^G-#pE}nY5JowO+=|Vj zE`Eo-10mQ78 z+hn-PXIJe4Icu6K*ziLaT^H0yQ16mes@WgMlr=-}#EL&fYHF4O$2OUxe!U8{BKDSdTG4~NbolXAY7p9xF0 zrA{{gUWEt=ttRkprf=L=#eR~nSw+dQ1HwoPMFL`H*7I4zsQ_y17vBir%mnw|chl$q zzQ{0%xVDwYjpPQUnj}A$;d@x`Ug|6K=LOHo8_e5)RfW^c+o}_a5xigSk`K;CvyWRi zLACMq6~slx+WiiYR2Y+VIG#I9BGhefxX7F}O~L0^l#d%_8>5zG(IEm!zg1TK^L)d* zzm6N#2&y>9a*cVyG*xW-gZiS>?upV%tXFO zzBuA2)?(ax4ifAE6ApkRC5Gu0$X|L1iCck+{qCeU8W9Ip;Mm|mwZ}{B8zBkbI6lKa zxFa*jp5hR#zGS39OJRkeD)+*ky9vsrq@+XCsOuWzN^{jpuFpO&RnPn6IeX*p-0%J?rc!CDnd!TPXzwub+-y@IJ7E0S znD4`bO^-6{@2*O~Jt!9H(3g~NZbhf9j)DA5RQBM@_t`E#5Eu z%q=8+q4)a&@+`;AEZi9xr~9^Ku~HY*+Jl!2x1G}=jt+rpZNMN!!nWSn50s@H>IJAj zMFcw(SdBWc47U=~Y%?I;Dtrd-m}te4?W4Aiuhjo_FDk!)RL)-?LjSw@OY%0rZ2K#- zo&Fs(e}|)sC6kGhiJ6r-lYQdPcErTqLzKjK`hkDdxqgN_NTmO0U)owMrI_F#Ahghl z{cJ2i4H>n+8pLM?IXNH{MP*ZEF+5}#)f2HGTst&UIO538(n|DzIWZ%hc>9A&v;4ON zvQ4s_x3Uu9he}EP{{h!g&8IWmH`UouX{qT(N{A7}YSMw;X zwr=7oiXM9X^ZXe)%soACE|~TM7$LUFSzJcoFJjlo3-~Dl7brC85tB5l;4vq!cqZ40 z6qMmb zi&Hu~_O!h{QARy^Q|ylYReMw9CssE{$+-(pab@KyK ziw;c^q}VAJ1|k4i%qXQIzpmz9czxW}P6LCz*n)emHJkJo7NFo@8((5c~ogge* zZ_4IopY9a;wDxlY+2LMR0Jkt7VIRk^dN9TTea#>Tq9SfZP{$})`%3N!P_Z3O%I+fkVbfj4nKyH5 za{ZP&2TTlqDvI_S?&*K#t{bTrG%GQzwXkE*O|B>Zkr4VF_6=tG3-0ZPx>q>yCdi{V zT<&{MbQUK;zFuz$XxcQafDq{aT3h2ICkpDnrUWY;5B4w0kxfPTrxu_eNrqIID5xa| z)RV*eYfjTg4HgD5Besr)!sHS2ey3vPq~WS4qI69V5B3SsW4|ms)3>zWYH6u|2)=;7 z70S-Yiv$6GleRP1|Ae|H9W9Vb}B`H%*8bs2ehLD~?=kldCWTKA0 z)2gn4<|{ik(WQwF|2Q_o72NZydJmKaihB|V=eH%O+$cnm$MbSyuI(}_{H{Zn?OuxP zB<0t%F43?|D(5C`6HtsxrlfDFvMSo&75y=<`W!u56<48BUwlkUan$(YBU)&HmDYLass!Vm! zyn}@c4dQQjPft%mn;6-DLU4FG>~VTJvbYN5g(GbUTe zvHJ!=5Sv=h8V|dg*{FICbrlB`nwr3pw;N<>XSbHto}!rHCkM`u@OhbB%5|Au->zX8 znjt3l{_o1ZdY*t;K!Jeh{}s~c{}ItlbufXiTIgCBpM(*UxX^*)#WCp-*t0`|b`-g#htY3Xw}K#VwF>V0r> zFz3(*^dNo_$bRH#@{pJWp~+>vP6WD=I|mFsXa4GM%Z zYI7h40f54aKvF2<*eKZ2mnfDE9_Z3nE80k2bQLSgTWMaTokeqPw4^AOut%8`YhSi< zR1V79BG~f&AQR&5xkpR{ zb2*+#PA)T1cO$6~&~l@JH@%`g@(T*mbDvSvIGvKDMIRy<62Wa-6VBmnV#cO9$fqve zZLnr3-Dtu26hPg0iCJrCOfYS%E6BrDKbZeRnqZwusC&%4&^0VU=xd781Z58Ri62Rq0WcO(Hkfhx9<;|4cGD0SlI;`A(&tBMRyWafmwn-+>DzsUrL1O z4M&rqEgUbFb3`V8h=s3-W`%5U-Kk3$Fpgtvj3~l>017F_En-8n*hFcT)<1J_d<0~NEF?xBUT=@i zXh`+)SaWgQg6+ZP-3ezy;P@d!(Yvk&H{bQfP1U>rX9*$bNSn5s_HDGQeL>2ErtX!z z7S=&TG8Cq~(mRjAxudtW`q)j%Q!6w2p4_>r^xaBNE;kzf8=2U}26KU)2!G@#Ie*PA ztfG1Ql3Tw=Qxp6_nmJI_o)4Rxf2__yq=d?h`+G*3qju)*62l%%=y)+eAZt#I47a@l zA~)KS#`YtRL*qW;7=u3!+bS}b&@3eUGo@^SO$PH8jihS%@+{hUnRy0pvji|C(KU{| zW;>Atk*qSDfS-MJc^*L(i!yjPv5+jWX~S2shfr3IfazYIIti>w*v>MK8Cq`i~u=Ma5Z45niA&%~W>Uc%zu@QX0zHj2z z2GVGfH2{Qu`MC={8x}`>Eb)yd(01`YzT4xw8V!az+1%0SuDny$XYQ<)e8R)_9hlO5 zWG)qrDifH{&+s>(*!@pOxodn~EEH%7G~}PH!C(qQOXCIQC^_jT&iWGM?P$4h>=GaI zPal_sDS8PsMz=g}OkPf0y%d%chgI%?+&$NPnHiwM%sjWwQKkVZ;^U05(^sgiazPIw z8@D&X=Sfr9G~}TawGY97M{{Lz@CSULe!+)dM6L8&N97~ea-A5OfxE=Vj|=bYhsfIa zn_MIcOJ*0${1g09NPE|iV)b4Mct~)6#O-K!}vJ|irs&H_1vEZ_Nsu8fRW%@2nUow}GJ=B!>=BCf;)v@cF z?q7t3&E#)mLU>|^FA1>v6LICNv-T}P*P!F=;_FrtQ0~#7)Ke37pfCZXHL;tG9ou#QZd>pU#zb= z5rLITJ% z#MV-N3JHs*EB=dB*Ez@JO?(+kK3Ke^L^tNr17mSZfbZU1kIgL*2P$sJ3uG?HR2K;`G>Qb^e=>|BldEooh#7b_LPz@j zoBmK`X+YydNLPurnnk{!czOogZ)KxYXGkr;bmAI# zEsn*&7dU|Qjb=MGC$bvzz!THfUXw=dBv?rl0-Usf-=Y2>+$AVk$T(nc`xNwh(*+A> zcw0CLz|kbl`875IC)F8oCE{kV761gp)d(}i zOG?NNQ!Lfzu}%X~;iWmCEVB#VWG`%Lfb68S= zrI{FWQG~aV9T;p6x3Z#0r?vLtTEz8zTEK ze5XJ(cIGGg^=ntK-d4j4NCG^_%2ls0=|zeEy@AbhGQvkrKjsw>hQQF#kvz|!H|Lt9 zKoCYvbu+ykXHI{BPD-X_YlHy5r&gj}plII>fSYWJr-HEeebz5-D#&P6$*W*UZJ~Q3ugb|qGL1NVe29Xq^~}Rr}AO7V_iNpLMpxPc_eX;tlIi-5EIzFGxs2eVDt506t&{< zpTjdG&wxDJ_a?V`53}RgcUrgpttfZ!ag{H#evdQAEU}7CvjzomI46ShQX+sM z6v-1sR*4yk=snaCg2loRYZMg>l>!~2YIhtY+XZJ-erJI-5wxUs+K%a=yQ#l|CW=*C{p{2A2QV6T$~){1=sn z6hi*%36LI3GL0;?4s3;wHcj3ydJsRHfQ{wDJNiZJx_QLyUe#ZLloKL#Pnacn{!xVdM1rN4Ni4U?*u&)4u%hi;0#a{Q4-m%k%c9}(O)$@6elJ~uytmpg9 z#^=o)|F2U^5X;--2nZ+yas~ubX=S*9jOvW>HTTQVj^no{|FR4PuBPk`%hVuAG2kno z`HUx*#;VJp{O!gsM6IqmOpWK3PP_hD^O@%Y^FTJWSj)Pb@2@ShigB7IPfsvxI4#Uz ziLr$SwaH`fcg}w@>e!LYf^1nx%Gk-6Zld04FH|eyBlXc~iV&mq()#p*et1bjmbl=z zE{*F?vSpgu;aE<1E((}mOBik$Y5}JPs1=Rp`!=u?_X+w}>Gb=Br(z&1EqHa^f3IAO zKM|}%cA@5eN|3lOEmyod6E8H{ecW z8CMdeu*_ZN)<-O)tr%j7r+ocB|NZ#d-R>fF4m9Eq`bM+5=a@Jj9n(G1g6Ch|a$Y2# z1TMAYSUJ1!&d5^ksFk9pZ}cZaT;!*rXW^D{m}teGYEa22`!a1bF9@8+PFb`?oJU3X z%`Kd|iNdkpFWyesSwzdt*Lxsf^pt(8!Z_vObo2M{#_(f5aGN4dlOW>VTY&z|M5Yd7 z2ku>)V-=pjb9f;_kuX7L)(O5!W5;t))u36Iabnl&%X0YJmnI$et5)_vU&ZeH9FXoQ zRjVr5d)ryWc=HdFe#w?kH_j@C{%dLSB&mS7SPtUCzK_zp^&iE3n*bn&;0yQq=GLx% zKx6WhA2L~HrcdPDb(@khI(Lb8L!?N1go@4}sdeI^3ZdK!hE?rqj11qzBaDt)kx)NR z)^S-j{}6LRR@535vwvTGJCQSgpb>L3%NHFbl9LUc)bzWKcVrw9!R|v!3SZxYrbBGe z+UxOTyVT{2`#1FUJar&5V*`SDpxiL;>$kG0moYhV}yG!l0cuO_iEM#yu9)E9P`-!V%H`W6LCQ zxZwZv;<1!VX>Ws1_BDsR zHFR}h(tDEM_w%I>e=Z->-OPvmD4prASp&; zm$9lgmJnek_$cVmh}SleH<1MLWKv{&rq-S}I=0<2J!$>9j*yu>x+D-xz+N0$_czhw zZ5{T=pj5*fH^ zH%$0Eo7ascGFl!OtLRhGIc76WZ82Ll_a&C^#>$4lo=W9GFG8cPj}`$QpiDx4>?hql zOaTpt)kj-q3yW+*HP}RPU>$xKwKsKNr^!=dT5Zr|lOzjFFEp6TSPC*lN{m(6_(^Q7 zXtrbRKmLxGzojUpI5C7xHGJee+htHx2d=(K2(Z{x`l_Anwx_QM04C?vSjk`Ax!Ooe zrNuEDHY-hFQx7+%Z%=??gex~Y;lscX!YE|?f?GbqFy!F2YnK`p&UGP&uYjXJnk+lhrPF&}VBD#BsA(5y-P7q_*`$c|!eo;6nKVA*F5n z`OUU7%okC$zELS;58w9rFJkKbKW6Rp_8LnvXMx z_@7MGJFiV!^I*5V;Rx6x7&8Vy6v!mlv`cjCW`nTj(t6vmQDuBX^t#-a^!5y4Vpa!e zmscI2!rRy^F4bm6EPveqW5wW|W!isWZHr@gHlt9{Mb5NO{t-=&Ty$l?8rI_f z%=p^K_#RWptOE@DdTzSe`Cm5^Btj<9MMl<(s6`B5XPLRi@TT>F-1QO^fjnXfOk~Mfa|l;nWM`Rr$l^)8 z&brRZz832Bg}ecqajpB=5byD`A7W53kB{)$^-J4Jz@Ikh+3?=q$ZF=))ly&ljrD_!!mjKhsM3!w5GgC{vVXOe9-BH83OZpb;a{ia1)3_-B8NB53S3GiGbGp}Uon+H_4SpM^?Nhp4Nne zzZWGLD8}7dCEW0v%K5g@<7bCX&7JjC&zt8#gCFz9>sb65t6M4fBBj6^$ESqc{mo4> z3xR`cq-FYaI95ho$rcFrWWSf?(SjG0Wlz4RhULT9dm-2!G*8t%#3`)jVQP#LKvg{W zDFy9e30$(DJ`|jKggFq(NV2MZBD|y1U}2r?w3(PYr@C3<1o&u^k4@6aBydQS%3#q} z9{;ju@E5nZojkb$=%}%%_KRz)rPbjdRss3(ZFwabklE1H3%5p9^~(dGi*y!)gTuVU zx#PkJ{satea;XE;yGlOk?uw<4CXyY~bc zE!HLGrmF+>yzquGKSxu#BUxc3V-@UVNp-_k?<8UmiXfn|kHMVH`0^FAd1n)vn^rS_ zb)H>~E6%?omigqT3fr#FgUMKt2)AjsI0rt6QU_wmI_|8b_B@p{bv+)~`b=NR)>9nA zHyBlhtu;2X9e=n^1E(iX_JM}J0Aapuq&hGh{F z=_;UVU51!Pavy`--Aa&^Jq@R{d!`i!Tn$zke?e?{eN!QonL$c1;q{z7RS29qYX=8d z@#A(wB}GlYuI^J>hP-Wyu*6JpI)8bv)RL@z6f%sotD!QagCQ1`_APJ3WB5;RC1)1H z=kx(xLF!wQMcArnqc>j(jUVKu8eGzL;@v>u^vfT{?=ES>GWf|>;)#7{CR1yL>jukC z-__ahxxm6OnqcgFda)W@J&B@hul9q9a~z(e*r}|ghwjb39YFb*k(=0f3^8Yi7ZNU! zR-mf*K{CLtQ_nl(C04NDZ*aMZN1+R?*Gu$kMj#)IK<+exGWBjWNos)gR`*bS*^UAU z1YY6zaN(qW%tmLXg%E&gL|Iwm7~^osx$TpEV=e~_PKTdYUlDgu6B|g1<0iO+ zS9*|}r<;8jl@o_CA@>KwHB}8MUZ~dT?Q3pIvgk;=Hxy8!dDk`2@eK7}q`Xi+!G*{N zcjd_1$j48=h5n8&!uErElDV+VX~F=G0t1;*E9uhNM_o4?ywiyiPb~c+9j+}rX`n@JN+bkfepMZpi>t~r|a-=je+0)*|#j{N<$8_r|7HNUrK&8 zU=kQ>zQ|0=W5Zvw=CM`sq&Yskk$MtK|BNk{N-~5=PR_pRN8y2y#2$z_bV3hQQ>0ID z7*5%sQ|<-{6p8`xkmkE8Lfid;h(DoDfRm&^NCUf+o0F|1yFz}!(`xG>0iN$KzBPpm zdn;$kC2qUT&(V0go}xh%`&+i~Nzw4J*68S>CY#Hf;*UZEbrkAUnCXHo!3(>;|2d3B ze~Av7k@bzO{{bJSydPWUk?R4R+^aYG`9j(wUc*)!1!n}>%u#8!%8`f8>pNpdBhatt#@PY8in& zLbe)8Xmz<`&tA4Z=U<2i;h`j+w%BLZ10->weh zpdg{y$1?53CO`V`gyMAB(G&qLA+H(Is%ehySBY2VSBZ=oe@7;YSH>Y4znV(Fr-P>j z?AMIQCC_&slwu$`xWe2B4YdkOO!6F$I)Wk@|Bni*1q_>lDYZ66UG%hp$GMCK8auD$ zrC*o4)B#>0S9i#Tviy;{w>ZmDYOBPGg(_M7u2)3#=2b*92atWqIGz!7EO)9}rWc@E zrVr+6r0tQEXcr&3!ca;o6E{kzAeu|qQ+azN`g~zW{Z2so8#GSD6iFjtg)an7C9@5> z7=Yc3e^2oZ+n7d7ZC7WX4YG(s zpkLYu!d@l?Pe8)sxwJp3kKa&OxzemF_*xfZaE^=%vrg{tC0L?1M;Lf8{*ORc_`oU) zp$LF_ln$_CaHc)9xbznn|9LDX>RFLoonv)O4nOsY9$zNB0nc;P{XGVxpG45L!M3k6 zQ+4j(9pk$v8s4$UV-`+5QXXFT>=i>$Vbz1(q4%rl6Gzr{2cWujs`PjO7X5}7RXMHhwk;<6_ zml@xs0(?yR2>F4UB>1wIvc0_W_;(+^6A+_CYN^mL<|%ebPWDJcc!7IHaIr9M56Ikd zQVtWHBHSTOhYRhw-&%Hw&Gd%%?i1}hzNEf%o?9Hf$?QMj&jssz)m+~wn!YpLCMczI zPy-!(k$o`n{gCM}vR=4s9QEZu#%~&k)T4q*omLBC6s9-=Ga9VJE$_kJ18kk+xEQ4r zBDBh&V6=FzE%S$JKj@QQnsa}k^*T5HEZ*7IXw}tsV(1FpX>Z!kf$dV%I8yv>D9(@_ zOnD<2@RgR?m-+A^fL8@G59tBsnEwP+mP7czXM`G8^U3~I@->TmK==M8t)I||dFdR9 zg2jZue-Ea$EE@9Ew2-O~R}e#5_Z!me!w+N62e4p~ms zh}cm%5?;VRR7TbcjbTw-#IF-Nv(vY{kJ7WV|2)2rDuJ}t%|@Yo#|Yub5(zPx^T6eG zbTuGMcBZQRF1iuYtJ2?29&@55ZEl$sBTCVtL;*#U_F-&yJ~YNe#J~ z`Xe>RDMfztkruBxR%aMyzQJ$=XB!fU{6sUSBmra7m^vr)%%QlAb|gL?qY+^rBJzv! z8`ME>scJ!poJY0|J0m6?UUi034)9(#k^PVP#_klYr#y{klF6DD4k5I;n_Dg|D|yO~ zR)-atF}B%9WGQ)tI4yg+L^VAF~F1 zdnRWADjlw(I6nz6rO6MI#WDy(%Im1cSS3_-45c5GzEQRIH(cN)T`Y2%=Kg8sI`z?9 zU!zt}k<5xAk?J5{vqE$`1O<|_h^Cb|JN|-BL>3F&O-E*zvS_DH$o`0QC3@bH`h}Eo z>VmiCiPw){kP*GDvw8KhMhe7r(Ik0?WO3n;20js_?vS+y$8*rgz@q~G1iT=Ptnls+ zQRQ|GC9QyKBdL^l?qNMS!xL=sv1_8>`cx=0inX0X`^A-k=mq-N_p&C%K=fE7IdfH9had^3fa7*St*-Xntw|?~*Pu;2V zKwMJ56)O0^M!(5kM-)$@i&|65hX7vgHU~fJ6jPyzR#Rgdu=g8iuI6GtSf&3PLjoUm zYzAdewF2j6x{A?SdcP%-k0shT$>asjX1hW&0zoKBc`6S$)S^19=>I3<5u39+{n>Fy z_3XLk_VT!JUvN-S_{RGxJm?f=fkpp7yNQi0G~`1d+5{{S!Dj6qHuO}z9y4&;K?eKV zmq0=(5nGa@VvP}vz{#hZh(x4G8G16pUYEP@?ECM)Y)N~gRmWHVWcpx<%E?NfP0qs( zYlO})C`@}GpOsjXARybSkzX%`1f9KbVvt8v{7+)C)rQxTUwcXe`+MRzyxhUf?rUbzqA7gMG<52K@^7!_|9vPmoZO{oTvz% zrD?5eO%53QH3?XpDWXox?{yP1Nuv-ra_qxDl3{%X$qW;kGe=EOvQ$D9koq9t8{EZ4 zFxtJE*3Y!&@Qr0@C(I>+&E4-GGj9?MkwzrR4`Hbm?<$M>oSG_A)w}7FWf02*PZ`TSxYOn7F!r`h`jw^M z!pRiNV4urt&F?tD@{0zfju8#8Iex)u3Y4409ENDcx!aj0kye{U9k%deT^E?N$MQ=8 z|DRegmq_tUBPa+62?R)^YbDHoThD;i*#AZg?LWnPgMSU>1M;6}(Nm3(sCK((etW5@iR3Li#Yc@|@_nB)OxxKx<;B`Un1aPFd>fkC5%>16+k;8ah`k0#8 zM6jdZuWu#AyfoV_^t@XQkEca|a~?x(aU zC^E&*ZV%)5eh5b-Li?IAe3-VLU&=|~1b>*>hY%2qZz6kt$VhYSsR!AFtIs-xnUqf8 z-fv<xV4BAwzQh0Gvy{3ob;S6#IYa#_g4%$@!y|NW*77`eL z!47fAGe?JPTNtlYO?0dOl{@wj$E*0J-g<5f@BiTHgho1~uROI`PWWwp&I1ko@K zk&^sPdsddtx|6tEpl`757Xv@3LOzuxV8ILDRM9My9kcO}YtD?{tk<}1S&OSRTrpBn zn)e)ohy;*1;Vbo}$zRR3Dey;1$@CI!zUd7EmMN7clVjdtPAx&Uv-jfYb3_jm+pQo? zEc63)GVE5V4n;O<1P0e6z%Buz$)1Gee;}&wpue z`S-633jc`@jmj;=;Qv+v-P%X~+o~xzkpunjv)%u%k28;JBFW--r#U~I0)~vu5sr}~ zT(aCNiz1>Nf*+0n28H1ia0dio6j2GtE;OH?^Ct@Io`cP*}+AUG-pk8W$epeM8G`= z8&KupZwQ{uK0q&ZbBY`?7KUky5Sh^_9 zHDGfs{#HQyjm-J5_5}Yc)Hq2h($-bz@NJ_gt>eLSEK6uYD%U2k)jHNF=H&3|ftwaB z-plyn#q;LYdHQ^+O(jxlWc%swRNNo>ij2GUtN16PO*OXkvuldXYa#^)kJB{vS~_bc zGuq#!<^{hXwUhmRY-S=#mQm$F?3U$r@sj9x@75n?4;RUW_4AKmSW&jGb%e*Ah|F8$ zMu9zcN1I&k8U(iI7WBC-T?##De1+!4PWp?^_X2I9$E~WJybcwg(VD&SIk(#}&fvfK zeWDip{?SK~GxT1sl#xxwN&6}RW$|8YC4yw4>6Yjr{h)$oDx(?WWnIHOJb!{>sVlC! zFOnR0Qrx#O`6FqgG^I%Q#nX_CIJ1m+*9(L0#k*K8-Aiiqbg!*<1!cEY7d>b6Dm9_}z4F!C;oy3!AweUG#EP%Eh2&;!gZ-=W z01L{yK_?}>4!xfZM%sEKRw-@6{7jeBl((Mwi*cl^qx#|wuCK+ehh6hOPx+^i@qSMy z>v>tVb1t$|cKv;Z*EPd(?sp7mV9KsegQ}k9E{SCxr|GXcotfdWaz-LTasBxhgA)lF;3J+cjeF z*inP+A~Ef+c?D;}$1&0#e0*Gs6Nb7oj4{FNJHT1P862lEFtifIJ5dVm#Sr6ap$v@Vj$dO? zp_{G3Glb}%YEL{1Z)GUL9#s#OTh4*i-rq=l`N9{(E9*kIVN#wu2BVW0s-y?C z867!Xg30~rw)gpfy*^*8!E-^ptP!_a22%LF2`+p>&k}vR3qhW#B8Ub7jx>dE=ad{b z+Ts+3UbTgq+J~=TQ~i7lx*b7&Y0Dvq9>G}t7z8hTP=O%aB>~cQ4_KUIK>({gAlUyw zAA*D>`2?`a6M~06QX%N9QQEs3e4G9t{C47SI(f<$fQKO@D=Ih423~wYXmx_Ow?Q0l z=}e5kyI|YJ1HwEybS!M1}yq(2R*)*OmjqA*D z^MtTigU~g@TM&*-m&5C4x$xG_R{;cZCBWTx0qh5xwRJ;`j3D*|s45eB?7JtA=g#v6 z<>@?zN)|wUV_qFP*#5U{rvu|c2wFUV4j@eUSAh^Mt{B1#3*1!w(?u6@gaCGw^1KS- zrH`dJO@p3~H?$kpg7eX@KsH(u3?|E+uat#3Y;SZLixyCh17+T-K zna6R7o?W;Qi>E8FFx01yr^|Cv5;~49xY>~e(##0_d~FDh9}WW8jq7v`7~gpZ0#R{e rwlOp$o?dTT7R3KJ3{JM~g*nFnct9Ox%c2WoKr_b;mNCK9aSr@H6^cVm delta 19347 zcmY&Jpym5b3M(v1Gwd3T< zvv#gLj~C#JC*X+6a^MhfARsUxAl+?NnhA(xu>bjNvvXrBOW-92#)-fUFe6Jm2W}ZN z#us>yxUIqk{RmnsIM2_pVO9{$q}|!kR4y_&v4Q%``br0ju_=Q;knT}iA+L+Z{Atf` zOia@t1qQh@CY(d~t@f>v6+zGOz&mYplsHsc{iXRt4qguR)=U#C32 zVE?;~Bt%d`i2v*(i5ZmQU!Vg@_b&(sW&0QOf^z)}{(#c_3#hfmo+TcDMlukkLWo--Zp;wr%w~yV6VcgvUA5Siv7lG7BIn zfm|l9Dm0xq?1D0MdY$jXn=QbP$N%hg!rFdRUoB3K?5RS{I{OX@;`q;w(f{5#n1AkE zV>KENG|GSeVULp+6Dt@92n7TP2x}4x2Y!+~F$$2Jh0)Z`*wr;f&E64J1lhlZSgJNm zqUqar+h*-R%^90sV;kzFA(%DvAL;(gdTRq2rfKw2GG8hFbGQocpr6!-TY(DA_6vqC zzHE*6{IgqASGBvlKrrpmjzF5wss)0qXYWe73qm2}1w!kcYj}ID@3lN06gPet-&#i>m)_&v(Gd!pR0Q_kArXMpl&9)$@b8<9kKXN6f%{0>WnI2!W ziTmUb!W53QuJk^o82>k?_KUU0!9Xe>@Pknu@1U0yG}L%Rt(b9DqWYCYj5}$YAtQYlycb#yP|jB5DYp(4oZT*O z`(3=PXd}qD~m@98PTfAU#BlAN|;cpGXp_F5^(7#IS0l`AV+g#+9#H8+O!)d7<; znl9gEP=a-|#I{kH2zsM`(WG?E3isAi=+)pVi&YGqSG0igI;6>ZXKLXPSZI>totbE z)0l%!UYIbAAWE}D{~_D|aqXv9ean9sw)>Z1YX6^M zNnHf=KqWL0bb(d|I|H~S4ZT}PFq5dY;z-G077_|dN(mWd)WOnxf=v$d){N}C7X`{l zqY$eDq#G%mYdbB%e96qVtc$I7*DD^&>#6tm$4lq{NdA@t;RR9JU{C<6CfKr*gx*r- z!ZzyWf~RM)RcoKQAfxE#^r|%pkdk3N(G#kjxXykD^JJr!mNc=LxmGz z6sTP9p3+>j#2*J8F-`Lw!eif&HwPYzJ(Ttt04O7IE1DA6R}dBcV266zKM@UA8-sL5r!Nm);WW7F>FnI!$&L4mICpp36@nWqbP>pQ^JVE!^mo;nvPXK zxf4j{q8)Y*)ePz&kJc10>N&HsmwlA9YvDdr56-lzhCj;i{waT2f`w|R^>q%bO&*;w z^SmW9pibg2zT29GGx5V;J_T!fmdfK87Fhgw2J@slr^4|%z(z2h-Jcw-CHlMgT28~d z-Uvt6OE_Ji7t#1`*S_&4oOw0QTmD8uagbjo_JuW=d73RvO0Wt$T_E7`M=-?IjVH6L z{}2GkW&mXG+v@cx%t zG+$C=W&jWnWgHL?=KoR)4~V{u^J&;HZpTSB;9@Rf%ri}ZhC<#?CLHh$af-6v37slS zhP5surm->5t64+Omqt^svaGqa8@`N0R>jxY_VRn{Q^T`??`FlrU-in`HY)^SWxMYF z`Z#|6`tm(rWDenNe}7iImX(c@#HhHLwx}5ee;<1!i3&KwH<~R)JuyR*FuKVkPk1k-@Xr z+XW!NvOdJkfnb^e*mMcpSV(2tGuTvKA?TC+S(>LVfinU58_0v3)nSlCAx0cOXCD{b zCpx=E<7RduRotgjy=}kw>?V=PyMe2hRm4=Ou_P^pxnSGDb0B3OX`^RK-q77`S{MWm zK|3q{0dnS)-{jXUJe{Gf$bq3xu6&?tJY@SPCf#t+B>in8%wTCLxL&vPzy$fWvGJ7i zHX)DZ4=9=OJ+P4RC>Fh+bDGd@ZJm|iIkbUbxD5~kw^F$l5F`Gw^Z_P;`Ylu9n3b{E zyxoj?jl7Q3SELcPd8g>|Y&{)UCaI;&YD6Y#y2cT!)6;Xv+Hf3G&!C z{p=-^CQ}Cs(vIe+&nMHczN;Dt>%mMM&Z!VWa{`z~()6Nl$^#z!TgN2i^3q}1a5z_{ z;RJXxssuZji_Ia8bjHY1`$dCJa+b#}((WLgb$b1_>KFYQU{R$%Oy&H(39pFk`cJb` z(fq8y-v%}~JXVAk#QiRLu;H}dW^Kkc%}EG!GZtffmZP*k(P9B9DDR8)bY~`HTalh6x&GXJ-17L83^!#a~ppvJ64Hn=N$qQMAr4oMo?3r$b` zwx5-)!JRF`>JNiN=OsRj9eewW!+LwT?tcA%U`AA}y-m(-snGDK+JYlmqt^2Qum|K8 ztlkTW9KD5z(wXV5x@qw9X4UG7bktw|cmXVCoweuG7189X-Fo}=Ut(4Bl1VBVr3P=# z&}^~3>iGbphx0~`ghiCR*E6hFU6iVbj0)1+Eay%kh5OSW?>uUd_UK+Jf@;fOczY5i zZtzLN;Ro>}u1!7lvI_Ywr{?QQ6oDA~dM_W@kegHjfw!hr^%p;ut&y*nr=z>3WzKNgZTK$OX zMA(rU&$t#wkCM|X7xTv15?&$8ntvXS^V&XdYyXf1G$5a zzXI=CyB4}!q{eDQk@lV&z}^GX5*83@laK9Rk|KpB-BMS>)bsBSYkxWsO9%fsc$nMm zQk;T49ylBo>Uysx#K}y5PXh*^Dye66Nxt3^Q39Y(hrc?aGBF6~2co=W`X^}>VT%xz zQLp%8>KsrrdSaiRW*#)?P0Rb4ypr_WNL3n1Q5X#pteigSCSSu8= zn(cEhaWl2BwrWVu=mQ#rJ`l){3X@NaB=Q;z=zFF= zHW;$gauU#r;%1djn0O<`_>@~y zI^#8V3I}m^c?;MK)mXohP}uRN&m)Q}uGl#4=b!l+FzV-BXM(RmRZs2JbDwhz<0}hS zd7kfH*$^@<2AX?*(jH~KMnT?*j!ZUVw$V6rd^!$;1`CR{$eVrWMo zs^>-RT+Q6L8f?tEo0OyvRM8p1!8+tcZw%3M%qXG>H)G_)yf)rjdY zdrr{`@f$@hRYQG*Br$R#S7#0dC)vezC}RR(X#P>$8_9VuY~x{#ZPmx2<&Q|oA7I&f zBF0>%a{+RX;#E9;#-rZF?Prr5MUW-yMK-SuCHc`^o*Zm8H{su{i=|!XHS?X`7`VMS zfO#T$>JJOxdS31~uGag&3y3M2)TAHp)p6Ma9XCD=SN-p{7=A~g{wc@al#r&+Lz1o2 zpeN6P!@sr*y+&WBg9C%xK-Vw%RtrYR#f!0{mk#9c%pt87I%dTpfct`XD?R?^j5vIk zc)ov4tZ{Q|plRp~cy7`Vph!UO`r;-$;R$_)+T{vGO4cG640|OU96dQ6Ctk8P6%t?< zh6ikPuilEZPqN#4>ulVy)`x5Uc3x$z*S3f;rC8jTculgS((+1hNk?m41=eJ`;?(3iKPv{`&X>OM99o1c_3G;+^$-(_Mx@ z4RmdGStaxqmwu?3UThVGbT#P@R9Hvc0~FBV*FEKD_qy{cV^9~wBPo3|#DNh$aK)B^vGj6eYXf%Hhj3XO8LqPI;Yf5b4oD!f3tIw}F%`RGk2Ef$b6 zETbC8&ksx8%9`9dF9fZl>nKHWH3lMUuluH}as^V1j8QN^Gfb-)KEbXWNn$8kTv$6K zo5ZyvTY!PtO2F0!=r7&+XHitR^>>%9Gg$P+4Pt`N6hvor)rQUNmgo{K4*lk z4&2zes>*1ss7sf$ejn3oue2tQk3O{~p8#7NBp30D@Mput`>W7@&nD3S^;nXa7~lT2 zgBY3aAQAs#w(Lk}tvvlb3SU7bF>4p+c8ZLSzoS%zs{?Z#*%Gv1BMsMM?t#MZWwAZ78W#%Nt(>tb=Zdlbbl9-8B9{X$XLsA4I-Z@9ifxY zAxjiSs!6l5Up_v)Q9S@Q`(268qpn|q&MBrEpZZoZ!#Da7v30`ZNg^-M{PZe(M>-TWdu#rg_xHbqQzb1 z8aF$rI~G_M%V}A>^s{L@=BAN%@6dA~#XeyoBcnTO815;wtUTMv+-F^Fq<*OvTb!H= zDWVm*0QRjbg;hArifJ(Avb>FJZ@RE7S#3|Z#+>zh-h{EAGNbdKu}=hmrsz^-#O0ou zNE7MFCVG}uJarv_$4SQ3T>2Xm;cE3{Mf5^Gh1S&=F#X3U{98EbK*Zd#8o+b#AWK_& z3BwUr8Pn(G0}de(+~ckyPY?a7{8g&mgcqsa5u6gC8w`%9N@{gtb(D)ICG9cSL57;? zRAz@U&w{=l@>PdMxJe=~zw^&+<(QX}Sl2tul$U+pOh!pvDZ&-^fC$xg9JYX0RsFp0 zu@y#sFCvzR5_UCfQEk2s4)!|?=MB#Mb5mtM1+MKllvk`FuHK~PiI~t{Eoo1V_v%kK zLd;Tw@ZUpwSW8GB;$|mr(Cpo0GGn81r1FRGxJIh-wJ?Xxaoak;r;2Y>@dE*~*?q*J zFyGfkzrjgbp-`qs3T?jzcnK26dJln`^FR(Gy&=4IZA)NODi1SbDi1s1XpMCY%yCPU z$3O!Pe__$4okGt5z7Mqb;&)CK{W;HOy&O3kSRSn7RSc0h^)uCrWu-p zKL7W?eLW!ehIhG87$`5np4u<2KSusiG3-X`ek1bhuvLeW%9#E!${#nPt+YYE(kZo) zouo}n(N`92`-ogzeh~t z?C;>ZT2KEV-nTR-KIk;<*Pc9F-HhsQcm<&_mDmTMWSy?KE4RpX3D!!StU-vQcwSBM zuxnY3vd7R{jsjfMm6*}?gFNjGR?=(q>`#@ZQ#Vhi7-;NpbxDmtSHeP1>*4cuZ-J-mtpQDDT7Dk zOK|-rb&<@Gp-pJ&%Ckz45SuYWCu6168+TY#U${*+VP4t*Qppcv&n?0YriyIFmz*W& za1^f{7zi%q8wv-9DBQ3$r9lcEM+7&%5_9~kefi1Pp?mS%2*9>Hkeaee-}B`Ye1iF zvTufn;+P1qOLIcHqJRpmK{FjRcB+Bs+avg~m)d<$#wpiE#D~r42!ryRn}|X$=9U%$ zJvXrDDkhJK_-9$?)G$xX;7_rr0L9S3R;}|+s7D+gjgv|wDW~M? zT2sJ&qL~~o^EjK6=zxr271%s({B%)(+Bs)A8j#3mo+{61z#5=j7qu_H(F4&xr$OqA z<-nJ33A~995P=oLwup!00tBa%`wRGp*7Hd$g{l0+V5TGqbq-aqr7XxSp1LhjHa!KO zNHk3_wq@2W07%HQ)*%vg+}4HA{G@75w$oK%$fu8)a%o1rDAfN%fyKI|7HiIk$*M^Z zX4aj6d%j?SWn}^op6Q+DCdBV)^!rK(4F_JU1A7GNjAJJwQ?H1_mCI=W?n+MxN=ie2 zMoOS1L;r9`c*D{Zl}FL0HM&e}^{t-P8&n@loS1pL+5M6?nr#c8gvc(oSWi$!Ad=Sx zYe2>HI;N5T#8^~aK2PE;!^RR`zl3QPN533v_XE>)fb^1A zbKEJ0{6P9hSTN5Lu-px*0D(eEnZ~Mw1N(@Bl=@AV?LTVCD;#?3Npt0|%rX%WD*6=Z|rx&4^=FMH(OC(4`}2%j_l!_I>KqQdyA&!eQCcz-Qb( zvC_v%F9bmCq84Q0!-r<0aHV@P?v8)>jjSUofC#z5v`{T zJYZhX*9&7QyRIp3GMRYK&PU2GSTMFQ?&7d+Wj^ykHWw`kV6jb6LzWzzyxyUejK7VF zNanm)&<#cG5wpTX?RnS{!!`zafLq(AN0ZM-bxEPyl`vX)}pnO)a-!6y=scfn2I7y`FW+}hvSv*DGR}hcu>Nl>d z->|JTdnQZ9lB;$Va4|Pn^vn1wiA)UfcZ4h5W>m|xieb^njPb7r)&Wf*k7Mn75os>g zdxGGF2Jj49DqUk&9)iJuGefAI9yWCCZ>;r#R6t8<*A zn1tU;1<+|C7ibRvm3fy<^I831j&KbAM?PY$LRinV!%1?Ff09D{PH~6kW|31rGVho7 zX14>X_0?y@|vR}RRAFr6Z>nO(RF{C|GkG=ov{o5>TS+axvi_ondApS+6_BaUt%s8v7kr6?_K|s|1&aH)#SQ3aE=X~hF z|5=jK4uKi|JD7q;00EKt&r;Hy4@^?^IXP0brk)m_2Kpyr2D33XTtt$UKMC;BvrTjN z`*PaS*}}5rR~%AD*CiW{gA67FyN>M)kMGR-+vaZ1)a&Cl5fGF)ww9u)k4!XvUxIE> zVQ^|RIWcit*;K+@Z`qWuM3uJOf?jXVgv~b9o>s?qg&Gq>AZ@|TePymyV^LEfbj4a> z^7Af4;>k`T|0mh*Z9I6Y;=bD`UgOr=FtIOqWi^MS6!@i&>#{)(H$ zP)#~IYOW>tXXmUn2e^ZftiHc62K7&X#9~9l!<+MJEzP1AIVSc~bQG}QH_|Ys(&8bb z3_iFjighkh0OlE5|wmVv2uAz$B;9I6*=6#124h&gb{F)RoeScQQ!!V(74&4KMCnzE0gP7 zjHk4A!Yy(f_J+GVRk>bEfhA4W`zSTl&CE`mdDQ_LDVd(V5kM}Vtwg&^(!Lr8ZmKGQ z3?kd_S+u>aBoisQ8B7L@hO82uHM0D+nGXE|AqJJ+nKk;i4-1YX+3ZTQ-#YoSY3YmR zD*xd=;Bhu_6DTb;h2zY7ph(ZMPu!9-#DPiokw4SKy}UxgZ}je77Ct;CL5>ewgnfqD{9(9N4`Tt;~^j>&Ox2X7by z4eE0Q&YXCT`U@@)Vw~43pk6-Babw?p0-T50)bnKxP^?bmwUE=}G;i+wl2g;@+8X?< zI6wk(fLimxwr<9_d0%aSLN8Z~g;)6Kq|# z1~FRuWQMQ$*;z9=42ddHRJQuFj))37RW)HIqGS z_eR&5sfwrF2kKP#2J)wxLU5cU4?(*o*f4K*XItdeR+o&s%lg6%Oq zpNhI3#*36D^Q@==AWeoM`Rf=*Hq9hW`+Ce#MY_|ssRjnmpWhyeyK&^-|MEchO{V8Obj~= zR%|C4Roxidq8z=Q@YpUf$M`}?2s2A`*QvV8Uke@0o;y+z5;)54w$Gja!AFx%Vc8K9 z^&O+StIpNO;@1fBL25 zY16iri3mf0o5@y4)JrGJf5n}i@3MHbp}oILNeu~)cRpF- zYD9mWfu@KBNB~mf%UxB<_g>WKI~acFz(s|15JlDAZ^LMUseG%*`21Z{he>zkIWRs> zeqMDCL!&-;%Ix2z%w$@u3-*)ZjeVtUQd7k5&#aV*E3kOa7d0){_!WmF+N*Uyj81RO z3E(=B7~!#!jg#nv)}*t^nXPV^EK8ZTm@wiPJ~QH)GWXb6pZ6`4dE;GZXz&-G?Y-Zw zp~zkvo~M>#XGhQ^wO+c-{F*KG z{8}I9->k;X&S+*I2~D9+(*pj672)92xfxFGC=4U$5k{QhZxkWoTJ>r{F%bWf@e?{~ zM<=U2hS--I&~5l&B9tAGS=DHY(t@TK*I7>|W0{;yk!w}E0G*HE&Vj(=qX2ViW9xX< zlROFhVqgA(#v`W!3@P7za%YTHZa{m-3>^`#Ht-8da$PZ~9dyo89NjIfk2zWce5Ou) zqnucH7df6g(85Na#+x9|F>H~_NQF;IJJ;d18A$$OHju{pc?AJgUWiT*}+-q zvM;q}nd2=`cK)1J{Ui0h*IOnfpw83dcaQ%SCL|W;>bZz28%e+H`rhDsz17aTIw>WhO=eU zYMJ8LUy?8Noau7p`>ncO-u{|x@;%$hqrxs_8z+b*9^Qh*wX3JJX}yk)t<67H%xc~l z5tq-Sb)tEPorGYi7Rmt3Ij0UAIe6K=)lP5SV1J3iTdS=k5i+Q)Gmd+>7YPf z4lkcM-j|p#LR~_;7?|Aq)0!0a#6F~d5m~G=5|N=ubWyCJ!Be1lObdhiGdHd1@n5-{m*8MxE%Ggdc$)cO@CAy;L< z%85>Y;qEEcl-boRk3h;T)clECX)@KE_Z~x~GDCj$DUIEmU>c8TXE&W1BiC6sC@00~ z)cl9?a9YUt;;tQIZkPqZbgl#j{=r4%4#46R1!qBj!rh^l1~{g)R+dE6LgLI(H=tGi)mB(hSN%9H{JRN zT6Ycl+f`w6yz~NL0p0C&p5U@i+TM{B<~j)dSFbplk3ckE%$gzG4+;K9#$t_ff2iFtyp$A+e>^%v~i8(^#$yz`*8 zuoxZ@X($54(0ar^Ay<8_tlB*0Rg=lf&tsz-BkG2g5oNRQ!Cxf>9|-@eE^n=pfd+ro zMGW%q;piVNAjsCgVp?!GUu%t4R)n*U#;|HZl}X?TEk-L;D+Hb`4^6Vli*Zjr`OK zIb-k|?-Se5a)NR*ZA zp$*5Mg66=EeIk^lpT-uBYeMLM0*Ic*RIOeKuie{HfB(c@+>ZcYKk0p?UEnGlA&H(= zGN^gkg}^^^Han5W#o=ftk*p%NKRcu$C0;S!qA-oYoz zcqayQky=v#>dtYfM?kTkPR_`fJs5M8iQ6G9;B6u_XS@|!~j62Q>_u(0htOibR> zYKC5uqiIDp79I@NtGPO-988NG0^1tPzz>aW0j#h40XY!fTTK{qYAs}7e3@`@pTi<< zM7cEwOU3ZWlJj)9g0>BBwNAabY}_UL=!iPmdL`b5|8fIIrE#sXBKF%ttOKs$Y(ODz zrtp!+_FOh2iJ7URHSHlqtm91;5_hUYbl3CvP8D6WCjFKdevsb z(qVVUWr*?*nlyfpzUj#W z2s%TmdO{S|YZ^vK_gtE)qYqRf;o!cBq2gIBbq#4!P50m-Np?7(`kEn33sacR&molv zizb*V!ug*972EbPW+Uu_&SXx~B3XSc&o(4np3n6mzZnH#4o)qA;X0nDly?_bXIC}A_*Sg*{U%KYd!m9x1j4wIxUPs#{ z93_eOo4CGMz?fa&e5bZ?SGkiqOm$m?3^rC7O+m4B{C1m80Xz|DFwHc!v!p?jPtZw8 zfEyl)J^dyAY%L{g{zgKRdF;6i0}criS;QpqLJKNiyFCwxeR_I7zPPr`BP^yGW^ z5XiZjn=jX3@m?WVr|d(cQmZ{&q-Eq%Ct;Wq2sBR+5n4KRyC_C-d!jAZY8x>ktlcG7 znrIO6)zB!0pXdQ2K4v&mVW`Tu3L5Nm3m&I?iyGrjk2tr>7E>zYpft6#tX12a>;*No zH5gs^JWmPmCv~Djaq1?Wa(5p%5SPtGUG(@T;@Tfr(s`94^k^AuW=P(&f)q`uEusgm z8Yk1mgX$=?^CVQvW0CKI3V&E>q)NA@OXZN0{}{Ai@fTl=g;-cqsHZwdz~ZC9oW)v{ z$6CWKF*;R@UQ=-^?y12EGTh=kG3axLpn9q|zcrfz5#ze04qRXOMJ$!W`Z7mK2H=Ln z{Z$U+^tH8Ams}@Jk$Xs;nR;YXVuaUOX$Pxz3qrAHwx8I8B>adYV0`t2TzWTb8HyRZenB=l|I%ee6+J_J(HHh0tr zkl&)k%FeNLmy_^EOQG}AI!6|28<>3zMX~o517go{B1W|bX(P{tE!He8VT48x;d0`+ zqo^PqbpQMo7h#X_UmQEy{47{wFugBCC!CW40+z?M&8m!5z#N&HRwUHG_@>EYdLrGQ zEJs2)&I5RV7UVe9W+BR6V(PdbOWGH0&KNL!zO- z$6kpR`zZKI%nuT6oMO|1s*_H6iY5;D{czRm2x({7&f7|LNJaLF{Kd*|pfQYP=V^iS zwUoznd~ykzoCIhR&nnFqANH<_p2@cXp70MWmPNG47cy?}#+(JNODQuE4*NL6;SE`{N3nUWd}OKi|GCA_f!2@sV05K;>e@s8u5|E7? zTDiraV7zhd+}AJt4@5KG*l%~82St=fv+tO#@bbpX_=o_OrQl;s-iFM6WVSMlMmM@# zR|GkwzkTBf0Uha*%&P9LE_5?LkorWAFDnj73g;ljJ2I$;w4aN{v8OF{BTcCG#4{^3)<6NOIIgcCP`Nv?O8V}4ud*9kHI9pV-u+b!aY~wmEsvzAB z9Pe9}UgOJv>DNbT>F-IbIgv!x;M(bZL6cL@f!*l?a@)xR=zSs|RwQy7U=)$%t=jd+ z17wKIT{lnm@a)X(ImLx%USysZ#_|Ce?vSncZj^Wcms6S-8rlKFPjp~k@(Wou?am0! zEmareXLM`%N7M`5C&_0hFrv4UnJr%lHPDFIhq-z87WA4_bao$0(0nDY;nmmyGWh|d3G5Yyhlx*C2-19>w?2Yv0$Aj-GP+1p08OFV*6|I}P) zncVoaICF^ULp|vwCCi5X1Gm>1ez3CU2fWvGF~+m}L94p#Rne3C(&D%UDi-*Ar9G;n z@*5>y72HCK8)~oA#}~-|wgUEYuAu)p>yAK!G*TB~ga7jeRrjE)!NlJ$w~`b})qzHT zTT|pO`|@rVeQSxbNDUFVg`jTLAt7{;g#Z>!@^b4S$IWvc#m!EOYq&omem7_@2f>sD zlBhxfL=)V4aH$|9QV-^ymKL7=y&2irACGUNN+7Mbby%Pe)G#*OVaS7#WH!8xE~bl1 zcz%;jkM{jp3Th{k+Y9iDh5eT{^T38_cSe0R@>~vq$@;X`_7f{v!e{x=_*U!oaeA?q zlUY+k3G8k-b%gJ8*uTG{@=lrbH!wIR7&|Ob#r%>N)JSbVT}76N&YykhJX$K-5Uemz zq^SUbV%P5@7F%<9UKn9!*YO)}Z8JRfP+;;NXs~@mt$SKNU|E{3;@shBJ6ET}7 z&|!i~97GKwPkOcBi1;Xk#5)Jykq8hD@x3d;J99W)}D) zYXUKL4A{T)&Y{w|f-5fVId~rh2a+@d7k9jeAijsh57MvWe2;b|5ZWBlyP7EOB^Ip- zwX)cCJ`yj^b8r6-uMOSf7ws%?u@ngm+0UA zHp3E0Dy7^>n77ElY<2tpP~QK^=O_=I8oeSf987j{vl+v0pO%R;lij+nC{(;sQCOfxy1~qB$2yIPYTwG1D%}&ic6aCGIX^XHxL82ho;Hx|^ z+pQ@o94iiib_{KZ4H6})<` zSOKMMN16d-vsLF9`#89vIEEiuoEWn#)f*a$EadUY@6oJPC6grIe*A@mJCMEO|BXppfOue7mzGfwbwVsQHTiP8M{R+h{;7OnO;k~ z<}_opASpwJ{ODQ)e=*&7uA*9?pE-@Om5iB}AtUjkH|*|-Ec9%8{E^N)L?D;OTAD4| zqcBl~VdD+ryR_k&pv?vBF~e?Ul6jGxmiPBGK46JHwB#}$bGenCF-j-;?-;)z2234r=Bg^uYV2l%8tn+ePzj2e&TUyq0J}qjVf&(3WbCtsHnh1F`}iQ z<|QY%OjXYzFhw_&9a3hBIE;Tgdk~p0$)bd^1!h||;ELul-Uu+oq@M>&=uw->d8Zf# z719=^KG0@RpQd#1g+Q}9lz2K1IIc8oxrKwVuv&upy;E=o}_SAl72Aur!_VR!8@ZJrhl80 zL#g|gu7p|R0Zh7lD7te2cP3B+6sk0#@M^pr&^Q0|6!sEpI8!WaVR~Qn>y^#f57IhV z7P#VDaNujMJAeQ%b=d#m zFpP@F!S^pM#)*9s%E7_zW5nHXn0wg^U%}JNe=2r2U zt&`)7uMOZl%ob5g&*_EcM}Nz7YfK5i*`gT#wd_dr37YcW=kW(U^a-Wo2{yB%*=v4d z4t11y1lWW&lx%SzUNIVOJzZP3Q^y7NXZ65b6#}&KY;)mdS!hH-U*|j zLTSc%4`Dtvnb~hY*rR^Yj&pcVoqhb9d;GuHBTtLzf5VP>ZHE8a`%G#1Ox4QgN-lq2f?R;T)*8;W7wm} zl03*0jvvl4sP9}@bloJ01I=Y#vh0*WtH8t8zhO&?6k_hjkPuN~auj{JmMV5Amo!9a zN*V_0Ra%gAH5*qY%c${DI_>(OJ~V1q$f-Nb-I5?`btreRn(1LhG;9(wh|#8bb?E#u zY@`Z!+D%(Bsns0e2&0OtRYS7yi~a8%`T6>qddP2^5fvr7r%jl95T71C{oYP+wLp}; zn6v8ntlf+Gpo;)_gp`7}F5y;K295@CS&0m`0kpGag68#vjbq+mZJVHPCVU|EX9EFN zv*7P~wg!eF`xcF`;l(&G89x-X_7NtyvyEP*m+!B<6;r@ude!9%GA9Y*Q6tFBA|6~C zKnqJV!U$MbcBGe5Eva&BO182`41xKf7%dx+d6FT%g}3;W_b7hYvsCwJUg`Ayx!X*8 z#(th$IgFHAuIbzTqw5cgW-J*B&h7|@rL_wF$P43PAGo<3sLScoSr6657=~Z58Xd#@UAXGd+i^Yd+I26H(SPwu^L6N*5_92K+2CE2Gt*Z}HMI#DY zRBEfVr9}Y&O`oVSK)6CU1gnU4c6LKte6RD*zW2*-e)G+dFWH%IKEsm-d_@h~em1|9gqN~D!56auT%I0SAdE)JP zRmaMCvd*nGVHwr-TMYGA3dtCUyjWdN9k=_tL(g2+inr@rYM*LWWad*-?=;bD_R~oP z!#uq_kXBW$t!1FEB{aM%z0M&+Z1avRXsoGj3nqV`9inWT+R<=K)%B{g>rSg@SyK*wVPep}qc!kbl)Xn96SJw?A|$fDX!3yw z=@0TN(aEp)`N>CQzm?`(m28V`FR%%$i!M;5jJLeKWyrp{{;p4?xKdcx?DZre+dlYD zaw|DUsx01t3H3?@Uk31tYK!09txh;$v-Z0u84t7c%zw4pSvAlsh;;i}m0nxZ7uT(C zYt0WW-S~zR{yM$#=;XJJKWFXdy4LG;&+wRb`*D;>>Y|$aON6yD8Cm$pyT$tRzm=%= z%CGGb35KO_e>`{#z2F&qkP5&cG;402EX%ZGdOQcd{Bdv-EloBNa7yN)NhboH$mY3I zcP8MT@MwWfjVXqh(vJh98{)QagmDjJgqFV`=Fen4jEaw1AG_frj#`I$LT}3yN_7PX zICG61$BL>c)Dzw|X9`teFIB+|ym_IEJTlOc6ZjPlknFK^_&*T?wFX9sC_%j3!g6eX z?O7ZGT$Foo$8y|RoOL)T8T14TS{P$iv=I1J96Z3M0-+kqX(2cQAa1cl?Tw}=fjSzS zFpLxpfc6$9F^WzrfuAx){cJKOKzOSzvV)a8WYi9-ZZiSNj-a&F1WhT*0Rd`TP4P%` z^ubMY9$3&utFUhIvs7>{B=f=XHl{b?HrN|Sh|K_@a}6{&M?v7Fb0BU8JQa4xG|g2T z44cgoJ@0~upSu}>idIIr0tzVH8GIrbR!pYn(x{!lhnFJ!c7QIkC^V@pqSv|92f_RU zj%FD4DXWoOcr*{KDQve!)}Sj4a79jsaWy_HqQh5+*dwq7<{k8SXhP6i9S>4FEouBT z#KoH#+!QD~nNb^o`0q(@rTYQBLE3g!JSEF^n-oa2gQ1m9w5Y6$z`qcI;QfBUr<)NB z>L&1!G>FXx-TxSjisl~j6iQcGlb$k zf|CG(`#ix&-!#+(%`4ql%MM*0)!5QcH#k!K226zSe|)$ad|3^iC`IIF{pM(SxV-qZ#)1B_kG5O?~Ea@wG<(FpKfG7ezjO9HR2XK?e696%D8 zQ&2 + exit 1 + ) + pwd + )" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=$(which readlink) if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin ; then - javaHome="$(dirname "\"$javaExecutable\"")" - javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + if $darwin; then + javaHome="$(dirname "$javaExecutable")" + javaExecutable="$(cd "$javaHome" && pwd -P)/javac" else - javaExecutable="$(readlink -f "\"$javaExecutable\"")" + javaExecutable="$(readlink -f "$javaExecutable")" fi - javaHome="$(dirname "\"$javaExecutable\"")" + javaHome="$(dirname "$javaExecutable")" javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME @@ -109,52 +118,60 @@ if [ -z "$JAVA_HOME" ]; then fi fi -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + JAVACMD="$( + \unset -f command 2>/dev/null + \command -v java + )" fi fi -if [ ! -x "$JAVACMD" ] ; then +if [ ! -x "$JAVACMD" ]; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." >&2 fi # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" >&2 return 1 fi basedir="$1" wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=$(cd "$wdir/.." || exit 1; pwd) + wdir=$( + cd "$wdir/.." || exit 1 + pwd + ) fi # end of workaround done - printf '%s' "$(cd "$basedir" || exit 1; pwd)" + printf '%s' "$( + cd "$basedir" || exit 1 + pwd + )" } # concatenates all lines of a file @@ -165,7 +182,7 @@ concat_lines() { # enabled. Otherwise, we may read lines that are delimited with # \r\n and produce $'-Xarg\r' rather than -Xarg due to word # splitting rules. - tr -s '\r\n' ' ' < "$1" + tr -s '\r\n' ' ' <"$1" fi } @@ -177,75 +194,85 @@ log() { BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then - exit 1; + exit 1 fi -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +export MAVEN_PROJECTBASEDIR log "$MAVEN_PROJECTBASEDIR" +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" + log "Found $wrapperJarPath" else - log "Couldn't find $wrapperJarPath, downloading it ..." + log "Couldn't find $wrapperJarPath, downloading it ..." - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + fi + while IFS="=" read -r key value; do + case "$key" in wrapperUrl) + wrapperUrl=$(trim "${value-}") + break + ;; + esac + done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget ${QUIET:+"$QUIET"} "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + wget ${QUIET:+"$QUIET"} --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; - esac - done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" - + elif command -v curl >/dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl ${QUIET:+"$QUIET"} -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl ${QUIET:+"$QUIET"} --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") fi - - if command -v wget > /dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi fi + fi fi ########################################################################################## # End of extension @@ -254,22 +281,25 @@ fi # If specified, validate the SHA-256 sum of the Maven wrapper jar file wrapperSha256Sum="" while IFS="=" read -r key value; do - case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + case "$key" in wrapperSha256Sum) + wrapperSha256Sum=$(trim "${value-}") + break + ;; esac -done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" if [ -n "$wrapperSha256Sum" ]; then wrapperSha256Result=false - if command -v sha256sum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + if command -v sha256sum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c - >/dev/null 2>&1; then wrapperSha256Result=true fi - elif command -v shasum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + elif command -v shasum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then wrapperSha256Result=true fi else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi if [ $wrapperSha256Result = false ]; then @@ -284,12 +314,12 @@ MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] \ + && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi # Provide a "standardized" way to retrieve the CLI args that will diff --git a/mvnw.cmd b/mvnw.cmd index f80fbad..c453424 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -18,7 +18,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @@ -59,22 +59,22 @@ set ERROR_CODE=0 @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome -echo. +echo. >&2 echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 -echo. +echo. >&2 goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init -echo. +echo. >&2 echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 -echo. +echo. >&2 goto error @REM ==== END VALIDATION ==== @@ -119,7 +119,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B @@ -133,7 +133,7 @@ if exist %WRAPPER_JAR% ( ) ) else ( if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... @@ -160,11 +160,12 @@ FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapp ) IF NOT %WRAPPER_SHA_256_SUM%=="" ( powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ " exit 1;"^ "}"^ "}" diff --git a/pom.xml b/pom.xml index b51eac5..2d92038 100644 --- a/pom.xml +++ b/pom.xml @@ -68,19 +68,19 @@ 5.14.4 47.0.2-1.5.1 2.4.4-1.0.2 - 1.0.1 + 1.1.0 24.2.2 1.37 - 2.22.1 + 2.22.2 3.5.16 - 3.15.0 - 3.5.6 - 3.5.0 + 3.16.0 + 3.6.0 + 3.5.1 3.4.0 3.12.0 - 1.7.3 + 1.8.0 diff --git a/wasmos-provider/pom.xml b/wasmos-provider/pom.xml index 8f5b656..90d689a 100644 --- a/wasmos-provider/pom.xml +++ b/wasmos-provider/pom.xml @@ -59,7 +59,7 @@ org.codehaus.mojo exec-maven-plugin - 3.6.3 + 3.6.4 3.9.16 3.9.16 - 3.15.2 + 3.16.0 2.2.1 @@ -100,7 +100,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.15.2 + 3.16.0 webassembly4j-bindgen true diff --git a/webassembly4j-component-builder/pom.xml b/webassembly4j-component-builder/pom.xml index 3e1f876..40ff7a6 100644 --- a/webassembly4j-component-builder/pom.xml +++ b/webassembly4j-component-builder/pom.xml @@ -25,7 +25,7 @@ 3.9.16 3.9.16 - 3.15.2 + 3.16.0 2.2.1 @@ -93,7 +93,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.15.2 + 3.16.0 webassembly4j-component true