Communication Between Java and JS
The MicroEJ engine allows to communicate between Java and JavaScript: Java API can be used from JavaScript code and vice-versa.
JavaScript Engine
The JavaScript code is executed in a single-threaded engine, which means only one JavaScript statement is executed at a given time. Each piece of JavaScript code that must be executed is pushed in a job queue. It is up to the engine to manage the job queue and execute the jobs.
One consequence of this design is that Java code called from a JavaScript code must not be blocker. When calling a Java API from a Javascript code, in order to avoid blocking the JavaScript engine, the Java code must return as quick as possible. Otherwise the JavaScript engine is stuck and cannot execute other JavaScript jobs. It is especially harmfull when the Java operation takes time, for example for network or IO operations. In such a case, it is therefore recommended to execute it in a new thread and return immediately.
Another consequence of the JavaScript engine design is that JavaScript code must always be executed by the engine, by the single thread. Therefore, any call to a JavaScript code from a Java code must create a job and add it to the job queue.
Calling Java from JavaScript
The MicroEJ engine allows to expose Java objects or methods to the JavaScript code by using the engine API and creating the adequate JavaScript object.
Import Java Types from JavaScript
Java objects can be exposed to JavaScript using the JavaImport
mechanism.
It takes a Java fully qualified name as argument and returns an object that
gives access to the constructors, static methods and static fields. All the
classes from the project’s classpath can be imported (project’s own classes and
its dependencies).
For instance, the following code imports java.lang.System and prints a string calling System.out.println():
var System = JavaImport("java.lang.System")
System.out.println("foo");
Here we instantiate a Java File
object and check that it exists:
var File = JavaImport("java.io.File")
var myFile = new File("myFile.txt")
if (myFile.exists()) {
print("myFile.txt exists")
} else {
print("myFile.txt does not exist")
}
Warning
You cannot instantiate an anonymous class from an interface or an abstract class with the new
keyword and JavaImport
. Nevertheless, you can still access to static fields and methods.
Implement JavaScript Functions in Java
We can also implement JavaScript functions in Java by adding their
implementation to the global object from Java. For example, here is the code to
create a JavaScript function named javaPrint
in the global scope:
JsRuntime.JS_GLOBAL_OBJECT.put("javaPrint", JsRuntime.createFunction(new JsClosure() {
@Override
public Object invoke(Object thisBinding, Object... arguments) {
System.out.println("Print from Java: " + arguments[0]);
return null;
}
}), false);
The function is created with a com.microej.js.objects.JsObjectFunction
object created with the API JsRuntime.createFunction(JsClosure jsClosure)
,
and injected in the object JsRuntime.JS_GLOBAL_OBJECT
which maps to the JavaScript global scope.
The function javaPrint
can then be used in JS:
javaPrint("foo")
This technique can also be used to share any Java object to JavaScript.
It is achieved by returning the Java object in the invoke
method of the JsClosure
object.
For example, a Java Date object can be exposed as follows:
JsRuntime.JS_GLOBAL_OBJECT.put("getCurrentDate", JsRuntime.createFunction(new JsClosure() {
@Override
public Object invoke(Object thisBinding, Object... arguments) {
return Calendar.getInstance().getTime();
}
}), false);
When a Java object is exposed in JavaScript, all its public methods can be called, therefore the JavaScript code can then use this Date object and get the time:
var date = getCurrentDate()
var time = date.getTime()
print("Current time: ", time)
for more information on how these called are managed by the MicroEJ JavaScript engine, please go to the Foreign Function Interface section.
Java objects can also be shared using one of the other Java JS adapter objects.
With this solution, the code of the Java object is executed at engine initialisation, contrary to the previous solution where it is executed only when the JavaScript code is called.
For example, here is the code to expose a Java string named javaString
in the JavaScript global scope:
JsRuntime.JS_GLOBAL_OBJECT.put("javaString", "Hello World!", false);
The string javaString
can then be used in JS:
var myString = javaString;
The available Java JS adapter objects are:
com.microej.js.objects.JsObject
: exposes a Java object as a JavaScript objectcom.microej.js.objects.JsObjectFunction
: exposes a Java “process” as a JavaScript function (using a JsClosure object)com.microej.js.objects.JsObjectString
: exposes a Java String as a JavaScript Stringcom.microej.js.objects.JsObjectArray
: exposes a Java items collection as a JavaScript Arraycom.microej.js.objects.JsObjectBoolean
: exposes a Java Boolean as a JavaScript Booleancom.microej.js.objects.JsObjectNumber
: exposes a Java Number as a JavaScript Number
Calling JavaScript from Java
The MicroEJ JavaScript engine API allows to call JavaScript code from Java code.
For example, given the following JavaScript function in a file in src/main/js
:
function sum(a, b) {
print(a + " + " + b + " = " + (a+b));
}
it can be called from a Java piece of code with:
JsObjectFunction functionObject = (JsObjectFunction) JsRuntime.JS_GLOBAL_OBJECT.get("sum");
JsRuntime.ENGINE.addJob(functionObject, JsRuntime.JS_GLOBAL_OBJECT, new Integer(5), new Integer(3));
The first line gets the JavaScript function from the global scope.
The second line adds a job in the JavaScript engine queue to execute the function, in the global scope, with the arguments 5
and 3
.
Passing Values Between JavaScript and Java
JavaScript base types are represented by Java objects and not Java base types. The following table shows the mapping between types in both languages:
JavaScript |
Java |
Number |
|
Boolean |
|
String |
|
Null |
|
Undefined |
|
In JavaScript, a Number
type is a 64-bits floating-point value.
Nevertheless, Kifaru may use integer values (Integer Java type) when
possible for performance reasons. Otherwhise, Double type will be used.
Note
Prefer passing Integer values as argument to a job added to the JavaScript execution queue, or return Integer
values when implementing a JsClosure
instead of Double when possible.
It is not possible to retrieve the returned value of a JavaScript function from Java. For instance, consider the following JavaScript function:
function sum(a, b) {
return a + b;
}
When calling this function from Java, we have no way to get the result back:
JsObjectFunction functionObject = (JsObjectFunction) JsRuntime.JS_GLOBAL_OBJECT.get("sum");
JsRuntime.ENGINE.addJob(functionObject, JsRuntime.JS_GLOBAL_OBJECT, new Integer(5), new Integer(3));
A workaround is to modify the JavaScript function so it takes a callback object as argument:
function sum(a, b, callback) {
callback.returnValue(a + b);
}
Here is a possible implementation of the callback object:
public class Callback<T> {
@Nullable
private T value;
private boolean returned;
/**
* Gets the value returned by this callback function when ready.
* <p>
* A call to this method waits for the value to be ready.
*
* @return the value return by the callback
*/
@Nullable
public T getValue() {
synchronized (this) {
while (!this.returned) {
try {
wait();
} catch (InterruptedException e) {
throw new JsErrorWrapper(""); //$NON-NLS-1$
}
}
}
return this.value;
}
/**
* Sets the value to return by this callback function.
*
* @param value
* the value to return
*/
public synchronized void returnValue(@Nullable T value) {
this.value = value;
this.returned = true;
notify();
}
}
We can now pass the callback to the job. The Java code will wait on the
callback.getValue()
until the result is ready.
JsObjectFunction functionObject = (JsObjectFunction) JsRuntime.JS_GLOBAL_OBJECT.get("sum");
Callback<Integer> callback = new Callback<>();
JsRuntime.ENGINE.addJob(functionObject, JsRuntime.JS_GLOBAL_OBJECT, new Integer(5), new Integer(3), callback);
Integer returnedValue = callback.getValue();
System.out.println("Result is " + returnedValue);