Metagnostic
home

JNIPort for Dolphin Smalltalk

Overview

Contents

Players

Layers

Examples

Configuration

InFAQ

Changes

Licence


Back to Goodies

Basic JNIPort

The basic level of JNIPort, in package 'CU Java Base', uses the raw JNI functions exposed via package 'CU JNI' and builds a framework for manipulating Java objects and classes via Smalltalk proxies.

The proxies for Java objects don't have a special name in JNIPort, I tend to refer to them as [instance] wrappers, or occasionally as proxies. They are instances of subclasses of JavaInstance.

The “proxies” for Java classes are called class statics; of course they aren't really proxies, in the normal sense of the word, because there is no object in Java that they are a proxy for. Still, they provide an Facade that exposes the static members of Java classes as if they were attached to some object (which, in fact, they probably are — hidden away inside the Java runtime somewhere). They are instances of subclasses of JavaStatic.

For more information on instance wrappers and class statics, see the description of the main players of JNIPort.

For an example of using this level of JNIPort, see here .


Class JavaObject

Class Hierarchy highlighting JavaObject

All JNIPort proxies, both for the class- and instance-sides, are subclassed from JavaObject. This class provides a uniform framework for calling methods, and accessing Java fields, (collectively called the “members”) of a Java class.

All these objects are owned by a single JVM object, which stands for the Java runtime. Instances respond to the #jvm message that, unsurprisingly, answers that object. The JVM object has some configuration options that determine, for instance, which Java runtime it will start, and what options it will pass to that runtime. The JVM object's role is also described in the Players section.

Instances obviously depend on the Java runtime; as long as the runtime is, er, running they are considered to be “live”; after the runtime has shut down, any surviving instances will be “dead” (as will their owning JVM). When you save and restart the image, the Java runtime is not restarted, and so all the Smalltalk objects that referred to it become dead. The simplest way to configure and start/stop Java runtimes is via the GUI tool, the Status Monitor.

The remainder of this sub-section discusses the common features of all JavaObjects.

Reading and Writing Java Fields

You can safely skip this sub-section if you are using ghost classes or the Wrapper Wizard.

Unfortunately, due to the way that Java/JNI work, you have to use different methods to access a Java field depending on its type. There are specific methods for each of Java's primitive types, and another for use when it holds object references.

The 'Java field access' method category of JavaObject contains methods for reading or writing the value of a field that holds a primitive type. A slew of “getter” methods like, #getBooleanField:, #getByteField:, #getIntField:, and so on, read a field which holds one of Java's primitive types. Then another slew of “setters”, #getBooleanField:to:, etc, write to the field. The values are represented by the corresponding Smalltalk types according to the rules below.

When you access a reference field in a Java object (or class), you must supply both the name of the field, and its signature. The signature of a field is a string that describes its statically-declared type. Signatures are described more fully here, but, for example, the signature of a field that is defined as:

	String	m_String;

is 'Ljava/lang/String;'.

You can read the value of a field that holds an object reference with the JavaObject>>getObjectField:signature: method. For instance, to read the m_String field of some object, the Smalltalk code would be:

	aProxy
		getObjectField: 'm_field'
		signature: 'Ljava/lang/String;'

The 'Java field access' method category of JavaObject contains a couple of convenience methods for use in cases where you know that the field is declared to hold a java.lang.Object or a java.lang.String.

Since our field does hold a string, we could recode the example as:

	aProxy getStringField: 'm_field'

