A Design for Java Support in Gnome

A design goal for Gnome is to support multiple programming languages. So far, Java has been conspicuously absent. Here are my suggestions for a plan of attack to fix this. I will start out by discussing Java support in a portable manner (i.e. independent of Java VM), and then discuss whatmore we want in the context of a free Java implementation - specifically one design for GCJ.

Note that I am not a Gnome expert, so I am sure I am making some mistakes. This is meant to start a discussion.

I will express native methods using CNI ("Compiled Native Interface"), rather than JNI ("Java Native Interface") because I am more familiar with CNI, and because it is easier to read.

This initial draft is written by Per Bothner per@bothner.com, with input from Miguel de Icaza, Paul Fisher, and others. However, I am responsible for it contents. Note that there is no implication that I or anyone else will implement these ideas. The purpose of this document is to throw out some ideas; if people agree the ideas are good, hopefully someone will be inspired to implement them.

Java classes from Gnome classes

Gtk/Gnome has its own class hierarchy, based on GtkObject. It seems reasonable that we should map this to a parallel Java class hierarchy. I.e. for every Gtk class, there should be a corresponding Java class. (Not necessarily vice versa - there is no obvious need for every Java class to also be a Gtk class.)

It also seems obvious that we want to generate the Java classes automatically from the Gtk classes, or at least semi-automatically. These Java classes can be generated either using Gtk introspection, or "descriptor files" (??? I heard about some files that have a Scheme-like syntax, generated automatically from gtk header files).

The tools that generate Java classes also need to generate code for native methods.

For the sake of argument, assume we use a Java hierarchy where GtkObject is mapped to the Java class gnu.gtk.Object, and GtkButton is mapped to gnu.gtk.Button. (Why gnu.gtk and not org.gnu.gtk? See this rationale.)

A portable mapping requires that Java objects refer to Gtk objects:

package gnu.gtk;
public class Button extends gnu.gtk.Bin
{
  long peer;
  ... methods ...;
}

Native code would cast the peer field: ((GtkButton*) button_instance->peer).

Object Arguments

Object arguments should be mapped to Java getter/setter methods:

public class Container extends ...
{
  public native int getBorderWidth();
  public native void setBorderWidth(int width);
}
The native code (CNI version):
void gnu::gtk::Container::setBorderWidth(int width)
{
  gtk_container_set_border_width ((GtkContainer*) this->peer, width);
}

Signals

Gtk signals (events) are handled using callback functions, while Java events are handled using event listener objects. The most natural mapping seems to be as follows.

For each signal (type), we (automatically) create a Java listener interface. Thus for the GtkButton "clicked" signal, we create:

public ButtonClickedListener
{
  public void clicked ();
}
Then do:
public class Button extends Bin
{
  ...
  public native void addClickedListener(ButtonClickedListener listener);
  public native void removeClickedListener(ButtonClickedListener listener);
}
gnu::gtk::Button::addClickedListener(ButtonClickedListener* listener)
{
  gtk_signal_connect(GTK_OBJECT(this->peer),
                      "clicked",
                      GTK_SIGNAL_FUNC(button_click_cb),
                      listener)
}

static void 
button_click_cb(GtkWidget* w, gpointer data)
{
  ((ButtonClickedListener*) data)->clicked();
}

In reality, the clicked method would take arguments giving more specifics about the emitted signal. For example, the method would probably want to know which Button object emitted the signal. I don't have enough knowledge to be more specific. Note that passing the Button object means converting the GtkWidget reference to a Java Button object. This may require creating a new Button wrapper on each button_click_cb. An alternative is to require that if the ButtonClickedListener's clicked method needs to access the Button instance, it should save a copy when the listener is created.

AWT Peers

The Java-wrapper-classes so far discussed provide a convenient way to do Gtk/Gnome programming in Java. But we also want to Java programming - i.e. write portable Java code that uses Gnome widgets. The classic approach is to provide a set a "peer" classes that implement the interfaces in java.awt.peer.

One elegant approach would be to provide a way to merge the automatically-generated Java-wrapper classes with the manually-written extra methods needed to generated the peer interfaces. Thus we might end up with:

public class Button extends Bin implements java.awt.peer.ButtonPeer
{
  ...;
  public native void setLabel(String label);
}

On the other hand, given that modern Java (and Swing in particular) has (mostly) moved away from using peers, this may not be the best approach. It might be easier to just implement the awt classes directly on top the gnu.gtk wrapper classes, without bothering about the peer interfaces. This especially makes sense if you (re-)implement the Swing widget classes, since in most (all?) cases the Swing widgets provide a superset of the functionality of the corresponding awt classes. I.e. we could implement the Swing widgets, and then implement the old awt widgets using the Swing widgets. In that case, there would be relatively little need for the peer classes. We might still want ComponentPeer, ContainerPeer, WindowPeer but the rest can be implemented as "pure Java".

Merging Java Objects

When using generic Java, we have no choice but to use wrapper objects and wrapper classes: A gnu.gtk.Button is a separate object in memory from the corresponding GtkButton object. There is a forwarding pointer from the gnu.gtk.Button to the corresponding GtkButton object, but no obvious way to go back, which may cause some performance loss. It also seems wastefull to allocate all these Java wrapper objects that don't actually do anything.

It would be very appealing to combine the Java wrappers and the gtk objects into a single object. The problem is that the Java run-time expects a specific Object header - and the Gtk run-time expects a GtkObject header. Trying to create a single merged header representation that works for both Java and Gtk would be very difficult.

However, there is a trick we can use, inspired by C++ multiple inheritance: We embed the Gtk object in a Java Object. We can express this in C++ using CNI:

class gnu::gtk::Button : public java::lang::Object, public GtkButton
{
  ...
}
or in pseudo-Java:
package gnu.gtk;
public class Button extends java.lang.Object
{
  /* GtkObject fields */
  GtkObjectClass klass;
  int flags;
  int refCount;
  GData objectData;
  ...
  /* GtkBin fields */
  ...
  /* GtkButton fields */
  Object child;
  int button_state; // in_button, button_down, relief
}

The above actually doesn't work, for various reasons. One problem is machine-specific alignment and field sizes. What we can do instead:

package gnu.gtk;
public class Button extends Bin
{
  private int field1;
  ...
  private int fieldN;
}
assuming Bin is the superclass of Button and where N is ((sizeof(GtkButton) - sizeof(GtkBin) + 3)/4). (You may also need to add some extra for alignment, in case the gtk object needs more than 32-bit alignment.) This looks ugly, but I think it is actually fairly clean. No code actually references the field1 ... fieldN, as their only purpose is to make sure an Button instance has enough space that it can embed a GtkButton.

For most Java implementations, converting between Java instance (such as a gnu.gtk.Button) and the Gtk instance (such as GtkButton) is a simple matter of adding a constant offset. Converting back, is just a matter of subtracting the offset. Call this the JAVA_OFFSET. (However, there is no portable way in JNI to figure out this offset. In fact, some Java implementations use an extra level of indirection, and for those this embedding trick may not work.)

Note that a Gtk object has no fields that can be accessed from Java. All fields have to be accessed using methods that are written in native (C or C++) code. If one of these fields is a pointer to some other GtkObject object, then we might want to have a method that takes the Java object, add the JAVA_OFFSET, extract the field, and then subtract JAVA_OFFSET to get a Java object reference.

Object creation and deletion

When we create a Gtk instance, we now we actually want to do is create te Java wrapper instance. For example, we want

gtk_button_new()
to be equivalent to:
(GtkWidget*) ((char*) new gnu::gtk::Button() + JAVA_OFFSET)

The way to do that is to put a hook in gtk_type_new: If there is a Java class "registered" as being equivalent to the Gtk type, then allocate an instance of the Java class.

We also need to make sure the Java object does not get prematurely de-allocated. Also, if the Java implementation uses a copying collector, we need to prevent the Java wrapper instance from being moved.

Swing

Swing is a huge set of classes. Most programmers use very little of it. It would be nice to have a free re-implementation of the most commonly used parts of Swing, at least.

Swing has a "pluggable-look-and-feel" - which is another word for themeability. I don't think Swing "pluggable-look-and-feel" is important. Instead, we can write the Swing widgets so they use the Gtk theme engine. Even better would be an implementation of the Java pluggable-look-and-feel api that is implemented using the Gtk theme engine. I don't know how difficult this would be.

More central to Swing is the idea of separating the "model" (data) from the "view" (widget). This is actually a very valuable idea, I think. For example, JEmacs depends on separating out buffers (model) and windows (view). I gather Gnome doesn't really have much support for this separation. Of course it is always possible to implement the model/view-separation in pure Java, and just use Gnome for display. However, that is more expensive than it should be, since it seems duplicating the data (both in the Java model and in the Gtk data structures). This may be difficult to fix - burt could be done incrementally, widget by widget.


Per Bothner
Last modified: Mon Jul 10 21:23:02 PDT 2000