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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Features

Bug Fixes
---------

* Allow discarded library class loaders to be collected on JVMs by associating cached options with their classes. Android retains the existing cache for compatibility with runtimes without `ClassValue`. See [#1742](https://github.com/java-native-access/jna/issues/1742).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow the established format you can see in the entry directly below this entry.

* [#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).

Breaking Changes
Expand Down
3 changes: 3 additions & 0 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@

<target name=":jar">
<uptodate property="-jar" targetfile="${build}/${jar}">
<srcfiles dir="${src}" includes="META-INF/proguard/*.pro"/>
<srcfiles dir="${classes}">
<patternset id="jar-compiled">
<include name="com/sun/jna/*"/>
Expand Down Expand Up @@ -450,6 +451,7 @@ osname=macosx;processor=aarch64
<requires module="java.desktop" />
</ModuleGenerator>
<jar jarfile="${build}/${jar}" duplicate="preserve" createUnicodeExtraFields="never" encoding="UTF-8" manifest="${build}/manifest/automatic.mf">
<zipfileset dir="${src}" includes="META-INF/proguard/*.pro"/>
<fileset dir="${classes}" excludes="${jar.omitted}">
<patternset refid="jar-compiled"/>
</fileset>
Expand Down Expand Up @@ -587,6 +589,7 @@ osname=macosx;processor=aarch64
<!-- Dummy entries in R.txt and the string resources are needed or a grails
build using this library will fail (tested with Android Studio 2.2.3 -->
<mkdir dir="${build.aar}/res/values" />
<copy file="${src}/META-INF/proguard/jna.pro" tofile="${build.aar}/proguard.txt"/>
<echo file="${build.aar}/res/values/values.xml"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="jna_library_appname">JNA bindings</string>
Expand Down
6 changes: 6 additions & 0 deletions src/META-INF/proguard/jna.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) 2026 curforever. All Rights Reserved.
# Dual-licensed under LGPL 2.1 or later and Apache License 2.0.
# See LGPL2.1 and AL2.0 distributed with JNA.
# Android selects the legacy options cache and never loads the JVM-only backend.
# Older Android boot class paths do not declare this optional Java API.
-dontwarn java.lang.ClassValue
52 changes: 51 additions & 1 deletion src/com/sun/jna/Native.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import java.util.Map;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
Expand Down Expand Up @@ -147,7 +148,56 @@ public final class Native implements Version {

// Used by tests, do not remove
static String jnidispatchPath = null;
private static final Map<Class<?>, Map<String, Object>> typeOptions = Collections.synchronizedMap(new WeakHashMap<Class<?>, Map<String, Object>>());
private static final TypeOptions typeOptions = TypeOptionsCache.create(Platform.isAndroid());

private interface TypeOptions {
Map<String, Object> get(Class<?> type);
void put(Class<?> type, Map<String, Object> options);
}

private static class TypeOptionsCache {
static TypeOptions create(boolean android) {
// Older Android runtimes do not provide java.lang.ClassValue.
return android ? new WeakMapTypeOptions() : new ClassValueTypeOptions();
}
}

private static class WeakMapTypeOptions implements TypeOptions {
private final Map<Class<?>, Map<String, Object>> options =
Collections.synchronizedMap(new WeakHashMap<Class<?>, Map<String, Object>>());

@Override
public Map<String, Object> get(Class<?> type) {
return options.get(type);
}

@Override
public void put(Class<?> type, Map<String, Object> value) {
options.put(type, value);
}
}

private static class ClassValueTypeOptions implements TypeOptions {
// Values may refer to the class or its loader (e.g. a custom TypeMapper).
// ClassValue keeps that cycle local to the class, rather than in a static map.
private final ClassValue<AtomicReference<Map<String, Object>>> options =
new ClassValue<AtomicReference<Map<String, Object>>>() {
@Override
protected AtomicReference<Map<String, Object>> computeValue(Class<?> type) {
return new AtomicReference<>();
}
};

@Override
public Map<String, Object> get(Class<?> type) {
return options.get(type).get();
}

@Override
public void put(Class<?> type, Map<String, Object> value) {
options.get(type).set(value);
}
}
private static final Map<Class<?>, Reference<?>> libraries = Collections.synchronizedMap(new WeakHashMap<Class<?>, Reference<?>>());
private static final String _OPTION_ENCLOSING_LIBRARY = "enclosing-library";
private static final UncaughtExceptionHandler DEFAULT_HANDLER =
Expand Down
124 changes: 124 additions & 0 deletions test/com/sun/jna/NativeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
*/
package com.sun.jna;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.Arrays;
Expand All @@ -38,6 +42,126 @@
//@SuppressWarnings("unused")
public class NativeTest extends TestCase {


public interface CollectibleLibrary extends Library {
int abs(int value);

class Mapper extends DefaultTypeMapper {
}

@Structure.FieldOrder({"value"})
class Value extends Structure {
public int value;
}
}

private static class CollectibleClassLoader extends ClassLoader {
CollectibleClassLoader() {
super(NativeTest.class.getClassLoader());
}

@Override
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (!name.startsWith(CollectibleLibrary.class.getName())) {
return super.loadClass(name, resolve);
}
Class<?> type = findLoadedClass(name);
if (type == null) {
String resource = "/" + name.replace('.', '/') + ".class";
try (InputStream stream = NativeTest.class.getResourceAsStream(resource)) {
if (stream == null) {
throw new ClassNotFoundException(name);
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = stream.read(buffer)) != -1) {
bytes.write(buffer, 0, length);
}
byte[] content = bytes.toByteArray();
type = defineClass(name, content, 0, content.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
if (resolve) {
resolveClass(type);
}
return type;
}
}

@SuppressWarnings("unchecked")
private WeakReference<ClassLoader> loadCollectibleLibrary(boolean withMapper,
boolean withLoader) throws Exception {
ClassLoader loader = new CollectibleClassLoader();
Class<? extends Library> type = (Class<? extends Library>)
loader.loadClass(CollectibleLibrary.class.getName());
Map<String, Object> options = new HashMap<>();
if (withMapper) {
Object mapper = loader.loadClass(CollectibleLibrary.Mapper.class.getName())
.getDeclaredConstructor().newInstance();
assertSame(loader, mapper.getClass().getClassLoader());
options.put(Library.OPTION_TYPE_MAPPER, mapper);
}
if (withLoader) {
options.put(Library.OPTION_CLASSLOADER, loader);
}
Library library = Native.load(Platform.C_LIBRARY_NAME, type, options);
assertEquals(7, type.getMethod("abs", int.class).invoke(library, -7));
Class<?> nested = loader.loadClass(CollectibleLibrary.Value.class.getName());
assertSame(loader, type.getClassLoader());
assertSame(loader, nested.getClassLoader());
assertSame(Native.getLibraryOptions(type), Native.getLibraryOptions(nested));
assertSame(type, Native.findEnclosingLibraryClass(nested));
assertSame(options.get(Library.OPTION_TYPE_MAPPER), Native.getTypeMapper(nested));
return new WeakReference<>(loader);
}

private void assertClassLoaderCollected(WeakReference<ClassLoader> loader) throws Exception {
for (int i = 0; i < 100 && loader.get() != null; i++) {
System.gc();
Thread.sleep(50);
}
assertNull("Discarded library class loader is retained by cached options", loader.get());
}

public void testDiscardedLibraryClassLoader() throws Exception {
assertClassLoaderCollected(loadCollectibleLibrary(false, false));
}

public void testDiscardedLibraryClassLoaderWithTypeMapper() throws Exception {
assertClassLoaderCollected(loadCollectibleLibrary(true, false));
}

public void testDiscardedLibraryClassLoaderWithLoaderOption() throws Exception {
assertClassLoaderCollected(loadCollectibleLibrary(false, true));
}

@SuppressWarnings("unchecked")
public void testLibraryOptionsRetainedWhileClassIsLive() throws Exception {
ClassLoader loader = new CollectibleClassLoader();
Class<? extends Library> type = (Class<? extends Library>)
loader.loadClass(CollectibleLibrary.class.getName());
Object mapper = loader.loadClass(CollectibleLibrary.Mapper.class.getName())
.getDeclaredConstructor().newInstance();
Map<String, Object> options = new HashMap<>();
options.put(Library.OPTION_TYPE_MAPPER, mapper);
Library library = Native.load(Platform.C_LIBRARY_NAME, type, options);
Class<?> nested = loader.loadClass(CollectibleLibrary.Value.class.getName());
Map<String, Object> cached = Native.getLibraryOptions(type);
assertSame(cached, Native.getLibraryOptions(nested));
for (int i = 0; i < 3; i++) {
System.gc();
assertSame(cached, Native.getLibraryOptions(type));
assertSame(cached, Native.getLibraryOptions(nested));
assertSame(mapper, Native.getTypeMapper(nested));
assertSame(type, Native.findEnclosingLibraryClass(nested));
assertEquals(7, type.getMethod("abs", int.class).invoke(library, -7));
}
}

private static final String UNICODE = "[\u0444]";

public void testLoadLibraryMethods() throws Exception {
Expand Down