Java#
We provide openjdk21 and openjdk8 packages in emscripten-forge that allow running unmodified .jar executables and java applets in the browser, without a Java server.
openjdk21 recipe documentation:#
Create one environment with the tools (openjdk21-wasm-link and the Emscripten
toolchain it uses), and one with the JVM:
CH="-c https://repo.prefix.dev/emscripten-forge-4x -c conda-forge"
micromamba create -y -n env_tools $CH openjdk-tools
micromamba activate env_tools
micromamba create -y -p ./jdk21env --platform emscripten-wasm32 $CH openjdk21
Link your application into a page:
openjdk21-wasm-link --prefix ./jdk21env -o out/app.html --app path/to/app.jar --arg -jar --arg /app/app.jar
Packing a multi-file jar or a directory is supported, for example
openjdk21-wasm-link --prefix ./jdk21env -o out/multifilejar.html --app ./multifilejardir --arg -jar --arg /app/multifilejardir/jarsdir/jarexecutable.jar
Option |
Description |
|---|---|
|
The emscripten-wasm32 environment with |
|
Output file, |
|
Copy a host file or directory into the virtual file system (default |
|
Default Java command-line argument. Repeatable. Under node, command-line arguments override these; in the browser, |
|
Environment variable for the program. Repeatable. In the browser, the page URL can set more with |
|
No display: uses |
|
Build for node (the program exits when |
|
Maximum wasm memory (default |
|
Number of pre-started pthread workers (default |
|
emcc optimisation level for the link (default |
|
Link only the runtime, as a module like the prelinked one below ( |
|
Pass everything after it to emcc. |
Hosting the page#
For this openjdk21 recipe, SharedArrayBuffer is needed. It must be served with these two headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
You don’t need to configure them yourself:
When the output is an
.htmlpage,openjdk21-wasm-linkcopiescoi-serviceworker.jsnext to it, and the page loads it. If the server doesn’t send the headers, this service worker adds them in the browser and reloads the page. This makes the page work on static hosts such as GitHub Pages. Keepcoi-serviceworker.jsin the same directory as the page.For local testing,
serve.pysends the headers directly:python3 $CONDA_PREFIX/share/openjdk-tools/serve.py out 8000(inenv_tools).
Either way, the page must be served over HTTPS or from localhost; browsers
ignore these headers on other plain-http addresses. Opening the file directly
(file://) does not work either: service workers only run on http(s) pages,
and the browser won’t load the .wasm and .data files from file://.
If you link to .js instead of .html, no service worker is added. Your page
must then send the headers itself. None of this applies to
programs linked with --node.
Prelinked runtime#
The openjdk21 package also contains the runtime already linked, like
openjdk8 does, so a page can run Java without openjdk21-wasm-link. Copy
these files into one directory on your web server:
ls jdk21env/share/openjdk21-wasm/
# openjdk21-web.js openjdk21.js openjdk21.wasm openjdk21-jre.json
# openjdk21-jre.data.gz coi-serviceworker.js ...
and start Java from a page, listing the files the program needs:
<div id="x11-wasm-root" style="position:relative;width:800px;height:600px"></div>
<script src="/jvm/openjdk21-web.js"></script>
<script>
OpenJDK21.run({
args: ['-jar', '/app/app.jar'],
root: document.getElementById('x11-wasm-root'), // where Java windows are drawn
files: { '/app/app.jar': 'app.jar' }, // VM path: URL to fetch
onExit: function (status) { console.log('Java exited with ' + status); },
});
</script>
The page must be cross-origin isolated, as above. If the server doesn’t send
the headers, openjdk21-web.js loads coi-serviceworker.js from its own
directory; a service worker only covers pages in its directory and below, so
put the page next to the files (or in a subdirectory) in that case.
OpenJDK21.run also takes env, jvmArgs, heap (e.g. '512m'),
onStdout, onStderr, onProgress, documentTitle: false (keep the page
title) and stdin: true (then feed System.in with module.writeStdin(text)
and module.closeStdin() on the module the returned promise gives).
openjdk21-java from openjdk-tools runs the same prelinked runtime under
Node.js, like the java command (headless AWT; the host’s files are visible):
OPENJDK21_WASM_HOME=./jdk21env/share/openjdk21-wasm openjdk21-java -cp classes Main
openjdk8 recipe documentation:#
openjdk8 runs Java applets in the browser. It can also run plain Java desktop
programs.
Install the package and copy its browser files into one directory on your web server:
micromamba create -y -p ./jdk8env --platform emscripten-wasm32 -c https://repo.prefix.dev/emscripten-forge-4x -c conda-forge openjdk8
ls jdk8env/share/openjdk8-wasm/
# openjdk8-web.js openjdk8.js openjdk8.wasm openjdk8-jre.json openjdk8-jre.data.gz
# openjdk8-javaws.jar
Then load openjdk8-web.js on the page that contains the applet:
<script src="/jvm/openjdk8-web.js"></script>
<applet code="MyApplet.class" codebase="classes/" archive="my.jar" width="400" height="300">
<param name="foo" value="bar">
</applet>
Replace /jvm/ with wherever you put the files. The loader loads the other
files from its own directory (or from a data-base="..." attribute on the
script tag). When the page has loaded, it starts every <applet>, and every
Java <object> or <embed>, in a single Java VM. To start them yourself
instead, add data-applets="off" to the script tag and call
OpenJDK8.runApplets().
To run a program instead of an applet, call OpenJDK8.run() and list the
files the program needs:
OpenJDK8.run({
args: ['-cp', '/app', 'Main'],
root: element, // where Java windows are drawn
files: { '/app/Main.class': 'Main.class' } // VM path: URL to fetch
})
Java Web Start#
On a page that loads openjdk8-web.js, following a link to a .jnlp file
starts that application with Java Web Start, as a browser with Java installed
did:
<script src="/jvm/openjdk8-web.js"></script>
<a href="app/launch.jnlp">Launch the application</a>
Web Start is IcedTea-Web’s javaws, in openjdk8-javaws.jar, which is only
downloaded when it is needed. It reads the JNLP file, downloads the jars,
shows the usual security dialogs (unsigned code, certificates, missing
manifest attributes), and runs the application in the sandbox or with the
permissions it was granted; the JNLP services (javax.jnlp.BasicService,
…) work. The application’s windows appear in a layer over the page. Call
OpenJDK8.runJNLP(url, options) to start one from a script, and add
data-jnlp="off" to the script tag to leave .jnlp links alone. The JNLP
file and its jars are fetched by the page, so they must come from the same
origin (or be served with CORS headers). Give the JNLP file a codebase, as
JNLP files normally have: without one, javaws treats the application as a
local file and asks whether to run code from its location. Each page runs one
Java VM, so it runs one Web Start application at a time.
run() and runJNLP() also take windowManager: true, which lets x11.wasm
draw title bars on top-level windows so they can be moved and closed
(OpenJDK21.run takes it too). It is experimental: with AWT the title bar
shows the name of a hidden helper window (“FocusProxy”) rather than the
frame’s title.
Outside the browser#
Outside the browser, openjdk8-java from the openjdk-tools package (see the
openjdk21 section) runs Java programs under Node.js 24 or later, with headless
AWT, like the java command, and openjdk8-javaws runs JNLP files. Point
them at the openjdk8 environment:
export OPENJDK8_WASM_HOME=./jdk8env/share/openjdk8-wasm
openjdk8-java -cp classes Main
openjdk8-javaws app.jnlp