Skip to main content

Tutorial 3 — Publish a Java library as a Wasm component

Goal: show a Java library appearing as a non-Java component to a Rust consumer that has no awareness of Fiji.

This tutorial is a different shape from Tutorial 1 (Hello, Fiji). Tutorial 1 demonstrates running a Java application inside a component. This tutorial demonstrates a Java library being the implementation behind a component contract — a contract whose vocabulary has nothing to do with Java or Fiji.

The shape

Three artifacts:

  1. A standard Java class (Greeter.java).
  2. A WIT contract (example:greeter) the library publishes.
  3. A Rust consumer that binds against example:greeter only.

The consumer never sees fiji:*. That is the structural boundary this tutorial demonstrates.

The Java side

public class Greeter {
public Greeter() {}
public String greet(String name) {
return "Hello, " + name;
}
}

Compiled with standard javac. No annotations, no Fiji-aware API, nothing imported from Fiji.

The contract

A WIT package the library publishes:

package example:greeter@0.1.0;

interface greeter {
greet: func(name: string) -> string;
}

world greeter-component {
export greeter;
}

That is the consumer-visible WIT. Note what is absent: no fiji:* import, no Java-shaped types, no JVM vocabulary. The contract names itself in its own namespace and nothing else.

Behind the scenes, an internal wrapper component imports fiji:jvm/jvm@0.1.0 (where the Java runtime lives), loads the Greeter class, and exports the example:greeter interface. When the wrapper is composed with the Fiji runtime via wac plug, the fiji:jvm import is satisfied internally; the resulting composed component exposes only the consumer-visible surface.

The consumer

A stranger Rust binary loading the composed component:

wasmtime::component::bindgen!({
path: "wit",
world: "greeter-component",
});

// ... (host setup, see crates/pkg-r-2-greeter-consumer/src/main.rs)

let bindings = GreeterComponent::instantiate(&mut store, &component, &linker)?;
let result = bindings
.example_greeter_greeter()
.call_greet(&mut store, "World")?;
println!("{}", result);
// → Hello, World

The consumer's WIT directory contains only the file shown above. The consumer's source contains no reference to Fiji or to Java. A grep for fiji across the consumer crate returns zero matches.

That is the categorical move: a Java implementation can disappear behind a component contract that doesn't admit it exists.

What's still required (today)

The composed component currently expects a JDK class image preopened by the consumer at /jdk. The consumer supplies it via an env var (PKG_R_2_JDK_IMAGE). That preopen is the named residual — it's not a contract leak (the WIT is clean, the symbols are clean, the vocabulary is clean), but it is an execution prerequisite the consumer has to satisfy.

Internalizing that requirement — bundling the JDK class image inside the composed component so the consumer needs zero Fiji-side prerequisites — is the next frontier the project is moving toward.

Where the code lives

In the repository:

  • crates/pkg-r-2-greeter-wrapper/ — the wasm-component wrapper that imports fiji:jvm and exports example:greeter.
  • crates/pkg-r-2-greeter-consumer/ — the host-side Rust consumer that binds to example:greeter only.
  • smokes/pkg-r-2-greeter-smoke.sh — the mechanical acceptance check: composed component exports example:greeter, consumer source and WIT contain no fiji, and greet("World") returns Hello, World.

Why this tutorial exists separately

Tutorial 1 answers: can I run my Java code on Fiji? — yes, the same .class your existing javac produces.

This tutorial answers a different question: can a Java implementation present itself to non-Java consumers as a typed component, with no awareness of Java required on the consumer side? — yes, that path now exists and is mechanically verified.

Those are different product surfaces. The first is Java portability. The second is Java becoming an implementation language for component-model packages.