[foreign] Library loading

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Thu Apr 29 12:10:58 UTC 2021


Hi,
lately, we’ve been thinking about our abstraction for loading native 
library, namely LibraryLookup. LibraryLookup is a cute little interface 
which lets you load libraries by name or by full path (similar to 
System::loadLibrary vs. System::load) - after you obtain a lookup 
object, you can then search for specific symbols and get back an address.

As many of you know, library loading with JNI is limited in two ways:

1. library loading is tied to the class loader
2. there is no way to get the “address” of a symbol in JNI (the lookup 
process, is hidden in VM and JDK code)

To be able to create downcall handles targeting specific library 
symbols, we need at least a solution for (2). And, since we were there, 
we thought that using the new abstraction to also address (1) was a good 
move.

But we started to have second thoughts about where we have landed. In 
many cases (see [1]), bindings distributed via Maven will attempt the 
standard trick of bundling native libraries with jars, extracting native 
library on a temp folder, from where they are loaded (using a full 
path). Unfortunately, tricks such as this _no longer works_  because the 
new LibraryLookup abstraction is completely orthogonal w.r.t. 
System::loadLibrary. This creates a new migration problem for bindings 
that want to gradually move away from JNI; as [1] demonstrates, when the 
old tricks are mixed with the new APIs, things simply don't work, as 
it's not possible to side-effect the set of libraries visible to the VM 
in the same way it was possible using System::loadLibrary.

Another issue is that the behavior of LibraryLookup::ofDefault is 
platform dependent (this is a known issue, see [2]). On some systems 
(e.g. MacOS), it “leaks” symbols from other, unrelated libraries 
(allowing, in this case, the bindings to surprisingly work!). In other 
systems, default lookup does not leak. Moreover (and we already knew 
this) default lookup on Windows is an hack that has been implemented by 
using a library which allows to walk all loaded libraries in current 
process (this is a functionality mostly intended for debugging 
purposes). In other words, the semantics of LibraryLookup::ofDefault is 
unstable. We have tried to explore what it would take to make this 
default lookup “stable”. And the answers weren't pretty, in the sense 
that we would need to parse output of system specific commands (e.g. 
`ldconfig -p`), or take the existing default lookup and "sanitize" it, 
by filtering out (either in Java, or at lower level, using linker map 
files) symbols that belong to the C standard library. While this seems 
like a reasonable task, that path is also full of thorns: certain 
symbols (errno, atexit, to name a few) are implemented in 
platform/compiler dependent ways, so it is difficult to give the user a 
view of a “standard C library” just in terms of a library lookup. In 
other words, putting together a C standard library is not just a mere 
matter of exposing the lookup which can be used to find its symbols; it 
probably involves building a full high-level, portable standard C API, 
which, while useful, is not something we're willing to tackle at this stage.

But, wait, there’s more; while inventing a new, classloader-neutral 
library mechanism sounds good on paper, as it allows to remove 
restrictions in the loading mechanism (JNI does not allow the same 
library to be loaded by multiple loaders), the classloader dependency 
also gave us a pretty nice way to manage the lifecycle of a native 
library: the library cannot be unloaded unless the owning classloader 
has been garbage collected. This avoids many catastrophic situations 
where a library is unloaded just before (or while) a native call on that 
library takes place. To address these issues, LibraryLookup is left 
reinventing much of the same reachability-based machinery (see all the 
javadoc in LibraryLookup, where terms like “X keeps a strong reference 
to Y” are employed throughout). One could argue that, now that we have 
ResourceScope, it would be possible, in principle, to build a 
ResourceScope-backed library loader. While this is possible, it must be 
noted the current lookup mechanism isn’t that, sadly.

So, what we’re left with is a LibraryLookup abstraction which looks nice 
and simple on paper, but that, in practice, is difficult to work with, 
has convoluted lifecycle properties, and does not provide a backward 
compatible option for clients that want to keep using the same JNI 
“hacks” (e.g. extracting libraries from jars, see above). In other 
words, LibraryLookup is probably not the primitive abstraction we want 
here. Luckily, a much simpler approach is possible:

MemoryAddress addr = MethodHandles.lookup().findNative("strlen");

Users will load libraries the usual way (System::load/loadLibrary) and 
findNative will look into the method handle lookup class’ classloader to 
lookup into all the libraries associated with that classloader. Turns 
out that a simple method like this is enough to address 95% of the use 
cases addressed by the current LibraryLookup abstraction --- basically 
everything except from default lookup, which is also the 5% which works 
unreliably.

Of course, since we’re still incubating, we would need to place this 
method somewhere else. e.g. a static method in CLinker (and maybe make 
it @CallerSensitive, to retrieve the classloader associated with the 
caller) - but that all seems doable enough. Note that this does not 
close the door, in the future, to building a more sophisticated library 
loading mechanism, where libraries are loaded and unloaded using a 
ResourceScope. Or, maybe ad-hoc library loading wrappers will naturally 
emerge (after all, it is trivial to write a custom library loader using 
dlopen/dlsym together with the Foreign Linker API). A (sharp) prototype 
of this approach is available at [3].

For all the reasons listed above, we plan to switch to the new, simpler, 
loading mechanism at some point after integrating the current Panama 
work into upstream jdk [2]. We believe that the proposed loading 
mechanism is more primitive, and provides a much better migration path 
for existing frameworks having to deal with distribution of native 
libraries via Maven or Gradle.

Maurizio

[1] - https://github.com/openjdk/panama-foreign/pull/509
[2] - https://bugs.openjdk.java.net/browse/JDK-8265222
[3] - 
https://github.com/openjdk/panama-foreign/compare/foreign-jextract...sundararajana:panama_uses_jni_loading?expand=1


More information about the panama-dev mailing list