by Benoît Thiébault - (no comment)
Objectives
In order to develop modular and dynamic Java applications for some of our projects at Artenum, we have decided to use the OSGi framework.The OSGi framework is a module system and service platform for the Java programming language that implements a complete and dynamic component model. SourceOSGi is already successfully used by Eclipse or Glassfish v3 for instance. The main advantage of OSGi is that it provides an easy way to build component-based applications that can be dynamically loaded and updated at runtime. While creating an OSGi bundle is rather simple, when dealing with native libraries, things tend to become much more complicated, especially because there is very little documentation available on the subject.The main objective of this (very long, sorry about that) article is to explain the specificities of MacOSX and the different issues Jérémie and myself faced while creating an OSGi bundle for VTK, the open source Visualization Toolkit, written in C++.This article is divided in four parts:
- 1) Introduction regarding JNI, dynamic linking on MacOSX and native libraries in OSGi, explains some particularities of OSX and OSGi regarding the use of native code in Java
- a) JNI
- b) Dynamic linking
- c) OSGi and native libraries
- 2) Compilation of VTK with Java wrapping, details how to take into account those OSX-specific aspects to compile VTK
- a) Prerequisites
- b) Changing .dylib extensions to .jnilib
- c) Inserting the @loader_path attribute in dynamic library dependencies
- d) Compiling VTK
- e) Removing the version numbers
- f) Downloading the result
- 3) Creating a simple Java application using VTK, demonstrates the basic usage of VTK from a Java application
- 4) Embedding VTK in an OSGi bundle, explains how to package VTK as an OSGi bundle
1) Introduction regarding JNI, dynamic linking on MacOSX and native libraries in OSGi
a) JNI
When you want to use a native library such as VTK in a Java application, one of the possibilities is to use JNI, the Java Native Interface, to allow the communication between the Java code running in the JVM and the native library. The details on how to create the native dynamic libraries and how to access them from Java code are out of the scope of this article, but you can find a very simple example about how to use JNI here.What is important to know here is that to use native code in a Java application, one needs:- The dynamic libraries compiled from the native code (.dylib or .jnilib files on OSX, .so files on Linux, .dll files on Windows)
- Java classes declaring the native functions to be used
- A way to indicate the JVM where dynamic libraries are located, as it will load them dynamically at runtime (hence the name)
- The first one is to use the system environment variable DYLD_LIBRARY_PATH (on Linux, it's LD_LIBRARY_PATH, on Windows PATH):
> export DYLD_LIBRARY_PATH=path/to/my/libs > java -jar myApplication.jar
- The second method is to use the java.library.path Java property to indicate the libraries path.
> java -Djava.library.path=path/to/my/libs -jar myApplication.jar
b) Dynamic linking
This is where we have to go a little deeper on how MacOSX actually deals with dynamic linking and how does the dynamic linker know where to find the libraries.As very well explained in this article http://www.mikeash.com/pyblog/friday-qa-2009-11-06-linking-and-install-names.html, MacOSX uses the "install name" to find libraries:An install name is just a pathname embedded within a dynamic library which tells the linker where that library can be found at runtime. For example, libfoo.dylib might have an install name of /usr/lib/libfoo.dylib. This install name gets copied into the application at link time. When the dynamic linker goes looking for libfoo.dylib at runtime, it will fetch the install name out of the application and know to look for the library in /usr/lib. SourceTo see the install name of a library, you can use the otool command. It will show the install name as well as the dependencies of your library. For instance, here is the result of the command on one of VTK dynamic library, generated by the default Java wrapping in VTK:
> otool -L libvtkCommonJava.5.6.dylib libvtkCommonJava.5.6.dylib: libvtkCommonJava.5.6.dylib (compatibility version 5.6.0, current version 5.6.1) libvtkCommon.5.6.dylib (compatibility version 5.6.0, current version 5.6.1) libvtksys.5.6.dylib (compatibility version 5.6.0, current version 5.6.1) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.0) /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
Starting in 10.4, Apple provided @loader_path which does what you want here. It expands to the full path, minus the last component, of whatever is actually causing the target library to be loaded. If it's an application, then it's the same as @executable_path. If it's a framework or plugin [or a dynamic library], though, then it's relative to that framework or plugin, which is much more useful. SourceThis is why when compiling VTK with Java wrapping, one needs to modify the library path to be, for instance:
libvtkCommonJava.dylib: @loader_path/libvtkCommonJava.5.6.dylib (compatibility version 5.6.0, current version 5.6.1) @loader_path/libvtkCommon.5.6.dylib (compatibility version 5.6.0, current version 5.6.1) @loader_path/libvtksys.5.6.dylib (compatibility version 5.6.0, current version 5.6.1) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.0) /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
c) OSGi and native libraries
The OSGi framework dynamically loads and updates Java components that are called bundles.In its most common form, a bundle is simply a JAR file whose manifest has OSGi-specific entries*. This means that to distribute the native libraries inside the bundle and to benefit from OSGi dynamism, one needs to add the libraries in the jar file.The JVM cannot however link Java code to library files contained in a JAR file. This is why, at runtime and on request, the OSGi framework is going to unpack the required native libraries in a temporary folder.Unfortunately, OSGi can only extract .jnilib files from the bundle and ignores .dylib files. This is very annoying, as by default, VTK Java wrapping creates .dylib library files. We will see in the next part of this article how to solve this problem and obtain, for example:libvtkCommonJava.jnilib: @loader_path/libvtkCommonJava.5.6.jnilib (compatibility version 5.6.0, current version 5.6.1) @loader_path/libvtkCommon.5.6.jnilib (compatibility version 5.6.0, current version 5.6.1) @loader_path/libvtksys.5.6.jnilib (compatibility version 5.6.0, current version 5.6.1) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.0) /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
libvtkCommonJava.jnilib: @loader_path/libvtkCommonJava.jnilib (compatibility version 5.6.0, current version 5.6.1) @loader_path/libvtkCommon.jnilib (compatibility version 5.6.0, current version 5.6.1) @loader_path/libvtksys.jnilib (compatibility version 5.6.0, current version 5.6.1) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.0) /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
* It is also possible in OSGi to load directly a folder (containing the Manifest with OSGi-specific attributes), without requiring to compress the bundle in a jar file. This saves some time when using native libraries as it is not necessary to unpack the native libs at runtime, but it uses more disk space, as jar file are compressed, and is less practical for distribution.
2) Compiling VTK for OSX with Java wrapping
a) Prerequisites
Now that we have seen the constraints on the Java wrapping imposed by MacOSX, it is time to see how to adapt CMake scripts to generate .jnilib libraries with @loader_path attributes.First, you need to download CMake, the cross-platform build tool and VTK latest source code.When you first launch CMake, it will ask you the path of the source code and the path of the build directory in which it is going to generate all CMake scripts necessary to compile VTK. Then, when you click on the "configure" button, it will ask you to choose the generator for the project. As illustrated on figure below, choose Unix Makefiles with default native compilers.You will then have to choose the settings of installation, as shown on figure below:Click on the "configure" button and then "generate".b) Changing .dylib extensions to .jnilib
With the Terminal application, go to the root folder of the build directory. We are now going to you sed command to replace all .dylib extensions by .jnilib extensions. Use the changeLibExtensions.sh script:Nota Bene: there are no double backslashes in the sed commands, its the wiki system I use that does not want to write a single backslash, so DO NOT copy/paste the following lines or replace the double backslashes by single ones. The double slash // is correct however. The best solution is to download the sh file directly> grep -r -H ".dylib" .|awk -F':' '{print $1}'|sort|uniq|grep -v "^Binary"|grep -v "CMakeCache.txt">dylibFiles.txt > cat dylibFiles.txt | xargs sed -i "" 's/\\.dylib/\\.jnilib/g'
c) Inserting the @loader_path attribute in dynamic library dependencies
We know have to insert the @loader_path attribute in the installation script. Use the addLoaderPath.sh script:Nota Bene: there are no double backslashes in the sed commands, its the wiki system I use that does not want to write a single backslash, so DO NOT copy/paste the following lines or replace the double backslashes by single ones. The double slash // is correct however. The best solution is to download the sh file directly> grep -r -H '\\-change' .|awk -F':' '{print $1}'|sort|uniq|grep -v "^Binary"|grep -v "$~"|grep -v "addLoaderPath.sh">changeLib.txt > cat changeLib.txt | xargs sed -i "" 's/id "/id "@loader_path\\//' > cat changeLib.txt | xargs sed -i "" 's/-change "\\(.*\\)" "\\(.*\\)"/-change "\\1" "@loader_path\\/\\2"/'
d) Compiling VTK
We just have now to compile and install VTK (depending on your machine this may take one to two hours, so be patient )> make -j 2 > make install
e) Removing the version numbers
To remove the version numbers, I have created a small script called copyNoVersion.sh.To use it, first create a new empty directory and execute it with the command:./copyNoVersion $JNILIB_FOLDER
f) Download the result
If you don't want to go through all these steps and just would like the result directly, you can download it for VTK 5.6.1 here: http://maven.artenum.com/repo/vtk/vtk-native/5.6.1/vtk-native-5.6.1-osx64b.zip.The CMakeCache.txt file I used to compile and wrap this version of VTK is here: CMakeCache.txt3) Creating a simple Java application using VTK
Now that we have a compiled version of VTK, lets create a concrete example showing how to use VTK from a pure Java application (no OSGi yet). This sample application will display a 3D cone in a JFrame.The source code (together with the compiled VTK lib) can be download here : test-vtk.zip.In this project, there are two classes:- org.test.vtk.TestVTK is a JFrame that contains one method start() to initialize a vtkPanel containing the 3D cone.
- org.test.vtk.Start is the class that contains the main(String[] args) method that will be executed on startup.
mvn clean package
- The first one is to use the system environment variable DYLD_LIBRARY_PATH. This is what is done in the runSysEnv.sh script placed in the project root folder.
export DYLD_LIBRARY_PATH=./dependencies/thirdparty/vtk/vtk-5.6.1/osx64b/ java -jar target/test-vtk-2.0-SNAPSHOT-jar-with-dependencies.jar
- The second method uses the java.library.path Java property to indicate the JVM where to find the libraries. This is what is done in the runJavaProp.sh script placed in the project root folder.
java -Djava.library.path=./dependencies/thirdparty/vtk/vtk-5.6.1/osx64b/ -jar target/test-vtk-2.0-SNAPSHOT-jar-with-dependencies.jar
4) Embedding VTK in an OSGi bundle
Now that we have seen how to create a Java project using VTK on OSX, we can try to create a VTK OSGi bundle.In this project, we have created two classes:- org.test.vtk.osgi.TestVTK is the same JFrame that contains the method start() to initialize a vtkPanel containing the 3D cone. It also contains the stop()method called when the bundle is stopped.
- org.test.vtk.osgi.Activator is the BundleActivator class that will be called by OSGi when the bundle is started or stopped. It statically loads the VTK libraries one by one, in the correct order of their dependencies, as shown below:
static { System.loadLibrary("vtkproj4"); System.loadLibrary("vtkalglib"); System.loadLibrary("vtksys"); System.loadLibrary("vtkCommon"); System.loadLibrary("vtkFiltering"); System.loadLibrary("vtkexpat"); System.loadLibrary("vtkjpeg"); System.loadLibrary("vtkzlib"); System.loadLibrary("vtklibxml2"); System.loadLibrary("vtktiff"); System.loadLibrary("vtkpng"); System.loadLibrary("vtksqlite"); System.loadLibrary("vtkmetaio"); System.loadLibrary("vtkNetCDF"); System.loadLibrary("vtkDICOMParser"); System.loadLibrary("vtkNetCDF_cxx"); System.loadLibrary("vtkIO"); System.loadLibrary("vtkImaging"); System.loadLibrary("vtkverdict"); System.loadLibrary("vtkGraphics"); System.loadLibrary("vtkfreetype"); System.loadLibrary("vtkftgl"); System.loadLibrary("vtkGenericFiltering"); System.loadLibrary("vtkRendering"); System.loadLibrary("vtkexoIIc"); System.loadLibrary("Cosmo"); System.loadLibrary("VPIC"); System.loadLibrary("vtkParallel"); System.loadLibrary("vtkHybrid"); System.loadLibrary("vtkWidgets"); System.loadLibrary("vtkVolumeRendering"); }
# First we delete the cache folder contraining previously loaded bundles rm -rf configuration/org.eclipse.osgi configuration *.log # Then we execute Equinox with the argument -Xcheck:jni to see JNI-related errors java -Xcheck:jni -jar org.eclipse.osgi_3.6.0.v20100517.jar -console osgi> install file:/Users/ben/Documents/workspace/test-osgi-vtk/target/test-osgi-vtk-2.0-SNAPSHOT.jar osgi> start 1
WARNING in native method: JNI call made with exception pending at vtk.vtkPanel.RenderCreate(Native Method) at vtk.vtkPanel.Render(vtkPanel.java:166) - locked <1060ffa88> (a vtk.vtkPanel) at vtk.vtkPanel.paint(vtkPanel.java:189) at sun.awt.RepaintArea.paintComponent(RepaintArea.java:276) at sun.awt.RepaintArea.paint(RepaintArea.java:241) at apple.awt.ComponentModel.handleEvent(ComponentModel.java:263) at java.awt.Component.dispatchEventImpl(Component.java:4790) at java.awt.Component.dispatchEvent(Component.java:4544) at java.awt.EventQueue.dispatchEvent(EventQueue.java:635) at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:296) at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:211) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:201) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:196) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:188) at java.awt.EventDispatchThread.run(EventDispatchThread.java:122) FATAL ERROR in native method: Bad global or local ref passed to JNI at vtk.vtkPanel.RenderCreate(Native Method) at vtk.vtkPanel.Render(vtkPanel.java:166) - locked <1060ffa88> (a vtk.vtkPanel) at vtk.vtkPanel.paint(vtkPanel.java:189) at sun.awt.RepaintArea.paintComponent(RepaintArea.java:276) at sun.awt.RepaintArea.paint(RepaintArea.java:241) at apple.awt.ComponentModel.handleEvent(ComponentModel.java:263) at java.awt.Component.dispatchEventImpl(Component.java:4790) at java.awt.Component.dispatchEvent(Component.java:4544) at java.awt.EventQueue.dispatchEvent(EventQueue.java:635) at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:296) at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:211) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:201) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:196) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:188) at java.awt.EventDispatchThread.run(EventDispatchThread.java:122) Invalid memory access of location 0x0 rip=0x1010af1f4 ./ben.sh: line 11: 23453 Abort trap java -Xcheck:jni -jar org.eclipse.osgi_3.6.0.v20100517.jar -console
- In Felix/Equinox configuration settings, add the "org.osgi.framework.bootdelegation=apple.*" property. This tells OSGi that all apple.* packages are visible to all bundles. This works, but you somewhat lose the modularity of OSGi, as everyone can access those packages.
- Explicitly import the package in the bundle manifest Import-Package entry. For OSGi to resolve this new dependency of your bundle, you can either:
- add org.osgi.framework.system.packages.extra=apple.awt in Felix/Equinox configuration settings (your configuration is however now platform-dependent)
- create a system bundle fragment that exports apple.awt (and contains no other files than the manifest)
# First we delete the cache folder contraining previously loaded bundles rm -rf configuration/org.eclipse.osgi configuration *.log # Then we execute Equinox with the argument -Xcheck:jni to see JNI-related errors java -Xcheck:jni -jar org.eclipse.osgi_3.6.0.v20100517.jar -console osgi> install file:/Users/ben/Documents/workspace/test-osgi-vtk/apple.awt.1.0.0.jar osgi> install file:/Users/ben/Documents/workspace/test-osgi-vtk/target/test-osgi-vtk-2.0-SNAPSHOT.jar osgi> start 2