Emscripten integration
TeaVM's WebAssembly GC backend can link native C/C++ code compiled with Emscripten into the same
page as your Java application. The Gradle plugin automates the build pipeline; you write Java and
C/C++ and wire them together with the @Import annotation.
A working example is in samples/emscripten. This page explains the concepts behind that example in detail.
How it works
TeaVM compiles Java to a .wasm file using the WebAssembly GC instruction set. Separately,
Emscripten compiles C/C++ to a relocatable WebAssembly module. The TeaVM runtime stitches both
modules together at load time. The two modules share a single region of linear memory — NIO direct
buffers live there alongside the C heap. This shared linear memory is the only bridge for passing
data between Java and C.
Configuring the Emscripten SDK location
You must tell it where the Emscripten SDK is
installed by setting the emscripten-location property to the directory containing the emcc
binary (typically <emsdk>/upstream/emscripten).
Because the SDK path differs per machine, the recommended place is either the project-local
teavm-local.properties file (which should be git-ignored) or the user-level
~/.gradle/gradle.properties:
# teavm-local.properties (or ~/.gradle/gradle.properties with a teavm. prefix)
emscripten-location=/path/to/emsdk/upstream/emscripten
# ~/.gradle/gradle.properties
teavm.emscripten-location=/path/to/emsdk/upstream/emscripten
See the Gradle plugin reference for the full property resolution order.
C/C++ source directory
C/C++ files go in the emcc subdirectory of whichever source set contains your TeaVM Java code.
If you use the teavm source set (recommended when you have a mixed server + client project, to
keep client-only dependencies out of the WAR), the directory is src/teavm/emcc/. If your Java
code lives in the ordinary main source set, use src/main/emcc/ instead.
The Gradle plugin documentation explains when and why to use the teavm source set versus main.
The emcc convention works the same way regardless of which source set you choose.
All .c, .cpp, .C, .cc, .cxx, and .c++ files found there are compiled together in a
single emcc invocation, so you can split native code across as many files as you like.
Gradle configuration
teavm.wasmGC {
addedToWebApp = true
mainClass = "com.example.Main"
emscripten {
enabled = true
exportedFunctions.add("_addInBuffer")
compilerArgs.add("-O2") // optional: passed directly to emcc
}
}
exportedFunctions lists every C function Java will call. The leading underscore is an
Emscripten convention and is required. compilerArgs forwards arbitrary flags to emcc — useful
for optimization levels, extra include paths, or debug info.
Declaring native methods with @Import
Every C function that Java calls must be declared as a static native method annotated with
@Import:
import org.teavm.interop.Import;
@Import(module = "native", name = "addInBuffer")
private static native void cppAdd(IntBuffer buffer);
name— must match the C function name exactly (afterextern "C"stripping of name mangling).module— an arbitrary string that identifies which Emscripten module provides the function. This same string is used as the key inemscriptenModuleswhen loading the Wasm module from JavaScript (see Loading in HTML below). Multiple@Importannotations in the same class can share the samemodulevalue if they all come from the same compiled binary.
Passing data between Java and C
Primitive types
Primitive numeric types map directly to the corresponding WebAssembly value types:
| Java type | C type | Wasm type |
|---|---|---|
boolean, byte, short, char, int |
int32_t |
i32 |
long |
int64_t |
i64 |
float |
float |
f32 |
double |
double |
f64 |
Sub-int types (boolean, byte, short, char) are widened to i32 before the call.
On the C side you may receive them as int32_t (or as the narrower C type if you want — the
value fits). Return types follow the same mapping; void is also valid.
Example — a C function that adds two integers and returns the result:
extern "C" {
int32_t add(int32_t a, int32_t b) {
return a + b;
}
}
@Import(module = "native", name = "add")
private static native int add(int a, int b);
Passing structured data via NIO direct buffers
Java objects live in GC-managed memory and cannot be passed as raw pointers to C. The only way to exchange structured or bulk data is through the shared linear memory, which NIO direct buffers are allocated in.
ByteBuffer.allocateDirect(n) allocates n bytes in linear memory. You can obtain typed views
with .asIntBuffer(), .asLongBuffer(), .asFloatBuffer(), etc. When such a buffer (or its
typed view) is passed to an @Import method, TeaVM passes the underlying memory address to C as
a pointer. The C function receives a plain pointer into linear memory and can read or write through
it freely.
// Java: allocate 3 ints in shared linear memory
var buffer = ByteBuffer.allocateDirect(3 * Integer.BYTES).asIntBuffer();
buffer.put(0, 23);
buffer.put(1, 42);
cppAdd(buffer); // pass as IntBuffer
int result = buffer.get(2);
// C: receive as a pointer to int32_t in linear memory
extern "C" {
void addInBuffer(int32_t* args) {
args[2] = args[0] + args[1];
}
}
The C type of the pointer should match the element type of the Java buffer view:
| Java buffer type | C pointer type |
|---|---|
ByteBuffer |
int8_t* / void* |
ShortBuffer |
int16_t* |
IntBuffer |
int32_t* |
LongBuffer |
int64_t* |
FloatBuffer |
float* |
DoubleBuffer |
double* |
Note that NIO buffer positions and limits are Java-side metadata — C sees only the raw base pointer.
If you use a buffer view obtained with .asIntBuffer(), the pointer passed to C already accounts
for the byte offset of that view inside the original ByteBuffer.
Important: Only
ByteBuffer.allocateDirect()allocates in shared linear memory. Heap-backed buffers (ByteBuffer.wrap(...),ByteBuffer.allocate(...)) are ordinary Java objects and cannot be passed to C code.
Loading in HTML
<script src="wasm-gc/app.wasm-runtime.js"></script>
<script>
async function launch() {
const teavm = await TeaVM.wasmGC.load("wasm-gc/app.wasm", {
emscriptenModules: {
"native": {
pathToJs: "app.wasm-native.js",
pathToWasm: "wasm-gc/app.wasm-native.wasm"
}
}
});
teavm.exports.main([]);
}
</script>
The key "native" in emscriptenModules must match the module value used in all @Import
annotations that refer to this binary. You can load multiple Emscripten modules simultaneously by
adding more entries to emscriptenModules, each with a distinct key.
For the full JavaScript loading API see Loader API.
Troubleshooting
emccnot found — verify thatemscripten-locationpoints to the directory containing theemccbinary. Check the property resolution order if the value does not seem to be picked up.- Undefined symbol at link time — the function name in
exportedFunctionsmust have a leading underscore and match the C function name exactly.