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:
- A standard Java class (
Greeter.java). - A WIT contract (
example:greeter) the library publishes. - A Rust consumer that binds against
example:greeteronly.
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 importsfiji:jvmand exportsexample:greeter.crates/pkg-r-2-greeter-consumer/— the host-side Rust consumer that binds toexample:greeteronly.smokes/pkg-r-2-greeter-smoke.sh— the mechanical acceptance check: composed component exportsexample:greeter, consumer source and WIT contain nofiji, andgreet("World")returnsHello, 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.