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: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Features

Bug Fixes
---------

* [#1633](https://github.com/java-native-access/jna/issues/1633): Do not register class-cached `FFIType` native memory with the JNA Cleaner, so the cleaner thread can exit after Structure instances are collected - [@arimu1](https://github.com/arimu1).

Release 5.19.1
==============
Expand Down
30 changes: 29 additions & 1 deletion src/com/sun/jna/Structure.java
Original file line number Diff line number Diff line change
Expand Up @@ -2131,6 +2131,34 @@ public FFIType(FFIType reference) {

public FFIType() {}

/**
* Native {@code ffi_type} descriptors are cached for the lifetime of
* the corresponding {@link Class} (see {@link #typeInfoMap}).
* Registering that storage with {@link com.sun.jna.internal.Cleaner}
* would keep the cleaner thread alive until the Class is unloaded,
* which typically never happens.
*/
static class TypeInfoMemory extends Memory {
TypeInfoMemory(long size) {
super();
this.size = size;
if (size <= 0) {
throw new IllegalArgumentException("Allocation size must be greater than zero");
}
peer = malloc(size);
if (peer == 0) {
throw new OutOfMemoryError("Cannot allocate " + size + " bytes");
}
}
}

@Override
protected Memory autoAllocate(int size) {
TypeInfoMemory memory = new TypeInfoMemory(size);
memory.clear();
return memory;
}

public FFIType(Structure ref) {
Pointer[] els;
ref.ensureAllocated(true);
Expand Down Expand Up @@ -2203,7 +2231,7 @@ public FFIType(Object array, Class<?> type) {
}

private void init(Pointer[] els) {
elements = new Memory(Native.POINTER_SIZE * els.length);
elements = new TypeInfoMemory(Native.POINTER_SIZE * els.length);
elements.write(0, els, 0, els.length);
write();
}
Expand Down
92 changes: 92 additions & 0 deletions test/com/sun/jna/FFITypeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* Copyright (c) 2026 arimu1, All Rights Reserved
*
* The contents of this file is dual-licensed under 2
* alternative Open Source/Free licenses: LGPL 2.1 or later and
* Apache License 2.0. (starting with JNA version 4.0.0).
*
* You can freely decide which license you want to apply to
* the project.
*
* You may obtain a copy of the LGPL License at:
*
* http://www.gnu.org/licenses/licenses.html
*
* A copy is also included in the downloadable source code package
* containing JNA, in file "LGPL2.1".
*
* You may obtain a copy of the Apache License at:
*
* http://www.apache.org/licenses/
*
* A copy is also included in the downloadable source code package
* containing JNA, in file "AL2.0".
*/
package com.sun.jna;

import java.lang.reflect.Field;

import com.sun.jna.Structure.FFIType;
import com.sun.jna.Structure.FieldOrder;

import junit.framework.TestCase;

/**
* {@code FFIType} descriptors are cached by Class in {@code typeInfoMap}.
* That cache is intentional (native cif pointers must remain valid), but the
* backing native memory must not be registered with the JNA Cleaner, or the
* cleaner thread never exits for ordinary application classes. See
* https://github.com/java-native-access/jna/issues/1633
*/
public class FFITypeTest extends TestCase {

@FieldOrder({ "field" })
public static class NativeWrapper extends Structure {
public boolean field;
}

public void testStructureFfiTypeDoesNotRegisterCleaner() throws Exception {
NativeWrapper nw = new NativeWrapper();
FFIType ffi = Structure.getTypeInfo(nw);

Field cleanable = Memory.class.getDeclaredField("cleanable");
cleanable.setAccessible(true);

Pointer backing = ffi.getPointer();
assertTrue("FFIType should allocate Memory", backing instanceof Memory);
assertTrue("Class-cached FFIType should use TypeInfoMemory",
backing instanceof FFIType.TypeInfoMemory);
assertNull("Class-cached FFIType memory must not register with Cleaner",
cleanable.get(backing));

assertNotNull("FFIType.elements should be allocated", ffi.elements);
assertTrue("FFIType.elements should allocate Memory",
ffi.elements instanceof Memory);
assertTrue("Class-cached FFIType.elements should use TypeInfoMemory",
ffi.elements instanceof FFIType.TypeInfoMemory);
assertNull("Class-cached FFIType.elements must not register with Cleaner",
cleanable.get(ffi.elements));
}

public void testFfiTypeSizeMatchesStructure() {
NativeWrapper nw = new NativeWrapper();
Pointer p = Structure.getTypeInfo(nw).getPointer();
assertEquals("libffi should accept the cached FFIType",
nw.size(), Native.initialize_ffi_type(p.peer));
}

public void testStructureFfiTypeRemainsCachedAfterInstanceGc() {
NativeWrapper nw = new NativeWrapper();
Pointer typeInfo = Structure.getTypeInfo(nw).getPointer();
nw = null;
System.gc();
Memory.purge();

FFIType again = Structure.getTypeInfo(new NativeWrapper());
assertSame("typeInfoMap should keep the Class-cached FFIType",
typeInfo, again.getPointer());
}

public static void main(String[] args) {
junit.textui.TestRunner.run(FFITypeTest.class);
}
}