GCJ & the Cygnus Native Interface

The GNU Compiler for the Java Programming Language is a GCC front-end for Java.


July 01, 2004
URL:http://www.drdobbs.com/jvm/gcj-the-cygnus-native-interface/184405736

July, 2004: GCJ & the Cygnus Native Interface

Coding native methods in C++

Gene works at TimeSys where he focuses on embedded Linux. He can be contacted at gene.sallyverizon.net.


The GNU Compiler for the Java Programming Language (GCJ) is a GCC front-end for Java that has been a part of the GCC distribution since Version 2.95 (http://www.gnu.org/software/gcc/java/). With GCJ, you can compile Java code into machine language for the target GCC machine. This lets you take advantage of Java's benefits without the runtime overhead—both in terms of memory and performance—usually associated with the VM used to execute Java code.

Like C/C++, Java is more like an environment than a language. While the language itself provides a great deal of power, the libraries included with these languages provide the functionality necessary to create useful applications without substantial additional work. GCJ provides the library functionality of JDK 1.2 as part of the normal distribution.

Most full installations of GCC include the GCJ compiler. The easiest way to check if you have GCJ installed on your system is by using the which command:

$ which gcj
/usr/bin/gcj

If you get a path, you have the binary on your system's $PATH. In the unlikely event you have the binary on your system but not on your path, you can search using:

$ find / -name gcj
/usr/bin/gcj

Once you locate GCJ and make sure it's on your path, you can begin using the tool for the examples in this article.

If it's not installed, the easiest way to get GCJ is by locating the compiled binaries and installing them on your system. If you're running Linux, you can use the RPM or apt-get tools to get the most recent version of GCC (currently 3.2).

For more information on the mechanics of getting GCC compiled and installed, see books such as The Definitive Guide to GCC by Kurt Wall and William Von Hagen, or GCC: The Complete Reference by Arthur Griffith.

Native Interfaces

The Java language designers built into the specification the capability to use code compiled specifically for the machine where the program is running. This language feature bristles Java purists, as it runs against the write-once/run-anywhere philosophy of the language. With the native interface, users can access libraries or system services—graphics services, device drivers, or other system-specific services—not accessible via Java's standard class libraries. Depending on the environment and requirements of the application, certain parts of the system may be written in a lower level language to garner performance advantages.

At this point, you may be wondering who cares about write-once/run-anywhere because this concept is broken by GCJ itself, as it produces machine code for a specific platform. However, Java has more to offer than just write-once/run-anywhere. Java's clean class/interface/inheritance model is a great language feature. Implementers benefit from the smartly implemented garbage collection system, too. Furthermore, the standard class library offers a great starting point for application development.

Java Native Interface

Understanding that users would want or need to implement in lower level languages from within Java, the designers incorporated the Java Native Interface (JNI) into the language. JNI offers an effective way for users to map lower level languages into a Java class by letting users mark certain methods in a class as native.

Telling the compiler that a method was native meant that it would not allow a body, as this would be implemented in a platform-specific way. Java uses the information to create function declarations that the user would then implement. JNI also supplied an API for invoking methods and inspecting and changing properties.

Creating code for JNI is a multistep process:

  1. Write Java class code, storing it, in this case, in the file CSimpleJNI.java; see Example 1(a).
  2. Compile the source Java code into object code (bytecode); see Example 1(b).
  3. Extract native method prototypes; see Example 1(c).
  4. Write the C code; see Example 1(d).

As you can see, JNI expects you to code in C. While there's nothing wrong with C, most Java engineers have become accustomed to the object-oriented features of the language, and C is a bit of a mental shift.

GCJ's CNI

Recognizing that C++ may offer a better match for Java engineers, the GCJ developers provide the capability to code native methods in C++ instead of C. With the Cygnus Native Interface (CNI), you can take advantage of the language features of C/C++ when creating native methods.

CNI (http://gcc.gnu.org/java/papers/cni/t1.html) contains well thought out constructs for method invocation, property access, exception handling, and synchronization. Using these constructs instead of their JNI procedural equivalents means you can get the same work done using less code. With CNI and C++, the interface between C++ and Java is much smoother and natural.

If you want to use CNI, you'd still follow pretty much the same steps as JNI to create the method prototypes; however, the step that extracts the native method prototypes emits a header file containing a class definition rather than function prototypes. For instance, using gcjh to create a CNI header file for the previous example yields the C++ code in Example 2(a). The code necessary to implement nativeMethod looks like Example 2(b).

Compared to JNI

As you can see, CNI produces an interface into Java with C++ code. So instead of generating a group of functions containing the class and method name, CNI generates C++ code declaring a class with the native methods left empty for you to implement. However, more than just the language has changed, as the philosophy behind how you interact with Java from a native perspective is much different, in a good, geeky kind of way.

The first thing to get your head around is the notion that the binding between the CNI class and the Java class is much stronger. Because JNI was implemented for a language that did not support any object-oriented concepts, handling the differences between the languages required extra code. When working in CNI, you have the feeling of working in situ with the Java class; gone is the messy code necessary to bridge the two languages.

Using CNI

To invoke a method, users must look up the class identifier, find the method ID, and then invoke the method, usually via one of the Call<type>Method(...) helper functions. Listing One is replaced by simply calling getValue(), the method defined in the class. This alone should bring smiles to JNI users, as the JNI semantics of method invocation defers all checking to runtime. So changing a method's name or signature without the same change in the C code results in a successful compilation, but failures when running the program. Although all good engineers check the error conditions for method and report errors at runtime, it's still better to catch changes like this sooner rather than later.

Exception handling occurs using the native exception handling in the C++ runtime, both for throwing and catching exceptions. For example, throwing an exception with JNI means looking up the class of the exception, and using a JNI-supplied helper function to create and immediately throw the exception. In Listing Two, for instance, the brittleness in the code appears right after the ThrowNew, where users must remember to return before doing anything else, if the exception is to be caught in the enclosing Java code. While experienced JNI programmers know this in their sleep, newer engineers may make the mistake of writing code with subtle defects.

For CNI users, throwing an exception means just doing: throw new CThrowMe(); and some combination of the C++ runtime and CNI does the rest.

Catching an exception is just as easy; exceptions thrown by classes percolate through as regular C++ exceptions. Consequently, you can catch them just by wrapping the code that may throw an exception in a try/catch block in your CNI code; see Listing Three. If the native code does not catch the exception, the call stack unwinds past the CNI code and the exception is caught in Java code or results in an unhandled exception being emitted by the Java runtime.

Contrast this with the JNI mechanism where you must poll the VM to determine if an exception has been raised (see Listing Four).

There are two potential problems to keep in mind: First, if you don't check to see if the exception has been raised, you'll never know if one was raised in the first place. Second, if you forget to call ExceptionClear, the exception may get processed twice if the Java code contains a try/catch block for the exception. Using IsInstanceOf to determine the type of exception could yield some unwieldy code, although most engineers create a helper function that hides this implementation detail. Aside from the mechanical issues of handing the exception, the author and maintainer must know enough about the code to check for exceptions in a timely manner, so the system is not placed in an inconsistent state.

Java implements synchronization through monitors attached to each object. The same principle applies for objects in CNI. To achieve the effect of a Java synchronize block, you need to write something like JvSynchronize sync (this);, which does the underlying mechanics to acquire the lock on the monitor when its constructor runs and the destructor releases the lock.

Emulating a synchronized block using JNI requires very little work—just call a function with the current object pointer as the parameter and use an unlock when done. You create a critical section with CNI by instantiating a JvSynchronize object or using the JvMonitorEnter/JvMonitorExit function calls.

Caveats and Limitations

While CNI provides great functionality, it does have some limitations and other rough edges:

Conclusion

CNI offers a great way to program in native code when working with the GCJ compiler front end. The way CNI maps your Java classes into C++ classes relieves you from the drudgery typically associated with JNI-coded interfaces and lets you use the constructs offered by C++ in concert with your Java code. If you have the chance, give this technology a try. You won't be disappointed.

DDJ



Listing One

(*env)->GetObjectClass(env, thisObj);
mthdID = (*env)->GetMethodID(env, clsID, "getValue", "()I");
jiValue = (*env)->CallIntMethod(env, thisObj, mthdID);
Back to article


Listing Two
jclass rteClass;
jthrowable rteInstance;
rteClass = (*env)->FindClass(env, "java/lang/RuntimeException");
(*env)->ThrowNew(env, rteClass, "An exception thrown from JNI");
/* party members in good standing should return here */
Back to article


Listing Three
try {
  // stuff
}
catch (CThrowMe e) {
  // do something !
}
Back to article


Listing Four
  if ((rteException = (*env)->ExceptionOccurred(env)) != NULL) {
    if ((*env)->IsInstanceOf(env, rteException, rteClass) == JNI_TRUE) {
      printf("Caught the exception in native code\n");
      (*env)->ExceptionClear(env);
    }
  }
Back to article

July, 2004: GCJ & the Cygnus Native Interface

(a)

public class CSimpleJNI {
    private int nValue;
    public int getValue() {
    return nValue;
    }
    public void putValue(int newValue) {
    nValue = newValue;
    }
    public int transformValue(int someValue) {
    putValue(getValue() + someValue);
    return getValue();
    }
    public native void nativeMethod(int param);
}


(b)
gcj -C CSimpleJNI.java


(c)
gcjh -jni CSimpleJNI


(d)
#include "CSimpleJNI.h"
void Java_CSimpleJNI_nativeMethod(JNIEnv *env, jobject thisObj, jint param1)
{
  jclass clsID;
  jmethodID mthdID;
  jmethodID putValueID;
  jint jiValue;
  (*env)->GetObjectClass(env, thisObj);
  mthdID = (*env)->GetMethodID(env, clsID, "getValue", "()I");
  jiValue = (*env)->CallIntMethod(env, thisObj, mthdID);
  /* in real life, this would complex code */
  jiValue++;
  mthdID = (*env)->GetMethodID(env, clsID, "putValue", "(I)V");
  (*env)->CallVoidMethod(env, thisObj, mthdID, jiValue);
}

Example 1: (a) Java class code stored in CSimpleJNI.java; (b) source Java code compiled into object code (bytecode); (c) extracting native method prototypes; (d) the C code.

July, 2004: GCJ & the Cygnus Native Interface

(a)

extern "Java"
{
  class CSimpleJNI;
};
class ::CSimpleJNI : public ::java::lang::Object
{
public:
  virtual jint getValue () { return nValue; }
  virtual void putValue (jint);
  virtual jint transformValue (jint);
  virtual void nativeMethod (jint);
  CSimpleJNI ();
private:
  jint nValue;
public:
  static ::java::lang::Class class$;
};


(b)
void CSimpleJNI::nativeMethod(jint param1) {
  jint value = getValue();
  // complex code omitted
  value++;
  putValue(value);
}

Example 2: (a) Using gcjh to create a CNI header file for the previous example yields this C++ code; (b) the code necessary to implement nativeMethod.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.