Skip to content

Commit 74969aa

Browse files
committed
Add Web Image: Export Java Method Example
1 parent 72ea06c commit 74969aa

4 files changed

Lines changed: 218 additions & 0 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: web-image-export-java-function
2+
on:
3+
push:
4+
paths:
5+
- 'native-image/web-image/export-java-function/**'
6+
- '.github/workflows/web-image-export-function-demo.yml'
7+
pull_request:
8+
paths:
9+
- 'native-image/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+
- name: Set up Binaryen
33+
run: |
34+
curl -sLO https://github.com/WebAssembly/binaryen/releases/download/version_122/binaryen-version_122-x86_64-linux.tar.gz
35+
tar xzf binaryen-version_122-x86_64-linux.tar.gz
36+
echo "$(pwd)/binaryen-version_122/bin" >> "$GITHUB_PATH"
37+
- name: Run 'native-image/wasm-javac'
38+
run: |
39+
cd native-image/web-image/export-java-function
40+
javac Adder.java
41+
native-image --tool:svm-wasm -H:-AutoRunVM Adder
42+
jwebserver -p 8000 &
43+
SERVER_PID=$!
44+
sleep 5
45+
curl -i http://127.0.0.1:8000
46+
kill $SERVER_PID
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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 or with Node.js.
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 `add(int a, int b)` 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 build produces the following artifacts in the working directory:
33+
- _adder.js_ - a JavaScript runtime wrapper;
34+
- _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);
35+
- _adder.js.wat_ - debug artifacts to understand how Java code and runtime components are lowered to WebAssembly.
36+
37+
3. Run the application in a browser using a simple HTTP server (with Python or Java):
38+
```bash
39+
python3 -m http.server 8000
40+
```
41+
```bash
42+
jwebserver -p 8000
43+
```
44+
Instead of just loading the script, you attach some logic after the runtime is ready.
45+
46+
4. Navigate to [http://localhost:8000](http://localhost:8000) in the browser. Enter some numbers, click **Add** and see the result displayed.
47+
48+
## Review the Sample Application
49+
50+
What actually happens? This is the Java source code:
51+
```java
52+
import java.util.function.BiFunction;
53+
import org.graalvm.webimage.api.JS;
54+
import org.graalvm.webimage.api.JSNumber;
55+
56+
public class Adder {
57+
public static int add(int a, int b) {
58+
return a + b;
59+
}
60+
61+
@JS(args = {"adder"}, value = "globalThis.adder = adder;")
62+
private static native void export(BiFunction<JSNumber, JSNumber, JSNumber> adder);
63+
64+
public static void main(String[] args) {
65+
export((a, b) -> {
66+
return JSNumber.of(add(a.asInt(), b.asInt()));
67+
});
68+
}
69+
}
70+
```
71+
72+
- `@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.
73+
- `args = {"adder"}` tells GraalVM that the BiFunction you pass in Java will be available as a JavaScript variable `adder`.
74+
- `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.
75+
76+
Further down you see the `export` method:
77+
```java
78+
export((a, b) -> {
79+
return JSNumber.of(add(a.asInt(), b.asInt()));
80+
});
81+
```
82+
83+
- 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`.
84+
When `export` is called, GraalVM runs the `@JS` snippet.
85+
This makes the lambda directly callable from JS as `globalThis.adder(...)`.
86+
87+
The next part is calling from JavaScript in HTML, which happens in this part of _index.html_:
88+
```js
89+
<script>
90+
GraalVM.run([], {}).then(() => {
91+
...
92+
addButton.addEventListener("click", () => {
93+
const a = parseInt(document.getElementById("num1").value);
94+
const b = parseInt(document.getElementById("num2").value);
95+
96+
// Call the Java add function via WebAssembly
97+
const result = globalThis.adder(a, b);
98+
99+
output.innerText = `Result: ${result}`;
100+
});
101+
...
102+
});
103+
</script>
104+
```
105+
106+
- `GraalVM.run([], {})` initializes the WASM module and the Java runtime inside the browser.
107+
- `globalThis.adder(a, b)` calls the `add` function you exported via WebAssembly.
108+
109+
### Conclusion
110+
111+
The focus of this demo is to demonstrate direct interaction between Java and JavaScript in the browser via WebAssembly.
112+
Note that the [GraalVM Web Image API](https://www.graalvm.org/sdk/javadoc/org/graalvm/webimage/api/JS.html) is still under active development.
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>

0 commit comments

Comments
 (0)