Scripting: Compiling Scripts in Java 6

This article was originally published in the Javalobby Tips & Tricks section of Javalobby.org

Java 6 introduces scripting engine support in to Java, and this new support provides a link between the Java we all know and hopefully love and the dynamic, delayed binding world of scripting languages.

Rhino (the Mozilla 100% Java Javascript implementation) ships with Sun's Java 6, and is about as full-featured as a Java 6 scripting engine can get. Not only is Rhino a Javascript/ECMAScript 1.6 compliant engine, but it also provides support for the 'compilable' feature of the scripting engine, and provides full access back into Java code - meaning you can use any Java library you want from within a Rhino script (that, however, is a topic for another tip).

Normally, when you want to run a script you would do something like this:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
Bindings bindings = engine.createBindings();
bindings.put("num", "20");
Object result = engine.eval(
	"fib(num);" +
	"function fib(n) {" +
	"  if(n <= 1) return n; " +
	"  return fib(n-1) + fib(n-2); " +
	"};", 
	bindings);
 
System.out.println(result);

This example simply computes the fibonacci of '20' using Javascript, and then outputs the result (6765) to System.out. This Javascript fibonacci implementation is not anywhere near as fast as Java. The Javscript implementation is at a significant disadvantage, however, as it is being re-parsed every time, re-interpreted every time, and doesn't have the advantage of being run through Java's hotspot compiler (which the Java fibonacci implementation most certainly does). In addition, because of the nature of the scripting implementation, a lot of reflection is involved, and all of the number usage is done with full objects as opposed to stack-based primitives. Because fibonacci calculations are almost completely numerical in nature, the JS implementation inherits a lot of overhead.

While we can't solve all of these problems (some of them are simply the nature of the code being interpreted like this), we can compile the script so that we don't incur the re-parse/re-interpret overhead each time. This allows the Rhino Javascript engine to work it's magic as much as possible.

Compilation is an optional feature of the scripting engine support in Java 6, and it just so happens that Rhino supports compilation. Here is an example:

Map<String, CompiledScript> m = new HashMap<String, CompiledScript>();
// ...
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
CompiledScript script = m.get("fib");
if(script == null) {
	Compilable compilingEngine = (Compilable)engine;
	script = compilingEngine.compile(
			"fib(num);" +
			"function fib(n) {" +
			"  if(n <= 1) return n; " +
			"  return fib(n-1) + fib(n-2); " +
			"};"		
	);
	m.put("fib", script);
}
Bindings bindings = engine.createBindings();
bindings.put("num", "20");
Object result = script.eval(bindings);
System.out.println(result);

In this example, you can see we have a cache of compiled scripts represented by the hashmap 'm'. Everytime the code is run, it will look for the pre-compiled script first, and use it if it is there. Otherwise, the script is compiled and then put in to the map.

Unfortunately, compiled scripts are not, by default, serializable, so they can't be pre-compiled as part of a deployment process, so compilation should be applied at runtime when you know it makes sense. After all, a script being run only once is going to incur more overhead (possibly only a small amount, but more, nonetheless) to be turned into a CompiledScript and then be evaluated than it would simply to be evaluated; in both cases the underlying scripting engine is probably performing a compilation, but there is no intermediate step in the second case, and hence more room for 'one-run' optimizations. The advantage of compilation is only going to show up if you are running a script over and over again.

One other thing to consider if you are working with multiple script implementations is that not all scripts are necessarily compilable. If you need to run a script and are in a situation where you don't know whether or not the engine to execute the script is compilable, you can code more flexibly:

Map<String, CompiledScript> m = new HashMap<String, CompiledScript>();
// ...
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
Bindings bindings = engine.createBindings();
bindings.put("num", "20");
if(engine instanceof Compilable) {
	CompiledScript script = m.get("fib");
	if(script == null) {
		Compilable compilingEngine = (Compilable)engine;
		script = compilingEngine.compile(
			"fib(num);" +
			"function fib(n) {" +
			"  if(n <= 1) return n; " +
			"  return fib(n-1) + fib(n-2); " +
			"};"		
		);
		m.put("fib", script);
	}
	script.eval(bindings);
}
else {
	engine.eval(r, bindings);
}