Metagnostic
home

JNIPort for Dolphin Smalltalk

Overview

Contents

Players

Layers

Examples

Configuration

InFAQ

Changes

Licence


Back to Goodies

Ghost Classes

JNIPort includes the Wrapper Wizard which uses Java's introspective facilities (such as they are) to generate wrapper methods for Java members. The ghost class concept takes this one step further. Ghost classes are (relatively) light-weight classes that are generated on demand, and which are discarded when the Java runtime shuts down. They are populated with ghost methods, which are essentially the same as the wrapper methods that would be generated by the Wrapper Wizard.

In case you are interested, I came up with the name “ghost classes” because they are ephemeral and in a sense invisible. The word “ghost” seemed to express something of those ideas, and had not yet been overtaken by the computing community's ravening appetite for jargon.

Ghost classes do not appear in the standard browsers, since they are not fully linked into the class hierarchy, and they are not installed in the global Smalltalk dictionary. Ghost classes are not written to the change log.

The name of a ghost class is the fully qualified name of the corresponding Java class for the instance side, and the same with '.static' added for the class side. The name is only used by instances' #printString, etc. methods.

Pros and Cons

The great advantage of ghost classes is that they are convenient. There is no need to mess about creating wrapper classes by hand (not even with the aid of the Wizard). They also have a performance edge, though the difference may not be significant for many applications.

The biggest disadvantage of ghost classes is that loading any Java class will (subject to configuration) cause a ghost to be generated for it. But doing that may well cause other classes to be loaded, and so on. For instance, in the Swing example, the first mention of javax.swing.JFrame causes a long pause as almost 300 Java classes, mostly bits of Swing, are loaded and have ghosts generated. This delay only occurs on the first mention of the class, but it is still annoying and creating a fix for it is very high priority. Stop press: actually a fix is available now, if you use a JVMLazyGhostClassMaker instead of an JVMGhostClassMaker — the feature is still experimental, though.

Ghost classes also take up more memory, naturally. The impact is not huge, but it may matter in some cases. For one example, loading the 300 Swing classes causes around 6000 ghost methods to be generated; they take up, between them, around 1MB of memory. It's worth mentioning that the planned improvements to ghost class generation will also reduce the ghost method footprint — most of the Swing classes are never used (by JNIPort), and so would not have ghost methods generated for them.

One other aspect of ghost classes might be considered to be a disadvantage. Ghost methods are generated as Smalltalk source (rather than by bytecode magic), so they need the compiler. Dolphin's licence does permit you to distribute the compiler DLL with a deployed application, but it is something of a nuisance, especially as the compiler is not included in “To-Go” applications.

Using Ghost Classes

Ghost classes are just dynamic special-cases of static wrapper classes, and all the observations about wrapper classes are true of ghost classes. In particular they use the same naming scheme for generated methods.

The big difference, of course, is that you don't have to have generated a static wrapper to call a Java object's methods.

You can use the ghost methods “raw” or you can set up helper methods. There are at least two ways of doing this in a reasonably well-structured way. You can use custom wrapper classes, or you can write a Facade/Adaptor class.

Using a custom wrapper class is just the same as writing a custom static wrapper class. You create a subclass of JavaInstance or JavaStatic, give it a class-side #javaClassName method, and the next time JNIPort is started, all ghosts that inherit from the corresponding Java class will be given ghost classes that inherit from your class. You can now add whatever Smalltalk methods you like to your class — it's a bit like being able to extend the Java class with new methods.

You can leave it at that; the single method #javaClassName is enough to tell JNIPort about the connection between your class and the Java class. However, you may prefer (as I do) to use the Wrapper Wizard to create and populate the wrapper class. If you do that then you'll have a complete static wrapper to work from, but at runtime all the static wrapper methods will be overridden by their more efficient ghost equivalents.

Alternatively, you can create an Adaptor or Facade. For instance the Smalltalk class JavaIteratorAdaptor (in package 'CU Java Additional Wrappers') is an Adaptor of Java's java.util.Iterator interface to Smalltalk ReadStream's protocols. Instances contain a reference to the ghost proxy for some Java object, and they implement Smalltalk methods like #atEnd by forwarding to the wrapper methods #hasNext_null and #next_null, which are the ghost class equivalents of the methods of java.util.Iterator. That means that any Java object that implements java.util.Iterator (as well as any other that happens to have the same two methods) can be used as a ReadStream via a JavaIteratorAdaptor.

