diff --git a/CHANGES.md b/CHANGES.md index dd1b8f9b8..a66fd2bdc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 ============== diff --git a/src/com/sun/jna/Structure.java b/src/com/sun/jna/Structure.java index 6a7d0c4df..00fabe9cd 100644 --- a/src/com/sun/jna/Structure.java +++ b/src/com/sun/jna/Structure.java @@ -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); @@ -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(); } diff --git a/test/com/sun/jna/FFITypeTest.java b/test/com/sun/jna/FFITypeTest.java new file mode 100644 index 000000000..651ab6237 --- /dev/null +++ b/test/com/sun/jna/FFITypeTest.java @@ -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); + } +}