Please note that the value of either expression would be a Smalltalk object wrapping an instance of java.lang.String, not a Smalltalk String. (You can convert it with #asString.)

You can change the value of a reference field with the JavaObject>>setObjectField:signature:to: method. For instance, to set the m_String field to some pre-existing Java string, the Smalltalk code would be:

	aJavaString := ...
	aProxy
		setObjectField: 'm_field'
		signature: 'Ljava/lang/String;'
		to: aJavaString.

Again, there are a few convenience methods for handling the commonest classes. In this case we could have written:

	aJavaString := ...
	aProxy
		setStringField: 'm_field'
		to: aJavaString.

And, in fact, that particular method knows how to convert Smalltalk strings into Java strings, where necessary. So we could also have written:

	aProxy
		setStringField: 'm_field'
		to: 'JNIPort!!'.

Other than that one automatic conversion, any the value read from, or written to, a reference field must either be a reference to a Java object, or must be nil, which stands for Java's null.

A warning (or possibly it's an opportunity): since JNIPort works via JNI, the normal Java protection mechanisms are simply ignored. That means that you can read/write “private“ fields as easily as “public“ ones. You can also change the value of “final“ fields. Changing the value of a “static final int“ field in an interface may seem fun, but it probably isn't a very good idea…

Calling Java Methods

You can safely skip this sub-section if you are using ghost classes or the Wrapper Wizard.

When you call a method of a Java object (or class), you must supply both the name of the method, and its signature. The signature of a method is a string that describes its argument types and return type. Signatures are described here, but, for example, the signature of a method that is defined as:

	String	toString(Double fuzz) { ... };

is '(D)Ljava/lang/String;'.

As with reading the value of a field, there are a number of different ways of invoking a Java method, and which you use depends on the Java method's return type. The methods are in the 'Java method calls' category of class JavaObject. They have names, #callBooleanMethod:signature:withArguments:, #callByteMethod:signature:withArguments:, and so on through all the Java primitive types (including void), plus #callObjectMethod:signature:withArguments:, which is used for any method that returns an object reference.

If the method takes no parameters then the signature is determined completely by the return type. There are a number of convenience methods that help with this special case; for instance #callByteMethod: which “knows” that the named method takes no arguments and so that the signature must by '()B'.

If the method does take parameters, then you must supply a signature yourself. You must also build up a JNIValueArray to hold the parameters you want to pass to the call. A JNIValueArray is an array-like object that holds a list of arguments; you create one of the appropriate size, and then fill in each argument with typed versions of #at:put: before passing it to the Java method. For instance, given a reference to a Java string, this code will invoke its substring(int, int) method:

	jString := ...
	args := (JNIValueArray new: 2)
			intAt: 1 put: 5;
			intAt: 2 put: 10;
			yourself.
	subString := jString
			callObjectMethod: 'substring'
			signature: '(II)Ljava/lang/String;'
			withArguments: args.

Primitive types are added to the list with #booleanAtPut:, #byteAtPut:, and so on. The values are represented by the corresponding Smalltalk types according to the rules below. Object references are added with #objectAtPut:, and must either be a reference to a Java object, or nil. There are many examples of using JNIValueArrays in JNIPort, browse for references to the class.

A warning: JNIValueArray is a kind of ExternalStructure and does not protect references from the garbage collector. For instance, if you are passing a temporary Java string to a method, then you must not code it like this:

	args := (JNIValueArray new: 1)
			objectAt: 1 put: ('test' asJavaString: jvm);
			yourself.
	subString := jString
			callObjectMethod: 'setName'
			signature: '(Ljava/lang/String;)V'
			withArguments: args.

because the temporary Java string, created by ('test' asJavaString: jvm), would quite probably be garbage collected before it could be used. You have to ensure that there is a separate reference to any object stored in a JNIValueArray. For instance:

	tmpString := 'test' asJavaString: jvm.
	args := (JNIValueArray new: 1)
			objectAt: 1 put: tmpString;
			yourself.
	subString := jString
			callObjectMethod: 'setName'
			signature: '(Ljava/lang/String;)V'
			withArguments: args.

Another warning: JNI, and hence JNIPort, will allow you to call any method of any object or class, including private ones.

Representing Primitive Types

JNIPort uses Smalltalk's own Integers to represent Java's byte, short, int and, long types. Integer are are range-checked before being passed to Java. Note that the range of Java's byte is -128 to +127 (however converting between a Java byte[] array and a Smalltalk ByteArray will also do a signed/unsigned conversion).

JNIPort uses the Smalltalk class Float for Java's float and, double types. The Smalltalk representation has the same precision as Java's double.

JNIPort uses true and false for Java's boolean type.

Representing Java's 16-bit char type is problematical. In general, anywhere where a character is passed to Java, JNIPort will accept either a Smalltalk Character or an integer in the range 0 to 65535 (i.e. an unsigned 16-bit representation). When a character is passed from Java to Smalltalk, JNIPort represents it as a Character if possible, or as an Integer if not. This conversion is not always possible, so, for instance, converting a Java java.lang.String to a Smalltalk String will throw an exception if it contains any characters with code points over 255. (Unfortunately, this applies to UnicodeString as well.)

Java's null isn't a primitive type (in fact it's difficult to say just what it should be called in Java type system — it's very much a special case), but this is as good a place as any to mention that JNIPort always represents Java null by Smalltalk nil.

Introspection

All JavaObjects are able to reflect on their Java object or class. They know what methods, fields, and possibly constructors, it has or inherits. See the methods in the category 'reflection'. The class static for any class is able to provide more reflective details about its instances than they themselves can. See the same category in JavaStatic.

It may help to know that the objects that are answered by the reflection methods (wrappers for instances of java.lang.reflect.Method, .Field, and .Constructor) are able to work out their own signature string. The message is #jniSignature.

Java exceptions

When a Java method, or any other JNI operation, throws an exception, JNIPort catches it and rethrows it as a Smalltalk exception.

In more detail, the Java runtime throws a Java exception, which is a Java object inheriting from java.lang.lang.Throwable. JNIPort sees the exception object and wraps it in an instance wrapper as usual for any Java object; this will actually be a JavaLangThrowable. It then creates a new instance of JavaException (which is a non-resumable subclass of Exception via Error), and raises that exception with the Java object as its “tag”.

That means that uncaught Java exceptions are handled, by default, in the same way (a walkback if it's a development session) as uncaught Smalltalk errors. Also they can be caught and handled by a #on:do: expression that lists any of JavaException, Error, or Exception in its filters.

A higher degree of specificity is possible; the class JavaLangThrowable implements the #, and #handles: methods necessary to act as an exception filter too. Therefore, if you have specialised subclasses that correspond to Java exception classes, then you can use the wrapper classes in an #on:do: expression.

A still higher degree of specificity is also possible; the class statics themselves understand the filter protocol, so you can use them (the objects, not their classes) as filters too.


Class JavaInstance

Class Hierarchy highlighting JavaInstance

JavaInstance is one of the two immediate subclasses of JavaObject (the other is JavaStatic, which is described below). Its instances all stand, in one way or another, for a real Java object. There are three significant subclasses. The most important is JavaClassInstance whose instances wrap a JNI reference to a Java object, and which act as proxies for those objects. The other two classes, JavaInterfaceInstance and JavaNonVirtual take an existing JavaClassInstance and wrap it in an Adaptor that presents an alternative view of the underlying Java object as if it were a member of a different class. All three are described below.

Every instance of JavaInstance has a (shared) class static, corresponding to the object's Java class. The message #static will answer the class static.

All instances understand all the methods of java.lang.Object, exposed in the normal JNIPort naming scheme. I.e. they all understand:

#equals_Object: #notify_null #wait_long:
#getClass_null #notifyAll_null #wait_long:int:
#hashCode_null #toString_null #wait_null

Additionally: #equals:, #getClass, #hashCode, and #toString, are Smalltalk-style aliases for the corresponding Java methods (note that #toString answers a reference to a Java string).

Smalltalk equality (#=, and #hash) is defined on these object so that it corresponds to an identity comparison of the underlying Java objects


Class JavaClassInstance

Class Hierarchy highlighting JavaClassInstance

These objects wrap raw JNI references, manage their lifetimes, and act as proxies for the underlying Java object. They are, with class statics and the JVM object, the most important objects in the JNIPort system.

This class establishes a framework for such proxies. It has arbitrarily many subclasses (especially if ghost classes are turned on!) that are used to wrap instances of specific Java classes. Often, such custom wrapper classes would be derived from JavaLangObject, since all Java classes inherit from java.lang.Object, but that is not a requirement.

JNIPort provides a way for subclasses to declare that they correspond to some specific Java class (it is actually the #javaClassName method on the class-side, see the section on wrapper classes for more details). When JNIPort encounters a reference to a Java object of the corresponding class (or a subclass) it creates a proxy of the designated Smalltalk class. The idea is that the custom wrapper class will define helper methods that hide the need to send messages like #callObjectMethod:signature:withArguments: as described above. You can create these wrapper classes by hand, but that quickly becomes tedious, so the higher level of JNIPort provides two ways of automating the process. The Wrapper Wizard, writes forwarding methods for you, and ghost classes take the idea further and makes the process entirely dynamic and automatic.

Canonical References

In normal operation, JNIPort creates a new JavaClassInstance whenever it encounters a new JNI reference to the underlying Java object. That is, more than one instance may be acting as a proxy for the same Java object. Under most circumstances that works perfectly well, but sometime it may be necessary to ensure that a Java object is always represented by one specific proxy. This may be important, for instance, if the wrapper class adds per-instance state, or simply to allow fast comparisons using #==). For this reason, JNIPort provides the notion of a canonical proxy. If an instance has been told to #beCanonical then JNIPort will ensure that any subsequent references to the Java object will be represented by that specific instance. This is implemented using a weak set held by each class static; see the object lifecycle for more information. This feature can be turned on on a per-class basis as well, see JavaClassStatic>>haveCanonicalInstances, JavaClassStatic>>haveCanonicalInstancesByDefault, and JavaClassInstance class>>haveCanonicalInstancesByDefault.

Strings and Arrays

JNIPort comes with some standard classes for wrapping Java strings and arrays. These are all subclassed from JavaAggregate. Which, incidentally, is an example of the way that the hierarchy of wrapper classes does not need to follow the Java hierarchy, if there is some useful commonality that can be exploited. In this case the commonality is that they share code to give them a Smalltalk-style interface. For instance, all Java arrays and strings are wrapped in objects that understand the #at: message (which takes a 1-based index). Arrays also understand #at:put:, strings don't because Java's strings are immutable. They also understand the basics of the Collections protocols, such as #do: and #size.

There are various ways of creating arrays; you can use the class-side #new:jvm: of the corresponding wrapper class, or you can send #newArray: to the class static. For instance this code creates an array of 9 doubles and 10 java.lang.Strings:

	jvm := JVM current.
	doubleArray := JavaDoubleArray new: 9 jvm: jvm.
	stringClass := jvm findClass: #'java.lang.String'.
	stringArray := stringClass newArray: 10.

There are also several ways of creating Java strings. The easiest is to send #asJavaString: to a Smalltalk string:

	jvm := JVM current.
	javaString := 'Test!' asJavaString: jvm.

You can convert Java arrays into Smalltalk arrays with #asArray, and Java strings into Smalltalk strings with #asString. Note, however, that not all Java strings can be represented in Dolphin Smalltalk since Dolphin does not yet support 16-bit characters.

Java byte[] arrays and Smalltalk ByteArrays can be converted into each other with #asByteArray and #asJavaByteArray: respectively. The conversion also maps between Smalltalk's unsigned bytes and Java's signed ones.


Class JavaInterfaceInstance

Class Hierarchy highlighting JavaInterfaceInstance

The second subclass of JavaInstance can be used to represent Java objects that are known to implement some interface.

Java makes heavy use of interfaces; they are one of Java's best features, but they do cause a problem for wrapper classes. For instance, the Java interface java.util.Iterator defines two useful methods for looping over Java collections, but there are many Java classes which implement the interface and they are not related by inheritance. The problem is that, while it is easy to generate a wrapper for any one of those classes, you wouldn't want to have to duplicate the wrapper code for all the rest of them.

There are several workable solutions to the problem; JNIPort has one built in — interface instances. Roughly, the idea is that, just as for instance wrappers, you tell JNIPort that a certain wrapper class should be used for Java objects that implement a certain interface. It will then arrange to wrap those objects in instances of your class. However, the picture is different in one important aspect: JNIPort always wraps a reference to a Java object in the proxy class that is most appropriate given the Java object's class; it can't do that with interfaces. A Java object has exactly one class, but may implement several interfaces, so JNIPort can't follow the usual pattern in this case. The way that this is handled is that interface instances are more like Adaptors; they take an existing wrapper object (with its own class static and so on), and put it inside another object, with a different class static, that “knows” about the interface and how to invoke its methods.

These secondary wrapper objects are created by sending asInstanceOf: to an existing wrapper, where the parameter is the class static of the interface you are interested in. Alternatively the asA: method takes the Symbol name of the interface.

For instance, if you have a reference to a Java object that you know implements java.util.Iterator, but which doesn't have a special wrapper class of its own. Because it doesn't have a custom wrapper class, the instance will be wrapped in the fallback JavaLangObject, and not know anything about the methods of Iterator. You can send asA: #'java.util.Iterator' to it to get back an adaptor that not only understands #hasNext and #next, but is able to understand #do:. (This example requires the 'CU Java Additional Wrappers' package.)


Class JavaNonVirtual

Class Hierarchy highlighting JavaNonVirtual

The last useful subclass of JavaInstance is of minor importance, indeed it is more than slightly esoteric. It wouldn't exist at all except that it falls, almost for nothing, out of the implementation, and provides a way of exposing one of JNI's features that otherwise would have been hidden.

A JavaNonVirtual is an object that pairs a class static with an instance wrapper. It acts as if the underlying object were an instance of the Java class corresponding to the class static, but it bypasses Java's virtual method invocation.

For example, sending #super to any instance proxy will answer a JavaNonVirtual that pretends that the underlying Java object is really an instance of its superclass. Given a Java Point object, calling its toString() method will answer a Java string describing the point:

	class := JVM current findClass: #'java.awt.Point'.
	point := class new.
	point toString.
		"--> a JavaLangString(java.awt.Point[x=0,y=0])"

but sending #super to it will answer a different view of the same object that does not override the implementation of toString() that it inherits from java.lang.Object:

	point super toString.
		"--> a JavaLangString(java.awt.Point@0)"

Please note the non-virtual object will not be a member of any of the wrapper classes so, except for the methods of java.lang.Object, it can only be accessed using the low-level methods of JavaObject.


Class JavaPrimitiveInstance

Class Hierarchy highlighting JavaPrimitiveInstance

The final subclass of JavaInstance is used internally by JNIPort. No instances of JavaPrimitiveInstance are ever created.


Class JavaStatic

Class Hierarchy highlighting JavaStatic

JavaStatic is second of the two immediate subclasses of JavaObject; its instances stand for Java classes and interfaces. They are known as class statics.

Class statics provide access to the “static members” of a Java class in the same way that instance wrappers provide access to the non-static members. They also provide a way to call the constructors of a Java class.

There are subclasses of JavaStatic that correspond to Java classes, interfaces, and primitive types. Although this breakdown resembles that below JavaInstance, the parallel is misleading. The subclasses of JavaInstance are fundamentally different things with different roles, whereas the subclasses of JavaStatic are all very similar. In fact the breakdown is more to allow JNIPort to substitute polymorphism for runtime tests, than it is to reflect real semantic differences. One exception is that only the subclass JavaClassStatic defines the methods for calling Java constructors (since you cannot instantiate interfaces or primitive types).

Instances are always paired with a java.lang.Class object for that class or type. The class object has a JNIPort wrapper in the normal way, actually an instance of JavaLangClass. The class object exposes the fixed set of 30 (or more) methods that all instances of java.lang.Class understand. The class static understands the programmer-defined fields and methods of the Java class. Class objects know what class static they are paired with (and answer it to #classStatic); class statics know what class object they are paired with (and answer it to #classObject).

Each instance wrapper knows that it “belongs to” the class static that stands for its Java class. The message #static will answer the owner.

The other common way to find a class static is to look it up by name; the JVM object understands #findClass:, which takes the Symbol name of a Java class, interface, or type, and answers the corresponding class static. For instance:

	jvm := JVM current.
	jvm findClass: #'java.awt.Point'.
	jvm findClass: #'int'.
	jvm findClass: #'java.lang.String[]'.
	jvm findClass: #'double[][]'.

(Class statics can also be obtained by asking other <javaClassFinder>s, such as the new “supplementary classloaders”, but that feature is still experimental, and is not discussed further here.)

Class statics know how to wrap JNI references in proxies of the correct class (this is their most important role in the implementation of JNIPort). They work with the class registry to maintain the mapping from Java classes to Smalltalk wrapper classes, See the Players section for more detail on this.

Class statics know about their place in the Java class hierarchy. There are a number of methods in the category 'Java class hierarchy' that provide access to this; #javaSuperclass is the most basic. Note that you can only ask for the known subclasses, neither JNIPort nor the Java runtime itself can possibly determine how many more subclasses may later be loaded (or even created).

Class statics also know how to create Java arrays of the right type to hold values of their Java class. For instance the class static for Java's int primitive type knows how to create int[] arrays:

	jvm := JVM current.
	intClass := jvm findClass: #'int'.
	array := intClass newArray: 10.
	     "or"
	array := intClass newArrayWithAll: #( 1 2 3 4).

See the methods in category 'arrays' for more ways to create arrays.

The subclass JavaClassStatic has methods for invoking the Java class's constructors. The are in category 'instance creation'. Calling a constructor is like calling a Java method, except that there is no method name. You have to supply a signature string and a JNIValueArray to hold any parameters.

JavaStatic is intended to be a superclass for custom subclasses that wrap the class-side of a Java class, in very much the same way that custom subclasses of JavaInstance wrap the instance-side. JNIPort uses the same kind of mechanism for associating a custom subclass with some specific Java class (and its subclasses). Similarly the Wrapper Wizard and ghost classes provide ways of automating the process. See the Wrapper Classes section for more detail. JNIPort has many examples of this.


Copyright © Chris Uppal, 2003-2005

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