diff --git a/docs/runtime/engines.adoc b/docs/runtime/engines.adoc index a733d5f3..eaae6e22 100644 --- a/docs/runtime/engines.adoc +++ b/docs/runtime/engines.adoc @@ -59,6 +59,79 @@ ES modules:: *Not supported as a loading mechanism.* XP loads every file as a Co Node.js APIs:: *Not supported.* No `process`, `Buffer` or `global`, and no `node_modules` resolution. Web APIs:: *Partially supported.* `TextEncoder` and `TextDecoder` are available. Further APIs may become available in future GraalJS versions, while others, such as `fetch` and `setTimeout`, might never be supported. For HTTP, mail, storage and the rest, use the platform <<../libraries#, libraries>>. +[#graaljs-interop] +=== Java interop + +The <> is identical on both engines: the same bridge object, the same beans, the same libraries. What differs is conversion. Nashorn converted values implicitly as they crossed between JavaScript and Java, whereas GraalJS converts nothing. Four rules follow from that, and code written to observe them runs unchanged on either engine. + +==== Convert values at the boundary + +Values are converted explicitly: on the way into Java with `toScriptValue`, and on the way back out with `toNativeObject`. + +[source,typescript] +---- +const bean = __.newBean('com.example.MyBean'); + +bean.setConfig(__.toScriptValue(params.config)); + +const result = __.toNativeObject(bean.execute()); +---- + +A Java map returned without conversion remains a Java object. Inspecting it with the standard JavaScript operations returns the methods of its class instead of its entries, and throws nothing along the way. An individual property read may still return the expected value, which is what makes the omission easy to overlook. + +[source,typescript] +---- +const map = bean.getConfig(); // a Java Map, unconverted + +const mode = map.mode; // 'live' - the read succeeds +const keys = Object.keys(map); // ['get', 'put', 'size', ...] +const json = JSON.stringify(map); // '{}' +const copy = {...map}; // {} + +const config = __.toNativeObject(bean.getConfig()); // a JavaScript object +---- + +Java lists are the exception. They present array semantics on both engines, so index access and iteration require no conversion. + +==== Call setters rather than assigning properties + +Nashorn accepted an assignment to a property named after the setter and rewrote it into a call to that setter. GraalJS exposes the bean's methods and its public fields, and nothing else, so no such property exists and the assignment fails with an `Unknown identifier` error. + +[source,typescript] +---- +bean.setValue(42); // supported on both engines +bean.value = 42; // Nashorn only; fails on GraalJS +---- + +TypeScript declarations written for a bean should therefore declare its setters and omit the properties, so that the unsupported form does not compile. + +==== Pass the type the method declares + +Arguments are not coerced to the declared parameter type. HTTP request parameters, for instance, always arrive as strings and must be parsed before they reach a Java method that expects a number. + +[source,typescript] +---- +bean.setCount(req.params.count); // fails: a string is not an int +bean.setCount(parseInt(req.params.count, 10)); // supported on both engines +---- + +==== Do not determine Java types from JavaScript + +The standard type-tag idiom reported the name of the Java class on Nashorn. On GraalJS it reports a generic tag, so a branch written against that string stops matching, and it does so silently. + +[source,typescript] +---- +Object.prototype.toString.call(value); +// Nashorn: '[object com.example.Thing]' +// GraalJS: '[object Object]' +---- + +Convert the value at the boundary and branch on the converted result, or perform the type test in Java, where the type is known. + +NOTE: The bridge XP supports is the `+__+` object, on both engines. Interop globals installed by an engine itself, among them the `Java` object, are not part of the XP API, and code written against them is not portable between engines. + +GraalJS is documented by its maintainers at https://www.graalvm.org/javascript/docs/[graalvm.org]. That reference describes the engine as it is distributed standalone; XP embeds it, so it should be read together with <> above. What the engine is capable of and what an application may rely on are not the same set. + [#contexts] === Script contexts diff --git a/docs/runtime/java-bridge.adoc b/docs/runtime/java-bridge.adoc index 9a01e2df..48a2859e 100644 --- a/docs/runtime/java-bridge.adoc +++ b/docs/runtime/java-bridge.adoc @@ -49,7 +49,7 @@ Java objects constructed by `newBean` may, but not required to, implement `com.e There are 2 ways to pass parameters to a Java method, from JavaScript: - Passing the parameters in the method call -- Setting the parameters as properties in the Java object, and then calling the method without parameters +- Calling a setter on the Java object for each parameter, and then calling the method without parameters The first one is recommended when there are few parameters (1 or 2) and of simple types. The second one is better when there are multiple parameters, or some of them are optional. @@ -65,8 +65,8 @@ exports.doSomething = function (param1, param2) { exports.doSomethingElse = function (params) { var bean = __.newBean('com.enonic.lib.mylib.MyClass'); - bean.text = __.nullOrValue(params.text) || ''; - bean.size = __.nullOrValue(params.size) || 250; + bean.setText(__.nullOrValue(params.text) || ''); + bean.setSize(__.nullOrValue(params.size) || 250); return bean.execute(); }; @@ -75,7 +75,18 @@ exports.doSomethingElse = function (params) { NOTE: When passing values that might be `null` or `undefined` it is recommended to filter them using the `__.nullOrValue` built-in function. This function converts any value that is `null` or `undefined` in JavaScript to `null` in Java. Otherwise returns the input value without changes. -To be able to set property values as in the 2nd example above, the Java object must implement a setter method for each field. +The Java object must implement a setter method for each parameter passed that way. + +[WARNING] +==== +The setter must be called. Nashorn also accepted an assignment to a property named after the setter and rewrote it into a call to that setter, but GraalJS exposes no such property, and the assignment fails with an `Unknown identifier` error. See <>. + +[source,typescript] +---- +bean.setText('value'); // supported on both engines +bean.text = 'value'; // Nashorn only; fails on GraalJS +---- +==== The Java class used in the example above looks like this: @@ -116,11 +127,28 @@ There are some type conversions that are made when calling from JavaScript to Ja - when passing a JavaScript `array`, the Java method should expect a Java `List` - when passing a JavaScript `object`, the Java method should expect a Java `Map` +The argument must already be of the type the method declares. Nashorn coerced it where the two differed; GraalJS does not. HTTP request parameters, for instance, always arrive as strings and must be parsed before they are passed to a parameter declared as a number. Objects and arrays are handed over as a Java map or list by converting them first. + +[source,typescript] +---- +bean.setSize(parseInt(req.params.size, 10)); // a Java Long +bean.setConfig(__.toScriptValue(params.config)); // a Java Map +---- + === Returning results from Java When returning simple values from Java to a JavaScript caller, the same type conversions applies. +Complex values are not converted. A Java map returned to JavaScript remains a Java object, and inspecting it with the standard JavaScript operations reports the methods of its class rather than its entries. It is converted once, in the module that makes the call, as the `readLines` example above does. + +[source,typescript] +---- +const config = __.toNativeObject(bean.getConfig()); +---- + +See <>. + To return complex object values, you should create a specific Java class to make the mapping. This class should implement the ``MapSerializable`` interface. It will implement the ``serialize`` method, which allows generating a JSON object.