Skip to content

feat: add Java integration via Chicory WebAssembly runtime#561

Closed
andreaTP wants to merge 7 commits intojdx:mainfrom
andreaTP:java-wasm
Closed

feat: add Java integration via Chicory WebAssembly runtime#561
andreaTP wants to merge 7 commits intojdx:mainfrom
andreaTP:java-wasm

Conversation

@andreaTP
Copy link
Copy Markdown

Thanks for the great project! @maxandersen asked me to look into it to see if it can be easily consumed from a Java application and here we are 🙂

Here I added support for running the usage CLI natively in Java as a plain dependency, powered by Chicory (a pure-Java WebAssembly runtime with zero native dependencies).

Changes

  • wasm32-wasip1 compatibility — gate xx dependency behind cfg(not(wasm32)), replace xx::file/xx::regex with std::fs and regex::Regex + LazyLock, add wasm32 stubs for shell execution
  • [profile.wasm] in Cargo.toml — dedicated Cargo profile (opt-level = "z", strip = "symbols") for smaller WASM output (4.5M vs 6.2M default)
  • mise.toml wasm tasks — migrated cli/Makefile targets to mise (wasm:build, wasm:get-wasisdk, wasm:get-binaryen, wasm:clean)
  • integrations/java/ — Java wrapper that runs usage compiled to WASM via Chicory with build-time AOT compilation
  • .github/workflows/test-java.yml — CI workflow for the Java integration (mvn test + jbang example)

Java API

// Commands that write to stdout work directly
Usage.builder().withStdin(spec).withArgs("generate", "json", "-f", "-").run();
Usage.builder().withStdin(spec).withArgs("generate", "completion", "bash", "mybinary", "-f", "-").run();
Usage.builder().withStdin(spec).withArgs("generate", "manpage", "-f", "-").run();

// Commands that need file output use withDirectory(Path) — works with ZeroFs for in-memory FS
Usage.builder().withStdin(spec)
    .withArgs("generate", "markdown", "-f", "-", "--out-file", outFile.toString())
    .withDirectory(outDir)
    .run();

Next steps

To move this forward I'd be happy to discuss if you have any interest in publishing the Java package (super minimal dependencies) or I can set up a usage4j repository under the roastedroot organization. Either way, the Rust changes (wasm compat + [profile.wasm] + mise tasks) would be nice to have here regardless.

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

