blob: 44eaab0e83a6ae6cafd824eb45de61d5a8dc5337 [file]
/*
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.loader;
import jdk.internal.misc.VM;
import jdk.internal.ref.CleanerFactory;
import jdk.internal.util.StaticProperty;
import java.io.File;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* Native libraries are loaded via {@link System#loadLibrary(String)},
* {@link System#load(String)}, {@link Runtime#loadLibrary(String)} and
* {@link Runtime#load(String)}. They are caller-sensitive.
*
* Each class loader has a NativeLibraries instance to register all of its
* loaded native libraries. System::loadLibrary (and other APIs) only
* allows a native library to be loaded by one class loader, i.e. one
* NativeLibraries instance. Any attempt to load a native library that
* has already been loaded by a class loader with another class loader
* will fail.
*/
public final class NativeLibraries {
private static final boolean loadLibraryOnlyIfPresent = ClassLoaderHelper.loadLibraryOnlyIfPresent();
private final Map<String, NativeLibraryImpl> libraries = new ConcurrentHashMap<>();
private final ClassLoader loader;
// caller, if non-null, is the fromClass parameter for NativeLibraries::loadLibrary
// unless specified
private final Class<?> caller; // may be null
private final boolean searchJavaLibraryPath;
/**
* Creates a NativeLibraries instance for loading JNI native libraries
* via for System::loadLibrary use.
*
* 1. Support of auto-unloading. The loaded native libraries are unloaded
* when the class loader is reclaimed.
* 2. Support of linking of native method. See JNI spec.
* 3. Restriction on a native library that can only be loaded by one class loader.
* Each class loader manages its own set of native libraries.
* The same JNI native library cannot be loaded into more than one class loader.
*
* This static factory method is intended only for System::loadLibrary use.
*
* @see <a href="${docroot}/specs/jni/invocation.html##library-and-version-management">
* JNI Specification: Library and Version Management</a>
*/
public static NativeLibraries newInstance(ClassLoader loader) {
return new NativeLibraries(loader, loader != null ? null : NativeLibraries.class, loader != null);
}
private NativeLibraries(ClassLoader loader, Class<?> caller, boolean searchJavaLibraryPath) {
this.loader = loader;
this.caller = caller;
this.searchJavaLibraryPath = searchJavaLibraryPath;
}
/*
* Find the address of the given symbol name from the native libraries
* loaded in this NativeLibraries instance.
*/
public long find(String name) {
if (libraries.isEmpty())
return 0;
// the native libraries map may be updated in another thread
// when a native library is being loaded. No symbol will be
// searched from it yet.
for (NativeLibrary lib : libraries.values()) {
long entry = lib.find(name);
if (entry != 0) return entry;
}
return 0;
}
/*
* Load a native library from the given file. Returns null if the given
* library is determined to be non-loadable, which is system-dependent.
*
* @param fromClass the caller class calling System::loadLibrary
* @param file the path of the native library
* @throws UnsatisfiedLinkError if any error in loading the native library
*/
public NativeLibrary loadLibrary(Class<?> fromClass, File file) {
// Check to see if we're attempting to access a static library
String name = findBuiltinLib(file.getName());
boolean isBuiltin = (name != null);
if (!isBuiltin) {
try {
if (loadLibraryOnlyIfPresent && !file.exists()) {
return null;
}
name = file.getCanonicalPath();
} catch (IOException e) {
return null;
}
}
return loadLibrary(fromClass, name, isBuiltin);
}
/**
* Returns a NativeLibrary of the given name.
*
* @param fromClass the caller class calling System::loadLibrary
* @param name library name
* @param isBuiltin built-in library
* @throws UnsatisfiedLinkError if the native library has already been loaded
* and registered in another NativeLibraries
*/
private NativeLibrary loadLibrary(Class<?> fromClass, String name, boolean isBuiltin) {
ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
if (this.loader != loader) {
throw new InternalError(fromClass.getName() + " not allowed to load library");
}
acquireNativeLibraryLock(name);
try {
// find if this library has already been loaded and registered in this NativeLibraries
NativeLibrary cached = libraries.get(name);
if (cached != null) {
return cached;
}
// cannot be loaded by other class loaders
if (loadedLibraryNames.contains(name)) {
throw new UnsatisfiedLinkError("Native Library " + name +
" already loaded in another classloader");
}
/*
* When a library is being loaded, JNI_OnLoad function can cause
* another loadLibrary invocation that should succeed.
*
* Each thread maintains its own stack to hold the list of
* libraries it is loading.
*
* If there is a pending load operation for the library, we
* immediately return success; if the pending load is from
* a different class loader, we raise UnsatisfiedLinkError.
*/
for (NativeLibraryImpl lib : NativeLibraryContext.current()) {
if (name.equals(lib.name())) {
if (loader == lib.fromClass.getClassLoader()) {
return lib;
} else {
throw new UnsatisfiedLinkError("Native Library " +
name + " is being loaded in another classloader");
}
}
}
NativeLibraryImpl lib = new NativeLibraryImpl(fromClass, name, isBuiltin);
// load the native library
NativeLibraryContext.push(lib);
try {
if (!lib.open()) {
return null; // fail to open the native library
}
// auto unloading is only supported for JNI native libraries
// loaded by custom class loaders that can be unloaded.
// built-in class loaders are never unloaded.
boolean autoUnload = !VM.isSystemDomainLoader(loader) && loader != ClassLoaders.appClassLoader();
if (autoUnload) {
// register the loaded native library for auto unloading
// when the class loader is reclaimed, all native libraries
// loaded that class loader will be unloaded.
// The entries in the libraries map are not removed since
// the entire map will be reclaimed altogether.
CleanerFactory.cleaner().register(loader, lib.unloader());
}
} finally {
NativeLibraryContext.pop();
}
// register the loaded native library
loadedLibraryNames.add(name);
libraries.put(name, lib);
return lib;
} finally {
releaseNativeLibraryLock(name);
}
}
/**
* Loads a native library from the system library path and java library path.
*
* @param name library name
*
* @throws UnsatisfiedLinkError if the native library has already been loaded
* and registered in another NativeLibraries
*/
public NativeLibrary loadLibrary(String name) {
assert name.indexOf(File.separatorChar) < 0;
return loadLibrary(caller, name);
}
/**
* Loads a native library from the system library path and java library path.
*
* @param name library name
* @param fromClass the caller class calling System::loadLibrary
*
* @throws UnsatisfiedLinkError if the native library has already been loaded
* and registered in another NativeLibraries
*/
public NativeLibrary loadLibrary(Class<?> fromClass, String name) {
assert name.indexOf(File.separatorChar) < 0;
NativeLibrary lib = findFromPaths(LibraryPaths.SYS_PATHS, fromClass, name);
if (lib == null && searchJavaLibraryPath) {
lib = findFromPaths(LibraryPaths.USER_PATHS, fromClass, name);
}
return lib;
}
private NativeLibrary findFromPaths(String[] paths, Class<?> fromClass, String name) {
for (String path : paths) {
File libfile = new File(path, System.mapLibraryName(name));
NativeLibrary nl = loadLibrary(fromClass, libfile);
if (nl != null) {
return nl;
}
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null) {
nl = loadLibrary(fromClass, libfile);
if (nl != null) {
return nl;
}
}
}
return null;
}
/**
* NativeLibraryImpl denotes a loaded native library instance.
* Each NativeLibraries contains a map of loaded native libraries in the
* private field {@code libraries}.
*
* Every native library requires a particular version of JNI. This is
* denoted by the private {@code jniVersion} field. This field is set by
* the VM when it loads the library, and used by the VM to pass the correct
* version of JNI to the native methods.
*/
static class NativeLibraryImpl extends NativeLibrary {
// the class from which the library is loaded, also indicates
// the loader this native library belongs.
final Class<?> fromClass;
// the canonicalized name of the native library.
// or static library name
final String name;
// Indicates if the native library is linked into the VM
final boolean isBuiltin;
// opaque handle to native library, used in native code.
long handle;
// the version of JNI environment the native library requires.
int jniVersion;
NativeLibraryImpl(Class<?> fromClass, String name, boolean isBuiltin) {
this.fromClass = fromClass;
this.name = name;
this.isBuiltin = isBuiltin;
}
@Override
public String name() {
return name;
}
@Override
public long find(String name) {
return findEntry0(handle, name);
}
/*
* Unloader::run method is invoked to unload the native library
* when this class loader becomes phantom reachable.
*/
private Runnable unloader() {
return new Unloader(name, handle, isBuiltin);
}
/*
* Loads the named native library
*/
boolean open() {
if (handle != 0) {
throw new InternalError("Native library " + name + " has been loaded");
}
return load(this, name, isBuiltin, throwExceptionIfFail());
}
private boolean throwExceptionIfFail() {
if (loadLibraryOnlyIfPresent) return true;
// If the file exists but fails to load, UnsatisfiedLinkException thrown by the VM
// will include the error message from dlopen to provide diagnostic information
File file = new File(name);
return file.exists();
}
/*
* Close this native library.
*/
void close() {
unload(name, isBuiltin, handle);
}
}
/*
* The run() method will be invoked when this class loader becomes
* phantom reachable to unload the native library.
*/
static class Unloader implements Runnable {
// This represents the context when a native library is unloaded
// and getFromClass() will return null,
static final NativeLibraryImpl UNLOADER =
new NativeLibraryImpl(null, "dummy", false);
final String name;
final long handle;
final boolean isBuiltin;
Unloader(String name, long handle, boolean isBuiltin) {
if (handle == 0) {
throw new IllegalArgumentException(
"Invalid handle for native library " + name);
}
this.name = name;
this.handle = handle;
this.isBuiltin = isBuiltin;
}
@Override
public void run() {
acquireNativeLibraryLock(name);
try {
/* remove the native library name */
if (!loadedLibraryNames.remove(name)) {
throw new IllegalStateException(name + " has already been unloaded");
}
NativeLibraryContext.push(UNLOADER);
try {
unload(name, isBuiltin, handle);
} finally {
NativeLibraryContext.pop();
}
} finally {
releaseNativeLibraryLock(name);
}
}
}
/*
* Holds system and user library paths derived from the
* {@code java.library.path} and {@code sun.boot.library.path} system
* properties. The system properties are eagerly read at bootstrap, then
* lazily parsed on first use to avoid initialization ordering issues.
*/
static class LibraryPaths {
// The paths searched for libraries
static final String[] SYS_PATHS = ClassLoaderHelper.parsePath(StaticProperty.sunBootLibraryPath());
static final String[] USER_PATHS = ClassLoaderHelper.parsePath(StaticProperty.javaLibraryPath());
}
// All native libraries we've loaded.
private static final Set<String> loadedLibraryNames =
ConcurrentHashMap.newKeySet();
// reentrant lock class that allows exact counting (with external synchronization)
@SuppressWarnings("serial")
private static final class CountedLock extends ReentrantLock {
private int counter = 0;
public void increment() {
if (counter == Integer.MAX_VALUE) {
// prevent overflow
throw new Error("Maximum lock count exceeded");
}
++counter;
}
public void decrement() {
--counter;
}
public int getCounter() {
return counter;
}
}
// Maps native library name to the corresponding lock object
private static final Map<String, CountedLock> nativeLibraryLockMap =
new ConcurrentHashMap<>();
private static void acquireNativeLibraryLock(String libraryName) {
nativeLibraryLockMap.compute(libraryName,
new BiFunction<>() {
public CountedLock apply(String name, CountedLock currentLock) {
if (currentLock == null) {
currentLock = new CountedLock();
}
// safe as compute BiFunction<> is executed atomically
currentLock.increment();
return currentLock;
}
}
).lock();
}
private static void releaseNativeLibraryLock(String libraryName) {
CountedLock lock = nativeLibraryLockMap.computeIfPresent(libraryName,
new BiFunction<>() {
public CountedLock apply(String name, CountedLock currentLock) {
if (currentLock.getCounter() == 1) {
// unlock and release the object if no other threads are queued
currentLock.unlock();
// remove the element
return null;
} else {
currentLock.decrement();
return currentLock;
}
}
}
);
if (lock != null) {
lock.unlock();
}
}
// native libraries being loaded
private static final class NativeLibraryContext {
// Maps thread object to the native library context stack, maintained by each thread
private static Map<Thread, Deque<NativeLibraryImpl>> nativeLibraryThreadContext =
new ConcurrentHashMap<>();
// returns a context associated with the current thread
private static Deque<NativeLibraryImpl> current() {
return nativeLibraryThreadContext.computeIfAbsent(
Thread.currentThread(),
new Function<>() {
public Deque<NativeLibraryImpl> apply(Thread t) {
return new ArrayDeque<>(8);
}
});
}
private static NativeLibraryImpl peek() {
return current().peek();
}
private static void push(NativeLibraryImpl lib) {
current().push(lib);
}
private static void pop() {
// this does not require synchronization since each
// thread has its own context
Deque<NativeLibraryImpl> libs = current();
libs.pop();
if (libs.isEmpty()) {
// context can be safely removed once empty
nativeLibraryThreadContext.remove(Thread.currentThread());
}
}
private static boolean isEmpty() {
Deque<NativeLibraryImpl> context =
nativeLibraryThreadContext.get(Thread.currentThread());
return (context == null || context.isEmpty());
}
}
// Invoked in the VM to determine the context class in JNI_OnLoad
// and JNI_OnUnload
private static Class<?> getFromClass() {
if (NativeLibraryContext.isEmpty()) { // only default library
return Object.class;
}
return NativeLibraryContext.peek().fromClass;
}
/*
* Return true if the given library is successfully loaded.
* If the given library cannot be loaded for any reason,
* if throwExceptionIfFail is false, then this method returns false;
* otherwise, UnsatisfiedLinkError will be thrown.
*
* JNI FindClass expects the caller class if invoked from JNI_OnLoad
* and JNI_OnUnload is NativeLibrary class.
*/
private static native boolean load(NativeLibraryImpl impl, String name,
boolean isBuiltin,
boolean throwExceptionIfFail);
/*
* Unload the named library. JNI_OnUnload, if present, will be invoked
* before the native library is unloaded.
*/
private static native void unload(String name, boolean isBuiltin, long handle);
private static native String findBuiltinLib(String name);
}