Is there a way in JNA to load multiple dependent libraries with Java?
I usually use Native.loadLibrary(...)
to load one DLL. But I guess its not working this way because I assign this function call to the instance member.
Is there a way in JNA to load multiple dependent libraries with Java?
I usually use Native.loadLibrary(...)
to load one DLL. But I guess its not working this way because I assign this function call to the instance member.
Let's say I have library foo
and library bar
. bar
has a dependency on foo
; it also has a dependency on baz
, which we are not mapping with JNA:
public class Foo {
public static final boolean LOADED;
static {
Native.register("foo");
LOADED = true;
}
public static native void call_foo();
}
public class Bar {
static {
// Reference "Foo" so that it is loaded first
if (Foo.LOADED) {
System.loadLibrary("baz");
// Or System.load("/path/to/libbaz.so")
Native.register("bar");
}
}
public static native void call_bar();
}
The call to System.load/loadLibrary
will only be necessary if baz
is neither on your library load path (PATH
/LD_LIBRARY_PATH
, for windows/linux respectively) nor in the same directory as bar
(windows only).
EDIT
You can also do this via interface mapping:
public interface Foo extends Library {
Foo INSTANCE = (Foo)Native.loadLibrary("foo");
}
public interface Bar extends Library {
// Reference Foo prior to instantiating Bar, just be sure
// to reference the Foo class prior to creating the Bar instance
Foo FOO = Foo.INSTANCE;
Bar INSTANCE = (Bar)Native.loadLibrary("bar");
}
Loading lib transient dependencies with JNA from JAR Resources.
My resources folder res:
res/
`-- linux-x86-64
|-- libapi.so
|-- libdependency.so
-
MyApiLibrary api = (MyApiLibrary) Native.loadLibrary("libapi.so", MyApiLibrary.class, options);
API Explodes: Caused by: java.lang.UnsatisfiedLinkError: Error loading shared library libdependency.so: No such file or directory
Can be solved by loading dependencies beforehand by hand:
import com.sun.jna.Library;
Native.loadLibrary("libdependency.so", Library.class);
MyApiLibrary api = (MyApiLibrary) Native.loadLibrary("libapi.so", MyApiLibrary.class, options);
Basically you have to build dependency tree in reverse, by hand, by yourself.
I recommend setting
java -Djna.debug_load=true -Djna.debug_load.jna=true
Furthermore, setting jna.library.path to Resource has no effect, because JNA extracts to filesystem, then it loads lib. Lib on filesystem can NOT access other libs within jar.
Context class loader classpath. Deployed native libraries may be installed on the classpath under ${os-prefix}/LIBRARY_FILENAME, where ${os-prefix} is the OS/Arch prefix returned by Platform.getNativeLibraryResourcePrefix(). If bundled in a jar file, the resource will be extracted to jna.tmpdir for loading, and later removed (but only if jna.nounpack is false or not set).
RTFM and happy coding. JNA v.4.1.0
I was in a similar situation, dealing with multiplatform and several dependent libraries, but needing to load only one. Here is my take.
Suppose you get a set 32/64 win/linux libraries with dependencies.
Suppose you only need to have a JNA binding for libapi
You'll need to organize them into your jar like this:
linux-x86-64
|-- libapi.so
|-- libdependency.so
linux-x86
|-- libapi.so
|-- libdependency.so
win32-x86-64
|-- libapi.dll
|-- libdependency.dll
win32-x86
|-- libapi.dll
|-- libdependency.dll
You can:
determine if executing from a JAR file (avoids performing the operation when executing from your favorite IDE ; see How to get the path of a running JAR file?)
use JNA to determine your current executing platform
extract all appropriate library files into java temp folder (using elements from this answer: https://stackoverflow.com/a/58318009/7237062 (or related answers) should do the trick)
Tell JNA to look into the newly created temp folder
and voilà !
missing in code example is the directory cleanup at application shutdown, but I leave that to you
The main part should look like that:
MainClass.java
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import java.util.jar.JarFile;
import com.sun.jna.Platform;
public class MainClass {
private static final String JAVA_IO_TMPDIR = "java.io.tmpdir";
private static final String TEMP_DIR = System.getProperty(JAVA_IO_TMPDIR);
private static final String JNA_LIBRARY_PATH = "jna.library.path";
public static void main(String[] args) {
// ...
// path management here maybe suboptimal ... feel free to improve
// from https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file
URL current_jar_dir = Overview.class.getProtectionDomain().getCodeSource().getLocation();
Path jar_path = Paths.get(current_jar_dir.toURI());
String folderContainingJar = jar_path.getParent().toString();
ResourceCopy r = new ResourceCopy(); // class from https://stackoverflow.com/a/58318009/7237062
Optional<JarFile> jar = r.jar(MainClass.class);
if (jar.isPresent()) {
try {
System.out.println("JAR detected");
File target_dir = new File(TEMP_DIR);
System.out.println(String.format("Trying copy from %s %s to %s", jar.get().getName(), Platform.RESOURCE_PREFIX, target_dir));
// perform dir copy
r.copyResourceDirectory(jar.get(), Platform.RESOURCE_PREFIX, target_dir);
// add created folders to JNA lib loading path
System.setProperty(JNA_LIBRARY_PATH, target_dir.getCanonicalPath().toString());
} catch(Exception e) {
e.printStackTrace(); // TODO: handle exception ?
}
} else {
System.out.println("NO JAR");
}
// ...
}
ResourceCopy.java (copy here for completeness; taken from https://stackoverflow.com/a/58318009)
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.util.Enumeration;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* A helper to copy resources from a JAR file into a directory. source :
* https://stackoverflow.com/a/58318009
*/
public final class ResourceCopy {
/**
* URI prefix for JAR files.
*/
private static final String JAR_URI_PREFIX = "jar:file:";
/**
* The default buffer size.
*/
private static final int BUFFER_SIZE = 8 * 1024;
/**
* Copies a set of resources into a temporal directory, optionally
* preserving the paths of the resources.
*
* @param preserve
* Whether the files should be placed directly in the directory
* or the source path should be kept
* @param paths
* The paths to the resources
* @return The temporal directory
* @throws IOException
* If there is an I/O error
*/
public File copyResourcesToTempDir(final boolean preserve, final String... paths) throws IOException {
final File parent = new File(System.getProperty("java.io.tmpdir"));
File directory;
do {
directory = new File(parent, String.valueOf(System.nanoTime()));
} while (!directory.mkdir());
return this.copyResourcesToDir(directory, preserve, paths);
}
/**
* Copies a set of resources into a directory, preserving the paths and
* names of the resources.
*
* @param directory
* The target directory
* @param preserve
* Whether the files should be placed directly in the directory
* or the source path should be kept
* @param paths
* The paths to the resources
* @return The temporal directory
* @throws IOException
* If there is an I/O error
*/
public File copyResourcesToDir(final File directory, final boolean preserve, final String... paths)
throws IOException {
for (final String path : paths) {
final File target;
if (preserve) {
target = new File(directory, path);
target.getParentFile().mkdirs();
} else {
target = new File(directory, new File(path).getName());
}
this.writeToFile(Thread.currentThread().getContextClassLoader().getResourceAsStream(path), target);
}
return directory;
}
/**
* Copies a resource directory from inside a JAR file to a target directory.
*
* @param source
* The JAR file
* @param path
* The path to the directory inside the JAR file
* @param target
* The target directory
* @throws IOException
* If there is an I/O error
*/
public void copyResourceDirectory(final JarFile source, final String path, final File target) throws IOException {
final Enumeration<JarEntry> entries = source.entries();
final String newpath = String.format("%s/", path);
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
if (entry.getName().startsWith(newpath) && !entry.isDirectory()) {
final File dest = new File(target, entry.getName().substring(newpath.length()));
final File parent = dest.getParentFile();
if (parent != null) {
parent.mkdirs();
}
this.writeToFile(source.getInputStream(entry), dest);
}
}
}
/**
* The JAR file containing the given class.
*
* @param clazz
* The class
* @return The JAR file or null
* @throws IOException
* If there is an I/O error
*/
public Optional<JarFile> jar(final Class<?> clazz) throws IOException {
final String path = String.format("/%s.class", clazz.getName().replace('.', '/'));
final URL url = clazz.getResource(path);
Optional<JarFile> optional = Optional.empty();
if (url != null) {
final String jar = url.toString();
final int bang = jar.indexOf('!');
if (jar.startsWith(ResourceCopy.JAR_URI_PREFIX) && bang != -1) {
optional = Optional.of(new JarFile(jar.substring(ResourceCopy.JAR_URI_PREFIX.length(), bang)));
}
}
return optional;
}
/**
* Writes an input stream to a file.
*
* @param input
* The input stream
* @param target
* The target file
* @throws IOException
* If there is an I/O error
*/
private void writeToFile(final InputStream input, final File target) throws IOException {
final OutputStream output = Files.newOutputStream(target.toPath());
final byte[] buffer = new byte[ResourceCopy.BUFFER_SIZE];
int length = input.read(buffer);
while (length > 0) {
output.write(buffer, 0, length);
length = input.read(buffer);
}
input.close();
output.close();
}
}