Interacting with JavaScript
TeaVM runs in browser and can't be isolated from browser's environment. Moreover, if you use TeaVM, you probably use it to alter an HTML page or draw something in Canvas element. Of course, TeaVM can interact with built-in JavaScript APIs as well as with existing JavaScript libraries. TeaVM was designed as a modular compiler, so core knows nothing about interaction with JavaScript. You need a TeaVM extension for this purpose. Fortunately, TeaVM comes with a bundled-in extension called JSO. Also, there is DOM module, that has existing wrappers around popular browser APIs. This section shows how to use JSO to interact with an existing JavaScript code.
Note, that if you are familiar with GWT, you can find that JSO concepts are quite similar to approach taken by GWT.
The API, described above, only works for JavaScript and WebAssembly GC backends. It won't work with classic (aka MVP) WebAssembly, WASI and C backends.
Maven dependencies
To create your own wrappers, you should include the following
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso</artifactId>
<version>0.13.1</version>
</dependency>
To use existing wrapper, you may also include the following
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>0.13.1</version>
</dependency>
Running JavaScript code from Java
To execute JavaScript code, you should declare native method and mark it with @JSBody annotation.
@JSBody has two parameters.
First, params, specifies which names in JavaScript correspond to parameters in Java, by position.
The number of items of params array must be equal to the number of parameters of the method.
Second, script, is JavaScript code.
Example:
@JSBody(params = { "message" }, script = "console.log(message)")
public static native void log(String message);
Using modules from @JSBody
It's possible to import external modules to use in @JSBody scripts. For this purpose use imports parameter.
For example, we have a module named testModule.js:
export function foo() {
console.log("foo called");
}
to access this function, you can write following declaration in TeaVM:
@JSBody(
script = "return testModule.foo();",
imports = @JSBodyImport(
alias = "testModule",
fromModule = "testModule.js"
)
)
private static native int callModule();
Calling Java from JavaScript
There are two ways to call Java code from JavaScript.
The preferred way is to use overlay types and functors to pass Java callbacks to JavaScript.
Read about overlay types below.
Another way is to call Java method directly from @JSBody script.
To call Java method from JavaScript, use the following syntax:
return javaMethods.get('method-reference').invoke(parameters);
where method-reference consists of fully qualified class name followed by method descriptor as described in JVM Specification. Parameters should be specified like parameters of function invocation.
If you call member method, the first parameter corresponds to invocation target.
See example below:
@JSBody(params = { "str", "count" }, script = ""
+ "return javaMethods.get('java.lang.String.substring(II)Ljava/lang/String;')"
+ ".invoke(str, 0, count);")
public static native void left(String str, int count);
Note that you can't pass non-constant string to javaMethods.get method.
Overlay types
Often you are not satisfied exchanging primitives with JavaScript. JSO comes with concept of overlay types, similar to overlay types in GWT. Overlay types allow to talk with JavaScript in terms of objects. However, as JSO is built upon Java, overlay objects are almost regular Java objects, so you get all advantages of static type system, IDE support, javadoc, etc.
An overlay type is a class or an interface that meets the following conditions:
- It must extend or implement JSObject interface, directly or indirectly.
- It must not have member fields.
- Final member methods must not implement or override parent method.
- In case of abstract class, it should extend either another overlay abstract class or
java.lang.Object. - If it's non-abstract class, it should be annotated with
@JSClass.
By default, each abstract or native method of an overlay class is mapped to a corresponding method of JavaScript, i.e. when you call a Java method, the JavaScript method of the same name is actually called. Parameters are converted before invocation to JavaScript and return value is converted back to Java.
For example,
public interface Node extends JSObject {
void appendChild(Node newChild);
Node cloneNode(boolean deep);
//...
}
Mapping properties
To access JavaScript properties from Java, you should declare getter and setter methods, both optional. Getters and setters must satisfy Java Beans naming conventions. You also must annotate getters and setters with the @JSProperty annotation. By default, these methods will access the JavaScript property with the corresponding name, but you can define another property name in @JSProperty.
For example,
public interface HTMLElement extends Element {
@JSProperty
String getTitle();
@JSProperty()
void setTitle(String title);
//...
}
Renaming methods
By default, an abstract or native method of an overlay type maps to a JavaScript method with the same name. To use a different JavaScript name, annotate the method with @JSMethod and supply the desired name:
public interface AbstractWrapper extends JSObject {
// calls JS method 'renamedJSMethod' instead of 'renamedMethod'
@JSMethod("renamedJSMethod")
int renamedMethod(int num);
}
@JSMethod is also implicitly applied to any abstract or native method that carries no other JSO annotation,
so the following two declarations are equivalent:
// implicit @JSMethod:
int foo(int n);
// explicit @JSMethod:
@JSMethod
int foo(int n);
Mapping constructors
To map a JS constructor into Java, just declare non-abstract class, annotate it with @JSClass and
define constructors (their body may be either empty or contain some logic; in latter case TeaVM will
ignore any code inside constructor). For example:
@JSClass
public class Int8Array extends ArrayBufferView {
public Int8Array(int length) {
}
public Int8Array(ArrayBuffer buffer) {
}
// etc
}
By default, the JavaScript class name is taken from the simple Java class name. To map to a JavaScript class with a different name, pass it explicitly:
@JSClass("Array")
public class JSArray<T extends JSObject> implements JSObject {
// ...
}
or equivalently, using the named attribute:
@JSClass(name = "Array")
public class JSArray<T extends JSObject> implements JSObject {
// ...
}
Public fields in @JSClass types
@JSClass types may declare public instance and static fields that map directly to JavaScript object
properties. The field type follows the same conversion rules as method parameters.
@JSClass
public class Rect implements JSObject {
public int x;
public int y;
public int width;
public int height;
public Rect(int x, int y, int width, int height) {}
}
On the Java side, field reads and writes translate directly to the corresponding JS property accesses.
Static fields map to static properties of the JS class:
@JSClass("ClassWithConstructor")
public class MyClass implements JSObject {
// static property on the class object itself
public static String staticProperty;
// top-level (global) variable
@JSProperty
@JSTopLevel
public static String topLevelProperty;
}
Extension methods
You can embed your custom logic to existing JavaScript object by declaring non-abstract non-native methods in overlay types. These methods, however, have additional restriction: they should not override methods of a parent class or interface. Please, note that the current version of TeaVM does not validate this, so you violate this restriction on your risk. Future versions of TeaVM will check this rule properly.
Example:
public interface HTMLElement extends JSObject {
@JSProperty
CSSStyleDeclaration getStyle();
// Subtypes can't override these methods
default void hide() {
getStyle().setProperty("display", "none");
}
default void show() {
getStyle().removeProperty("display");
}
}
Also, you can declare @JSBody methods in abstract classes. Rewrite our example this way:
public abstract class HTMLElement implements JSObject {
@JSBody(script = "this.style.display = 'none';")
public native void hide();
@JSBody(script = "this.style.display = '';")
public native void show();
}
Static methods
Overlay classes can declare static methods, either with Java or JavaScript implementation. Example:
@JSClass("Array")
public class JSArray<T extends JSObject> implements JSObject {
public JSArray() {
}
// Does not exist in JS Array, implemented on Java side
public static <S extends JSObject> JSArray<S> of(
Collection<S> elements) {
var array = new JSArray<S>(elements.size());
for (int i = 0; i < elements.size(); ++i) {
array.set(i, elements.get(i));
}
return array;
}
// Wraps JS method Array.isArray
public static native boolean isArray(Object object);
}
Wrapping indexers
To access JavaScript objects as arrays or maps, you can declare indexer methods. Indexer methods are either get indexers or set indexers.
- getter indexers take one parameter and return a non-void value;
- setter indexers take two parameters; first is index, second is value to set;
You are free to name your indexers as your want.
To tell TeaVM that method is either get or set indexer, you should annotate it with @JSIndexer.
For example,
public interface Int8Array extends JSObject {
@JSIndexer
byte get(int index);
@JSIndexer
void set(int index, byte value);
//...
}
Passing Java objects to JavaScript
Some JavaScript APIs expect that you pass a callback object. You can simply implement JSObject interfaces in Java and pass these implementations to JavaScript wrappers. However, these implementation can only support method invocations, no properties, indexers and constructors.
For example, if you have
public interface Element extends JSObject {
//...
void addEventListener(String type, EventListener listener);
//...
}
public interface EventListener extends JSObject {
void handleEvent(Event evt);
}
you can do the following:
var window = Window.current();
var element = window.getDocument().getElementById("my-elem");
element.addListener("click", evt -> window.alert(evt));
Passing Java objects as JavaScript functions
Often, JavaScript APIs expect you to pass a callback function. This case is similar to passing as JavaScript objects, however you need to tell TeaVM to pass your Java classes as JavaScript functions. To do this, simply add the @JSFunctor annotation. Functor interfaces must contain exactly one abstract method. Default and static methods are allowed and are not treated as the functor method.
For example,
@JSFunctor
public interface TimerHandler extends JSObject {
void onTimer();
}
@JSBody(params = { "handler", "delay" }, script = "setTimeout(handler, delay);")
static native void setTimeout(TimerHandler handler, int delay);
static void doWork() {
var doc = HTMLDocument.current();
setTimeout(() -> doc.getBody().appendChild(doc.createTextNode("-")), 1000);
}
Functor methods may use varargs, which are passed as individual JavaScript arguments:
@JSFunctor
interface FunctorWithVarargs extends JSObject {
String accept(int first, String... remaining);
}
When a functor is passed to JavaScript more than once, TeaVM guarantees the same JS function object is reused, preserving referential identity.
A null functor is passed as null to JavaScript, and a null returned from JavaScript is converted
back to null on the Java side.
Passing arrays without copying
By default, TeaVM copies all arrays in the gap between JavaScript and Java.
To override this behaviour, use @JSByRef annotation on parameters or method.
For example:
@JSByRef
@JSBody(script = "return new Int32Array(10);")
private native int[] getArrayFromJS();
@JSBody(params = "array", script = "console.log(array.byteLength);")
private native void passArrayToJs(@JSByRef float[] array);
You should be careful when using @JSByRef with return type. In Java arrays never overlap, but using
@JSByRef you can make TeaVM violate this contract:
@JSByRef(params = "array", script = "return new Int8Array(array.buffer, 1);")
private native byte[] subarray(@JSByRef byte[] array);
This can have unexpected consequences and non-obvious errors. Please, avoid this!
@JSByRef annotation is not supported by WebAssembly GC backend.
This is not a limitation of TeaVM, but a limitation of the WebAssembly GC spec itself.
To write a portable code that compiles both to JS and WebAssembly GC without rewriting,
you can set optional argument of @JSByRef to true.
Passing NIO buffers
You can pass NIO buffer directly to JS method, for example:
void texImage2D(int target, int level, int internalformat,
int width, int height, int border, int format,
int type, Buffer pixels);
TeaVM will do its best to determine target JS class (ArrayBuffer, Int8Array, etc.) by Java type signature.
You can override it by specifying @JSBuffer annotation, like this:
void uniform1uiv(WebGLUniformLocation location,
@JSBuffer(JSBufferType.UINT32) Buffer data);
Note that this will work in WebAssembly GC only with direct buffers. Passing array-backed buffers will cause runtime error.
As of now, passing direct NIO buffers to JS methods is the fastest way to interact with JS APIs from WebAssembly GC. This is a limitation of WebAssembly spec.
Returning NIO buffers from JS method is not supported. If you want to pass a large amount of data from JS to WebAssembly, you should pass a direct NIO buffer to JS method and expect this method to fill the buffer.
Defining top-level functions and properties
You can also define top-level functions and properties using @JSTopLevel with static class methods.
For example:
public class Window {
@JSTopLevel
public static native String atob(String s);
@JSTopLevel
public static native String btoa(String s);
@JSTopLevel
@JSProperty
public static native HTMLDocument getDocument();
}
@JSTopLevel can also be placed on an entire class, making every static member of that class a
top-level declaration. This is useful when grouping related top-level declarations together:
@JSClass
@JSTopLevel
public class TopLevelDeclarations implements JSObject {
private TopLevelDeclarations() {}
public static native String topLevelFunction();
@JSProperty
public static native String getTopLevelProperty();
@JSProperty
public static native void setTopLevelProperty(String value);
}
When @JSTopLevel is on the class, individual methods do not need @JSTopLevel themselves.
@JSTopLevel can also be applied to static fields:
@JSClass
public class MyClass implements JSObject {
// maps to the global variable 'globalCounter'
@JSTopLevel
public static int globalCounter;
}
Importing declarations from module
You can import classes, functions and properties from external modules.
To do so, annotate corresponding elements with @JSModule.
For example:
@JSClass
@JSModule("./myModule.js")
public class ImportedClass implements JSObject {
}
@JSClass
public class ImportedDeclarations implements JSObject {
@JSTopLevel
@JSModule("./myModule.js")
public static native void someFunction();
@JSTopLevel
@JSProperty
@JSModule("./myModule.js")
public static native String getSomeProperty();
}
@JSModule can also be placed on methods inside a @JSClass type.
This means that any top-level static declarations in this class will be imported from the module.
In the example below, topLevelFunction is imported from the module even though the class
itself does not carry @JSModule:
@JSClass(name = "ClassWithConstructor")
@JSModule("./testModule.js")
public class ClassWithConstructorInModule implements JSObject {
public ClassWithConstructorInModule(int foo) {}
public ClassWithConstructorInModule() {}
@JSProperty
public native int getFoo();
public native String bar();
@JSTopLevel
public static native String topLevelFunction();
@JSTopLevel
@JSProperty
public static native String getTopLevelProperty();
}
Conversion rules
TeaVM automatically converts from and to JS following types:
boolean,byte,short,char,int,float,doublewhich correspond to JavaScript numeric values. Note thatcharis treated as its numeric (UTF-16) code pointlongwhich corresponds to JavaScriptBigIntobject.java.lang.Stringwhich corresponds to JavaScriptStringobject.- arrays of objects and primitives listed above. Note that arrays of primitives correspond to JavaScript typed arrays, not regular arrays.
TeaVM does not convert Java collections and primitive wrappers. Additionally, TeaVM only performs conversion when type is directly known from method's signature. This means that with generics you won't get expected results, because type arguments from generics only known at compile time.
In following example TeaVM is able to convert JS string to java.lang.String:
private static void test() {
System.out.println(read());
}
@JSBody(script = "return document.getElementById('value-input').value;")
private static native String read();
however, with generics TeaVM will produce ClassCastException on runtime:
private static void test() {
readAsync().then(value -> System.out.println(value));
}
private static native JSPromise<String> readAsync();
the right way to fix this is to declare JS wrapper as the type argument:
private static void test() {
readAsync().then(value -> System.out.println(value.stringValue()));
}
private static native JSPromise<JSString> readAsync();
Dynamic type casting
TeaVM only supports instanceof against non-interface overlay types.
The reason is that there's no such thing as "interface" in JavaScript.
Some APIs in JavaScript declare that they consume or produce an object with given properties,
but this object should not necessarily extend some class.
Due to duck typing in JavaScript, there's no need to declare interfaces.
To express such APIs in statically typed Java, you can use interfaces, but these interfaces don't exist on runtime.
Additionally, you may want to express such "anonymous" JavaScript object with abstract classes.
In this case you can prevent TeaVM from inserting type checks for such classes
by adding @JSClass(transparent = true).
For example:
// `instanceof SomeClass` will always produce true
@JSClass(transparent = true)
public abstract class SomeClass {
public abstract void foo();
@JSProperty
public abstract String getBar();
}
Exception handling
Exceptions cross the Java–JavaScript boundary in both directions.
Java exceptions propagating through JS code — when a Java callback (functor or JSObject implementation)
throws an exception and the call passes through @JSBody JavaScript code, the exception is wrapped in
a native JS object and re-thrown. It will be caught by the nearest Java catch block as normal.
Native JS exceptions caught in Java — when JavaScript code throws a native exception (for example,
new Error("boom")) and it propagates out of a @JSBody method, TeaVM wraps it in a RuntimeException
whose message has the form (JavaScript) <toString of the JS value>:
@JSBody(script = "throw new Error('boom');")
private static native void throwNativeException();
try {
throwNativeException();
} catch (RuntimeException e) {
// e.getMessage() == "(JavaScript) Error: boom"
}
Accessing the original JS exception object — use JSExceptions.getJSException(Throwable) to
retrieve the underlying JS object from a caught Java exception that originated in JavaScript:
try {
throwNativeException();
} catch (RuntimeException e) {
JSObject jsEx = JSExceptions.getJSException(e);
// jsEx is the original JS Error object
}
Accessing the original Java exception — when a Java exception propagates through JS and is caught
inside @JSBody JavaScript code, use JSExceptions.getJavaException(JSObject) to get the original
Throwable back:
@JSFunctor
interface JSRunnable extends JSObject {
void run();
}
@JSBody(params = "runnable", script = "runnable();")
private static native void runJsCode(JSRunnable runnable);
try {
JSError.catchNative(() -> {
runJsCode(() -> { throw new RuntimeException("from Java"); });
return null;
}, jsErr -> {
Throwable t = JSExceptions.getJavaException(jsErr);
System.out.println(t.getMessage()); // "from Java"
return null;
});
} catch (RuntimeException ignored) {}
Explicit JS try/catch — JSError.catchNative(tryClause, catchClause) lets you perform a
JavaScript-level try/catch from Java, which is necessary when you need to intercept native JS
exceptions before they leave the JS stack:
JSError.catchNative(() -> {
throwNativeException();
return null;
}, e -> {
if (e instanceof JSError) {
JSError error = (JSError) e;
System.out.println(error.getMessage()); // "foo"
}
return null;
});