Java scripting languages: Which is right for you?

When hooking scripting language support into a Java app, choose wisely

Some Java applications' requirements make integration with a scripting language necessary. For example, your users may need to write scripts that drive the application, extend it, or contain loops and other flow-control constructs. In such cases, it's sensible to support a scripting language interpreter that can read user scripts, then run them against your Java application's classes. To accomplish that task, run a Java-based scripting language interpreter in the same JVM as your application.

Support libraries, such as IBM's Bean Scripting Framework or the library Ramnivas Laddad developed in "Scripting Power Saves the Day for Your Java Apps" (JavaWorld, October 1999), successfully help you plug different scripting languages into your Java program. Such frameworks don't require major changes to your Java application, and they let your Java program run scripts written in Tcl, Python, and other languages.

A user's scripts can also directly reference your Java application's classes, just as if those scripts were another part of your program. That's good and bad. It's good if you want scripting to drive regression tests against your program and need to make low-level calls from the script into your application. It's bad if a user's script operates against your program's internals instead of against an agreed-upon API, thus compromising your program's integrity. So plan on publishing the API you want your users to write scripts against and clarify that the rest of the program remains off limits. You could also obfuscate the class names and methods you don't want customers to write scripts against but leave the API classes and method names alone. By doing so, you'll make it less likely an adventurous user would code against a class you didn't want them to.

Hooking in several scripting languages into your Java program is remarkable, but think twice if you're writing a commercial application -- you're opening a can of worms by trying to be all things to all users. You must consider the configuration management issue, since at least some of the different scripting interpreters get updated and released periodically. So you'll need to ensure which version of each scripting interpreter makes sense with which release of your application. If a user puts a newer version of one of these interpreters in your application's install tree (hoping to fix a bug in the older version), they will now run an untested configuration of your Java application. Days or weeks later, when the user finds and reports a bug in your application unearthed by the newer scripting interpreter version, they probably won't mention the scripting interpreter change to your customer support staff -- making it difficult for your engineers to reproduce the problem.

Moreover, customers likely will insist you offer a bug fix for the scripting interpreter your application supports. Some interpreters are actively maintained and updated through an open source model; in those cases experts may help you work around the problem, patch the interpreter, or get a bug fix included in a future release. That's important because without support, you could be stuck with the unpleasant task of fixing the problem yourself, and scripting interpreters run from between 25,000 to 55,000 lines of code.

To avoid the fix-it-yourself scenario, you can thoroughly test any scripting interpreter you plan to support with your application. For each interpreter, ensure that the interpreter gracefully handles the most common usage scenarios, that big memory chunks don't leak when you hammer on the interpreter with long and demanding scripts, and that nothing unexpected happens when you put your program and scripting interpreters in the hands of demanding beta-testers. Yes, such up-front testing costs time and resources; nevertheless, testing is time well spent.

The solution: Keep it simple

If you must support scripting in your Java application, pick a scripting interpreter that best suits your application needs and customer base. Thusly, you simplify the interpreter-integration code, reduce customer support costs, and improve your application's consistency. The hard question is: if you must standardize on just one scripting language, which one do you choose?

I compared several scripting interpreters, starting with a list of languages including Tcl, Python, Perl, JavaScript, and BeanShell. Then, without doing a detailed analysis, I bumped Perl from consideration. Why? Because there isn't a Perl interpreter written in Java. If the scripting interpreter you choose is implemented in native code, like Perl, then the interaction between your application and the script code is less direct, and you must ship at least one native binary with your Java program for each operating system you care about. Since many developers choose Java because of the language's portability, I stay true to that advantage by sticking with a scripting interpreter that does not create a dependence on native binaries. Java is cross-platform, and I want my scripting interpreter to be also. In contrast, Java-based interpreters do exist for Tcl, Python, JavaScript, and BeanShell, so they can run in the same process and JVM as the rest of your Java application.

Based on those criteria, the scripting interpreter comparison list comprises:

  • Jacl: The Tcl Java implementation
  • Jython: The Python Java implementation
  • Rhino: The JavaScript Java implementation
  • BeanShell: A Java source interpreter written in Java

Now that we've filtered the scripting interpreter language list down to Tcl, Python, JavaScript, and BeanShell, that brings us to the first comparison criteria.

The first benchmark: Feasibility

For the first benchmark, feasibility, I examined the four interpreters to see if anything made them impossible to use. I wrote simple test programs in each language, ran my test cases against them, and found that each performed well. All worked reliably or proved easy to integrate with. While each interpreter seems a worthy candidate, what would make a developer choose one over another?

  • Jacl: If you desire Tk constructs in your scripts to create user interface objects, look at the Swank project for Java classes that wrap Java's Swing widgets into Tk. The distribution does not include a debugger for Jacl scripts.
  • Jython: Supports scripts written in the Python syntax. Instead of using curly braces or begin-end markers to indicate flow of control, as many languages do, Python uses indentation levels to show which blocks of code belong together. Is that a problem? It depends on you and your customers and whether you mind. The distribution does not include a debugger for Jython scripts.
  • Rhino: Many programmers associate JavaScript with Webpage programming, but this JavaScript version doesn't need to run inside a Web browser. I found no problems while working with it. The distribution comes with a simple but useful script debugger.
  • BeanShell: Java programmers will immediately feel at home with this source interpreter's behavior. BeanShell's documentation is nicely done, but don't look for a book on BeanShell programming at your bookstore -- there aren't any. And BeanShell's development team is very small, too. However, that's only a problem if the principals move on to other interests and others don't step in to fill their shoes. The distribution does not include a debugger for BeanShell scripts.

The second benchmark: Performance

For the second benchmark, performance, I examined how quickly the scripting interpreters executed simple programs. I didn't ask the interpreters to sort huge arrays or perform complex math. Instead, I stuck to basic, general tasks such as looping, comparing integers against other integers, and allocating and initializing large one- and two-dimensional arrays. It doesn't get much simpler than that, and these tasks are common enough that most commercial applications will perform them at one time or another. I also checked to see how much memory each interpreter required for instantiation and to execute a tiny script.

For consistency, I coded each test as similarly as possible in each scripting language. I ran the tests on a Toshiba Tecra 8100 laptop with a 700-MHz Pentium III processor and 256 MB of RAM. When invoking the JVM, I used the default heap size.

In the interest of offering perspective for how fast or slow these numbers are, I also coded the test cases in Java and ran them using Java 1.3.1. I also reran the Tcl scripts I wrote for the Jacl scripting interpreter inside a native Tcl interpreter. Consequently, in the tables below, you can see how the interpreters stack up against native interpreters.

Table 1. For loop counting from 1 to 1,000,000
Scripting interpreter
Time
Java10 milliseconds
Tcl1.4 seconds
Jacl140 seconds
Jython1.2 seconds
Rhino5 seconds
BeanShell80 seconds
Table 2. Compare 1,000,000 integers for equality
Scripting interpreter
Time
Java10 milliseconds
Tcl2 seconds
Jacl300 seconds
Jython4 seconds
Rhino8 seconds
BeanShell80 seconds
Table 3. Allocate and initialize a 100,000 element array
Scripting interpreter
Time
Java10 milliseconds
Tcl.5 seconds
Jacl25 seconds
Jython1 second
Rhino1.3 seconds
BeanShell22 seconds
Table 4. Allocate and initialize a 500 x 500 element array
Scripting interpreter
Time
Java20 milliseconds
Tcl2 seconds
Jacl45 seconds
Jython1 second
Rhino7 seconds
BeanShell18 seconds
Table 5. Memory required to initialize the interpreter in the JVM
Scripting interpreter
Memory size
JaclAbout 1 MB
JythonAbout 2 MB
RhinoAbout 1 MB
BeanShellAbout 1 MB

What the numbers mean

Jython proves the fastest on the benchmarks by a considerable margin, with Rhino a reasonably close second. BeanShell is slower, with Jacl bringing up the rear.

Whether these performance numbers matter to you depends on the tasks you want to do with your scripting language. If you have many hundreds of thousands of iterations to perform in your scripting functions, then Jacl or BeanShell might prove intolerable. If your scripts run few repetitive functions, then the relative differences in speeds between these interpreters seem less important.

It's worth mentioning that Jython doesn't seem to have built-in direct support for declaring two-dimensional arrays, but this can be worked around by using an array-of-arrays structure.

Although it was not a performance benchmark, it did take me more time to write the scripts in Jython than for the others. No doubt my unfamiliarity with Python caused some of the trouble. If you are a proficient Java programmer but are unfamiliar with Python or Tcl, you may find it easier to get going writing scripts with JavaScript or BeanShell than you will with Jython or Jacl, since there is less new ground to cover.

