Discover new dimensions of scripted Java

BeanShell scripts become real Java classes

Scripting, albeit slower than execution of precompiled byte code, has many advantages. First, it allows high-level code execution at runtime, without compiler access. This proves particularly useful in situations where the compiler is no longer available (deployment environment) or for languages where compilers simply do not exist. Second, scripted code provides extension points to modular programs. For example, jEdit, an excellent text editor, uses BeanShell for storing user-defined actions (macro definitions). The popular Web browser Mozilla utilizes JavaScript extensively to provide highly configurable and extensible user interface components. I could cite numerous more examples—the need for scripted code execution at runtime is evident by the sheer number of different language interpreters available (see Resources for a list).

In spite of being so popular, incorporating support for a scripting framework usually requires explicit code fragments that evaluate the interpreted code, handle exceptions, and pass parameters between the script and the rest of the application (Ramnivas Laddad explains this task in "Scripting Power Saves the Day for Your Java Apps," (October 1999)). While all this complexity may seem reasonable for languages that do not share Java's object-oriented paradigm, in this article, I show that this often painful overhead can be quite easily trimmed for object-oriented language interpreters.

This article shows how to freely and transparently mix interpreted (scripted) code with precompiled Java code. Moreover, I attempt to mix the code in such a way that neither the JVM nor other running classes can distinguish between precompiled classes and scripted classes. I focus on BeanShell because it is widely known and extends Java syntax to make the examples easier to understand. You could adopt this article's approach to any object-oriented scripting language interpreter.

I assume you are familiar with certain Java topics, such as reflection and class loading. The article provides learning opportunities in dynamic code compilation and loading, and utilization of scripted code in a runtime environment. Furthermore, it also demonstrates the powerful new synergies that can be achieved by combining various open source projects like BCEL (Byte Code Engineering Library), BeanShell, and Ant.

Think big: Set up the goals

BeanShell is an excellent Java interpreter with several extensions to the Java syntax that make it particularly interesting. The script can access real Java classes, execute their methods, load new classes if necessary, or even implement an interface and return to the JVM as an instance of that interface. For example, the Java code below creates a fully functional scripted instance of the java.lang.Runnable interface:

Interpreter interpreter = new Interpreter();
// Define a method called run() in the interface
interpreter.eval("run() { ... }");
// Fetch a reference to the interpreter as a Runnable
Runnable runnable = (Runnable)interpreter.getInterface( Runnable.class );

The capability of acting as any Java interface is one of BeanShell's great advantages. Also note that the script does not even need to declare all of the interface's methods! If Java invokes an undefined method, the interpreter first attempts to evaluate the default handler for undefined methods—a method named simply invoke(). If the script also fails to specify that default handler, an exception will result.

Although many scripting languages are object-oriented, the dynamic nature of scripting engines normally prohibits them from exposing these objects directly to compiled code. As with most scripting languages, BeanShell scripts were, until recently, unable to serve as general Java classes. (The interface in the above example was implemented using the java.lang.reflect.Proxy class introduced in Java 1.3.) Scripts could not extend real Java classes or implement more than one interface. Furthermore, Java reflection did not work with scripted objects. If interface wrappers like the one shown above were not used, scripted code evaluation would require explicit calls to BeanShell's Interpreter class, which was inconvenient and less understandable. In this article we will discuss one implementation of true object support for the Beanshell language that is generally applicable to other scripting languages in Java. Later, we will compare briefly with how these features were implemented in BeanShell 2.0 (see sidebar, "On to BeanShell 2.0.")

Knowing what used to be BeanShell's weaknesses prior to version 2.0, we can start thinking of improvements. We will specifically try to achieve the following:

  • Real Java classes that somehow encapsulate the scripted BeanShell code. To be fully compatible with Java reflection mechanisms, these classes and their instances must expose method signatures.
  • The ability to extend a real Java class with a script and even extend a scripted class with another script (because scripted classes should behave just as any ordinary Java classes).
  • Neither the JVM nor the rest of the application should be aware of the scripted nature of the wrapped BeanShell scripts. We thus avoid explicit evaluation code, and referencing BeanShell classes proves unnecessary.
  • To design our improvements as an extension to BeanShell without modifying the project's core. We should therefore avoid changing any of BeanShell's classes.

A quick glance at BeanShell's syntax

When BeanShell evaluates a script, it parses the characters in the input stream into lexical entities, called ASTs; Beanshell then executes these lexical entities once they form self-contained structures, such as method declarations, variable assignments, and expressions. Almost every element of a script has an associated Namespace object, which holds variables and methods accessible from the scope of that element. Consider the following BeanShell code snippet:

int counter;
counter = 1;
int addTwo(first,second) {
   return first + second;
}
Integer counterNext() {
   global.counter++;
   return global.counter;
}

The code above declares one field, counter, and two methods (or rather functions), addTwo() and counterNext(). The global namespace is defined to be at the script's top level -- counter, method declarations, and the code of counter's value assignment belong to it. The explicit use of the global namespace inside counterNext() was necessary in earlier versions of BeanShell, where local variables were always the default. If the explicit namespace was omitted, a new variable counter would be created inside that method's scope (in its local namespace). The latest versions of BeanShell have adopted standard Java style scoping rules so global is now unecessary. Note how variables automatically convert between primitive and wrapper types (int and java.lang.Integer in counterNext()). Method addTwo()'s parameters are not even declared at all! We must consider these and other subtle syntactical differences between Java and BeanShell when designing our script binding solution.

Map BeanShell's extended syntax to Java class requirements

Unfortunately, we must sacrifice some of BeanShell's extended constructs to fit the Java language syntax. We can still use all of the benefits BeanShell provides inside the scripted code, but to the outside world of the JVM, scripts must appear just as regular Java classes. The assumed constraints and mapping of scripts' syntax to Java are described below:

  • One BeanShell script constitutes one Java class. For scripts kept in files, the name of the script file with the bsh file extension forms that class's name.
  • Functions declared in the global namespace form scripted class methods. All function parameters must be strongly typed (input parameters and return parameter, if present). We could theoretically allow loose typing by casting or wrapping parameters into an Object reference, but I leave this task as an exercise for you.
  • Any code blocks declared in the global namespace execute when an instance of the scripted object is first created. Hence, global code blocks are effectively equivalent to constructors.
  • Scripted classes always have a parameter-less constructor. This implies that any superclass the script extends must also have a public or protected parameter-less constructor.
  • extends and implements keywords for the scripted classes are declared in special comments so BeanShell's interpreter can ignore them (remember, we're striving to preserve the existing BeanShell syntax). These comments assume the form: /*% @extends package.ClassName %*/ and /*% @implements package.InterfaceName %*/.

These assumptions will make it easier for us to map between functions in a BeanShell script and Java class method signatures, which I show how to create in the next section.

Create real Java classes for scripts

All Java classes must be loaded to the JVM in the form of byte code. Moreover, before the classes load, they must pass the byte code verifier, which prevents potentially malicious (or simply corrupted) code from executing. Consequently, there seems to be only two ways of converting a BeanShell script into a Java class: generate all the byte code for the script, effectively compiling it, or use a lightweight Java wrapper class solely for providing appropriate runtime information to the JVM and delegate all method calls to the actual script. The former approach is much more complex and, in the end, accomplished best by the Java compiler itself, so we focus on the latter.

The wrapper class's real purpose is to merely provide the JVM with information about the script's extended superclass, implemented interfaces, and method signatures. All remaining complexity, like loading the script's code and initializing BeanShell's Interpreter object, is identical for all wrappers. Moving all that common code to a separate class named BshScriptWrapper decreases the amount of dynamically generated code. The BCEL library we will use for byte-code generation is quite complex, so moving code generation to one class also slightly increases code clarity. The lightweight wrapper class therefore only initializes a private instance of the BshScriptWrapper class and delegates all method calls to that instance. A class diagram should clarify the dependencies between these classes; see Figure 1.

Figure 1. The lightweight wrapper class and its associated objects

Let's analyze how the approach presented in Figure 1 works on a simple BeanShell script:

int counter;
counter = 1;
int addTwo(int first, int second) {
   return first + second;
}
Integer counterNext() {
   global.counter++;
   return global.counter;
}

For the script above, the corresponding lightweight wrapper class resembles the code below (I will explain this code's details later):

import com.dawidweiss.bsh.dynbinding.BshScriptWrapper;
public class SimpleExampleTyped {
   private final BshScriptWrapper scriptWrapper =
      new BshScriptWrapper(this, "SimpleExampleTyped",
         "SimpleExampleTyped.bsh");
   
   public int addTwo(int arg0, int arg1) {
      Object aobj[] = { new Integer(arg0), new Integer(arg1) };
      return ((Integer)scriptWrapper.invoke("addTwo@@{int}{int}@@int", aobj))
         .intValue();
   }
   
   public Integer counterNext() {
      Object aobj[] = new Object[0];
      return (Integer) scriptWrapper
         .invoke("counterNext@@@@java.lang.Integer", aobj);
   }
}

The private and final scriptWrapper field is initialized when the wrapper class loads into memory. At this time, the wrapper class attempts to load the actual BeanShell script and if this step fails, an InstantiationError is thrown. Thus, it is impossible to create the wrapper class without its associated script. Once the script loads, it is parsed, and method signatures are extracted from it. These signatures should naturally be identical to that of the wrapper class, unless the script has changed since the wrapper class was generated. This ends the wrapper class initialization process.

For all functions extracted from the script body, an appropriate delegate method exists in the wrapper class. All methods simply delegate the call to scriptWrapper, which handles further complexity of invoking BeanShell's interpreter:

public Object invoke(String methodName, Object [] args)
   throws InvocationTargetException {
   Object method = methods.get(methodName);
   if (method == null)
      throw new java.lang.NoSuchMethodError();
   try {
      Object retValue =
          ((BshMethod) method).invoke(args, interpreter, new CallStack());
      if (retValue instanceof bsh.Primitive)
          return Primitive.unwrap(retValue);
      return retValue;
   } catch (EvalError e) {
      throw new java.lang.reflect.InvocationTargetException(e,
         "Method evaluation error: " + methodName);
   }
}

The BshScriptWrapper class's invoke() method is quite straightforward: it first searches for the presence of a given method signature in the script. If the method exists, it invokes the BeanShell interpreter's local instance, otherwise it throws an exception of type NoSuchMethodError. Note that it is possible to (and quite easy to implement) hot-swap method implementation at runtime. Simple script reloading that changes the mapping between method signatures and their implementations would allow you to change the implementation of a class at runtime, without falling back to advanced techniques like classloader invalidation.

1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more