Skip to content

Commit 03211b7

Browse files
committed
Preserve JNA native and module compatibility in affinity initialisation
1 parent 8f2f16b commit 03211b7

5 files changed

Lines changed: 78 additions & 13 deletions

File tree

affinity/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
<artifactId>junit-jupiter</artifactId>
7878
<scope>test</scope>
7979
</dependency>
80+
<dependency>
81+
<groupId>net.java.dev.jna</groupId>
82+
<artifactId>jna-jpms</artifactId>
83+
<version>5.17.0</version>
84+
<scope>test</scope>
85+
</dependency>
8086

8187
<dependency>
8288
<groupId>org.slf4j</groupId>

affinity/src/main/java/net/openhft/affinity/Affinity.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import java.io.PrintWriter;
1212
import java.io.StringWriter;
13+
import java.lang.invoke.MethodHandles;
1314
import java.lang.reflect.Field;
1415
import java.util.BitSet;
1516

@@ -176,6 +177,7 @@ public static void setThreadId() {
176177
}
177178
}
178179

180+
@SuppressWarnings("removal") // ThreadDeath must propagate on supported older JDKs.
179181
public static boolean isJNAAvailable() {
180182
Boolean available = jnaAvailable;
181183
if (available == null) {
@@ -185,12 +187,10 @@ public static boolean isJNAAvailable() {
185187
boolean result;
186188
try {
187189
Class<?> nativeClass = Class.forName("com.sun.jna.Native");
188-
Field versionField = nativeClass.getField("VERSION");
189-
// Older JNA versions declare this public field on a package-private interface.
190-
versionField.setAccessible(true);
191-
Object versionObj = versionField.get(null);
192-
String version = versionObj == null ? "0" : versionObj.toString();
193-
int majorVersion = Integer.parseInt(version.split("\\.")[0]);
190+
// Access the inherited public field through Native without opening JNA's module.
191+
String version = (String) MethodHandles.publicLookup()
192+
.findStaticGetter(nativeClass, "VERSION", String.class).invokeExact();
193+
int majorVersion = version == null ? 0 : Integer.parseInt(version.split("\\.")[0]);
194194
if (majorVersion < 5) {
195195
LOGGER.warn("Affinity library requires JNA version >= 5");
196196
result = false;
@@ -202,7 +202,10 @@ public static boolean isJNAAvailable() {
202202
result = false;
203203
}
204204
}
205-
} catch (ReflectiveOperationException | LinkageError | RuntimeException t) {
205+
} catch (VirtualMachineError | ThreadDeath | AssertionError fatal) {
206+
throw fatal;
207+
} catch (Throwable t) {
208+
// JNA also reports an incompatible jnidispatch with a plain Error.
206209
LOGGER.warn("JNA not available, falling back to NullAffinity", t);
207210
result = false;
208211
}

affinity/src/test/java/net/openhft/affinity/AffinityInitializationTest.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@
44
package net.openhft.affinity;
55

66
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.condition.EnabledForJreRange;
78
import org.junit.jupiter.api.condition.EnabledOnOs;
9+
import org.junit.jupiter.api.condition.JRE;
810
import org.junit.jupiter.api.condition.OS;
911

1012
import java.io.File;
13+
import java.io.InputStream;
1114
import java.net.URL;
1215
import java.net.URLClassLoader;
16+
import java.nio.file.Files;
17+
import java.util.Arrays;
18+
import java.util.jar.JarEntry;
19+
import java.util.jar.JarFile;
1320

1421
import static org.junit.jupiter.api.Assertions.*;
1522

@@ -34,13 +41,46 @@ void fallsBackWhenJnaNativeLoadingFails() throws Exception {
3441
}
3542

3643
@Test
37-
void propagatesVmAndAssertionFailures() throws Exception {
44+
void fallsBackWhenJnaNativeVersionIsIncompatible() throws Exception {
45+
// The newer fixture's native ABI is incompatible with the BOM's JNA 5.5 Java classes.
46+
try (JarFile jar = new JarFile(jnaModuleJar())) {
47+
String resource = "com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX + "/libjnidispatch.so";
48+
JarEntry entry = jar.getJarEntry(resource);
49+
assertNotNull(entry, resource);
50+
try (InputStream input = jar.getInputStream(entry)) {
51+
Files.copy(input, directory.resolve("libjnidispatch.so"));
52+
}
53+
}
54+
String log = runProbe(AffinityInitializationTest.class, "native-failure", true,
55+
"-Djna.boot.library.path=" + directory.toAbsolutePath(),
56+
"-Djna.nosys=true", "-Djna.noclasspath=true");
57+
assertTrue(log.contains("java.lang.Error:"), log);
58+
assertTrue(log.contains("incompatible JNA native library"), log);
59+
}
60+
61+
@Test
62+
@EnabledForJreRange(min = JRE.JAVA_9)
63+
void selectsJnaFromExportedButUnopenedModule() throws Exception {
64+
runProbe(AffinityInitializationTest.class, "module", false,
65+
"--module-path=" + jnaModuleJar(), "--add-modules=com.sun.jna");
66+
}
67+
68+
private static String jnaModuleJar() {
69+
return Arrays.stream(testClasspath().split(File.pathSeparator))
70+
.filter(entry -> new File(entry).getName().startsWith("jna-jpms-"))
71+
.findFirst().orElseThrow(() -> new AssertionError("Missing JNA module test fixture"));
72+
}
73+
74+
@Test
75+
@SuppressWarnings("removal") // ThreadDeath remains relevant to supported older JDKs.
76+
void propagatesVmThreadTerminationAndAssertionFailures() throws Exception {
3877
String[] entries = testClasspath().split(File.pathSeparator);
3978
URL[] urls = new URL[entries.length];
4079
for (int i = 0; i < entries.length; i++) {
4180
urls[i] = new File(entries[i]).toURI().toURL();
4281
}
43-
for (Error failure : new Error[]{new OutOfMemoryError("test loading failure"), new AssertionError("test loading failure")}) {
82+
for (Error failure : new Error[]{new OutOfMemoryError("test loading failure"), new ThreadDeath(),
83+
new AssertionError("test loading failure")}) {
4484
try (URLClassLoader loader = new URLClassLoader(urls, null) {
4585
@Override
4686
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
@@ -66,10 +106,17 @@ public static void main(String[] args) throws Exception {
66106
} else {
67107
assertNotNull(Class.forName("com.sun.jna.Native", false, loader));
68108
}
69-
if (scenario.equals("normal")) {
109+
if (scenario.equals("normal") || scenario.equals("module")) {
70110
assertTrue(Affinity.isJNAAvailable());
71111
assertEquals("net.openhft.affinity.impl.LinuxJNAAffinity", Affinity.getAffinityImpl().getClass().getName());
72112
assertFalse(Affinity.getAffinity().isEmpty());
113+
if (scenario.equals("module")) {
114+
// Reflection here keeps these tests compilable on Java 8; the module run requires Java 9+.
115+
Object module = Class.class.getMethod("getModule").invoke(Class.forName("com.sun.jna.Native"));
116+
assertEquals(Boolean.TRUE, module.getClass().getMethod("isNamed").invoke(module));
117+
assertEquals(Boolean.TRUE, module.getClass().getMethod("isExported", String.class).invoke(module, "com.sun.jna"));
118+
assertEquals(Boolean.FALSE, module.getClass().getMethod("isOpen", String.class).invoke(module, "com.sun.jna"));
119+
}
73120
} else {
74121
assertFalse(Affinity.isJNAAvailable());
75122
assertEquals("net.openhft.affinity.impl.NullAffinity", Affinity.getAffinityImpl().getClass().getName());

affinity/src/test/java/net/openhft/affinity/AffinityTestProcess.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.stream.Collectors;
1717

1818
import static org.junit.jupiter.api.Assertions.*;
19+
import static org.junit.jupiter.api.Assumptions.assumeFalse;
1920

2021
abstract class AffinityTestProcess {
2122
@TempDir
@@ -25,7 +26,7 @@ static String testClasspath() {
2526
return System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"));
2627
}
2728

28-
void runProbe(Class<?> probe, String scenario, boolean withJna, String... options) throws Exception {
29+
String runProbe(Class<?> probe, String scenario, boolean withJna, String... options) throws Exception {
2930
String classpath = Arrays.stream(testClasspath().split(File.pathSeparator))
3031
.filter(entry -> withJna || !new File(entry).getName().startsWith("jna-"))
3132
.collect(Collectors.joining(File.pathSeparator));
@@ -53,7 +54,9 @@ void runProbe(Class<?> probe, String scenario, boolean withJna, String... option
5354
assertEquals(0, process.exitValue(), () -> "Failed: " + command + "\n" + log);
5455
assertFalse(log.contains("WARNING in native method"), log);
5556
assertFalse(log.contains("FATAL ERROR in native method"), log);
56-
assertTrue(log.contains("PASS " + scenario), log);
5757
System.out.print(log);
58+
assumeFalse(log.contains("SKIP " + scenario), log);
59+
assertTrue(log.contains("PASS " + scenario), log);
60+
return log;
5861
}
5962
}

affinity/src/test/java/net/openhft/affinity/NativeAffinityIntegrationTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import net.openhft.affinity.impl.LinuxJNAAffinity;
77
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
89
import org.junit.jupiter.api.condition.EnabledOnOs;
910
import org.junit.jupiter.api.condition.OS;
1011
import software.chronicle.enterprise.internals.impl.NativeAffinity;
@@ -16,6 +17,8 @@
1617
import static org.junit.jupiter.api.Assumptions.assumeFalse;
1718

1819
@EnabledOnOs(OS.LINUX)
20+
// Match the make-c profile's ARM32 exclusion; aarch64 remains eligible.
21+
@DisabledIfSystemProperty(named = "os.arch", matches = "(?i)arm")
1922
public class NativeAffinityIntegrationTest extends AffinityTestProcess {
2023
@Test
2124
void roundTripsShortMask() throws Exception {
@@ -62,7 +65,10 @@ public static void main(String[] args) {
6265
if (scenario.equals("short-mask") || scenario.equals("jna-parity")) {
6366
BitSet single = new BitSet();
6467
single.set(original.nextSetBit(0));
65-
assertTrue(single.toByteArray().length < 128, "A permitted CPU must fit in a short cpu_set_t mask");
68+
if (scenario.equals("short-mask") && single.toByteArray().length >= 128) {
69+
System.out.println("SKIP short-mask: no permitted CPU fits in fewer than 128 bytes");
70+
return;
71+
}
6672
nativeAffinity.setAffinity(single);
6773
assertEquals(single, nativeAffinity.getAffinity());
6874
if (scenario.equals("jna-parity")) {

0 commit comments

Comments
 (0)