Upgrading From Nashorn

From RPTools Wiki
Jump to navigation Jump to search

Languages:  English


Note this only affects js.eval* functions, not the HTML5 javascript engine.

If you used js.eval() prior to Maptool 1.8.x, it used the old Nashorn Javascript engine. Starting with 1.10.0, Maptool now uses Graalvm's Javascript engine.

This has a few incompatibilities with Nashorn, so since existing scripts will need some attention anyway, now is the best time to make other incompatible changes to the Javascript API.


Below is a basic guide on updating existing js.eval() code.

atob and btoa

atob and btoa are not provided by graalvmjs. They can easily be reimplemented via the following functions

function atob(s) {
  MTScript.setVariable("toDecode", s)
  return MTScript.evalMacro("[r: atob(toDecode)]")
}

function btoa(s) {
  MTScript.setVariable("toEncode", s)
  return MTScript.evalMacro("[r: btoa(toEncode)]")
}

load

load() is likewise gone. While it is possible to re-implement it over top of REST.get(), that is discouraged. Instead, external scripts can be moved into library tokens and accessed via the new Library Token URI Access protocol.

[r: js.eval("load('http://example.com/something.js')")]

becomes

[r: js.evalURI("name", "lib://example/macro/something.js")]

Trusted Namespaces

One of the new features possible with graalvmjs is the ability to have multiple Javascript contexts (called namespaces to avoid confusion with MTScript contexts, and because they are named). These namespaces are manipulated through js.createNS() and js.removeNS(), or implicitly created the first time they are accessed. Additionally, these namespaces can be trusted (if made from a trusted MTScript context), or not trusted (if made by an untrusted context, or explicitly created with the js.createNS() and set to untrusted). Any player can run javascript in an untrusted namespace, but MTScript macros run from within an untrusted namespace are untrusted, and some javascript functions behave differently in untrusted namespaces.

If you want to register a javascript UDF, and restrict it, or alter its behavior based on the if the calling context is trusted, use

if (MTScript.evalMacro('[r: isTrusted()]')) {
  //protected code here.
}

js.eval

js.eval() is now always untrusted. Additionally, it creates a new anonymous namespace for each run. This is normally fine, as internally it wraps the code fragment in an anonymous function, but if you have code which escapes the anonymous function to persist values, it will need changing.

The simplest replacement is js.evalNS(), which takes the namespace to use as its first parameter. Note that the return semantics are also slightly different from js.eval(), as it does not wrap its code fragment in an anonymous function, and uses tail-return to return a value (the statement in [Position] is returned). Also, the magic args parameter from js.eval() is gone. Instead, JS:MTScript.getCallingArgs returns the argument list as a javascript array.

[r: js.eval("var a = [1,2,3]; return a[1] + args[0]", 5)]

becomes

[r: js.evalNS("demo", "var a = [1,2,3]; a[1] + (MTScript.getCallingArgs()[0]|0)", 5)]

Note the \ converts the argument to an integer.

Also note that subsequent calls to the same namespace will have whatever variables were left previously still scattered around. This means if you use {{{1}}} at the top level, repeated invocations of that will fail, as let doesn't allow you to declare a variable which already exists. You can avoid this by using your own anonymous function, or by calling js.removeNS() between invocations.

Namespaces

It is best to keep related javascript code in isolated namespaces. To avoid namespace collisions, use a good, unique string for your namespace. This is not as cumbersome as it might first appear, because you can use a variable to hold the name for repeated calls, and because new code is unlikely to make many js.evalNS() calls.

[h: myNS = "com.example.maptool.myLibrary"]
[r: js.evalNS(myNS, "function add(a,b) { return a+b; }")]

Javascript UDFs

As mentioned in the previous section, using js.eval* directly is mostly not necessary anymore. Instead, put the javascript code in a Lib:Token with URI access enabled, and then take the entry points and expose them as javascript UDFs.

[h: js.evalURI(myNS, "lib://myLibrary/macro/myFirstLib.js")]
[h: js.evalURI(myNS, "lib://myLibrary/macro/mySecondLib.js")]

If the scripts don't interact, change the namespace used and they cannot see each other.

For a complete example using Javascript UDFs, see here