Skip to content

Commit 5ad2099

Browse files
committed
Add Web Image: Export Java Method Example
1 parent 9433919 commit 5ad2099

7 files changed

Lines changed: 231 additions & 5 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: web-image-export-java-function
2+
on:
3+
push:
4+
paths:
5+
- 'web-image/export-java-function/**'
6+
- '.github/workflows/web-image-export-function-demo.yml'
7+
pull_request:
8+
paths:
9+
- 'web-image/export-java-function/**'
10+
- '.github/workflows/web-image-export-java-function.yml'
11+
schedule:
12+
- cron: "0 0 1 * *" # run every month
13+
workflow_dispatch:
14+
permissions:
15+
contents: read
16+
jobs:
17+
run:
18+
name: Run 'web-image-export-java-function'
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 15
21+
strategy:
22+
matrix:
23+
java-version: ['25e1-ea']
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: graalvm/setup-graalvm@v1
27+
with:
28+
java-version: ${{ matrix.java-version }}
29+
distribution: 'graalvm'
30+
github-token: ${{ secrets.GITHUB_TOKEN }}
31+
native-image-job-reports: 'true'
32+
- uses: actions/setup-node@v4
33+
with:
34+
node-version: 22
35+
- name: Set up Binaryen
36+
run: |
37+
curl -sLO https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-linux.tar.gz
38+
tar xzf binaryen-version_122-x86_64-linux.tar.gz
39+
echo "$(pwd)/binaryen-version_122/bin" >> "$GITHUB_PATH"
40+
- name: Run 'web-image/export-java-function'
41+
run: |
42+
cd web-image/export-java-function
43+
javac Adder.java
44+
native-image --tool:svm-wasm -H:-AutoRunVM Adder
45+
node --experimental-wasm-exnref log.js

.gitignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,6 @@ micronaut-webapp/docker-compose.yml
238238
/hello-graal/Hello
239239
/hello-graal/Hello.build_artifacts.txt
240240
/native-heapdump-examples/helloworld
241-
native-list-dir/listdir
242-
native-list-dir/listdir.build_artifacts.txt
243241
shared
244242
micronaut-hello-rest-gradle/hello/
245243
micronaut-webapp/loadTests/results-*
@@ -262,7 +260,6 @@ fastR-examples/*.*
262260
venv/
263261
espresso-jshell/espresso-jshell
264262
espresso-jshell/espresso-jshell.build_artifacts.txt
265-
native-list-dir/extlistdir
266-
native-list-dir/extlistdir.build_artifacts.txt
267263
*.pyc
268264
*.iprof
265+
web-image/export-java-function/*.js

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ Demos for building microservices ahead of time using frameworks such as Micronau
9191

9292
Demos illustrating how to use **Web Image**, GraalVM's new experimental feature to produce a WebAssembly module together with a JavaScript wrapper that can run in browsers or on Node.js.
9393

94-
* [web-image](web-image/hello-wasm) - Demonstrates the experimental GraalVM Web Image feature by compiling a simple JVM application into a WebAssembly module with a JavaScript wrapper.
94+
* [hello-wasm](web-image/hello-wasm) - Demonstrates the experimental GraalVM Web Image feature by compiling a simple JVM application into a WebAssembly module with a JavaScript wrapper.
95+
* [export-java-function](web-image/export-java-function) - Shows how you can call Java methods directly from JavaScript via WebAssembly using the `@JS` annotation.
9596

9697
## Compiler Demos
9798

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import java.util.function.BiFunction;
2+
import org.graalvm.webimage.api.JS;
3+
import org.graalvm.webimage.api.JSNumber;
4+
5+
public class Adder {
6+
public static int add(int a, int b) {
7+
return a + b;
8+
}
9+
10+
@JS(args = {"adder"}, value = "globalThis.adder = adder;")
11+
private static native void export(BiFunction<JSNumber, JSNumber, JSNumber> adder);
12+
13+
public static void main(String[] args) {
14+
export((a, b) -> {
15+
return JSNumber.of(add(a.asInt(), b.asInt()));
16+
});
17+
}
18+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Web Image: Export Java Method Example
2+
3+
This demo illustrates the use of **Web Image** - an experimental backend for [GraalVM Native Image](https://www.graalvm.org/latest/reference-manual/native-image/) that compiles a Java application ahead-of-time and produces a WebAssembly (Wasm) module with a JavaScript wrapper.
4+
Then it can be run in browsers, Node.js, or on the [GraalJS-based](https://github.com/oracle/graaljs/tree/master/graal-nodejs) Node runtime.
5+
6+
The key idea is to show how you can currently **call Java methods directly from JavaScript** without relying on the `main()` method.
7+
This demo exposes a simple Java method to the global JavaScript scope using the `@JS` annotation from the [Annotation Interface](https://www.graalvm.org/sdk/javadoc/org/graalvm/webimage/api/JS.html).
8+
9+
> Note: Web Image is an experimental technology and under active development. APIs, tooling, and capabilities may change.
10+
11+
## Prerequisites
12+
13+
* An [Early Access build](https://github.com/graalvm/oracle-graalvm-ea-builds/releases) of Oracle GraalVM 25 (25e1) or later.
14+
* All [prerequisites](https://www.graalvm.org/latest/reference-manual/native-image/#prerequisites) required for Native Image building.
15+
* [Binaryen toolchain](https://github.com/WebAssembly/binaryen) version 119 or later, available on the system path. Web Image uses `wasm-as` from `binaryen` as its assembler.
16+
* **macOS**: It is recommended to install Binaryen using Homebrew, as the pre-built binaries from GitHub may be quarantined by the operating system:
17+
```bash
18+
brew install binaryen
19+
```
20+
* **Other platforms**: Download a pre-built release for your platform from [GitHub](https://github.com/WebAssembly/binaryen/releases).
21+
22+
## Building the WebAssembly Module
23+
24+
1. Compile the Java source file:
25+
```bash
26+
javac Adder.java
27+
```
28+
2. Compile the application to WASM by passing the `--tool:svm-wasm` option (it should be the first argument):
29+
```bash
30+
native-image --tool:svm-wasm -H:-AutoRunVM Adder
31+
```
32+
The `-H:-AutoRunVM` option prevents the JVM from starting `main()` automatically. Calling `GraalVM.run` directly allows you to execute code only after the `main` method finished and `globalThis.adder()` is guaranteed to be available.
33+
34+
The build produces the following artifacts in the working directory:
35+
- _adder.js_ - a JavaScript runtime wrapper;
36+
- _adder.js.wasm_- the compiled WebAssembly module containing Java code and runtime elements (object layout, parts of [Substrate VM](https://github.com/oracle/graal/tree/master/substratevm) adapted for Wasm);
37+
- _adder.js.wat_ - debug artifacts to understand how Java code and runtime components are lowered to WebAssembly.
38+
39+
3. Run the application in a browser using a simple HTTP server (with Python or Java):
40+
```bash
41+
python3 -m http.server 8000
42+
```
43+
```bash
44+
jwebserver -p 8000
45+
```
46+
47+
4. Navigate to [http://localhost:8000](http://localhost:8000) in the browser. Enter some numbers, click **Add** and see the result displayed.
48+
49+
## Review the Sample Application
50+
51+
What actually happens? This is the Java source code:
52+
```java
53+
import java.util.function.BiFunction;
54+
import org.graalvm.webimage.api.JS;
55+
import org.graalvm.webimage.api.JSNumber;
56+
57+
public class Adder {
58+
public static int add(int a, int b) {
59+
return a + b;
60+
}
61+
62+
@JS(args = {"adder"}, value = "globalThis.adder = adder;")
63+
private static native void export(BiFunction<JSNumber, JSNumber, JSNumber> adder);
64+
65+
public static void main(String[] args) {
66+
export((a, b) -> {
67+
return JSNumber.of(add(a.asInt(), b.asInt()));
68+
});
69+
}
70+
}
71+
```
72+
73+
- `@JS` annotation is part of [GraalVM Web Image API](https://www.graalvm.org/sdk/javadoc/org/graalvm/webimage/api/JS.html). It allows you to bridge Java and JavaScript.
74+
- `args = {"adder"}` tells GraalVM that the BiFunction you pass in Java will be available as a JavaScript variable `adder` (not necessary if the Java source code is compiled with the `-parameters` option).
75+
- `value = "globalThis.adder = adder;"` is a raw JavaScript code executed when the export happens; it sets a variable called `adder` to be globally accessible in browsers.
76+
77+
Further down you see the `export` method:
78+
```java
79+
export((a, b) -> {
80+
return JSNumber.of(add(a.asInt(), b.asInt()));
81+
});
82+
```
83+
84+
- A lambda passed in the `export` method converts JS numbers (`JSNumber`) to Java integers, calls the `add` method, and converts the result back to `JSNumber`.
85+
When `export` is called, GraalVM runs the `@JS` snippet.
86+
This makes the lambda directly callable from JS as `globalThis.adder(...)`.
87+
88+
The next part is calling from JavaScript in HTML, which happens in this part of _index.html_:
89+
```js
90+
<script>
91+
GraalVM.run([]).then(() => {
92+
...
93+
addButton.addEventListener("click", () => {
94+
const a = parseInt(document.getElementById("num1").value);
95+
const b = parseInt(document.getElementById("num2").value);
96+
97+
// Call the Java add function via WebAssembly
98+
const result = globalThis.adder(a, b);
99+
100+
output.innerText = `Result: ${result}`;
101+
});
102+
...
103+
});
104+
</script>
105+
```
106+
107+
- `GraalVM.run([], {})` initializes the Wasm module and the Java runtime inside the browser.
108+
- `globalThis.adder(a, b)` calls the `add` function you exported via WebAssembly, after the runtime is ready.
109+
110+
### Conclusion
111+
112+
The focus of this demo is to demonstrate direct interaction between Java and JavaScript in the browser via WebAssembly.
113+
Note that the [GraalVM Web Image API](https://www.graalvm.org/sdk/javadoc/org/graalvm/webimage/api/JS.html) is still under active development, there will be better ways to export Java methods to JavaScript.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<body>
4+
<h2>GraalVM Web Image</h2>
5+
6+
<input type="number" id="num1" value="0">
7+
<br><br>
8+
<input type="number" id="num2" value="0">
9+
<br><br>
10+
11+
<button id="addButton">Add</button>
12+
<button id="clearButton">Clear</button>
13+
<br><br>
14+
15+
<div id="output"></div>
16+
17+
<script src="adder.js"></script>
18+
<script>
19+
GraalVM.run([]).then(() => {
20+
const addButton = document.getElementById("addButton");
21+
const clearButton = document.getElementById("clearButton");
22+
const output = document.getElementById("output");
23+
24+
addButton.addEventListener("click", () => {
25+
const a = parseInt(document.getElementById("num1").value);
26+
const b = parseInt(document.getElementById("num2").value);
27+
28+
// Call the Java add function via WebAssembly
29+
const result = globalThis.adder(a, b);
30+
31+
output.innerText = `Result: ${result}`;
32+
});
33+
34+
clearButton.addEventListener("click", () => {
35+
document.getElementById("num1").value = 0;
36+
document.getElementById("num2").value = 0;
37+
output.innerText = "";
38+
});
39+
});
40+
</script>
41+
</body>
42+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require("./adder.js");
2+
3+
GraalVM.run([]).then(() => {
4+
const a = 3;
5+
const b = 4;
6+
7+
const result = globalThis.adder(a, b);
8+
console.log(`Result: ${result}`);
9+
10+
});

0 commit comments

Comments
 (0)