Build your own scripting language for Java

An introduction to JSR 223

1 2 Page 2
Page 2 of 2

But what are the global scope and engine scope in Figure 3? A global scope is a scope shared by multiple script engines. If you want some piece of data to be accessible across multiple script engines, a global scope is the place to put the data. Note that a global scope is not global to all script engines. It's only global to the script engines created by the script engine manager in which the global scope resides.

An engine scope is a scope shared by multiple scripts. If you want some piece of data to be accessible across multiple scripts, an engine scope is the place to put the data. For example, say we have two scripts like this:

 

(True & x) | y //Script A

(True & x) //Script B

If we want to share the same value for x across the two scripts, we can put that value in the engine scope held by the script engine that we will use to evaluate the two scripts. And suppose we want to keep the value of y only to Script A. To do that, we can create a scope, remembering that this scope is visible only to Script A, and put the value of y in it.

As an example, the main method in BoolScriptHostApp.java has the following code for evaluating (x & y):

 //bsEngine is an instance of ScriptEngine
bsEngine.put("x", BoolTermEvaluator.tTrue);
bsEngine.put("y", BoolTermEvaluator.tTrue);
bsEngine.eval("x & y\n\n");

The code puts the values of both x and y in the engine scope. Then it calls the eval() method on the engine to evaluate the BoolScript code. If you look at the ScriptEngine interface, you'll see that the eval() method is overloaded with different parameters. If we call eval() with a string just as we did in the code snippet above, the script engine will evaluate the code in its context. If we don't want to evaluate the code in the script engine's context, then we have to supply the context we'd like to use when we call eval().

Our implementation of the eval() method delegates the job of evaluating BoolScript code all the way down the method invocation chain until the following method in BoolTermEvaluator is called:

 

public static BoolTerm evaluate(BoolTerm term, ScriptContext context) { ... else if (term instanceof Var) { Var var = (Var) term; Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); if (!bindings.containsKey(var.getName())) throw new IllegalArgumentException("Variable " + var.getName() + " not set.");

Boolean varValue = (Boolean) bindings.get(var.getName()); if (varValue == Boolean.TRUE) return BoolTermEvaluator.tTrue; else return BoolTermEvaluator.tFalse; } ... }

This method evaluates BoolScript code by evaluating terms that are True, False, or variables. When it sees that a term is a variable as shown in the code excerpt above, it gets a reference to the engine scope by calling getBindings() on the context that's passed to it as a parameter. Because more than one scope might be in a context, we indicate that we want to get the engine scope by passing the constant ScriptContex.ENGINE_SCOPE to getBindings(). After we get the engine scope, we look up the variable's value by the variable's name in the engine scope. If we cannot find a value for the variable, we throw an exception. Otherwise, we have successfully evaluated the variable and we return the value back.

Finally, I am ready to explain why a script engine manager initializes a script engine by calling the engine's setBindings() method: When a script engine manager calls an engine's setBindings() method, it passes its global scope as a parameter to the method. The engine's implementation of the setBinding() method is expected to store the global scope in the engine's script context.

Before we leave this section, let's look at a few classes in the scripting API. I said that a ScriptEngineManager contains an instance of Bindings that represents a global scope. If you look at the javax.script.ScriptEngineManager class, you'll see that there is a getBindings() method for getting the bindings and a setBindings() method for setting the bindings in a ScriptEngineManager.

Similarly, a ScriptEngine contains an instance of ScriptContext. If you look at the javax.script.ScriptEngine interface, you'll see a method getContext() and a method setContext() for getting and setting the script context in a ScriptEngine.

So nothing prevents you from sharing a global scope among several script engine managers. To do that, you just need to call getBindings() on one script engine manager to get its global scope and then call setBindings() with that global scope on other script engine managers.

If you look at our example script engine class BoolScriptEngine, you won't see it keeping a reference to an instance of ScriptContext explicitly. That is because BoolScriptEngine inherits from AbstractScriptEngine and AbstractScriptEngine already has an instance of ScriptContext as its member. If you ever need to implement a script engine from scratch without inheriting from a class such as AbstractScriptEngine, you will need to keep an instance of ScriptContext in your script engine and implement the getContext() and setContext() methods accordingly.

Compilable and Invocable

By now, we have implemented the minimum for our BoolScript engine to qualify as a JSR 223 script engine. Every time a Java client program wants to use our script engine, it passes in the BoolScript code as a string. Internally, the script engine has a parser that parses the string into a tree of objects commonly called an abstract syntax tree. And then it passes the tree to the BoolTermEvaluator.evaluate() method we saw earlier. This whole process of evaluating BoolScript code is called interpretation, as opposed to compilation. And in this role, the BoolScript engine is called an interpreter, as opposed to a compiler. To be a compiler, the BoolScript engine needs to transform the textual BoolScript code into an intermediate form so that it won't have to parse the code into an abstract syntax tree every time it wants to evaluate it. This section shows how this functionality is achieved.

