From efccd7519255ce0c22b07c4855fc6426039f90ca Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:32:50 +0700 Subject: [PATCH] Do not register class-cached FFIType memory with Cleaner. FFIType descriptors stay in typeInfoMap for the Class lifetime so native cif pointers remain valid. Registering that storage with the JNA Cleaner kept the cleaner thread looping after Structure instances were collected. Fixes #1633 --- CHANGES.md | 2 +- src/com/sun/jna/Structure.java | 30 +++++++++- test/com/sun/jna/FFITypeTest.java | 92 +++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 test/com/sun/jna/FFITypeTest.java diff --git a/CHANGES.md b/CHANGES.md index dd1b8f9b81..a66fd2bdcf 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 6a7d0c4dff..00fabe9cd9 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 0000000000..651ab62379 --- /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); + } +}