Chapter 5. Native Code and Fragment Bundles

OSGi has support for loading native code in an application, which may be used to provide access to platform-specific functionality or for performance reasons. This chapter will present an overview of the Java Native Interface, and then cover how native code can be bundled in with plug-ins. It will also cover how fragment bundles can provide extensions to bundles in an OSGi runtime, such as native code libraries and Java patches.

Native code and Eclipse

The Java Native Interface (JNI) is a standard way in which any Java program can interact with native code. The process for working with native code can be summarized as follows:

  1. Write a Java class with a native method.
  2. Compile the Java class as normal.
  3. Run javah with the class name, which generates a header stub.
  4. Write the native C function and export it with the given function signature.
  5. Compile the code into a dynamically linked library.
  6. Load the library into the runtime with System.loadLibrary.
  7. Execute the native method as normal.

The name of the library is dependent on the operating system; some call the library name.dll, some call it libname.so, and others libname.dylib. However, Java just uses the real portion of the library name; so, all three platforms use the same Java code, System.loadLibrary("name"), to load the library.

Creating a simple native library

For the purpose of this chapter, a native library will be created to perform a simple Maths operation class that adds two numbers. Although this could be easily implemented in Java, it is used to demonstrate the principles of how native code works.

Create a new plug-in project called com.packtpub.e4.advanced.c and, inside that, a class called Maths with a package named for the project. In it, create a native method called add, which takes two int arguments and returns an int. To ensure the native library is loaded, add a static initializer block that calls System.loadLibrary:

package com.packtpub.e4.advanced.c;
public class Maths {
  static {
    System.loadLibrary("maths");
  }
}

Now compile this class and then run javah from the command line, specifying the fully qualified class name:

$ javah -d native -classpath bin com.packtpub.e4.advanced.c.Maths
$ ls native/
com_packtpub_e4_advanced_c_Maths.h

Now implement a C function that has the same signature as defined in the header:

#include "com_packtpub_e4_advanced_c_Maths.h"
JNIEXPORT jint JNICALL Java_com_packtpub_e4_advanced_c_Maths_add
 (JNIEnv *env, jclass c, jint a, jint b) {
  return a + b;
}

Note

The function signature is generated by javah, and uses macros such as JNIEXPORT and JNICALL, which are platform-specific #define statements in case any additional compiler or platform flags are required to register these as exported symbols. The name of the function is calculated from the fully qualified name of the class and method name, using underscores instead of dots. Each JNI function also has a pointer to JNIEnv, which is a handle to the JVM, as well as jclass (for static methods) or jobject (for instance methods).

The final step is to compile it into a platform-specific dynamic link library. The process differs from one operating system to another. This will typically include a path to the location of the JNI header files and output flags that say what the resulting file should be called.

Mac OS X

The Mac OS X developer tools are located either in /Developer or under /Applications/Xcode.app/Contents/Developer. The JNI header is located under the JavaVM.framework/Headers directory, so an include path -I needs to be specified to locate the files. Adjust the path as necessary when building this code, or use xcrun --show-sdk-path to find out where the SDK folder is located:

$ xcrun --show-sdk-path --sdk macosx10.9
/Applications/Xcode.app/Contents/Developer/Platforms
/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk

The -dynamiclib option is used to generate a dynamic linked library, which will allow it to be loaded into the Java runtime.

The output filename is declared with -o and, in order to load it into a Java runtime, must be of the form lib<name>.dylib.

The -arch i386 -arch x86_64 compiler flags generate a universal binary, which is a combination of both 32-bit and 64-bit code in the same library. OS X is the only major operating system to support multi-architecture builds by default.

The following command can therefore be used to build the library:

$ clang 
  com_packtpub_e4_advanced_c_Maths.c
  -dynamiclib
  -o libmaths.dylib 
  -I /Applications/Xcode.app/Contents/Developer/Platforms
     /MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk
     /System/Library/Frameworks/JavaVM.framework/Headers
  -arch i386
  -arch x86_64

This results in a file named libmaths.dylib.

Note

On OS X, the dylib extension is used for dynamic libraries, and conventionally, they have a lib prefix.

Linux

Linux distributions currently use GCC, although it may not be installed by default. Consult the operating system's package manager to determine where and how to install it if it is missing. (On Ubuntu and Debian, this will be apt-get install gcc, and on Red Hat and derivatives, yum install gcc.)

To build a library with gcc, the -I flag tells the compiler where to find the include files, and -shared tells it to create a library. The -o flag tells the compiler what to call the output:

$ gcc 
  com_packtpub_e4_advanced_c_Maths.c
  -shared
  -o libmaths.so 
  -I /usr/include/java

This results in a file named libmaths.so.