Java programs are compiled into an intermediate form called Java bytecode and stored in .class files. At runtime, .class files are loaded by classloaders, and the JVM executes the bytecode. Instead of defining our own intermediate form and implementing our own virtual machine, we'll simply stand on the shoulder of Java by compiling BoolScript code into Java bytecode.

The construct JSR 223 defines to model the concept of compilation is javax.script.Compilable, which is the interface BoolScriptEngine needs to implement. The following code in BoolScriptHostApp.java shows how to use a compilable script engine to compile and execute script code:

 

List<Boolean> boolAnswers = null; //bsEngine is an instance of ScriptEngine Compilable compiler = (Compilable) bsEngine; CompiledScript compiledScript = compiler.compile("x & y\n\n"); Bindings bindings = new SimpleBindings(); bindings.put("x", new Boolean(true)); bindings.put("y", new Boolean(true)); boolAnswers = (List<Boolean>) compiledScript.eval(bindings); printAnswers(boolAnswers);

Invocable invocable = (Invocable) bsEngine; boolAnswers = (List<Boolean>) invocable.invoke("eval", new Boolean(true), new Boolean(false)); printAnswers(boolAnswers);

In the code above, bsEngine is an instance of ScriptEngine that we know also implements the Compilable interface. We cast it to an instance of Compilable and call its compile() method to compile the code x & y. Internally, the compile() method transforms x & y into the following Java code:

 

package net.sf.model4lang.boolscript.generated; import java.util.*; import java.lang.reflect.*;

class TempBoolClass { public static List<Boolean> eval(boolean x, boolean y) { List<Boolean> resultList = new ArrayList

();
    boolean result = false;
    result = x & y;
    resultList.add(new Boolean(result));
    return resultList;
  }
}

The transformation converts BoolScript code into a Java method inside a Java class. The class name and method name are hard coded. Each variable in BoolScript code becomes a parameter in the Java method.

Transforming BoolScript code to Java code is just half the story. The other half is about compiling the generated Java code into bytecode. I chose to compile the generated Java code in-memory using JSR 199, the Java Compiler API, another new feature in Java SE 6.0. Details of the Java Compiler API reach beyond this article's scope. See Resources for more information.

The Compilable interface dictates that the compile() method must return an instance of CompiledScript. The class CompiledScript is the construct JSR 223 defines to model the result of a compilation. No matter how we compile our script code, after all is said and done, we need to package the compilation result as an instance of CompiledScript. In the example code, we defined a class BoolCompiledScript and derived it from CompiledScript to store the compiled BoolScript code.

Once the script code is compiled, the client Java program can repeatedly execute the compiled code by calling the eval() method on the CompiledScript instance that represents the compilation result. In our case, as shown in the code excerpt from BoolScriptHostApp.java listed above, when we call the eval() method on the CompiledScript instance, we need to pass in a script context that contains the values for variables x and y.

The eval() method of CompiledScript is not the only way to execute compiled script code. If the script engine implements the Invocable interface, we can call the invoke() method of the Invocable interface to execute compiled script code too. In our simple example, there might not seem to be any difference between using CompiledScript and Invocable for script code execution. However, practically, users of a script engine will use CompiledScript to execute a whole script file and Invocable to execute individual functions (methods, in Java terms) in a script. And if we look at Invocable's invoke() method, distinguishing this difference between CompiledScript and Invocable is not difficult. Unlike CompiledScript's eval() method, which takes an optional script context as a parameter, the invoke() method takes as a parameter the name of the particular function you'd like to invoke in the compiled script.

In the code excerpt from BoolScriptHostApp.java above, bsEngine is an instance of ScriptEngine that we know also implements the Invocable interface. We cast it to an instance of Invocable and call its invoke() method. Invoking a compiled script function is much like invoking a Java method using Java reflection. You must tell the invoke() method the name of the function you want to invoke, and you also need to supply the invoke() method with the parameters required by the function. We know that in our generated Java code, the method name is hard coded as eval. So we pass the string eval as the first parameter to invoke(). We also know that eval() takes two Boolean values as its input parameters. So we pass two Boolean values to invoke() as well.

Conclusion

In this article, I've covered several major areas of JSR 223, such as the script engine discovery mechanism, Java bindings, Compilable, and Invocable. One part of JSR 223 not mentioned in this article is Web scripting. If we implement Web scripting in the BoolScript engine, then clients of our script engine will be able to use it to generate Web contents in a servlet container.

Developing a language compiler or interpreter is a huge undertaking, let alone integrating it with Java. Depending on the complexity of the language you want to design, developing a compiler or interpreter can remain a daunting task. Thanks to JSR 223, the integration between your language and Java has never been easier.

Chaur Wu is a software developer and published author. He has coauthored books on design patterns and software modeling. He is the project administrator of Model4Lang, an open source project dedicated to a model-based approach to language design and construction.

Learn more about this topic

1 2 Page 2
Page 2 of 2