Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Bug Fixes
---------
* [#1740](https://github.com/java-native-access/jna/issues/1740): Fix `c.s.j.p.unix.solaris.LibKstat.KstatCtl` mapping `kc_chain` as an inline `Kstat` instead of a pointer, which made every call taking a `KstatCtl` write past the end of the structure `kstat_open()` allocated, and map the `Kstat` function pointers `ks_update` and `ks_snapshot` as `Pointer` instead of `int` - [@dbwiddis](https://github.com/dbwiddis).

Features
--------
* [#1694](https://github.com/java-native-access/jna/issues/1694): Add support for multiple Linux native library variants by detecting the process libc and selecting the matching `glibc` or `musl` resource, falling back to the legacy resource when the libc is unknown or the matching variant is unavailable - [@EduardoRadieske](https://github.com/EduardoRadieske).

Breaking Changes
----------------
* `c.s.j.p.unix.solaris.LibKstat.KstatCtl#kc_chain` is now a `Pointer` instead of a `Kstat`. Use `KstatCtl#chain()` to get the head of the kstat chain as a `Kstat`. The previous mapping never returned usable data: the inline `Kstat` decoded the native chain pointer as its `ks_crtime` timestamp, and every following field from bytes past the end of the native structure, so code that read this field could not have relied on it.
Expand Down
13 changes: 12 additions & 1 deletion src/com/sun/jna/Native.java
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,10 @@ public static File extractFromResourcePath(String name) throws IOException {
* @throws IOException if resource not found
*/
public static File extractFromResourcePath(String name, ClassLoader loader) throws IOException {
return extractFromResourcePath(name, loader, Platform.LIBC);
}

public static File extractFromResourcePath(String name, ClassLoader loader, Platform.LibC libc) throws IOException {

final Level DEBUG = (DEBUG_LOAD
|| (DEBUG_JNA_LOAD && name.contains("jnidispatch"))) ? Level.INFO : Level.FINE;
Expand All @@ -1174,7 +1178,14 @@ public static File extractFromResourcePath(String name, ClassLoader loader) thro
if (resourcePath.startsWith("/")) {
resourcePath = resourcePath.substring(1);
}
URL url = loader.getResource(resourcePath);
URL url = null;
if (!name.startsWith("/") && Platform.isLinux() && libc != Platform.LibC.UNKNOWN) {
String variant = Platform.RESOURCE_PREFIX + "-" + libc.name().toLowerCase();
url = loader.getResource(variant + "/" + libname);
}
if (url == null) {
url = loader.getResource(resourcePath);
}
if (url == null) {
if (resourcePath.startsWith(Platform.RESOURCE_PREFIX)) {
// Fallback for legacy darwin behaviour: darwin was in the past
Expand Down
96 changes: 96 additions & 0 deletions src/com/sun/jna/Platform.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,24 @@
*/
package com.sun.jna;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Provide simplified platform information. */
public final class Platform {

public enum LibC {
GLIBC,
MUSL,
UNKNOWN
}

public static final int UNSPECIFIED = -1;
public static final int MAC = 0;
public static final int LINUX = 1;
Expand Down Expand Up @@ -68,6 +79,9 @@ public final class Platform {
private static final int osType;
/** Current platform architecture. */
public static final String ARCH;

/** The C library implementation used by the current platform. */
public static final LibC LIBC;

static {
String osName = System.getProperty("os.name");
Expand Down Expand Up @@ -137,6 +151,7 @@ else if (osName.equalsIgnoreCase("dragonflybsd")) {
// Windows aarch64 callbacks disabled via ASMFN_OFF (no mingw support)
HAS_DLL_CALLBACKS = osType == WINDOWS && !ARCH.startsWith("aarch");
RESOURCE_PREFIX = getNativeLibraryResourcePrefix();
LIBC = getLibC();
}
private Platform() { }
public static final int getOSType() {
Expand Down Expand Up @@ -297,6 +312,87 @@ static boolean isSoftFloat() {
return false;
}

static LibC getLibC() {
if (!isLinux()) {
return LibC.UNKNOWN;
}

try {
return getLibC(new FileReader("/proc/self/maps"));
} catch (IOException ex) {
return LibC.UNKNOWN;
} catch (SecurityException ex) {
return LibC.UNKNOWN;
}
}

/**
* Determines the libc implementation from process memory mappings. The supplied
* reader is consumed and closed.
*/
static LibC getLibC(Reader mapsReader) {
boolean glibc = false;
boolean musl = false;

try (BufferedReader reader = new BufferedReader(mapsReader)) {
String line;

while ((line = reader.readLine()) != null) {
String filename = getMappedFileName(line);

if (filename == null) {
continue;
}

String lower = filename.toLowerCase(Locale.ROOT);

if (lower.startsWith("libc.musl-") || lower.startsWith("ld-musl-")) {
musl = true;
}

if ("libc.so.6".equals(lower) || lower.startsWith("ld-linux")) {
glibc = true;
}

if (glibc && musl) {
return LibC.UNKNOWN;
}
}
} catch (IOException ex) {
return LibC.UNKNOWN;
} catch (SecurityException ex) {
return LibC.UNKNOWN;
}

if (glibc == musl) {
return LibC.UNKNOWN;
}

return musl ? LibC.MUSL : LibC.GLIBC;
}

private static String getMappedFileName(String line) {
// /proc/self/maps:
// address perms offset dev inode pathname
String[] fields = line.split("\\s+", 6);

if (fields.length < 6) {
return null;
}

String pathname = fields[5];
String deletedSuffix = " (deleted)";

if (pathname.endsWith(deletedSuffix)) {
pathname = pathname.substring(0, pathname.length() - deletedSuffix.length());
}

int lastSlash = pathname.lastIndexOf('/');
String filename = pathname.substring(lastSlash + 1);

return filename.isEmpty() ? null : filename;
}

/** Generate a canonical String prefix based on the current OS
type/arch/name.
*/
Expand Down
189 changes: 189 additions & 0 deletions test/com/sun/jna/LibraryLoadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.UUID;

import junit.framework.TestCase;

Expand Down Expand Up @@ -80,6 +81,194 @@ public void testExtractFromResourcePathWithNullClassLoader() throws Exception {
assertNotNull(Native.extractFromResourcePath("/com/sun/jna/LibraryLoadTest.class", null));
}

private static final String LIBC_SELECTION_LIBRARY = "jna-libc-selection-test";
private static final String LIBC_GLIBC_MARKER = "glibc variant";
private static final String LIBC_MUSL_MARKER = "musl variant";
private static final String LIBC_LEGACY_MARKER = "legacy variant";

private File createLibcSelectionClasspath() throws IOException {
File root = new File(Native.getTempDir(), "jna-libc-selection-" + UUID.randomUUID());

assertTrue("Could not create test classpath directory", root.mkdirs());

return root;
}

private void writeLibcSelectionResource(File root, String prefix, String marker) throws IOException {
String libraryName = NativeLibrary.mapSharedLibraryName(LIBC_SELECTION_LIBRARY);
File resource = new File(new File(root, prefix), libraryName);

assertTrue("Could not create test resource directory",
resource.getParentFile().exists() || resource.getParentFile().mkdirs());

FileOutputStream output = new FileOutputStream(resource);

try {
output.write(marker.getBytes("UTF-8"));
} finally {
output.close();
}
}

private String readLibcSelectionMarker(File resource) throws IOException {
FileInputStream input = new FileInputStream(resource);
try {
byte[] buffer = new byte[64];
int length = input.read(buffer);
return length < 0 ? "" : new String(buffer, 0, length, "UTF-8");
} finally {
input.close();
}
}

private void deleteRecursively(File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
}
file.delete();
}

private URLClassLoader createLibcSelectionLoader(File root) throws MalformedURLException {
return new URLClassLoader(new URL[] { root.toURI().toURL() }, new CloverLoader());
}

private void assertLibcSelection(File root, Platform.LibC libc, String expectedMarker) throws Exception {
URLClassLoader loader = createLibcSelectionLoader(root);

try {
File extracted = Native.extractFromResourcePath(LIBC_SELECTION_LIBRARY, loader, libc);

assertEquals(expectedMarker, readLibcSelectionMarker(extracted));
} finally {
loader.close();
}
}

private void assertLibcSelectionFails(File root, Platform.LibC libc) throws Exception {
URLClassLoader loader = createLibcSelectionLoader(root);

try {
try {
Native.extractFromResourcePath(LIBC_SELECTION_LIBRARY, loader, libc);

fail("Expected IOException when no compatible resource exists for libc " + libc);
} catch (IOException expected) {
// No compatible resource is available.
}
} finally {
loader.close();
}
}

public void testExtractPrefersMuslVariant() throws Exception {
File root = createLibcSelectionClasspath();
try {
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER);

assertLibcSelection(root, Platform.LibC.MUSL, LIBC_MUSL_MARKER);
} finally {
deleteRecursively(root);
}
}

public void testExtractPrefersGlibcVariant() throws Exception {
File root = createLibcSelectionClasspath();

try {
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER);

assertLibcSelection(root, Platform.LibC.GLIBC, LIBC_GLIBC_MARKER);
} finally {
deleteRecursively(root);
}
}

public void testExtractFallsBackToLegacyResource() throws Exception {
File root = createLibcSelectionClasspath();

try {
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER);

assertLibcSelection(root, Platform.LibC.MUSL, LIBC_LEGACY_MARKER);
} finally {
deleteRecursively(root);
}
}

public void testExtractSpecificVariantPrecedesLegacyResource() throws Exception {
File root = createLibcSelectionClasspath();

try {
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER);

assertLibcSelection(root, Platform.LibC.MUSL, LIBC_MUSL_MARKER);
} finally {
deleteRecursively(root);
}
}

public void testExtractFallsBackToLegacyWhenRequestedVariantIsMissing() throws Exception {
File root = createLibcSelectionClasspath();

try {
// A GLIBC variant must not be selected for a MUSL request.
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER);

assertLibcSelection(root, Platform.LibC.MUSL, LIBC_LEGACY_MARKER);
} finally {
deleteRecursively(root);
}
}

public void testExtractUnknownLibcFallsBackToLegacyResource() throws Exception {
File root = createLibcSelectionClasspath();

try {
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX, LIBC_LEGACY_MARKER);

assertLibcSelection(root, Platform.LibC.UNKNOWN, LIBC_LEGACY_MARKER);
} finally {
deleteRecursively(root);
}
}

public void testExtractUnknownLibcDoesNotSelectVariant() throws Exception {
File root = createLibcSelectionClasspath();

try {
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-musl", LIBC_MUSL_MARKER);
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER);

assertLibcSelectionFails(root, Platform.LibC.UNKNOWN);
} finally {
deleteRecursively(root);
}
}

public void testExtractDoesNotSelectOtherLibcVariant() throws Exception {
File root = createLibcSelectionClasspath();

try {
// Only GLIBC is available; MUSL must not select it.
writeLibcSelectionResource(root, Platform.RESOURCE_PREFIX + "-glibc", LIBC_GLIBC_MARKER);

assertLibcSelectionFails(root, Platform.LibC.MUSL);
} finally {
deleteRecursively(root);
}
}

public void testLoadFromJNALibraryPath() {
// Tests are already configured to load from this path
NativeLibrary.getInstance("testlib");
Expand Down
Loading