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

--prefix DIR

The emscripten-wasm32 environment with openjdk21. Required.

-o FILE

Output file, .html or .js. Required.

--app SRC[@DEST]

Copy a host file or directory into the virtual file system (default DEST: /app/<basename>). Repeatable.

--arg ARG

Default Java command-line argument. Repeatable. Under node, command-line arguments override these; in the browser, ?args=a,b in the page URL overrides them.

--env NAME=VALUE

Environment variable for the program. Repeatable. In the browser, the page URL can set more with ?env=NAME=VALUE,...; these are added to the --env ones and replace any with the same name.

--headless

No display: uses libawt_headless and leaves out x11.wasm.

--node

Build for node (the program exits when main returns). Implies --headless.

--max-memory MB

Maximum wasm memory (default 2048).

--pool N

Number of pre-started pthread workers (default 12).

--opt LEVEL

emcc optimisation level for the link (default -O2).

--module

Link only the runtime, as a module like the prelinked one below (-o DIR/openjdk21.js).

--

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 .html page, openjdk21-wasm-link copies coi-serviceworker.js next 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. Keep coi-serviceworker.js in the same directory as the page.

  • For local testing, serve.py sends the headers directly: python3 $CONDA_PREFIX/share/openjdk-tools/serve.py out 8000 (in env_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