mise.toml Outdated
dir = "cli"
run = """
if [ ! -d "binaryen" ]; then
wget https://github.com/WebAssembly/binaryen/releases/download/version_125/binaryen-version_125-x86_64-linux.tar.gz
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be enough in CI, we can expand on OS/arch compatibility as a follow up

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR adds Java integration for the usage CLI, enabling Java applications to parse and use usage specs at runtime without any native code. It does this in two parts: (1) making the Rust codebase compile to wasm32-wasip1 by gating xx dependencies behind cfg(not(target_arch = "wasm32")), replacing xx::file/xx::regex with std::fs and regex + LazyLock, and providing no-op stubs for shell execution; (2) adding integrations/java/ with a Chicory-powered Java wrapper that AOT-compiles the WASM binary at Maven build time into a regular Java class and exposes a clean builder API.

Key highlights:

  • The Rust cfg gating is well-structured across lib/src/sh.rs, lib/src/spec/mod.rs, lib/src/error.rs, lib/src/docs/markdown/renderer.rs, cli/src/lib.rs, and cli/src/cli/complete_word.rs.
  • The Java API is clean: a single Usage.builder()...run() call returns a UsageResult with stdout, stderr, and exit code.
  • The [profile.wasm] Cargo profile (opt-level = "z", strip = "symbols") produces a compact WASM binary (~4.5MB vs 6.2MB default).
  • wasm:clean now correctly removes target/wasm32-wasip1 from the workspace root (previously-flagged issue resolved in this version).
  • wasm:get-binaryen was removed (previously-flagged dead-code issue resolved).
  • wasm:get-wasisdk remains hardcoded to x86_64-linux (previously flagged, CI passes because it runs on ubuntu-latest).
  • The actions/setup-java step in CI is missing cache: maven, which would avoid re-downloading Maven dependencies on every run.
  • The WASM output path is duplicated between pom.xml's plugin config and the WasmResource.java template — a Maven property could keep them in sync.

Confidence Score: 4/5

  • This PR is on the happy path to merge; the Rust wasm32 compat changes and Java integration are well-implemented with good test coverage. The one unresolved previously-flagged concern (wasm:get-wasisdk hardcoded to x86_64-linux) is a developer-experience issue, not a correctness or security problem, and CI passes cleanly.
  • Both rounds of previously-flagged issues have been addressed (wasm:clean now runs from the workspace root, wasm:get-binaryen was removed). The only remaining flagged item is the hardcoded platform in wasm:get-wasisdk, which doesn't affect CI. New findings are P2 suggestions (Maven caching, unreachable success-path return, duplicated WASM path). The Rust changes are minimal and surgical; the Java integration is clean, thread-safe, and well-tested.
  • No files require special attention beyond the P2 suggestions noted inline.

Important Files Changed

Filename Overview
.github/workflows/test-java.yml New CI workflow for the Java integration: builds WASM, runs mvn clean install, and exercises the jbang example. Missing Maven dependency caching (cache: maven in actions/setup-java) which would speed up runs.
integrations/java/src/main/java/dev/jdx/usage/Usage.java Clean Java wrapper: shares a static AOT-compiled WasmModule, creates a fresh Instance per call for isolation, captures stdout/stderr via ByteArrayOutputStream. The final success-path return (line 57) is unreachable for well-behaved WASI binaries (Chicory always throws WasiExitException even for exit code 0), but serves as a safe fallback.
integrations/java/pom.xml Well-structured Maven build: AOT-compiles the WASM via chicory-compiler-maven-plugin, uses templating-maven-plugin for the build-time WasmResource path, targets Java 11. The WASM file path is duplicated between the plugin config and WasmResource.java template.
lib/src/sh.rs New file: cleanly gates shell execution behind cfg(not(target_arch = "wasm32")) with a stub returning Err(UsageErr::IO(...)) on wasm32. Return-type consistency between the two conditional variants is correct.
mise.toml Adds wasm:build, wasm:get-wasisdk, and wasm:clean tasks. wasm:clean correctly runs from the repo root (no dir override) so it properly removes workspace-root target/wasm32-wasip1. wasm:get-wasisdk is still hardcoded to x86_64-linux (previously flagged). wasm:get-binaryen was removed compared to earlier drafts.
lib/src/spec/mod.rs Replaces xx::file and xx::regex helpers with std::fs::read_to_string and regex::Regex + std::sync::LazyLock for wasm32 compatibility. Logic is preserved correctly.
cli/src/cli/complete_word.rs Adds a wasm32 stub for the local sh() helper that returns miette::miette!(...). The non-wasm variant uses xx::XXResult and the wasm variant uses miette::Result — both are compatible with the ? at the call site since the enclosing function returns miette::Result.
integrations/java/src/test/java/dev/jdx/usage/UsageTest.java Good coverage: JSON spec, manpage, bash completion, markdown with in-memory ZeroFs filesystem, and invalid-args exit-code check. Tests validate both stdout content and exit code.
lib/src/error.rs Correctly gates the XXError variant with #[cfg(not(target_arch = "wasm32"))], preventing a compile error when xx is excluded from the wasm32 build.
Cargo.toml Adds [profile.wasm] inheriting from release with opt-level = "z" and strip = "symbols" for minimal WASM output size. Clean, correct addition.

Sequence Diagram

sequenceDiagram
    participant Client as Java Client
    participant Builder as Usage.Builder
    participant Exec as Usage.exec()
    participant Chicory as Chicory WASI Runtime
    participant WASM as usage.wasm (Rust binary)

    Client->>Builder: Usage.builder()<br/>.withStdin(spec)<br/>.withArgs("generate","json","-f","-")
    Builder->>Exec: run() → exec(stdin, args, directory)
    Exec->>Exec: Create ByteArrayOutputStream (stdout, stderr)
    Exec->>Chicory: WasiOptions.builder()<br/>.withStdin()<br/>.withStdout()<br/>.withStderr()<br/>.withArguments()
    Note over Exec,Chicory: Optional: .withDirectory(path) for file output
    Exec->>Chicory: WasiPreview1.builder().withOptions().build()
    Exec->>Chicory: Instance.builder(MODULE)<br/>.withMachineFactory(UsageModule::create)<br/>.withImportValues(wasi.toHostFunctions())<br/>.build()
    Chicory->>WASM: Execute _start (AOT-compiled)
    WASM-->>Chicory: writes to stdout/stderr via WASI
    WASM->>Chicory: proc_exit(code)
    Chicory-->>Exec: throws WasiExitException(exitCode)
    Exec->>Exec: wasi.close() (try-with-resources)
    Exec-->>Builder: new UsageResult(stdout, stderr, exitCode)
    Builder-->>Client: UsageResult
Loading

Comments Outside Diff (1)

  1. integrations/java/src/main/java/dev/jdx/usage/Usage.java, line 44-57 (link)

    Success-path return is unreachable for well-behaved WASI binaries

    Chicory's WASI implementation always throws WasiExitException when proc_exit is called — including for exit(0) from a normally-terminating Rust binary. This means exec() will always return via the catch (WasiExitException e) branch; the return new UsageResult(..., 0) on line 57 is unreachable in practice.

    This isn't a bug (it acts as a sensible safety net if a WASM module somehow exits without calling proc_exit), but worth documenting:

    // Note: Chicory throws WasiExitException even for exit(0), so this
    // fallback is only reached if the WASM module exits without proc_exit.
    return new UsageResult(stdout.toByteArray(), stderr.toByteArray(), 0);

Reviews (3): Last reviewed commit: "[autofix.ci] apply automated fixes" | Re-trigger Greptile

andreaTP and others added 3 commits March 23, 2026 19:03
- Make cli and lib compile for wasm32-wasip1: gate xx dependency behind
  cfg(not(wasm32)), replace xx::file/xx::regex with std::fs and
  regex::Regex + LazyLock, add wasm32 stubs for shell execution
- Add [profile.wasm] Cargo profile (opt-level=z, strip=symbols) for
  smaller WASM output (4.5M vs 6.2M default)
- Migrate cli/Makefile targets to mise wasm:* tasks
- Add integrations/java/ with Usage wrapper that runs the usage CLI
  compiled to WASM via Chicory with build-time AOT compilation
- Add CI workflow for Java integration (mvn test + jbang example)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
- Fix wasm:clean to remove workspace-root target/wasm32-wasip1 instead
  of cli/target which doesn't exist in a Cargo workspace
- Remove unused wasm:get-binaryen task

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 23, 2026

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 6.00000% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.75%. Comparing base (1b428b7) to head (1dd488a).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
lib/src/docs/markdown/renderer.rs 16.66% 6 Missing and 9 partials ⚠️
lib/src/spec/mod.rs 0.00% 4 Missing and 7 partials ⚠️
cli/src/cli/complete_word.rs 0.00% 3 Missing and 3 partials ⚠️
cli/src/cli/generate/markdown.rs 0.00% 2 Missing and 4 partials ⚠️
cli/src/cli/generate/fig.rs 0.00% 4 Missing ⚠️
cli/src/cli/generate/manpage.rs 0.00% 1 Missing and 3 partials ⚠️
lib/src/sh.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #561      +/-   ##
==========================================
- Coverage   61.98%   58.75%   -3.24%     
==========================================
  Files          48       48              
  Lines        7637     7777     +140     
  Branches     7637     7777     +140     
==========================================
- Hits         4734     4569     -165     
- Misses       1543     1671     +128     
- Partials     1360     1537     +177     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andreaTP
Copy link
Copy Markdown
Author

I'll fix the CI failure tomorrow, sorry for the noise.

andreaTP and others added 2 commits March 24, 2026 10:00
The previous SHA didn't exist. Pin to v0.1.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pin maven-surefire-plugin to 3.5.2 for reliable JUnit 5 discovery
- Return defensive copies from UsageResult.stdout()/stderr()
- Catch RuntimeException (WASM traps) and surface as exit code 1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andreaTP
Copy link
Copy Markdown
Author

I think I addressed all the comments but:

} catch (RuntimeException e) {
    // WASM trap or other Chicory error — surface as non-zero exit
    return new UsageResult(stdout.toByteArray(), stderr.toByteArray(), 1);
}

I disagree we should catch RuntimeException they're programming errors, not normal exit paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andreaTP
Copy link
Copy Markdown
Author

@jdx is this interesting to you or outside the scope of the project?

Would you be up in accepting a minimal set of changes to enable the compilation to Wasm without having to patch sources?

Thanks in advance 🙏

@jdx
Copy link
Copy Markdown
Owner

jdx commented Mar 31, 2026

I wouldn't accept this as is. I'm not sure what a minimal patch would look like but possibly. wasm definitely isn't a priority for me

@andreaTP
Copy link
Copy Markdown
Author

Thanks for getting back!

I put up #570 for the minimal patch for you to evaluate the impact 🙏

Closing this one.

@andreaTP andreaTP closed this Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants