Safely loading different versions of a native library in JNA (or JNI)
Join the DZone community and get the full member experience.
Join For FreeOver the past years I have been working on a Java project that uses a native library written in C by a third party to process data. Recently the data format was changed and a new version of the native library was made available. Unfortunately, the native library, for very good reasons, is not backward compatible with the old data format but my project, also for very good reasons, needs to be able to process both formats of data. Therefore my project needs to determine if the data format is the old one or the new one and use the corresponding library to process the data.
Unloading a native library in Java is considered unsafe because it depends on the garbage collector to run. It is impossible to force a GC run (you can only suggest a GC run to the JVM) and there is no guarantee that, when the GC has run, a library (or any other object in memory) has been garbage collected. However, the concept of Dynamically Loaded libraries in C allows for safely unloading a library and loading it or a different version again.
An example library
In order to clarify how to unload a library and load a different version, this "library" will be used as an example. The header file version.h looks like this
int getVersion(void);
So it will be a very simple piece of code returning a version number. Of course this is not a real life example The corresponding C code version.c is
#include <stdio.h> #include "version.h" int getVersion(void) { return 1; }
To use the getVersion() function in C, this code can be used
#include <stdlib.h> #include <stdio.h> #include "version/version.h" int main(int argc, char **argv) { printf("%d\n", getVersion()); return 0; }
To build all this and separate the library with the getVersion() function from the code that uses it, I created a directory called "src" and in it a directory called "version". So, the directory structure is
- src
- version
- version.c
- version.h
- test.c
- version
Finally, all needs to be built before we can execute it. I am not a Make file guru, so I created a bash script:
#!/bin/bash cd src/version gcc -c -Wall -Werror -fpic version.c -o version.o && gcc -shared -o libVersion.so version.o cd .. gcc -I./version -L./version -Wall test.c -o test -lVersion cd .. export LD_LIBRARY_PATH=src/version:$LD_LIBRARY_PATH echo "Calling library from C:" ./src/test
Running the script generates this output:
$ ./compile.sh 1 $
Calling the library from Java
Please refer to the JNA documentation for more info on JNA.
To call the getVersion() function in the libVersion.so library from Java, I created this class
package version; import com.sun.jna.Library; import com.sun.jna.Native; public class VersionModule { private Version version; private static VersionModule vm; public static void main(String[] args) { vm = new VersionModule(); vm.callVersion(); } private void callVersion() { vm.init(); System.out.println(vm.getVersion()); } public void init() { version = (Version) Native.loadLibrary(Version.LIBRARY_NAME, Version.class); } public int getVersion() { return version.getVersion(); } private interface Version extends Library { String LIBRARY_NAME = "Version"; int getVersion(); } }
I put the java file in the src/java/version directory, like this
- src
- java
- version
-
VersionModule.java
- version
- version
- version.c
- version.h
- test.c
- java
and I modified the complie.sh bash script to call the Java class as well:
#!/bin/bash cd src/version gcc -c -Wall -Werror -fpic version.c -o version.o && gcc -shared -o libVersion.so version.o cd .. gcc -I./version -L./version -Wall test.c -o test -lVersion cd .. export LD_LIBRARY_PATH=src/version:$LD_LIBRARY_PATH echo "Calling library from C:" ./src/test cd src/java javac -cp .:/usr/share/java/jna-3.2.7.jar version/VersionModule.java echo "Calling library from Java:" java -Djna.library.path=../version -cp .:/usr/share/java/jna-3.2.7.jar version.VersionModule
Running the script now generates this output:
$ ./compile.sh Calling library from C: 1 Calling library from Java: 1
Using two versions of the library via a proxy library
Suppose now that we get a new version of the library. The header file didn't change but the implementation of the getVersion() function did. Here is the new version
#include <stdio.h> #include "version.h" int getVersion(void) { return 2; }
In order to keep both versions next to each other, I renamed the directory holding the old code to version1 and I put the new code in a separate folder called version2 (for want of better names):
- src
- java
- version
-
VersionModule.java
- version
- version1
- version.c
- version.h
- version2
- version.c
- version.h
- test.c
- java
In order to switch from one version to another, we introduce yet another C code file. This code will act as a proxy library and will perform the actual loading and unloading of the correct version of the library as well as calling the corresponding functions in the loaded library. In order to minimize code changes in the test.c file, the Java code and the compile.sh bash script, the header and code files are placed in the src/version directory. The header file version.h looks like this
void set_library_path(char *_library_path); int getVersion(void);
The proxy C code looks like this
#include <stdlib.h> #include <stdio.h> #include <dlfcn.h> void *handle; void set_library_path(char *_library_path) { if (handle) { dlclose(handle); } handle = dlopen(_library_path, RTLD_NOW); if (!handle) { fputs (dlerror(), stderr); } } int getVersion(void) { int (*getVersion)(void); char *error; getVersion = dlsym(handle, "getVersion"); if ((error = dlerror()) != NULL) { fputs(error, stderr); } return (*getVersion)(); }
The set_library_path function loads the requested version of the library using functions in the dlfcn (dynamic library function) library. Then, each function (in the API of the library for which different versions exist) needs to be mapped to a proxy function that tries to execute the required function in a way similar to Java reflection. It can be hard to get the proper mapping, depending on the definition of the functions that you need to proxy for. More on dynamically loading libraries can be found here.
The source tree now looks like this
- src
- java
- version
-
VersionModule.java
- version
- version
- version.c
- version.h
- version1
- version.c
- version.h
- version2
- version.c
- version.h
- test.c
- java
Please note that in order for the Java code to be able to load the proxy library, that library needs to be called libVersion.so or else JNA will not be able to load the library! The test.c code to use the old library and then the new is this
#include <stdlib.h> #include <stdio.h> #include "version/version.h" int main(int argc, char **argv) { set_library_path("src/version1/libVersion.so"); printf("%d\n", getVersion()); set_library_path("src/version2/libVersion.so"); printf("%d\n", getVersion()); return 0; }
Here is the compile.sh bash script to compile the code and execute the C test program:
#!/bin/bash cd src/version1 gcc -c -Wall -Werror -fpic version.c -o version.o && gcc -shared -o libVersion.so version.o cd ../version2 gcc -c -Wall -Werror -fpic version.c -o version.o && gcc -shared -o libVersion.so version.o cd ../version gcc -c -Wall -Werror -fpic -rdynamic version.c -o version.o && gcc -shared -o libVersion.so version.o cd .. gcc -I./version -L./version -Wall test.c -o test -lVersion -ldl cd .. export LD_LIBRARY_PATH=src/version:$LD_LIBRARY_PATH echo "Calling library from C:" ./src/test
The script now generates this output:
$ ./compile.sh Calling library from C: 1 2
Calling the proxy library from Java
With this new proxy library, the code changes to call the different versions from Java are quite small as well. Here is the new code
package version; import com.sun.jna.Library; import com.sun.jna.Native; public class VersionModule { private Version version; private static VersionModule vm; public static void main(String[] args) { vm = new VersionModule(); vm.callVersion(); } private void callVersion() { String pwd = System.getProperty("user.dir"); vm.init("../version1/libVersion.so"); System.out.println(vm.getVersion()); vm.init("../version2/libVersion.so"); System.out.println(vm.getVersion()); } public void init(String path) { version = (Version) Native.loadLibrary(Version.LIBRARY_NAME, Version.class); version.set_library_path(path); } public int getVersion() { return version.getVersion(); } private interface Version extends Library { String LIBRARY_NAME = "Version"; int getVersion(); void set_library_path(String _library_path); } }
and the new compile.sh bash script:
#!/bin/bash cd src/version1 gcc -c -Wall -Werror -fpic version.c -o version.o && gcc -shared -o libVersion.so version.o cd ../version2 gcc -c -Wall -Werror -fpic version.c -o version.o && gcc -shared -o libVersion.so version.o cd ../version gcc -c -Wall -Werror -fpic -rdynamic version.c -o version.o && gcc -shared -o libVersion.so version.o cd .. gcc -I./version -L./version -Wall test.c -o test -lVersion -ldl cd .. export LD_LIBRARY_PATH=src/version:$LD_LIBRARY_PATH echo "Calling library from C:" ./src/test cd src/java javac -cp .:/usr/share/java/jna-3.2.7.jar version/VersionModule.java echo "Calling library from Java:" java -Djna.library.path=../version -cp .:/usr/share/java/jna-3.2.7.jar version.VersionModule
Running the script produces this output
$ ./compile.sh Calling library from C: 1 2 Calling library from Java: 1 2
Performance impact
The 3rd party native library that is used by my project required me to create proxy functions for no less than 81 functions! Clearly this raises concerns about the performance of the proxy library w.r.t. the performance of direct use of one version of the library, even though 72 of these functions are getters and setters. A simple test, which was to call the whole processing sequence several times in a row with the old library and then the same with the proxy library calling the old library, The test shows there is no measurable overhead.
Caveats
There are a few caveats for this method to be aware of. The most important one is what to do when the API changes. A function in the new version may have the same signature but with different arguments or different argument types. Functions may exist in one version of the library and not the other. From the JNA point of view, all that matters is that the methods in the interface extending Library map one on one to functions in the proxy library. It is perfectly ok to construct the proxy library in such a way that its functions call functions in the loaded library with an entirely different name. As long as those functions get called with the proper arguments, it will work fine. If you do such a thing, then of course make sure to document this well, either in inline comments or in a technical design document, or both!
By the way, it is very tempting to delegate determining the location of the libraries to load and which functions can be called and which not to the end user. But this should be avoided as much as possible. You know about the internals of your Java code and proxy library so it is your responsibility to make sure that end users do not need to go to the trouble of making sure it all works fine.
Then of course it is a very bad practise to hard code the path to the libraries in source code. It is much better to determine the location of the library to load at runtime. This can be done via properties, command line flags, values in a database or whatever way you prefer. And again, make sure to document this well in your software user manual!
Finally, loading and unloading libraries takes time so it is important to minimize the need for that as much as possible. My project, for example, needs to deal with mixed content of data so I have made sure that I gather together all data of the old format and all data of the new format and then process them sequentially.
Opinions expressed by DZone contributors are their own.
Trending
-
MLOps: Definition, Importance, and Implementation
-
Why You Should Consider Using React Router V6: An Overview of Changes
-
Mastering Time Series Analysis: Techniques, Models, and Strategies
-
Apache Kafka vs. Message Queue: Trade-Offs, Integration, Migration
Comments