The third benchmark: Integration difficulty

The integration benchmark covers two tasks. The first shows how much code instantiates the scripting language interpreter. The second task writes a script that instantiates a Java JFrame, populates it with a JTree, and sizes and displays the JFrame. Although simple, these tasks prove valuable because they measure the effort to start using the interpreter, and also how a script written for the interpreter looks when it calls Java class code.

Jacl

To integrate Jacl into your Java application, you add the Jacl jar file to your classpath at invocation, then instantiate the Jacl interpreter prior to executing a script. Here's the code to create a Jacl interpreter:

import tcl.lang.*;
public class SimpleEmbedded {
   public static void main(String args[]) {
      try {
        Interp interp = new Interp();
      } catch (Exception e) {
      }
} 

The Jacl script to create a JTree, put it in a JFrame, and size and show the JFrame, looks like this:

package require java
set env(TCL_CLASSPATH) 
set mid [java::new javax.swing.JTree]
set f [java::new javax.swing.JFrame]
$f setSize 200 200
set layout [java::new java.awt.BorderLayout]
$f setLayout $layout
$f add $mid 
$f show

Jython

To integrate Jython with your Java application, add the Jython jar file to your classpath at invocation, then instantiate the interpreter prior to executing a script. The code that gets you this far is straightforward:

import org.python.util.PythonInterpreter;
import org.python.core.*;
public class SimpleEmbedded {
    public static void main(String []args) throws PyException {
        PythonInterpreter interp = new PythonInterpreter();
   }
}

The Jython script to create a JTree, put it in a JFrame, and show the JFrame is shown below. I avoided sizing the frame this time:

from pawt import swing
import java, sys
frame = swing.JFrame('Jython example', visible=1)
tree = swing.JTree()
frame.contentPane.add(tree)
frame.pack()

Rhino

As with the other interpreters, you add the Rhino jar file to your classpath at invocation, then instantiate the interpreter prior to executing a script:

import org.mozilla.javascript.*;
import org.mozilla.javascript.tools.ToolErrorReporter;
public class SimpleEmbedded {
    public static void main(String args[]) {
        Context cx = Context.enter();
   }
}

The Rhino script to create a JTree, put it in a JFrame, and size and show the JFrame proves simple:

importPackage(java.awt);
importPackage(Packages.javax.swing);
frame = new Frame("JavaScript");
frame.setSize(new Dimension(200,200)); 
frame.setLayout(new BorderLayout());
t = new JTree();
frame.add(t, BorderLayout.CENTER);
frame.pack();
frame.show();

BeanShell

BeanShell integration with your application is simple, like the rest. Add the BeanShell jar file to your classpath at invocation, then instantiate the interpreter prior to executing a script:

import bsh.Interpreter;
public class SimpleEmbedded {
    public static void main(String []args) throws bsh.EvalError {
       Interpreter i = new Interpreter();
    }
 }

The BeanShell script to create a JTree, put it in a JFrame, and size and show the JFrame is simple and Java-like:

frame = new JFrame();
tree = new JTree();
frame.getContentPane().add(tree);
frame.pack();
frame.show();

What these integration examples mean

The information above shows that it's easy to integrate any of the interpreters. And once you understand the correct syntax from your scripts, you can be productive writing scripts, too. For the trivial example I used, you can see that scripts written in BeanShell and JavaScript are the most Java-like in their formatting, with Jacl and Jython showing some differences, but not proving unreadable. As the scripts for each interpreter show, no firewall exists between the script code and classes defined in your application's Java libraries. So take note: the script code seamlessly runs against your application classes, so be sure that's what you want. If you want to protect parts of your application from scripts at runtime, take steps such as obfuscating classes not part of your published API to prevent people from coding scripts against them.

The fourth benchmark: Library support issues

Though scripting support gives your application extra power, it requires your application to depend on those scripting libraries. Before you tie yourself to a scripting interpreter, consider the chance you someday may own the code you are integrating against.

If the support team seldom updates and releases the interpreter, that's a bad sign. It either means the code is perfect as it stands, or the developers who took care of the code have moved on to other projects. Guess which case is more likely?

It is also worth checking to see how much source code it took to implement the interpreter. It's unrealistic to think that you can understand an interpreter's every code line and extend it or tune it to fit your needs. The interpreters are just too big. Still, it's worth knowing these interpreters' sizes. At some point you may need to modify the interpreter source code or dig through the code to figure out the interpreter's behavior.

Let's go through the library support issues for each interpreter in our survey.

Jacl

Jacl has an active support group. Although the download link at the development site points you at a release that is several years old, newer versions can be found using the CVS version control system. Jacl consists of about 37,000 lines of Java code.

Jython

Jython seems to feature active support, maintenance, and updates. Jython consists of about 55,000 lines of Java code.

Rhino

Rhino receives updates and new releases on a frequent basis, and comprises about 44,000 lines of Java code.

BeanShell

BeanShell features regular updates and releases; it consists of about 25,000 lines of Java code plus a substantial number of BeanShell scripts.

What the support information means

All four interpreters are big, complex beasts. You'll be ahead in the game if you can rely on a support team to make improvements and fix bugs. Before you choose an interpreter, check to see how often it is updated and released. You might contact one of the developers to see what the long term plans for the interpreter are and to see what their process is for fixing bugs.

Licensing

Even though you can download these interpreters for free, what are the licensing rules about embedding the scripting interpreter in a commercial application?

Fortunately, licensing does not present a problem for any of these libraries. The way I read the license agreements for Jacl, Jython, JavaScript, and BeanShell, the user must abide by the GNU Lesser General Public license or an equivalent. That means you can ship the libraries with your application even if your application is not free. However, you cannot strip the copyright headers out of their source files or script files, and you may need to clarify to users that the rights to the scripting interpreter bundled with your application belong to someone else. Naturally, don't take my word for it. When you get interpreter distribution you care about, read and interpret the licensing information for yourself, and have your legal department affirm you can use it with your application without creating any problems for you.

Final thoughts

If you need to integrate scripting support code into your Java application, I advise you to standardize on a single scripting interpreter. Each additional scripting language you support in your product has associated costs, so avoid hooking more than one scripting interpreter into your application. When adding scripting support, you can simplify things further by using a Java-based interpreter instead of a native interpreter such as Perl. That will make your solution more portable and simplify the integration task between your Java program and the interpreter.

If your customers expect a particular language to customize your products, look seriously at integrating with a scripting interpreter that supports that language, in the way that Jacl supports Tcl syntax. If you are not locked to a particular language, compare the interpreters from a variety of standpoints to see which are better suited to particular tasks than others.

For instance, Jacl moves extremely slowly compared to the other interpreters, but proves useful if you need your scripts written in Tcl. If you're porting an application from Tcl/Tk to Java, the ability to run your old Tcl scripts in the new Java application might be so valuable that it would outweigh other concerns. Also, Tcl is a popular programming language, so many developers are already familiar with it, and Tcl books are easy to find.

If you want Java-like scripting code and prefer painless integration, BeanShell looks good. On the downside, the user guide for BeanShell syntax and programming is limited to what ships with the release, and it runs slower than some of the other interpreters. On the other hand, I found BeanShell easy to work with and liked it quite a bit. Its well organized libraries make integration simple. If performance is not the most important criteria for your scripting interpreter, consider BeanShell.

Rhino runs significantly faster than BeanShell and likewise supports Java-like syntax in its scripting. Moreover, it appears well written and well supported, and you'll find numerous JavaScript syntax books at the bookstore. I had no problems and recommend it if you have equally balanced needs for performance, Java-like syntax, and strong support for the libraries.

Jython is the fastest scripting interpreters I looked at and has some powerful programming features. My only real concern was about Jython's control-flow syntax, which might or might not be important to you. As with Jacl, writing scripts for Jython may have more of a learning curve to it than JavaScript or BeanShell do, since there is more new ground to cover. If you want to write nontrivial scripts in Python, I recommend buying a book. Python is a popular programming language, so you'll have plenty to choose from at the bookstore.

Many thanks to Geetha Pannala and Blair West of Mentor Graphics for their assistance in looking at the different scripting interpreters.

David Kearns is a Sun Certified Java 2 Programmer and Developer. He possesses 13-years experience designing and developing electronic design automation (EDA) tools and object-oriented application frameworks in C++ and Java. For the last 5 years, he has worked on commercial Java applications for configuration management, process control, tool scripting, and design simulation waveform viewing. David works at Mentor Graphics in Wilsonville, Ore. as a staff engineer. His current project at Mentor Graphics is developing a high-performance Java waveform viewer.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies