RingoJS

Rhino Hacker Guide

This is a guide to getting started hacking on Mozilla Rhino, the underlying JavaScript engine of Ringo. It's not intended for typical users of Ringo.

Prerequisites

You'll need the following tools:

Getting and building Rhino

  1. Rhino source code lives at https://github.com/mozilla/rhino. You can either clone the master repository, or create your own fork on github. In case you clone the master repository, you can create and push to your own fork later on.

    git clone https://github.com/mozilla/rhino.git
    
  2. Change into rhino directory and run ant jar

    This should create a file called build/rhino1_7R3/js.jar where the name of the second directory may vary depending on the current version.

You can now run Rhino using the following command:

java -jar build/rhino1_7R3/js.jar

To run a script simply add it to the command line:

java -jar build/rhino_7R3/js.jar test.js

Congratulations, you're now ready to use Rhino. See https://developer.mozilla.org/en/Rhino_documentation for more information on running Rhino.

Running the tests.

Rhino comes with its own tests, but the majority of tests are shared with Mozilla's Spider-/Tracemonkey JavaScript engine.

For the time being, we're still getting these tests from the old CVS repository. Set your CVSROOT environement variable to :pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot for anonymous access to the Mozilla CVS repository. Then change to the parent directory of your cloned Rhino directory and issue the following command:

cvs co mozilla/js/tests

This should give you a tests directory at the same level as your rhino directory.

To run the tests, cd back to rhino and run

ant junit-all

This should run all tests and create a summary of the test results at build/test/report/index.html.

Running benchmarks

Rhino currently contains a copy of the v8-benchmark suite version 5. To run the benchmark, change to directory testsrc/benchmarks/v8 and run script run.js.

There's a problem with run.js in that it doesn't warm up the JVM so it doesn't reach its full performance. To fix this, I usually run the benchmark in a script that loads the main script several times:

for (var i = 0; i < 5; i++) {
    print("round", i + 1)
    load("run.js");
}

Eventually we'll want to add other benchmarks such as SunSpider and some microbenchmarks. Microbenchmarks testing very specific features will be especially helpful to assess the effect of optimizations until we get a version that passes a complete test suite.

Understanding the code

Rhino code is structured in several top-level directories. The main code is in src in the org.mozilla.javascript package. The API Documentation provides a good overview of the public classes.

Some of the most important classes in org.mozilla.javascript are:

Context

Each thread executing JavaScript code in Rhino must be associated with a Context object. The static Context.enter() method can be used to associate a Context with the current thread, Context.exit() will leave the context. Context.getCurrentContext() can be used to retrieve the Context associated with the current thread.

ScriptRuntime

The ScriptRuntime is a purely static class providing methods used both by the Interpreter and the bytecode generated by the Optimizer. This includes things like looking up properties, invoking functions, converting objects etc.

Scriptable

Scriptable is the basic interface for implementing native JavaScript objects. It provides methods get(), put(), has(), and delete() to query and manipulate object properties. It also provides ways to get and set the object prototype and parent scope.

ScriptableObject

ScriptableObject is the base implementation of the Scriptable interface subclassed by almost all JavaScript objects in Rhino. It provides its own hash table implementation to support the property methods. It also provides methods to define JavaScript host objects from Java classes.

Function

Function is the interface implemented by all callable functions in Rhino. There are two ways to call a function, call() and construct(), depending on whether the function is called with the new keyword.

Parser

The Parser class is not really part of the public Rhino API as it's mostly driven through methods provided by the Context class. It might be interesting to look at in combination with the AST nodes in the org.mozilla.javascript.ast package which it creates.

Interpreter

The Interpreter provides a way to run JavaScript code without generating Java bytecode. By default, Rhino will run with bytecode generation. To run in interpreter mode add -opt -1 to the Rhino command line options.

Bytecode Generation

Rhino comes with its own Java bytecode generation library that lives in src/org/mozilla/classfile. This may seem strange given the existence of generic bytecode manipulation libraries such as ASM. But having a small, custom-made bytecode generation library probably contributes a big part to Rhino's very responsive bytecode generation.

Creating .class files

Normally Rhino generates bytecode on the fly, but it comes with a JavaScript compiler tool that allows to create actual .class files from scripts. To run the JSC tool enter the following command:

java -cp build/rhino1_7R3/js.jar  org.mozilla.javascript.tools.jsc.Main

Run it with -h to get an overview of available options.

Executing a script

There are two major ways to execute JavaScript code: Executing a script or calling a function. Compiled Rhino scripts have an exec() method to execute the script. This is called by the main() method so a script can be run as follows from the command line (assuming script.class is in directory scriptdir):

java -cp build/rhino1_7R3/js.jar:scriptdir script

For calling functions script classes have a call() method which is described in the following section.

One class per script

One pecularity of Rhino bytecode is that for each script exactly one Java class is generated. Since a script may contain multiple functions and functions are first class objects in JavaScript, a new instance of the class has to be created for each function. The way this is done is to have a single call() method that multiplexes all functions in the script using a case statement on an instance int field.