diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/darwin/DarwinMainCFRunLoopSupportImpl.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/darwin/DarwinMainCFRunLoopSupportImpl.java new file mode 100644 index 000000000000..f3f7c7ddebec --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/darwin/DarwinMainCFRunLoopSupportImpl.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 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.core.posix.darwin; + +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRunLoopAddTimer; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRunLoopGetCurrent; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRunLoopRunInMode; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRunLoopTimerCreate; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRelease; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.kCFAllocatorDefault; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.kCFRunLoopDefaultMode; +import static com.oracle.svm.core.posix.headers.darwin.CoreFoundation.kCFRunLoopRunFinished; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import org.graalvm.nativeimage.c.function.CEntryPoint; +import org.graalvm.nativeimage.c.function.CEntryPointLiteral; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.word.WordFactory; + +import com.oracle.svm.core.DarwinMainCFRunLoopSupport; +import com.oracle.svm.core.SubstrateOptions; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRunLoopTimerCallBack; +import com.oracle.svm.core.posix.headers.darwin.CoreFoundation.CFRunLoopTimerRef; +import com.oracle.svm.guest.staging.c.function.CEntryPointOptions; +import com.oracle.svm.guest.staging.c.function.CEntryPointOptions.NoEpilogue; +import com.oracle.svm.guest.staging.c.function.CEntryPointOptions.NoPrologue; +import com.oracle.svm.shared.Uninterruptible; +import com.oracle.svm.shared.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.AllAccess; +import com.oracle.svm.shared.singletons.traits.BuiltinTraits.SingleLayer; +import com.oracle.svm.shared.singletons.traits.SingletonLayeredInstallationKind.InitialLayerOnly; +import com.oracle.svm.shared.singletons.traits.SingletonTraits; +import com.oracle.svm.shared.util.VMError; + +import org.graalvm.nativeimage.ImageSingletons; + +/** + * Parks the process main thread in a CFRunLoop, matching OpenJDK libjli / + * {@code sdk/.../launcher.cc} {@code ParkEventLoop}. + * + * @see oracle/graal#13994 + */ +@Platforms(Platform.DARWIN.class) +@SingletonTraits(access = AllAccess.class, layeredCallbacks = SingleLayer.class, layeredInstallationKind = InitialLayerOnly.class) +public final class DarwinMainCFRunLoopSupportImpl implements DarwinMainCFRunLoopSupport { + + /** + * Far-future fire date so the timer exists only as a run-loop source (libjli pattern). + */ + private static final double FAR_FUTURE = 1.0e20; + + private static final CEntryPointLiteral DUMMY_TIMER_CALLBACK = CEntryPointLiteral.create(DarwinMainCFRunLoopSupportImpl.class, "dummyTimer", + CFRunLoopTimerRef.class, VoidPointer.class); + + @Override + @Uninterruptible(reason = "Called while the launcher thread is detached from the isolate.") + public void parkEventLoop() { + /* + * See OpenJDK java_md_macosx.m ParkEventLoop and Graal launcher.cc ParkEventLoop: the run + * loop needs at least one source; 1e20 is far into the future. + */ + CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault(), FAR_FUTURE, 0.0, 0, 0, DUMMY_TIMER_CALLBACK.getFunctionPointer(), WordFactory.nullPointer()); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode()); + CFRelease(timer); + + int result; + do { + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode(), FAR_FUTURE, false); + } while (result != kCFRunLoopRunFinished()); + } + + @SuppressWarnings("unused") + @Uninterruptible(reason = "Called from CFRunLoop.") + @CEntryPoint(include = DarwinParkMainInCFRunLoopEnabled.class) + @CEntryPointOptions(prologue = NoPrologue.class, epilogue = NoEpilogue.class) + static void dummyTimer(CFRunLoopTimerRef timer, VoidPointer info) { + /* Intentionally empty — timer exists only to keep the run loop alive. */ + } + + private static final class DarwinParkMainInCFRunLoopEnabled implements java.util.function.BooleanSupplier { + @Override + public boolean getAsBoolean() { + return SubstrateOptions.DarwinParkMainInCFRunLoop.getValue(); + } + } +} + +@Platforms(Platform.DARWIN.class) +@AutomaticallyRegisteredFeature +class DarwinMainCFRunLoopFeature implements InternalFeature { + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return SubstrateOptions.DarwinParkMainInCFRunLoop.getValue(); + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + if (!Platform.includedIn(Platform.DARWIN.class)) { + throw VMError.shouldNotReachHere("-H:+DarwinParkMainInCFRunLoop is only supported on Darwin"); + } + ImageSingletons.add(DarwinMainCFRunLoopSupport.class, new DarwinMainCFRunLoopSupportImpl()); + } +} diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/darwin/CoreFoundation.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/darwin/CoreFoundation.java new file mode 100644 index 000000000000..01f2fd3c5a88 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/darwin/CoreFoundation.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 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.core.posix.headers.darwin; + +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.constant.CConstant; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.function.CFunction.Transition; +import org.graalvm.nativeimage.c.function.CFunctionPointer; +import org.graalvm.nativeimage.c.function.CLibrary; +import org.graalvm.nativeimage.c.function.InvokeCFunctionPointer; +import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.word.PointerBase; + +import com.oracle.svm.core.posix.headers.darwin.CoreFoundationDirectives; + +// Checkstyle: stop + +/** + * Minimal CoreFoundation bindings for parking the Darwin main thread in a CFRunLoop, matching the + * pattern used by OpenJDK libjli and {@code org.graalvm.launcher.native} {@code ParkEventLoop}. + */ +@CContext(CoreFoundationDirectives.class) +@CLibrary("-framework CoreFoundation") +public class CoreFoundation { + + public interface CFTypeRef extends PointerBase { + } + + public interface CFAllocatorRef extends CFTypeRef { + } + + public interface CFRunLoopRef extends CFTypeRef { + } + + public interface CFRunLoopTimerRef extends CFTypeRef { + } + + public interface CFStringRef extends CFTypeRef { + } + + public interface CFRunLoopTimerCallBack extends CFunctionPointer { + @InvokeCFunctionPointer + void invoke(CFRunLoopTimerRef timer, VoidPointer info); + } + + @CConstant + public static native CFAllocatorRef kCFAllocatorDefault(); + + @CConstant + public static native CFStringRef kCFRunLoopDefaultMode(); + + @CConstant + public static native int kCFRunLoopRunFinished(); + + @CFunction(transition = Transition.NO_TRANSITION) + public static native CFRunLoopRef CFRunLoopGetCurrent(); + + @CFunction(transition = Transition.NO_TRANSITION) + public static native CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, double fireDate, double interval, long flags, long order, + CFRunLoopTimerCallBack callout, VoidPointer context); + + @CFunction(transition = Transition.NO_TRANSITION) + public static native void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); + + @CFunction(transition = Transition.NO_TRANSITION) + public static native int CFRunLoopRunInMode(CFStringRef mode, double seconds, boolean returnAfterSourceHandled); + + @CFunction(transition = Transition.NO_TRANSITION) + public static native void CFRelease(CFTypeRef cf); +} diff --git a/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/darwin/CoreFoundationDirectives.java b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/darwin/CoreFoundationDirectives.java new file mode 100644 index 000000000000..872e24348ef4 --- /dev/null +++ b/substratevm/src/com.oracle.svm.core.posix/src/com/oracle/svm/core/posix/headers/darwin/CoreFoundationDirectives.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 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.core.posix.headers.darwin; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.c.CContext; + +public class CoreFoundationDirectives implements CContext.Directives { + @Override + public boolean isInConfiguration() { + return Platform.includedIn(Platform.DARWIN.class); + } + + @Override + public List getHeaderFiles() { + return Collections.singletonList(""); + } + + @Override + public List getOptions() { + /* Info-query linking must resolve CF constants such as kCFAllocatorDefault. */ + return Arrays.asList("-framework", "CoreFoundation"); + } + + @Override + public List getLibraries() { + return Collections.singletonList("-framework CoreFoundation"); + } +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DarwinMainCFRunLoopSupport.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DarwinMainCFRunLoopSupport.java new file mode 100644 index 000000000000..b45cd781808e --- /dev/null +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/DarwinMainCFRunLoopSupport.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 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.core; + +import com.oracle.svm.shared.Uninterruptible; + +/** + * Parks the Darwin process main thread in a CoreFoundation run loop so AppKit/Metal work posted to + * the main thread can run while Java executes on a dedicated platform thread. + *

+ * Mirrors OpenJDK {@code libjli} / Graal {@code launcher.cc} {@code ParkEventLoop}. + */ +public interface DarwinMainCFRunLoopSupport { + + /** + * Blocks the current (process main) thread in {@code CFRunLoopRunInMode}. Under normal + * shutdown the Java main runner calls {@code LibC.exit} after waiting for non-daemon threads, + * so this method does not return. + */ + @Uninterruptible(reason = "Called while the launcher thread is detached from the isolate.") + void parkEventLoop(); +} diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java index 729e99806491..c2680f17e797 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/JavaMainWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -49,6 +49,7 @@ import org.graalvm.word.WordBase; import org.graalvm.word.impl.Word; +import com.oracle.svm.core.headers.LibC; import com.oracle.svm.core.jfr.events.ShutdownEvent; import com.oracle.svm.core.jni.JNIJavaVMList; import com.oracle.svm.core.jni.functions.JNIFunctionTables; @@ -78,6 +79,8 @@ import com.oracle.svm.shared.util.SubstrateUtil; import com.oracle.svm.shared.util.VMError; +import jdk.graal.compiler.api.replacements.Fold; + /** * Native-image Java launcher entry point and runtime control flow. *

@@ -244,13 +247,44 @@ private static void runShutdown0() { @CEntryPoint(include = CEntryPoint.NotIncludedAutomatically.class) @CEntryPointOptions(prologue = NoPrologue.class, epilogue = NoEpilogue.class) public static int run(int argc, CCharPointerPointer argv) { - if (SubstrateOptions.RunMainInNewThread.getValue()) { + if (SubstrateOptions.DarwinParkMainInCFRunLoop.getValue()) { + return doRunWithDarwinCFRunLoop(argc, argv); + } else if (SubstrateOptions.RunMainInNewThread.getValue()) { return doRunInNewThread(argc, argv); } else { return doRun(argc, argv); } } + /** + * Darwin headed AWT/Swing: run Java on a side thread and park the process main thread in a + * CFRunLoop so AppKit/Metal {@code performSelector:waitUntilDone:} can complete. Matches + * OpenJDK libjli and Graal {@code launcher.cc} {@code ParkEventLoop}. See + * oracle/graal#13994. + */ + @Uninterruptible(reason = "Thread state not setup yet.") + private static int doRunWithDarwinCFRunLoop(int argc, CCharPointerPointer argv) { + try { + Isolate isolate = createMainIsolate(argc, argv); + long javaStackSize = getJavaStackSize(); + /* + * The launcher parks in CFRunLoop (no-transition), so detach it before the runner can + * request VM operations. + */ + detachCurrentThread(); + startMainRunnerUnmanaged(isolate, javaStackSize); + darwinMainCFRunLoopSupport().parkEventLoop(); + throw VMError.shouldNotReachHere("Darwin CFRunLoop park returned; expected LibC.exit from the Java main runner"); + } catch (Throwable e) { + throw VMError.shouldNotReachHere(e); + } + } + + @Fold + static DarwinMainCFRunLoopSupport darwinMainCFRunLoopSupport() { + return ImageSingletons.lookup(DarwinMainCFRunLoopSupport.class); + } + /** SVM start-up logic should be pinned to the initial layer. */ @Uninterruptible(reason = "Thread state not setup yet.") private static int doRun(int argc, CCharPointerPointer argv) { @@ -337,6 +371,27 @@ private static int startAndJoinMainRunner0(Isolate isolate, long javaStackSize, } } + /** + * Starts the main runner without joining. Used with {@link SubstrateOptions#DarwinParkMainInCFRunLoop}: + * the launcher parks in CFRunLoop and the runner calls {@link LibC#exit} after shutdown. + */ + @Uninterruptible(reason = "Thread state detached.") + private static void startMainRunnerUnmanaged(Isolate isolate, long javaStackSize) { + CFunctionPointer runMainRoutine = RUN_MAIN_ROUTINE.getFunctionPointer(); + startMainRunnerUnmanaged0(isolate, javaStackSize, runMainRoutine); + } + + @Uninterruptible(reason = "Thread state detached.") + @LayeredCompilationBehavior(Behavior.PINNED_TO_INITIAL_LAYER) + private static void startMainRunnerUnmanaged0(Isolate isolate, long javaStackSize, CFunctionPointer runMainRoutine) { + OSThreadHandle osThreadHandle = PlatformThreads.singleton().startThreadUnmanaged(runMainRoutine, isolate, javaStackSize, true); + if (osThreadHandle.isNull()) { + CEntryPointActions.failFatally(1, START_THREAD_UNMANAGED_ERROR_MESSAGE.get()); + } + /* Joinable handle is intentionally not joined; process exits via LibC.exit from the runner. */ + PlatformThreads.singleton().closeOSThreadHandle(osThreadHandle); + } + @Uninterruptible(reason = "Thread state not setup yet.") @LayeredCompilationBehavior(Behavior.PINNED_TO_INITIAL_LAYER) private static Isolate createMainIsolate(int argc, CCharPointerPointer argv) { @@ -364,7 +419,7 @@ public boolean getAsBoolean() { if (!ImageSingletons.contains(JavaMainSupport.class)) { return false; } - return SubstrateOptions.RunMainInNewThread.getValue(); + return SubstrateOptions.RunMainInNewThread.getValue() || SubstrateOptions.DarwinParkMainInCFRunLoop.getValue(); } } @@ -390,6 +445,16 @@ static WordBase runMainRoutine(PointerBase data) { try { reassignMainThreadObject(); int exitStatus = runCore(); + if (SubstrateOptions.DarwinParkMainInCFRunLoop.getValue()) { + /* + * Wait for non-daemon threads and run isolate shutdown while the process main + * thread remains parked in CFRunLoop (DestroyJavaVM on the HotSpot/libjli side + * thread). Then terminate the process — matching Graal launcher.cc which calls + * exit() after the JVM main thread returns on Darwin. + */ + runShutdown(); + LibC.exit(exitStatus); + } CEntryPointSetup.LeaveDetachThreadEpilogue.leave(); return Word.signed(exitStatus); } catch (Throwable e) { diff --git a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java index 29b3937bebc0..7521b2998a62 100644 --- a/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java +++ b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/SubstrateOptions.java @@ -1472,6 +1472,12 @@ public Boolean getValue(OptionValues values) { } }; + @Option(help = """ + On Darwin, run the Java main entry point on a dedicated platform thread and park the process main thread in a CoreFoundation CFRunLoop. + + Uses the OpenJDK libjli / Graal launcher ParkEventLoop pattern. Required for headed AWT/Swing so AppKit/Metal work posted to the main thread can complete. Default false; experimental.""", type = Expert)// + public static final HostedOptionKey DarwinParkMainInCFRunLoop = new HostedOptionKey<>(false); + @Option(help = "Instead of abort, only warn if image builder classes are found on the image class-path.", type = OptionType.Debug, // deprecated = true, deprecationMessage = "This option was introduced to simplify migration to GraalVM 23.0 and will be removed in a future release")// public static final HostedOptionKey AllowDeprecatedBuilderClassesOnImageClasspath = new HostedOptionKey<>(false);