-
Notifications
You must be signed in to change notification settings - Fork 47
JIT VM API DRAFT
This document describes the interaction between the J2ME compiler and the rest of the runtime system. NOTE: This is NOT how things are currently happening.
Java class inheritance is modeled using JavaScript prototype chains. This gives us virtual dispatch for free and also lets us benefit from many of the optimizations in the JS engine that are tuned to common JS code patterns.
Consider the following class hierarchy:
class A {
static int baz = 1;
static void foo(int x) {}
int car = 2;
void bar(int x) {}
}
class B extends A {
static int cat;
void bar(int x) {}
}
Class A
is mapped to JavaScript as a constructor function that initializes all class instance fields to their default values. All instance fields of the class as well as any of the instance fields of its base class live on the object itself and are initialized by one single constructor function.
function A() {
this.car = 0;
}
There is only one A
function object, shared across all isolates. If you want an isolate specific version of the A
class, you'll need to use A.$
. Accessing the class via A.$
also guards against class initialization.
Object.defineProperty(A, "$", {
get: function () {
if (!$.A) {
$.A = A.bind(null); // Create a new isolate class that can also be used as a constructor.
A.staticConstructor.call($.A); // Call the constructor.
}
return $.A;
}
});
The $
property is a getter. A global property $
holds a reference to the current isolate (or runtime). Since JavaScript doesn't have proper threads and only one thread can execute at any one time, we can get away with keeping the current isolate in the global $
property.
Static class methods are stored in the global object, using a mangled name that is derived from the class name, method name, and method signature. This guarantees no name collisions:
var A_foo = function () { ... };
Instance functions are stored in the A.prototype
object.
A.prototype.bar = function () { ... };
The class static constructor is stored in A.staticConstructor
A.staticConstructor = function () { this.baz = 1; };
Every object has a reference to its class, and we store this on A.prototype
. This gives you the shared class object, not the isolate class object. If the latter is needed, we emit A.$.class
. Usually, we only care about the shared class object and this use case is fast.
A.prototype.class = A;
Static fields are stored in the A.$
object. This is needed since we need to have per isolate static state.
Declaring B
to derive from A
is straight forward:
function B() {
this.car = 0;
}
B.prototype = Object.create(A.prototype, null);
B.prototype.class = B;
B.prototype.bar = function () { ... };
The compiler notifies the runtime system about the symbols that it needs. The runtime system needs to make these available on the global object in case they are ever accessed.
Common exceptions like the NullPointerExceptions
exception doesn't need to be explicitly checked. These kinds of checks are very common in Java and checking for them explicitly would be quite expensive. Instead, we simply ignore the checks and let the JS engine throw.
var a = null;
a.foo();
This triggers a exception that is caught by the interpreter somewhere up the call stack. The interpreter translates the JS exception into a proper NullPointerExceptions
Java exception and re-throws or handles it. Check casts, array bounds, etc. need explicit checks.
Class type checks are emitted as calls to two runtime methods: classCheckCast(o, C)
and classInstanceOf(o, C)
. These are implemented efficiently using a display table. Each class holds a table of its base classes and its depth in the class hierarchy. This table is created during class loading. The subtype check becomes a lookup in this table:
function classInstanceOf(o, C) {
var c = o.class;
var i = c.depth - C.depth;
if (i < 0) {
return false;
}
return c.baseClasses[i] === C;
}
See Fast Subtype Checking in the Hotspot JVM for details.
Interface type checks are emitted as calls to two runtime methods: interfaceCheckCast(o, C)
and interfaceInstanceOf(o, C)
. These require linear scans and are considerably slower.
Whether the type check is for an interface or class is known at compile time.
The J2ME runtime supports multi-threading. This means that a thread may need to yield to another thread. Suspending and resuming threads is easy for the interpreter since it has full control of the stack, but more difficult for the compiler.
Methods that can potentially yield need to be annotated as such. The compiler performs a call graph analysis to determine what call sites yield and then for each such call site it emits code that saves the state of the current frame following the call site. Consider the following function that calls a method y
that yields.
int x() {
int a = 1;
int b = 2 + y();
int c = 3;
return a + b + c;
}
Method x
is compiled as:
function x() {
var t = y();
if (isYielding) {
$.pushFrame([1, 0, 0], [2], 7); // pushFrame(<locals>, <stack>, bytecodePosition);
return;
}
return t + 6;
}
The state of the Java frame at the y
call site (bytecode position 7) is locals = [1, 0, 0]
, stack = [2]
.
function y() {
if (...) {
isYielding = true; // Yield
return;
}
}
If y
yields by setting the global flag isYielding
to true
then the code following the call to y
saves its frame by calling $.pushFrame
and returns. This unwinds the stack of compiled methods all the way up to an interpreter frame where a proper interpreter call stack can be built and saved. This is possible because the compiler keeps a mapping of live values at each call yielding call site.
Yielding code is expensive, thus it's important that we only insert it when we really need to.
The compiler uses name mangling scheme uniquely encode references to class names, fields and methods. Names are hashed to 32-bit numbers and then encoded using variable encoding of the following 64 chars: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$_
, a combination of which happen to be valid identifier names.
Classes are mangled by hashing the package name and class name.
Methods are mangled by hashing the method name and signature.
Field names are mangled by escaping invalid identifier characters and making sure they don't conflict with JS identifiers like toString
, class
, etc.
To compile methods, use the shell.js
script in opt/build/
. The command syntax is:
js opt/build/shell.js -cp [jar-file, ...] [-d] [-r] [-f]
-
-d
: Include debug Information, comments usually. -
-r
: Enable release mode, no compile time assertions. -
-f
: Regexp class filter,.*
by default.
js opt/build/shell.js -cp java/classes.jar tests/tests.jar -d -r -f "SimpleClass"