Configuration

Ghost class generation is controlled by the 'ghostClassSettings' sub-setting.

Most of the settings determine what members will be exposed by ghost methods, they are the same as the corresponding flags that control the defaults for static wrapper generation. By default JNIPort will only expose “public” members. Custom wrapper classes can override the settings, but nothing in this version of JNIPort uses the feature.

The 'retainMethodSource' flag controls whether JNIPort will keep the source to ghost methods. If it does then the source is stored as Strings in the image, not written to the change log. This setting is ignored except in development sessions.

The remaining options control what kinds of objects JNIPort will generate ghost classes for:

  • 'useGhostInstances' and 'useGhostClasses' tell JNIPort to create ghost classes for the instance and class sides of all normal Java classes (java.lang.Object and its subclasses except arrays). Both of these are on by default; it wouldn't really make a lot of sense to turn them off.

  • 'useGhostInstancesForInterfaces' and 'useGhostClassesForInterfaces' tell JNIPort to create ghost classes for Java's interfaces. There's quite a big difference between the class-side and instance-side in this case. On the class-side, interfaces are like classes and are represented by normal class statics. Interfaces quite often have lists of public constants, so JNIPort generates ghosts for the class-side by default. Interface “instances” are JNIPort's built-in way of creating adaptors for Java objects known to implement some interface (more information is here). Since ghost classes don't need adaptors as much as statically-generated wrappers, it's not clear whether this option should be turned on by default. In the end I decided not to, for no better reason that to demonstrate that it could be done.

  • 'useGhostInstancesForArrays' and 'useGhostClassesForArrays' tell JNIPort to create ghost classes for the instance and class sides of the synthetic Java classes that represent arrays. Both of these are turned off by default; there's not a lot of point in turning them on since arrays don't add any extra methods on either the instance or class side (the length field of arrays is a sham, implemented by the Java compiler, and although Java arrays do implement java.lang.Cloneable, they don't have a genuine public clone() method). It's rare to use the methods inherited from java.lang.Object, but if you are doing this a lot, and need the extra performance (unlikely) then you could turn on 'useGhostInstancesForArrays'.

How they work

Please note: this description has become slightly out of date. It is still accurate, but it should be updated to discuss the new lazy ghosts too

When JNIPort creates a class static for a Java class that it hasn't seen before (see the class lifecycle), the GhostClassMaker is told about it. It uses the 'ghostClassSettings' to decide whether to generate a class-side ghost class or an instance-side ghost class, or both. If either is called for, then it generates the class(es) and populates them with ghost methods. (By the way, the code for doing this actually lives in JavaClassStatic, but that is just an implementation detail, the responsibility rests with the ghost maker.)

If the Java class has a superclass (interfaces do not, and neither does java.lang.Object), then it finds the instance class and class static class used for the superclass. For instance when it generates ghost classes for java.util.Hashtable it starts with the instance and class static classes used for java.util.Dictionary. If the Java class does not have a superclass then it starts with either JavaClassInstance or JavaInterfaceInstance for the instance class, and either JavaClassStatic or JavaInterfaceStatic for the class static class. (I'm ignoring array classes, which have further specialisations.) It then creates ghost subclass(es) of them. A ghost subclass is just an ordinary class that has not been added to the Smalltalk namespace, nor to its superclass's list of subclasses. (They also answer true to #isGhostClass, and instances answer true to #isGhost.)

After creating the appropriate class(es), the ghost maker populates them with ghost methods. It adds the same set of methods as a similarly configured Wrapper Wizard would, but the implementations of those methods is not the same (more on this below).

In normal circumstances the ghost methods expose (a subset of) the members of the Java class. However there is one situation where ghost methods are generated for inherited members too. This happens when the ghost maker is subclassing a non-ghost wrapper class. For instance, after bootstrapping but before any static wrapper classes have been converted into ghosts, the class registry is (temporarily) set up so that instances of java.lang.String would be wrapped by instances of the (non-ghost) class JavaLangString. The ghost maker creates a ghost subclass of JavaLangString and registers that as the preferred wrapper class for java.lang.String. This is necessary to ensure that any extra methods that have been added to JavaLangString will be inherited by the new ghost class. However, although JavaLangString is a subclass of JavaLangObject, it is not a subclass of the new ghost class for java.lang.Object. Therefore the ghost maker must generate ghost methods exposing the members inherited from java.lang.Object as well as those that are specific to java.lang.String.

There is one other special case, constructors. In Java, constructors are not inherited (not even in the sense that static methods can be considered to be “inherited”). For statically-generated wrappers that doesn't matter because attempting to use an inherited wrapper for a constructor will cause a run-time error when the dynamic lookup of the “inherited” constructor fails (or at least it should do, I have noticed that the BEA JRockit runtime gets this wrong). For ghost methods, the lookup is performed as the ghost method is created and the result is bound into the method. That means that if the constructor wrapper were inherited, then when it was called from a subclass, it would be calling the superclass's constructor, rather than creating an instance of the subclass. The way that the ghost maker avoids this problems is to ensure that all inherited constructors are overridden to call self shouldNotImplement. (This behaviour can be configured for statically-generated wrapper classes too; it is controlled by the 'generateSNIContructors' option of the 'wrapperGeneratorSettings.)

Ghost Methods

Ghost methods are created by the ghost maker to expose the underlying Java object's (or class's) methods and fields. They are created by compiling generated Smalltalk code. Normally the code is discarded immediately unless the 'retainMethodSource' is set and you are in a development session. The generated code is highly stereotyped, so the corresponding bytecodes tend to repeat often; ghost methods share their bytecode arrays wherever possible (in my current image, there are 14K ghost methods, but they only use 252 distinct bytecode arrays).

Because the Smalltalk code is automatically generated, and isn't intended for people to read or modify, the ghost maker can take several performance-enhancing shortcuts. The generated code does not resemble that generated by the Wrapper Wizard.

One optimisation is that ghost methods use the lowest levels of JNIPort directly, rather that going through the framework provided by level 2 ('Java Base') of JNIPort.

A second optimisation is that JNIPort can make use of information available when the method is generated, to write code that otherwise would require a run-time lookup each time the method was called. One application of this is that the JNI “method ID” can be resolved at this time and hard-wired in the code (see below). The other is that, in some cases, the method can be determined to return a Java object of a “final” class, for instance java.lang.String. When JNIPort creates a wrapper object for a JNI reference, it needs to find the class static corresponding to the exact class of the Java object, which will then act as a factory for creating a proxy of the required type. If the exact class is known in advance, then JNIPort can generate code that directly asks the class static to create the proxy, rather than asking the JVM object to find it at runtime.

The third optimisation is that JNIPort can embed references to the significant objects directly in the method's literal frame. The effect is similar to using Dolphin's ##(...code...) syntax, although the details are different. The way it is implemented is that the generated Smalltalk code uses Symbol literals to stand for the object references that are to be hard-wired. After the code has been compiled, a second pass finds the Symbols in the compiled method's literal frame and replaces them with references to the intended objects. Currently, JNIPort hardwires references to:

  • The JVM object.
  • The JNI Method ID or Field ID.
  • The class static that will be asked to generate a wrapper object for the result.

If you have the 'retainMethodSource' option turned on, then you will see the Symbols if you step into a ghost method in the debugger. Please note: you cannot recompile the source of a ghost method and still expect it to work! (Which means that you can't set breakpoints in ghost methods.)

The effects of these optimisations are that, compared to statically generated wrapper methods:

  • Measured by calling java.lang.String.length() (which simply accesses a field in the String object), the minimum overhead of calling a Java method drops from 18.5 microseconds to 6.6 microseconds. For comparison, around 3.2 microseconds of that time is spent in JNI itself, and around 0.7 microseconds in Dolphin's external call mechanism. The real execution of length() is hardly measurable — roughly a nanosecond.

  • If the optimisation for predetermining the wrapper factory is applicable, then the time taken to create the proxy is approximately halved, to around 50 microseconds.

(All times measured on a Pentium3 laptop running at 650Mhz, using Sun's J2SDK 1.4.1 and Dolphin 5.0.3)


Copyright © Chris Uppal, 2003-2005

Java, JNI (probably), JVM (possibly), and God knows what else, are trademarks of Sun Microsystems, Inc.