jnr.ffi.LibraryLoader Maven / Gradle / Ivy
/*
* Copyright (C) 2012 Wayne Meissner
*
* This file is part of the JNR project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jnr.ffi;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import jnr.ffi.mapper.CompositeFunctionMapper;
import jnr.ffi.mapper.CompositeTypeMapper;
import jnr.ffi.mapper.DataConverter;
import jnr.ffi.mapper.FromNativeConverter;
import jnr.ffi.mapper.FunctionMapper;
import jnr.ffi.mapper.SignatureTypeMapper;
import jnr.ffi.mapper.SignatureTypeMapperAdapter;
import jnr.ffi.mapper.ToNativeConverter;
import jnr.ffi.mapper.TypeMapper;
import jnr.ffi.provider.FFIProvider;
import jnr.ffi.provider.LoadedLibrary;
/**
* Loads a native library and maps it to a java interface.
*
* Example usage
*
* {@code
*
* public interface LibC {
* int puts(String str);
* }
*
* LibC libc = LibraryLoader.create(LibC.class).load("c");
*
* libc.puts("Hello, World");
*
* }
*
*/
public abstract class LibraryLoader {
public static final String DEFAULT_LIBRARY = "RTLD_DEFAULT";
private final List searchPaths = new ArrayList();
private final List libraryNames = new ArrayList();
private final List typeMappers = new ArrayList();
private final List functionMappers = new ArrayList();
private final Map optionMap = new EnumMap(LibraryOption.class);
private final TypeMapper.Builder typeMapperBuilder = new TypeMapper.Builder();
private final FunctionMapper.Builder functionMapperBuilder = new FunctionMapper.Builder();
private final Class interfaceClass;
private boolean failImmediately = false;
/**
* Creates a new {@code LibraryLoader} instance.
*
* @param The library type.
* @param interfaceClass the interface that describes the native library functions
* @return A {@code LibraryLoader} instance.
*/
public static LibraryLoader create(Class interfaceClass) {
return FFIProvider.getSystemProvider().createLibraryLoader(interfaceClass);
}
protected LibraryLoader(Class interfaceClass) {
this.interfaceClass = interfaceClass;
}
/**
* When either the {@link jnr.ffi.annotations.SaveError} or
* {@link jnr.ffi.annotations.IgnoreError} annotations are used, the
* following matrix applies:
*
* (SL = save at library level, IM = ignore at method level, etc)
*
*
* | none | SL | IL | SL+IL|
* -------------------------------------
* none | save | save | ignr | save |
* SM | save | save | save | save |
* IM | ignr | ignr | ignr | ignr |
* SM + IM | save | save | save | save |
*
*
* @param options options
* @param methodHasSave whether the method has error-saving enabled
* @param methodHasIgnore whether the method ignores errors
* @return true if the error was saved, false otherwise
*/
public static boolean saveError(Map options, boolean methodHasSave, boolean methodHasIgnore) {
// default to save
boolean saveError = options.containsKey(LibraryOption.SaveError) || !options.containsKey(LibraryOption.IgnoreError);
// toggle only according to above matrix
if (saveError) {
if (methodHasIgnore && !methodHasSave) {
saveError = false;
}
} else {
if (methodHasSave) {
saveError = true;
}
}
return saveError;
}
/**
* Loads a native library and links the methods defined in {@code interfaceClass}
* to native methods in the library.
*
* @param the interface type.
* @param libraryNames the name of the library to load
* @param interfaceClass the interface that describes the native library interface
* @param searchPaths a map of library names to paths that should be searched
* @param libraryOptions options
* @return an instance of {@code interfaceclass} that will call the native methods.
*/
public static T loadLibrary(
Class interfaceClass,
Map libraryOptions,
Map> searchPaths,
String... libraryNames) {
LibraryLoader loader = FFIProvider.getSystemProvider().createLibraryLoader(interfaceClass);
for (String libraryName : libraryNames) {
if (libraryName.equals(LibraryLoader.DEFAULT_LIBRARY)) {
loader.searchDefault();
continue;
}
loader.library(libraryName);
List paths = searchPaths.get(libraryName);
if (paths != null) for (String path : paths) {
loader.search(path);
}
}
if (libraryOptions != null) {
for (Map.Entry option : libraryOptions.entrySet()) {
loader.option(option.getKey(), option.getValue());
}
}
return loader.failImmediately().load();
}
/**
* Same as calling {@link #loadLibrary(Class, Map, Map, String...)} with an empty search path map.
*
* @see #loadLibrary(Class, Map, Map, String...)
*
* @param interfaceClass the interface to implement
* @param libraryOptions options
* @param libraryNames names to try when searching for the library
* @param the interface type
* @return a new object implementing the interface and bound to the library
*/
public static T loadLibrary(
Class interfaceClass,
Map libraryOptions,
String... libraryNames) {
return (T) loadLibrary(interfaceClass, libraryOptions, Collections.EMPTY_MAP, libraryNames);
}
/**
* Adds a library to be loaded. Multiple libraries can be specified using additional calls
* to this method, and all libraries will be searched to resolve symbols (e.g. functions, variables).
*
* @param libraryName The name or path of library to load.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader library(String libraryName) {
if (libraryName.equals(DEFAULT_LIBRARY)) {
return searchDefault();
}
this.libraryNames.add(libraryName);
return this;
}
/**
* Add the default library to the search order. This will search all loaded libraries in the current process
* according to the system's implementation of dlsym(RTLD_DEFAULT).
*
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader searchDefault() {
this.libraryNames.add(DEFAULT_LIBRARY);
return this;
}
/**
* Adds a path to search for libraries. Multiple paths can be specified using multiple calls
* to this method, and all paths will be searched..
*
* @param path A directory to search.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader search(String path) {
searchPaths.add(path);
return this;
}
/**
* Sets an option when loading libraries.
*
* @see LibraryOption
*
* @param option The option to set.
* @param value The value for the option.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader option(LibraryOption option, Object value) {
switch (option) {
case TypeMapper:
if (value instanceof SignatureTypeMapper) {
mapper((SignatureTypeMapper) value);
} else if (value instanceof TypeMapper) {
mapper((TypeMapper) value);
} else if (value != null) {
throw new IllegalArgumentException("invalid TypeMapper: " + value.getClass());
}
break;
case FunctionMapper:
mapper((FunctionMapper) value);
break;
default:
optionMap.put(option, value);
}
return this;
}
/**
* Adds a type mapper to use when resolving method parameter and result types.
*
* Multiple type mappers can be specified by additional calls to this method, and
* each mapper will be tried in order until one is successful.
*
* @param typeMapper The type mapper to use.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader mapper(TypeMapper typeMapper) {
typeMappers.add(new SignatureTypeMapperAdapter(typeMapper));
return this;
}
/**
* Adds a type mapper to use when resolving method parameter and result types.
*
* Multiple type mappers can be specified by additional calls to this method, and
* each mapper will be tried in order until one is successful.
*
* @param typeMapper The type mapper to use.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader mapper(SignatureTypeMapper typeMapper) {
typeMappers.add(typeMapper);
return this;
}
/**
* Adds a custom java type mapping.
*
* @param The Java type.
* @param javaType The java type to convert to a native type.
* @param toNativeConverter A {@link jnr.ffi.mapper.ToNativeConverter} that will convert from the java type to a native type.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader map(Class extends J> javaType, ToNativeConverter extends J, ?> toNativeConverter) {
typeMapperBuilder.map(javaType, toNativeConverter);
return this;
}
/**
* Adds a custom java type mapping.
*
* @param The Java type.
* @param javaType The java type to convert to a native type.
* @param fromNativeConverter A {@link jnr.ffi.mapper.ToNativeConverter} that will convert from the native type to a java type.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader map(Class extends J> javaType, FromNativeConverter extends J, ?> fromNativeConverter) {
typeMapperBuilder.map(javaType, fromNativeConverter);
return this;
}
public LibraryLoader map(Class extends J> javaType, DataConverter extends J, ?> dataConverter) {
typeMapperBuilder.map(javaType, dataConverter);
return this;
}
/**
* Adds a function mapper to use when resolving symbols in this library.
*
* Multiple function mappers can be specified by additional calls to this method, and
* each mapper will be tried in order, until one is successful.
*
* @param functionMapper The function mapper to use.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader mapper(FunctionMapper functionMapper) {
functionMappers.add(functionMapper);
return this;
}
/**
* Adds a function name mapping to use when resolving symbols in this library.
*
* @param javaName The java method name.
* @param nativeFunction The native library symbol to map the java method name to.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader map(String javaName, String nativeFunction) {
functionMapperBuilder.map(javaName, nativeFunction);
return this;
}
/**
* Sets the native function calling convention.
*
* This is only needed on windows platforms - unless explicitly specified, all platforms assume
* {@link CallingConvention#DEFAULT} as the calling convention.
*
* @param convention The calling convention.
* @return The {@code LibraryLoader} instance.
*/
public LibraryLoader convention(CallingConvention convention) {
optionMap.put(LibraryOption.CallingConvention, convention);
return this;
}
/**
* Sets the calling convention of the library to the Windows stdcall calling convention
*
* @return This {@code LibraryLoader} instance.
*/
public final LibraryLoader stdcall() {
return convention(CallingConvention.STDCALL);
}
/**
* Turns off lazy propagation of load failures. By default, {@link jnr.ffi.LibraryLoader#load()} will not fail
* immediately if any libraries cannot be loaded - instead, it will create an instance of the library interface
* that re-throws any load errors when invoked.
*
* Calling this method will make {@link jnr.ffi.LibraryLoader#load()} throw errors immediately.
*
* @return This {@code LibraryLoader} instance.
*/
public final LibraryLoader failImmediately() {
failImmediately = true;
return this;
}
/**
* Loads a native library and links the methods defined in {@code interfaceClass}
* to native methods in the library.
*
* @param libraryName The name or path of library to load.
* @return an implementation of the interface provided to {@link #create(Class)} that will call the native methods.
*/
public T load(String libraryName) {
return library(libraryName).load();
}
/**
* Loads a native library and links the methods defined in {@code interfaceClass}
* to native methods in the library.
*
* @return an implementation of the interface provided to {@link #create(Class)} that will call the native methods.
*/
public T load() {
if (libraryNames.isEmpty()) {
throw new UnsatisfiedLinkError("no library names specified");
}
typeMappers.add(0, new SignatureTypeMapperAdapter(typeMapperBuilder.build()));
optionMap.put(LibraryOption.TypeMapper, typeMappers.size() > 1 ? new CompositeTypeMapper(typeMappers) : typeMappers.get(0));
functionMappers.add(0, functionMapperBuilder.build());
optionMap.put(LibraryOption.FunctionMapper, functionMappers.size() > 1 ? new CompositeFunctionMapper(functionMappers) : functionMappers.get(0));
try {
return loadLibrary(interfaceClass, Collections.unmodifiableList(libraryNames), getSearchPaths(),
Collections.unmodifiableMap(optionMap), failImmediately);
} catch (LinkageError error) {
if (failImmediately) throw error;
return createErrorProxy(error);
} catch (Exception ex) {
RuntimeException re = ex instanceof RuntimeException ? (RuntimeException) ex : new RuntimeException(ex);
if (failImmediately) throw re;
return createErrorProxy(re);
}
}
private T createErrorProxy(final Throwable ex) {
return interfaceClass.cast(Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class[] { interfaceClass, LoadedLibrary.class },
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
throw ex;
}
})
);
}
private Collection getSearchPaths() {
// custom paths before default paths because user may have same name as a system lib
List paths = new ArrayList(searchPaths);
paths.addAll(DefaultLibPaths.PATHS);
return Collections.unmodifiableList(paths);
}
/**
* Implemented by FFI providers to load the actual library.
*
* @param interfaceClass The java class that describes the functions to be mapped.
* @param libraryNames A list of libraries to load and search for symbols.
* @param searchPaths The paths to search for libraries to be loaded.
* @param options The options to apply when loading the library.
* @param failImmediately whether to fast-fail when the library does not implement the requested functions
* @return an instance of {@code interfaceClass} that will call the native methods.
*/
protected abstract T loadLibrary(Class interfaceClass, Collection libraryNames,
Collection searchPaths, Map options,
boolean failImmediately);
static final class DefaultLibPaths {
static final List PATHS;
private static void addPathsFromFile(Collection paths, File file) {
if (!file.isFile() || !file.exists()) return;
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
String line = in.readLine();
while (line != null) {
// ignore empties, comments and include directives
if (!line.trim().isEmpty()
&& !line.startsWith("#") && !line.startsWith("include ")) {
// add even if doesn't exist! We're just adding the default paths, we check for validity elsewhere
paths.add(line);
}
line = in.readLine();
}
}
catch(IOException ignored) {
}
finally {
if (in != null) {
try { in.close(); }
catch(IOException ignored) {}
}
}
}
static {
// paths should have no duplicate entries and have insertion order
LinkedHashSet paths = new LinkedHashSet();
try {
paths.addAll(getPropertyPaths("jnr.ffi.library.path"));
paths.addAll(getPropertyPaths("jaffl.library.path"));
// Add JNA paths for compatibility
paths.addAll(getPropertyPaths("jna.library.path"));
// java.library.path should take care of Windows defaults
paths.addAll(getPropertyPaths("java.library.path"));
} catch (Exception ignored) {
}
if (Platform.getNativePlatform().isUnix()) {
// order is intentional!
paths.add("/usr/local/lib");
paths.add("/usr/lib");
paths.add("/lib");
}
switch (Platform.getNativePlatform().getOS()) {
case FREEBSD:
case OPENBSD:
case NETBSD:
case LINUX:
case ZLINUX:
case MIDNIGHTBSD:
// only for oracle jdk on Linux and non-OSX BSD parse /etc/ld.so.conf and /etc/ld.so.conf.d/*
// more details:
// https://github.com/jruby/jruby/issues/2913
// https://github.com/jruby/jruby/issues/3145
// https://github.com/elastic/logstash/issues/3127#issuecomment-101068714
File ldSoConf = new File("/etc/ld.so.conf");
File ldSoConfD = new File("/etc/ld.so.conf.d");
if (ldSoConf.exists()) {
addPathsFromFile(paths, ldSoConf);
}
if (ldSoConfD.isDirectory()) {
for (File file : ldSoConfD.listFiles()) {
addPathsFromFile(paths, file);
}
}
break;
}
PATHS = Collections.unmodifiableList(new ArrayList(paths));
}
}
private static List getPropertyPaths(String propName) {
String value = System.getProperty(propName);
if (value != null) {
String[] paths = value.split(File.pathSeparator);
return new ArrayList(Arrays.asList(paths));
}
return Collections.emptyList();
}
}