diff --git a/substratevm/mx.substratevm/mx_substratevm.py b/substratevm/mx.substratevm/mx_substratevm.py index 2b48bb315304..9ea1077d5eab 100644 --- a/substratevm/mx.substratevm/mx_substratevm.py +++ b/substratevm/mx.substratevm/mx_substratevm.py @@ -249,6 +249,16 @@ def _vm_home(config): def locale_US_args(): return ['-Duser.country=US', '-Duser.language=en'] + +def _is_post_merge_or_weekly_job(): + build_name = mx.get_env('BUILD_NAME', '').lower() + return 'post-merge' in build_name or 'weekly' in build_name + + +def _should_run_java_desktop_integration(): + tags = tuple(Task.tags or ()) + return _is_post_merge_or_weekly_job() or any(tag == GraalTags.headless_java_desktop_integration for tag in tags) + class Tags(set): def __getattr__(self, name): if name in self: @@ -261,6 +271,7 @@ def __getattr__(self, name): 'debuginfotest', 'standalone_pointsto_unittests', 'native_unittests', + 'headless_java_desktop_integration', 'build', 'benchmarktest', "nativeimagehelp", @@ -512,6 +523,14 @@ def svm_gate_body(args, tasks): with native_image_context(IMAGE_ASSERTION_FLAGS): native_unittests_task(args.extra_image_builder_arguments) + with Task('java.desktop integration tests', tasks, tags=[GraalTags.headless_java_desktop_integration]) as t: + if t and _should_run_java_desktop_integration(): + if mx.is_windows(): + mx.warn('Headless java.desktop integration test does not run on Windows') + else: + with native_image_context(IMAGE_ASSERTION_FLAGS) as native_image: + java_desktop_integration_task(native_image, args.extra_image_builder_arguments) + with Task('conditional configuration tests', tasks, tags=[GraalTags.condconfig]) as t: if t: with native_image_context(IMAGE_ASSERTION_FLAGS) as native_image: @@ -737,6 +756,16 @@ def native_unittests_task(extra_build_args=None): computed = _compute_native_unittest_args(extra_build_args, include_svm_test_features=True) native_image_context_run(_native_unittest, computed) + +def java_desktop_integration_task(native_image, extra_build_args=None): + build_args = _compute_native_unittest_args(extra_build_args, include_svm_test_features=False) + build_args += svm_experimental_options(['-H:Preserve=module=java.desktop']) + _native_unittest(native_image, [ + '--test-classes-per-run', '1', + 'com.oracle.svm.integrationtest.HeadlessJavaDesktopTest', + 'com.oracle.svm.integrationtest.NonHeadlessJavaDesktopTest', + ] + build_args) + def conditional_config_task(native_image): agent_path = build_native_image_agent(native_image) conditional_config_filter_path = join(svmbuild_dir(), 'conditional-config-filter.json') @@ -2529,6 +2558,7 @@ def collector(line): mx.logvv('Skipping line: ' + line.rstrip()) return collector + symbol_dump_command = '' if mx.is_windows(): symbol_dump_command = 'dumpbin /SYMBOLS' elif mx.is_darwin(): @@ -2537,7 +2567,6 @@ def collector(line): symbol_dump_command = 'objdump --wide --syms' else: mx.abort('gen_fallbacks not supported on ' + sys.platform) - raise AssertionError('unreachable') seen_gnu_property_type_5_warnings = False def suppress_gnu_property_type_5_warnings(line): diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index 176cb7df6693..b9474e42c0cb 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -1169,6 +1169,7 @@ ], "requires": [ "java.compiler", + "java.desktop", "jdk.jfr", "java.management", "jdk.management.jfr", diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/NativeLibraries.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/NativeLibraries.java index 57f310923f78..c6fd16df068d 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/NativeLibraries.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/NativeLibraries.java @@ -38,6 +38,7 @@ import org.graalvm.word.impl.Word; import com.oracle.svm.core.NeverInline; +import com.oracle.svm.core.OS; import com.oracle.svm.core.SubstrateOptions; import com.oracle.svm.core.snippets.KnownIntrinsics; import com.oracle.svm.shared.util.StringUtil; @@ -102,6 +103,9 @@ public void loadLibraryAbsolute(File file) { if (loadLibrary0(file, false)) { return; } + if (loadBuiltinDarwinLibraryAbsoluteFallback(file)) { + return; + } throw new UnsatisfiedLinkError("Can't load library: " + file); } @@ -151,6 +155,56 @@ private boolean loadLibrary0(File file, boolean builtin) { } } + private static String asBuiltinLibraryName(String fileName) { + if (OS.getCurrent() == OS.DARWIN && fileName.startsWith("lib") && fileName.endsWith(".dylib")) { + return fileName.substring("lib".length(), fileName.length() - ".dylib".length()); + } + return null; + } + + private boolean loadBuiltinDarwinLibraryAbsoluteFallback(File file) { + String builtInName = asBuiltinLibraryName(file.getName()); + if (builtInName == null) { + return false; + } + if (file.exists()) { + /* + * Preserve System.load() semantics for real files. The fallback is only + * intended for builtin JDK libraries whose image/java.home path does not exist as a + * loadable dylib in the generated image. + */ + return false; + } + try { + return isBuiltinDarwinLibraryLocation(file) && addLibrary(builtInName, true); + } catch (IOException e) { + return false; + } + } + + private static boolean isBuiltinDarwinLibraryLocation(File file) throws IOException { + if (OS.getCurrent() != OS.DARWIN) { + return false; + } + + File canonicalParent = file.getCanonicalFile().getParentFile(); + if (canonicalParent == null) { + return false; + } + + String imageDirectory = getImageDirectory(); + if (imageDirectory != null && canonicalParent.equals(new File(imageDirectory).getCanonicalFile())) { + return true; + } + + String javaHome = System.getProperty("java.home"); + if (javaHome != null && canonicalParent.equals(new File(javaHome, "lib").getCanonicalFile())) { + return true; + } + + return false; + } + protected abstract boolean addLibrary(String canonical, boolean builtin); public abstract PointerBase findSymbol(String name); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/JNILibraryInitializer.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/JNILibraryInitializer.java index 811a9389e3d8..30f2c38b191f 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/JNILibraryInitializer.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/JNILibraryInitializer.java @@ -66,8 +66,8 @@ private static String getOnLoadName(String libName, boolean isBuiltIn) { public boolean fillCGlobalDataMap(Collection staticLibNames) { List libsWithOnLoad = Arrays.asList("net", "java", "nio", "zip", "sunec", "jaas", "sctp", "extnet", - "j2gss", "j2pkcs11", "j2pcsc", "prefs", "verify", "awt", "awt_xawt", "awt_headless", "lcms", - "fontmanager", "javajpeg", "mlib_image", "attach"); + "j2gss", "j2pkcs11", "j2pcsc", "prefs", "verify", "awt", "awt_xawt", "awt_headless", "awt_lwawt", + "lcms", "fontmanager", "javajpeg", "mlib_image", "osxapp", "attach"); // TODO: This check should be removed when all static libs will have JNI_OnLoad function ArrayList localStaticLibNames = new ArrayList<>(staticLibNames); localStaticLibNames.retainAll(libsWithOnLoad); diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNINativeLinkage.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNINativeLinkage.java index f2c3e7d9728c..bc8412f297b2 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNINativeLinkage.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jni/access/JNINativeLinkage.java @@ -26,7 +26,7 @@ import static com.oracle.svm.core.jni.access.JNIReflectionDictionary.WRAPPED_CSTRING_EQUIVALENCE; -import java.util.function.Function; +import java.util.function.Supplier; import org.graalvm.nativeimage.c.function.CFunctionPointer; import org.graalvm.word.PointerBase; @@ -85,10 +85,10 @@ public boolean isBuiltInFunction() { return (PlatformNativeLibrarySupport.singleton().isBuiltinPkgNative(this.getShortName())); } - public CGlobalDataInfo getOrCreateBuiltInAddress(Function createSymbol) { + public CGlobalDataInfo getOrCreateBuiltInAddress(Supplier createSymbol) { assert isBuiltInFunction(); if (builtInAddress == null) { - builtInAddress = createSymbol.apply(getShortName()); + builtInAddress = createSymbol.get(); } return builtInAddress; } @@ -156,7 +156,7 @@ public PointerBase getOrFindEntryPoint() { return entryPoint; } - private String getShortName() { + public String getShortName() { StringBuilder sb = new StringBuilder("Java_"); mangleName(getDeclaringClassName(), 1, getDeclaringClassName().length() - 1, sb); sb.append('_'); @@ -164,6 +164,10 @@ private String getShortName() { return sb.toString(); } + public String getLongName() { + return getShortName() + "__" + getSignature(); + } + private String getSignature() { int closing = getDescriptor().indexOf(')'); assert getDescriptor().startsWith("(") && getDescriptor().indexOf(')') == closing && closing != -1; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterSupport.java index 381a8945a488..d84bca9f08e2 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ProgressReporterSupport.java @@ -134,7 +134,7 @@ private static boolean recommendTraceAgentForAWT() { if (!ImageSingletons.contains(JNIRegistrationSupport.class) || !ImageSingletons.contains(JNIReflectionDictionary.class)) { return false; } - if (!JNIRegistrationSupport.singleton().isRegisteredLibrary("awt")) { + if (!JNIRegistrationSupport.singleton().isAnyLayerRegisteredLibrary("awt")) { return false; // AWT not used } // check if any class located in java.awt or sun.awt is registered for JNI access diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationAWTSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationAWTSupport.java index 3c6a457eeec9..e87864889593 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationAWTSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationAWTSupport.java @@ -30,20 +30,43 @@ import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; import com.oracle.svm.core.feature.InternalFeature; import com.oracle.svm.core.jdk.JNIRegistrationUtil; +import com.oracle.svm.core.jdk.NativeLibrarySupport; +import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport; import com.oracle.svm.shared.singletons.traits.BuiltinTraits.BuildtimeAccessOnly; import com.oracle.svm.shared.singletons.traits.BuiltinTraits.NoLayeredCallbacks; import com.oracle.svm.shared.singletons.traits.BuiltinTraits.PartiallyLayerAware; import com.oracle.svm.shared.singletons.traits.SingletonTraits; import com.oracle.svm.hosted.FeatureImpl.BeforeImageWriteAccessImpl; +import com.oracle.svm.hosted.c.NativeLibraries; +import com.oracle.svm.util.dynamicaccess.JVMCIRuntimeJNIAccess; -@Platforms({Platform.WINDOWS.class, Platform.LINUX.class}) +import jdk.vm.ci.meta.ResolvedJavaMethod; + +@Platforms({Platform.WINDOWS.class, Platform.LINUX.class, Platform.DARWIN.class}) @SingletonTraits(access = BuildtimeAccessOnly.class, layeredCallbacks = NoLayeredCallbacks.class, other = PartiallyLayerAware.class) @AutomaticallyRegisteredFeature public class JNIRegistrationAWTSupport extends JNIRegistrationUtil implements InternalFeature { + private ResolvedJavaMethod systemLoadMethod; + private boolean headlessJavaDesktopSupportRegistered; + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + systemLoadMethod = method(access, "java.lang.System", "load", String.class); + if (isDarwin()) { + registerDarwinBuiltinPkgNatives(); + } + if (isLinux() || isDarwin()) { + JNIRegistrationSupport.singleton().addLibraryRegistrationHandler(this::registerHeadlessJavaDesktopSupport); + if (JNIRegistrationSupport.singleton().isPreviousLayerRegisteredLibrary("awt")) { + registerHeadlessJavaDesktopSupport("awt"); + } + } + } + @Override public void afterAnalysis(AfterAnalysisAccess access) { JNIRegistrationSupport jniRegistrationSupport = JNIRegistrationSupport.singleton(); - if (jniRegistrationSupport.isRegisteredLibrary("awt")) { + if (jniRegistrationSupport.isAnyLayerRegisteredLibrary("awt")) { jniRegistrationSupport.addJvmShimExports( "JVM_IsStaticallyLinked"); jniRegistrationSupport.addJavaShimExports( @@ -85,11 +108,11 @@ public void afterAnalysis(AfterAnalysisAccess access) { jniRegistrationSupport.registerLibrary("awt_xawt"); } } - if (jniRegistrationSupport.isRegisteredLibrary("javaaccessbridge")) { + if (jniRegistrationSupport.isAnyLayerRegisteredLibrary("javaaccessbridge")) { /* Dependency on `jawt` is not expressed in Java, so we register it manually here. */ jniRegistrationSupport.registerLibrary("jawt"); } - if (jniRegistrationSupport.isRegisteredLibrary("javajpeg")) { + if (jniRegistrationSupport.isAnyLayerRegisteredLibrary("javajpeg")) { jniRegistrationSupport.addJavaShimExports( "JNU_GetEnv", "JNU_ThrowByName", @@ -100,7 +123,22 @@ public void afterAnalysis(AfterAnalysisAccess access) { @Override public void beforeImageWrite(BeforeImageWriteAccess access) { - if (isWindows() && JNIRegistrationSupport.singleton().isRegisteredLibrary("awt")) { + if (isDarwin() && JNIRegistrationSupport.singleton().isAnyLayerRegisteredLibrary("awt")) { + ((BeforeImageWriteAccessImpl) access).registerLinkerInvocationTransformer(linkerInvocation -> { + linkerInvocation.addNativeLinkerOption("-Wl,-framework,AppKit"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,Accelerate"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,ApplicationServices"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,CoreText"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,JavaRuntimeSupport"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,QuartzCore"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,Metal"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,OpenGL"); + linkerInvocation.addNativeLinkerOption("-Wl,-framework,Security"); + linkerInvocation.addNativeLinkerOption("-lc++"); + return linkerInvocation; + }); + } + if (isWindows() && JNIRegistrationSupport.singleton().isAnyLayerRegisteredLibrary("awt")) { ((BeforeImageWriteAccessImpl) access).registerLinkerInvocationTransformer(linkerInvocation -> { /* * Add Windows libraries that are pulled in as a side effect of exporting the @@ -112,4 +150,41 @@ public void beforeImageWrite(BeforeImageWriteAccess access) { }); } } + + private static void registerDarwinBuiltinPkgNatives() { + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("java_awt_image"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("java_awt_Font"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("java_awt_Toolkit"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_awt_image"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_awt_CGraphicsDevice"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_awt_CGraphicsEnvironment"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_awt_PlatformGraphicsInfo"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_java2d"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_font"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("sun_lwawt_macosx_LWCToolkit"); + PlatformNativeLibrarySupport.singleton().addBuiltinPkgNativePrefix("com_sun_imageio_plugins_jpeg"); + } + + private void registerHeadlessJavaDesktopSupport(String libname) { + if (!"awt".equals(libname) || headlessJavaDesktopSupportRegistered) { + return; + } + headlessJavaDesktopSupportRegistered = true; + JVMCIRuntimeJNIAccess.register(systemLoadMethod); + if (isDarwin()) { + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("awt"); + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("awt_lwawt"); + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("osxapp"); + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("javajpeg"); + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("lcms"); + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("mlib_image"); + NativeLibrarySupport.singleton().preregisterUninitializedBuiltinLibrary("fontmanager"); + } + if (!isDarwin()) { + return; + } + NativeLibraries.singleton().addStaticJniLibrary("awt", "awt_lwawt", "javajpeg", "lcms", "mlib_image"); + NativeLibraries.singleton().addStaticJniLibrary("awt_lwawt", "osxapp"); + NativeLibraries.singleton().addStaticJniLibrary("fontmanager", "freetype"); + } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationSupport.java index a81be8e7bf23..08f982d13604 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JNIRegistrationSupport.java @@ -39,6 +39,7 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; import java.util.stream.Stream; import org.graalvm.nativeimage.ImageSingletons; @@ -107,6 +108,7 @@ public static class Options { private NativeLibraries nativeLibraries = null; private JNIRegistrationSupportSingleton jniRegistrationSupportSingleton = null; private boolean isSunMSCAPIProviderReachable = false; + private final List> libraryRegistrationHandlers = new CopyOnWriteArrayList<>(); public static JNIRegistrationSupport singleton() { return ImageSingletons.lookup(JNIRegistrationSupport.class); @@ -165,10 +167,17 @@ public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Rec void registerLibrary(String libname) { if (libname != null && !jniRegistrationSupportSingleton.currentLayerRegisteredLibraries.contains(libname)) { jniRegistrationSupportSingleton.currentLayerRegisteredLibraries.add(libname); + for (Consumer handler : libraryRegistrationHandlers) { + handler.accept(libname); + } addLibrary(libname); } } + void addLibraryRegistrationHandler(Consumer handler) { + libraryRegistrationHandlers.add(handler); + } + private void addLibrary(String libname) { /* * If a library is in our list of static standard libraries, add the library to the linker @@ -179,10 +188,18 @@ private void addLibrary(String libname) { } } - public boolean isRegisteredLibrary(String libname) { + boolean isCurrentLayerRegisteredLibrary(String libname) { return jniRegistrationSupportSingleton.currentLayerRegisteredLibraries.contains(libname); } + boolean isPreviousLayerRegisteredLibrary(String libname) { + return jniRegistrationSupportSingleton.prevLayerRegisteredLibraries.contains(libname); + } + + public boolean isAnyLayerRegisteredLibrary(String libname) { + return isCurrentLayerRegisteredLibrary(libname) || isPreviousLayerRegisteredLibrary(libname); + } + /** Adds exports that `jvm` shim should re-export. */ void addJvmShimExports(String... exports) { addShimExports("jvm", exports); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/DarwinBuiltinJNISymbolSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/DarwinBuiltinJNISymbolSupport.java new file mode 100644 index 000000000000..d3dd897fef30 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/DarwinBuiltinJNISymbolSupport.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.hosted.jni; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.graalvm.nativeimage.Platform; + +import com.oracle.svm.core.jni.access.JNINativeLinkage; +import com.oracle.svm.hosted.c.NativeLibraries; +import com.oracle.svm.hosted.c.util.FileUtils; +import com.oracle.svm.shared.util.LogUtils; + +final class DarwinBuiltinJNISymbolSupport { + private static final DarwinBuiltinJNISymbolSupport SINGLETON = new DarwinBuiltinJNISymbolSupport(); + + private volatile Set availableSymbols; + private volatile boolean scanFailed; + private final Set warnedSymbols = ConcurrentHashMap.newKeySet(); + + private DarwinBuiltinJNISymbolSupport() { + } + + static String builtInSymbolName(JNINativeLinkage linkage) { + return SINGLETON.getBuiltInSymbolName(linkage); + } + + private String getBuiltInSymbolName(JNINativeLinkage linkage) { + if (!Platform.includedIn(Platform.DARWIN.class) || !linkage.isBuiltInFunction()) { + return linkage.isBuiltInFunction() ? linkage.getShortName() : null; + } + if (scanFailed) { + return linkage.getShortName(); + } + Set symbols = getAvailableSymbols(); + String shortName = linkage.getShortName(); + if (symbols.contains(shortName)) { + return shortName; + } + String longName = linkage.getLongName(); + if (symbols.contains(longName)) { + return longName; + } + if (warnedSymbols.add(shortName)) { + LogUtils.warning("JNI built-in symbol %s is absent from the Darwin static JDK archives. Native Image will use runtime lookup for %s instead of a link-time reference.", + shortName, shortName); + } + return null; + } + + private Set getAvailableSymbols() { + Set symbols = availableSymbols; + if (symbols == null && !scanFailed) { + synchronized (this) { + symbols = availableSymbols; + if (symbols == null && !scanFailed) { + try { + symbols = scanAvailableSymbols(); + availableSymbols = symbols; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + scanFailed = true; + LogUtils.warning("Could not scan Darwin static JDK archives for JNI symbols. Built-in JNI methods will keep using link-time symbol references. Cause: %s", + e.getMessage()); + return Set.of(); + } + } + } + } + return symbols == null ? Set.of() : symbols; + } + + private static Set scanAvailableSymbols() throws IOException, InterruptedException { + LinkedHashSet symbols = new LinkedHashSet<>(); + for (String libraryPath : NativeLibraries.singleton().getLibraryPaths()) { + try (Stream paths = Files.list(Paths.get(libraryPath))) { + for (Path path : paths.filter(Files::isRegularFile).filter(candidate -> candidate.getFileName().toString().endsWith(".a")).toList()) { + symbols.addAll(readDefinedSymbols(path)); + } + } + } + return Set.copyOf(symbols); + } + + private static Set readDefinedSymbols(Path staticLibrary) throws IOException, InterruptedException { + List commandLine = List.of("nm", "--extern-only", "--defined-only", "--format=just-symbols", staticLibrary.toString()); + ProcessBuilder command = FileUtils.prepareCommand(commandLine, null).redirectErrorStream(true); + FileUtils.traceCommand(command); + Process process = command.start(); + try (Closeable _ = process::destroy; InputStream inputStream = process.getInputStream()) { + List lines = FileUtils.readAllLines(inputStream); + FileUtils.traceCommandOutput(lines); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Native symbol scan failed with exit code " + exitCode + " for " + staticLibrary.getFileName()); + } + LinkedHashSet symbols = new LinkedHashSet<>(); + for (String line : lines) { + String symbol = normalizeNmOutput(line); + if (symbol != null) { + symbols.add(symbol); + } + } + return symbols; + } + } + + private static String normalizeNmOutput(String line) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.endsWith(":")) { + return null; + } + if (trimmed.startsWith("_")) { + return trimmed.substring(1); + } + return trimmed; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNINativeCallWrapperMethod.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNINativeCallWrapperMethod.java index d3b5c85a9687..19fbdcbdeba6 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNINativeCallWrapperMethod.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jni/JNINativeCallWrapperMethod.java @@ -28,7 +28,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import com.oracle.graal.pointsto.ObjectScanner; import com.oracle.graal.pointsto.ObjectScanner.ScanReason; @@ -110,9 +109,9 @@ public StructuredGraph buildGraph(DebugContext debug, AnalysisMethod method, Hos JNIGraphKit kit = new JNIGraphKit(debug, providers, method); ValueNode callAddress; - if (linkage.isBuiltInFunction()) { - Function createSymbol = symbolName -> CGlobalDataFeature.singleton().registerAsAccessedOrGet(CGlobalDataFactory.forSymbol(symbolName)); - CGlobalDataInfo builtinAddress = linkage.getOrCreateBuiltInAddress(createSymbol); + String builtInSymbolName = DarwinBuiltinJNISymbolSupport.builtInSymbolName(linkage); + if (builtInSymbolName != null) { + CGlobalDataInfo builtinAddress = linkage.getOrCreateBuiltInAddress(() -> CGlobalDataFeature.singleton().registerAsAccessedOrGet(CGlobalDataFactory.forSymbol(builtInSymbolName))); callAddress = kit.unique(new CGlobalDataLoadAddressNode(builtinAddress)); ScanReason reason = new ObjectScanner.OtherReason("Manual rescan triggered for " + method.getQualifiedName()); diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/integrationtest/HeadlessJavaDesktopTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/integrationtest/HeadlessJavaDesktopTest.java new file mode 100644 index 000000000000..e0261397d2a9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/integrationtest/HeadlessJavaDesktopTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.integrationtest; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; + +import javax.imageio.ImageIO; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +public class HeadlessJavaDesktopTest { + @Test + public void headlessJavaDesktopSmokeTest() throws Exception { + String osName = System.getProperty("os.name", ""); + Assume.assumeTrue(osName.startsWith("Linux") || osName.startsWith("Mac")); + + System.setProperty("java.awt.headless", "true"); + + BufferedImage image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + graphics.setColor(Color.RED); + graphics.fillRect(0, 0, 4, 4); + graphics.dispose(); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Assert.assertTrue("No PNG writer available", ImageIO.write(image, "png", output)); + + byte[] bytes = output.toByteArray(); + Assert.assertTrue("PNG output is empty", bytes.length > 8); + Assert.assertEquals((byte) 0x89, bytes[0]); + Assert.assertEquals((byte) 'P', bytes[1]); + Assert.assertEquals((byte) 'N', bytes[2]); + Assert.assertEquals((byte) 'G', bytes[3]); + } +} diff --git a/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/integrationtest/NonHeadlessJavaDesktopTest.java b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/integrationtest/NonHeadlessJavaDesktopTest.java new file mode 100644 index 000000000000..3f7b97f55b13 --- /dev/null +++ b/substratevm/src/com.oracle.svm.test/src/com/oracle/svm/integrationtest/NonHeadlessJavaDesktopTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.svm.integrationtest; + +import java.awt.Dimension; +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; + +import javax.imageio.ImageIO; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +public class NonHeadlessJavaDesktopTest { + @Test + public void nonHeadlessJavaDesktopSmokeTest() throws Exception { + String osName = System.getProperty("os.name", ""); + Assume.assumeTrue(osName.startsWith("Linux")); + + String display = System.getenv("DISPLAY"); + Assume.assumeTrue(display != null && !display.isEmpty()); + + System.clearProperty("java.awt.headless"); + Assume.assumeFalse(GraphicsEnvironment.isHeadless()); + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Dimension screenSize = toolkit.getScreenSize(); + Assert.assertTrue("Invalid screen width: " + screenSize.width, screenSize.width > 0); + Assert.assertTrue("Invalid screen height: " + screenSize.height, screenSize.height > 0); + + BufferedImage image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_ARGB); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Assert.assertTrue("No PNG writer available", ImageIO.write(image, "png", output)); + + byte[] bytes = output.toByteArray(); + Assert.assertTrue("PNG output is empty", bytes.length > 8); + Assert.assertEquals((byte) 0x89, bytes[0]); + Assert.assertEquals((byte) 'P', bytes[1]); + Assert.assertEquals((byte) 'N', bytes[2]); + Assert.assertEquals((byte) 'G', bytes[3]); + } +}