Note

On Linux, the so extension is used for shared object libraries, and conventionally, they have a lib prefix.

Note that depending on the operating system, the installation of the JDK (and therefore its include files) may be located elsewhere. For example, on Debian, this will be located under /usr/lib/jvm.

Tip

To find the correct location, run find /usr -name jni.h and see which directory is reported:

$ find /usr -name jni.h
/usr/lib/jvm/java-7-openjdk-i386/include/jni.h

Windows

Windows doesn't come with a compiler by default, although there is a Visual Studio Express version that is available at no charge.

The Windows Studio Express download ships with a C compiler and linker called cl. Options can be specified with / or – and are interchangeable depending on preference. The -LD option tells the compiler to generate a dynamic link library, and the -Fe option gives the output name (in this case, maths.dll). As with other compilers, -I indicates where the include files are placed for the JDK that has been installed:

cl
  com_packtpub_e4_advanced_c_Maths.c
  -LD
  -Femaths.dll 
  -IC:Javainclude

This results in a file called maths.dll. Note that the lowercase e is part of the -Fe option, and not part of the dynamic link library itself.

Note

On Windows, the dll extension is used for dynamic link libraries, and there is no prefix.

Loading the native library

Once the binary has been compiled, a simple test for the Maths class can exercise the functionality:

package com.packtpub.e4.advanced.c;
public class MathsTest {
  public static void main(String[] args) {
    System.out.println(Maths.add(1,2));
  }
}

When run, this should print out 3.

If the native library cannot be found, the following exception will be displayed:

Exception in thread "main" java.lang.UnsatisfiedLinkError:
 no maths in java.library.path
  at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886)
  at java.lang.Runtime.loadLibrary0(Runtime.java:849)
  at java.lang.System.loadLibrary(System.java:1088)
  at com.packtpub.e4.advanced.c.Maths.<clinit>(Maths.java:13)
  at com.packtpub.e4.advanced.c.MathsTest.main(MathsTest.java:14)

Note

The <clinit> method (which stands for class initializer) in the stack trace is the special name given to the static initializer—in this case, the static block in the Maths class. This is also generated if any static variables are assigned non-default values.

The instance constructor is called <init>, and is generated whenever instance variables are assigned or if a constructor is provided.

The exception is triggered when the Maths class is first used, which is called from MathsTest. Note that the static initializer of a class is executed prior to any methods being invoked on that class.

If the System class is unable to find the library relative to the class, it will consult the list of directories specified in the java.library.path system property. If the native library is found, then it will be returned; otherwise, an error is thrown.

To fix the previous error, modify the Java runtime to add a directory (relative or absolute) and invoke it with the system property set accordingly:

$ java -classpath . -Djava.library.path=/path/to/dir
  com.packtpub.e4.advanced.c.MathsTest

The library must be loaded as a File rather than as an embedded resource in a JAR file; in other words, an InputStream object cannot be used to load the contents. OSGi runtimes such as Equinox and Felix extract the native libraries on demand to a temporary file so that they can be loaded by the operating system.

Library dependencies

One thing to be aware of is the fact that native libraries go through a slightly different resolution process as compared to libraries loaded by Java. If the JNI library has an external dependency, then this will be loaded automatically by the operating system. However, the operating system won't know about java.library.path, and hence may fail to find the required native library dependencies.

For Windows systems, the current directory is always consulted if a library can't be found elsewhere, and this typically results in the library being loadable. It will default to the Windows system directory (such as c:WindowsSytstem32 or similar) if it can't be found in the PATH variable.

On Linux and OS X, the value of the LD_LIBRARY_PATH variable or the DYLD_LIBRARY_PATH variable is consulted. Generally, these are set to include /usr/lib and /lib by default, so that standard libraries (such as libc and libssl) can always be loaded.

To see the problem in action, create a new dynamic library called other, which defines a single function:

// other.h
int otherAdd(int a, int b);
// other.c
#include "other.h"
int otherAdd(int a, int b) { return a+b; }

Compile this as a dynamic linked library called other. Then, modify the maths library to use the other library:

#include "com_packtpub_e4_advanced_c_Maths.h"
#include "other.h"
JNIEXPORT jint JNICALL Java_com_packtpub_e4_advanced_c_Maths_add
 (JNIEnv *env, jclass c, jint a, jint b) {
  return otherAdd(a,b);
}

The maths library will need to be passed an argument to link this with the native library as well, such as -L. -lother on Unix or by passing the name of the library on the cl command line for Windows.

If the other library is in the current directory when the Java virtual machine is started, then the MathsTest class will work as expected. If it is moved into a different directory, then the test will fail:

$ java -Djava.library.path=native
 com.packtpub.e4.advanced.c.MathsTest
Exception in thread "main" java.lang.UnsatisfiedLinkError:
 com.packtpub.e4.advanced/com.packtpub.e4.advanced.c/
  native/libmaths.dylib:
 dlopen(com.packtpub.e4.advanced/com.packtpub.e4.advanced.c/
  native/libmaths.dylib, 1):
Library not loaded: libother.dylib
Referenced from:
 com.packtpub.e4.advanced/com.packtpub.e4.advanced.c/
  native/libmaths.dylib
Reason: image not found
 at java.lang.ClassLoader$NativeLibrary.load(Native Method)

The problem is that the value of java.library.path is only known by the Java runtime. Java knows where to find the first library, but when that library needs the dependent library and the native operating system needs to resolve it, it will use the operating system's native resolution to find the library.

Note

Java doesn't provide a way to modify environment variables at runtime, and in any case, the operating system's library loader doesn't re-read the environment variables after the start of a process for efficiency reasons.

On Windows, the dependent library may be loaded and cached in memory by calling System.loadLibrary separately. The Maths class can be changed as follows:

static {
  System.loadLibrary("other");
  System.loadLibrary("maths");
}

Now when run on Windows, the program works as expected. That's because the Windows platform loads and resolves the symbols in the first library and then loads and resolves the symbols in the second library.

On Unix platforms such as Mac OS X and Linux, this doesn't work, because the library will have an embedded reference to the dependent library. The native loader will complain that the dynamic link library is not satisfied, even if the library has been loaded already. That's because on Unix, the native libraries are loaded lazily (and so not resolved), with the result that when the function is first called, it is resolved on demand.

The solution to this problem is to perform one of the following steps instead:

  • In the native code, only rely on system libraries (those in /usr/lib or equivalent)
  • Statically link any dependent code
  • Set the appropriate environment variable to a location containing all the dependencies before launching
  • Avoid using JNI libraries to provide operating system hooks
  • Use an alternative library such as jnr (https://github.com/jnr), which includes a POSIX compatibility layer

Native code patterns

When writing native code methods in Java, any time the method signature changes, the method has to be recompiled and relinked. This may be inconvenient for Java developers, especially if multiple platforms need to be rebuilt.

Best practice is to internalize native dependencies by ensuring that all native methods are marked as private.

Instead of exporting the native function to callers directly, mark it as private and provide Java public methods that wrap the native call:

private native static int nativeAdd(int a, int b);
public static int add(int a, int b) {
  return nativeAdd(a,b);
}

This permits the native library to be isolated from any Java changes that may occur in future, such as changing the signature type or adding exceptions. In addition, any fixes that are required may be implemented in a Java layer instead of the native layer. For example, in the previous case, if the method needed to be changed to add long values instead, the native layer could still be used in the common case but fall back to a pure Java path in the case the values are larger than the native layer can handle:

public static long add(long a, long b) {
  if (a < Integer.MAX_VALUE / 2 && a > Integer.MIN_VALUE / 2
   && b < Integer.MAX_VALUE / 2 && b > Integer.MIN_VALUE / 2) {
    return nativeAdd((int)a, (int)b);
  } else {
    return a+b;
  }
}

Note that the arguments are range tested to ensure that the resulting value will be within the int range and that an overflow does not occur. Similar argument testing can be done for other types of arguments before they are passed into the native method. If certain values are known to cause problems, they can be handled in the Java layer appropriately.

Another reason to use a Java frontend is that exceptions are often easier to generate (and messages easier to update) when compiled in Java than in the native layer. JNI provides a means to throw an exception with the (*env)->ThrowNew method, but if there is a requirement to update the information passed into the exception object, it is easier to change this in the Java code once instead of changing it once per platform.

An alternative approach is to provide thin Java bindings over every native method in as close to one-to-one correspondence as possible. SWT provides a means to manipulate the underlying operating system's resources using Java classes; for example, on Mac OS X, the org.eclipse.swt.internal.cocoa package provides objects such as NSView and id that have one-to-one correspondence with their underlying native counterparts. The SWT library is then constructed by manipulating the Java wrappers, in much the same way that AWT abstracts the native libraries in the original GUI toolkit for Java.

Note

Unlike AWT, SWT can be upgraded without requiring a JVM update. The other key reason why AWT is no longer used is that the AWT implementation aimed to provide a least-common-denominator approach, which meant that it always lagged behind OS upgrades. SWT keeps up-to-date with the operating system's new features, adding support for touches, gestures, and new UIs such as GTK3. Wherever possible, the native objects are returned so that the look, feel, and behavior are appropriate for the application; but for operating systems that don't support particular elements, a fallback implementation in Java can often be used.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset