Metagnostic
home

JNIPort for Dolphin Smalltalk

Overview

Contents

Players

Layers

Examples

Configuration

InFAQ

Changes

Licence


Back to Goodies

Calling from Java into Smalltalk

JNIPort has a limited ability to allow Java code to callback into Smalltalk. I must emphasise that the operative word here is limited; the feature is not suitable for creating a tight integration between the two worlds because:

  • It requires much more effort from the programmer than I'm happy with.
  • It is slow.

However, it is better than nothing.

An example of using it to display the Smalltalk class hierarchy in a Swing JTree component is here.


The Callback Queue

Java's “native”, as it were, way of calling out from Java to another language is to use “native method”, which are like normal Java methods except that a programmer uses JNI to implement the method body, rather than writing it in Java. Because of the problem with threads (which you should read if you are thinking of using callbacks at all), JNIPort does not directly support this way of working (although it is used internally by JNIPort). Instead it provides Java classes that maintain a queue of Requests and Notifications; you create a request or notification (collectively a callback, or callback object) and place it on the queue, Dolphin takes requests off the queue and processes them, then returns the answer (if any is required) to the caller. The section on threads talks about the implementation of this queue, this section is about how to use it.

The section on the problem with threads provides much more detail, but it is important to realise that the way that the queue works depends crucially on the operating system thread that is using it. Dolphin executes all Smalltalk code on one OS thread, which I call the Smalltalk thread, and if that thread is executing Java code then no Smalltalk code can run at all. This has implications for anyone programming with callbacks. One consequence is that you have to be careful about deadlocks (the threads section has the details), another is that callbacks made from the Smalltalk thread are handled differently from those made by other threads.

If Java code running on some other thread issues a callback, then the callback object is placed on the queue and then that thread either waits for a response, or just goes about its other business. Dolphin, running on the Smalltalk thread, will see the request, process it, and (optionally) return an answer to the caller. On the other hand, if the Java code was running on the Smalltalk thread in the first place, then it places the callback on the queue in the normal way, and then calls back (a JNI callback) into Dolphin where all the requests on the requests on the queue are handled before control returns to the original caller. (So the caller is blocked, as far as Java is concerned, even if it did not want to wait for an answer.)


Callback Objects

Callback objects are instances of the Java classes org.metagnostic.jniport.DolphinRequest and .DolphinNotification. They all have a tag, an originator, and a parameter. The tag is a Java object (of any class) that uniquely identifies the callback; it us used by the callback registry (see below) to find the corresponding Smalltalk handler (I tend to use Strings for tags since they are easy to identify in logs or the debugger), the tag must not be null. The originator can be any object, or null; it is conventionally used to identify the this of the callback. The parameter likewise can be any object or null, and is conventionally used to pass any arguments (possibly as an array) to the callback. JNIPort makes no use of the originator and tag, they are passed through to the handler code without interpretation.

You supply the tag, originator, and parameter when you create a notification object (there are also a bunch of convenience constructors that package arguments up into the parameter object in various ways). You send a notification by invoking its send() method. If Dolphin has initialised the callback queue then the object will be placed on the queue and send() will return immediately (subject to the above caveat about Java code executing on the Smalltalk thread). If Dolphin has not initialised the queue then it will throw an org.metagnostic.jniport.RequestNotHandedException.

