Jim Driscoll's Blog

Notes on Technology and the Web

Archive for March 2014

Running a no-dependencies Node module in Java

leave a comment »

Sometimes you do something just because you wonder if you can. Today’s example is a prime example of that.

I wonder if it’s possible to run a no-dependencies Node module in Java, without running Project Avatar? The answer, of course, is yes, it is.

For my no-dependencies Node module, I picked semver.js – there’s a pretty well defined external interface there, and all it’s really doing in String manipulation, so there’s no external dependencies to worry about.

Before I go further, a caveat: I actually did this example in Groovy, mostly to save the extra typing necessary in Java, but the example shouldn’t require any knowledge of Groovy, and it should all work with only minor modifications in pure Java (JDK 8, since I’m using nashorn’s engine, but there’s no reason something similar shouldn’t work with Rhino as well).

If you like, you can run the example just by downloading the git repository and typing

gradlew

at the command line (provided that node and npm are already installed and in your path – if not, installing node is easy).

If you’ve never used JavaScript from within Java, it’s pretty easy.

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Invocable inv = (Invocable) engine;

Get a manager, use it to obtain an engine, and (optionally) cast it to an Invocable instance.

With the engine, you can conveniently say

engine.eval("javascript code to run")

while with the invocable, you can say:

inv.invokeMethod(contextObject,"methodname",[param1,param2])

which is far more convenient if you don’t want to continuously do .toString and string concatenation.

So, with those two basic methods, let’s run a node module. First, we’ll need to set up an exports object, which node modules expect to exist.

engine.eval('exports = {}')

Then load and evaluate the server code:

File semverjs = new File('./node_modules/semver/semver.js')
engine.eval(semverjs.text);

Set the exports object to be the same as a server object, so our JS code will look a little more natural, and grab that object to use as the context object for invokeMethod (engine.get(varname) will fetch the JS object from Nashorn, and let you use it in Java):

engine.eval('semver = exports')
def semver = engine.get('semver')

With that setup, we can do a simple eval:

println engine.eval('semver.clean("1.2.3")')

or a simple invokeMethod:

println inv.invokeMethod(semver,"clean", "1.2.3");

A somewhat more complex invokeMethod (passing multiple arguments as an array, I needed to say “as Object[]” in Groovy to do an inline cast to an array of Objects):

println inv.invokeMethod(semver,"lt",['1.2.3','4.5.6'] as Object[])

but when we pass in an array as one of the parameters, it all goes sideways:

println inv.invokeMethod(semver, 'maxSatisfying',
                [['1.2.3','1.3.0'] as Object[],'~1',true] as Object[])

will return

TypeError: [Ljava.lang.Object;@c667f46 has no such function "filter" in  at line number 912

So, that’s not good. What’s going on? When you call invokeMethod with the array of parameters, Nashorn will place each of them as it receives them into the list of parameters on the JavaScript function. But for whatever reason, the Nashorn dev team decided that they would not convert Java arrays automatically into JavaScript arrays during this process – and when ‘semver.maxSatisfying’ tries to manipulate the first parameter as if it was a JavaScript array, it fails. And I can not find a public Java API in Nashorn to do the conversion. But I can find the JavaScript Nashorn function Java.from, which does that conversion.

There are two ways around this for this use case. I’m not especially fond of either.

First, you can install a shim, so that instead of calling the function which expects the JavaScript array, call the shim, which will do the conversion from Java to JavaScript.

def shim =
'''
semver.maxSatisfyingHack = maxSatisfyingHack;
function maxSatisfyingHack(rversions, range, loose) {
  var versions = Java.from(rversions)
  return maxSatisfying(versions,range,loose);
}
'''           
engine.eval(shim)
println inv.invokeMethod(semver, 'maxSatisfyingHack', [['1.2.3','1.3.0'],'~1',true] as Object[])

That works, but now you’re modifying the underlying JS, which isn’t too nice.

Alternately, you can use the underlying JS Java object, and call the from method on it using invokeMethod.

println inv.invokeMethod(semver, 'maxSatisfying', 
    [inv.invokeMethod(engine.get('Java'),'from',['1.2.3','1.3.0']),'~1',true] as Object[])

The downside of this method is that you’re using invokeMethod twice for each invoke, which is going to be a bit expensive.

Essentially, a node module without dependencies is nothing more than straight JavaScript with some conventions, so it’s not surprising that integration is possible. At some point, I’ll try integrating modules with dependencies – that should be much more involved.

Advertisements

Written by jamesgdriscoll

March 8, 2014 at 3:22 PM

Posted in Groovy, Java, JavaScript, node