The pattern is similar for requests, the important difference is that Dolphin will supply either a return value or will record the fact that an exception was thrown from the handler. The simplest way to get the return value is to call the request object's value() method. It will block until Dolphin has handled the request and then either return the Object returned by the handler, or will throw an exception. The exception can be any of:

  • A RequestNotHandedException indicating that Dolphin was not listening for requests on the queue.

  • A RequestNotHandedException indicating that no handler had been set for the callback's tag.

  • Any Java java.lang.Throwable that was thrown from the Smalltalk handler code (and not caught in the handler).

  • A RequestNotHandedException indicating that the handler threw an uncaught Smalltalk exception.

  • A RequestNotHandedException indicating that the handler did not answer a valid Java object or nil (this is really a special case of the previous case, but it's worth mentioning explicitly).

You can, instead, call the request object's startEvaluation() method. This puts the request on the queue but does not wait for an answer from Dolphin (unless, as mentioned above, it is running on the Smalltalk thread). You can then call value() later which will only then block until Dolphin has processed the request (it may already have done so, in which case there is no blocking). Or you can use accessor methods like getException(), and getReturnValue(). These also block until the answer from Dolphin is available, but they do not throw exceptions.


The Callback Registry

Please note: I am still unhappy with the details of how handlers are set and invoked — expect changes in future versions of JNIPort.

On the other side of the wall, in Smalltalk space, Java callbacks are managed by the callback registry. It takes callback objects off the queue, and then uses each request's tag to find the corresponding handler. The handler is an object that understands #value:value:. The callback registry looks up the handler in a lookup table keyed by the callback's tag (it's an identity comparison of Java objects) and then evaluates the handler passing in the callback's originator and parameter as arguments.

If the callback was a Notification, then the handler is evaluated in a context where all Smalltalk or Java exceptions are trapped and ignored. The result of evaluating the handler is also discarded.

If the callback was a request then the handler is evaluated in a context where all Smalltalk or Java exceptions are trapped and recorded in the request object. If no exception is thrown, then the result of evaluating the handler is recorded in the request. This must be a Java object or nil (i.e. Java's null).

Once the handler has returned, JNIPort notifies the callback object that it is now “complete” which wakes up any Java thread that was blocked waiting for the result.

You find the callback registry by sending callbackRegistry to the JVM object. Handlers are set by sending #setCallback:handler:, and removed with #clearCallback:. There are also some higher-level methods in the 'operations' category of class JavaCallbackRegistry that are intended to make it easier to arrange various kinds of handlers.


Java Events

Fortunately Java API's do not often make very much use of coding patterns that would require callbacks from Java to Smalltalk; for example Java's idiomatic pattern of iteration uses external iterators (java.util.Iterator and java.util.Enumeration) rather than internal ones like Smalltalk's idiomatic use of #do:.

There is one glaring exception to this tendency: AWT/Swing “events”. Java (since version 1.4) has provided some support for handling events without having to code explicit subclasses (like inner classes, for instance), and JNIPort builds on that to provide a way to translate Java events into Smalltalk events.

This part of Java is new enough to warrant a little background explanation.

In Java, an event is represented by a method in an interface. For instance the window closing event is represented by the windowClosing() method in interface java.awt.event.WindowListener. The window maintains a list of “listeners” that all implement WindowListener, and when it closes it calls each one's windowClosing() method. Additional data about the event is passed as a parameter that is some subclass of java.util.EventObject.

The normal way that some other object “receives” the event is that it (or one of its inner-class objects) implements the WindowListener interface, and overrides windowClosing() with its own code to handle the event. That works fairly well in most cases, but it does mean that creating a handler always requires some kind of subclassing.

Java 1.4 introduced a new way of creating handlers that is less inflexible, Proxies and Invocation Handlers. A proxy is an adaptor that is created to implement some interface by forwarding the event methods to an invocation handler. The invocation handler is a object with the single method, invoke(), and the proxy calls it for every event method (passing parameters that identify the event and its arguments). You create a proxy by identifying one or more interfaces that you want it to implement, and then the Java runtime creates a stub class on the fly that implements the methods of the interfaces by forwarding them to an invocation handler. (I'm simplifying quite a bit, see java.lang.reflect.InvocationHandler and .Proxy for the details).

JNIPort uses this to translate Java events into Smalltalk events. To observe (in Smalltalk) events generated by some Java object, you first find the event method represented as a java.lang.reflect.Method. Probably the easiest way to do this is to look in the #abstractMethods list of the interface's class static; that will find a JavaLangReflectMethod instance that wraps the Java reflection object.

Next you ask it for an event forwarder. JNIPort uses the new Java features to create a listener for the event (i.e. an object implementing the interface) which forwards the event data (the java.util.EventObject) to Dolphin. You create the forwarder by sending #eventForwarder: to the method object. The parameter is the event Symbol that is to be triggered in Smalltalk space. The alternative form, #asynchronousEventForwarder:, is identical except that the data is passed in a Notification instead of a Request.

A side effect of #eventForwarder: is that it sets up a handler in the callback registry for the event callback. When the handler sees the callback, it retrieves the source of the event (which is passed as the callback's originator) and the EventObject (which is passed as the parameter), and triggers the event Symbol off the originator. The EventObject is passed as the single argument to the event.

Please note that the event's source must be represented by a canonical Smalltalk object, or else the event will be triggered off a newly created reference to the Java object each time, not from whatever one your code is observing.

The last step is to add the forwarder object to the list of listeners of any Java objects that you are interested in. Once it has been added (you can and should re-use it for all the event sources) it will translate the event from Java space into Smalltalk space.

For example, say we have references to two Java objects, both of which generate the event onPinged(), which is defined in interface PingListener. Following Java conventions, they will both have a method addPingListener() that takes an object implementing PingListener and adds it to their lists of observers.

In Smalltalk we can create a forwarder for the ping event by finding the right method:

	jvm := JVM current.
	interface := jvm findClass: #PingListener.
	event := interface abstractMethods detect:
			[:each | each name = 'onPinged'].

and asking it for an event forwarder:

	forwarder := event eventForwarder: #pinged:.

We can then add the forwarder to both source objects' lists of listeners:

	source1 addPingListener_PingListener: forwarder.
	source2 addPingListener_PingListener: forwarder.

Which is enough to forward events into Smalltalk space. However we have to ensure that the Smalltalk events are triggered off the right wrapper objects:

	source1 beCanonical.
	source2 beCanonical.

before we can start observing the triggered events:

	source1
		when: #pinged:
		send: #onSource1Pinged:
		to: "... wherever ...".
	source2
		when: #pinged:
		send: #onSource2Pinged:
		to: "... wherever ...".

One warning: JNIPort uses the method object as the tag to identify the callback, which means that you can't have more than one forwarder for the same event. I can't think of any reason why you'd need more than one forwarder (since they all do the same thing), but if I ever do then I may remove the restriction.


Copyright © Chris Uppal, 2003-2005

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