#13994·graal

[Native Image][Darwin] Swing GUI hangs: pthread main does not run AppKit CFRunLoop (Metal/CGL deadlock)

Author: The-AlchemistCreated Jul 16, 2026Updated Sep 7, 2026
Labelsbugnative-image

Summary

On GraalVM CE nightly (Darwin aarch64), building a Swing GUI app with -H:Preserve=module=java.desktop successfully copies AWT dylibs and creates libjvm/libjava shims (past the link failure in #13272). At runtime the process still hangs forever with no window: the EDT blocks in Metal (or CGL) init waiting on the AppKit main thread, while pthread main is parked in SubstrateVM and never runs a CFRunLoop / [NSApplication run].

HotSpot runs the same bytecode without hanging (main stays in AppKit’s run loop via libjli).

Working repository

A full working macOS setup (GraalVM CE nightly build script, MacSwingLauncher AppKit CFRunLoop workaround, reachability metadata, and build/ runtime layout) is here:

https://github.com/The-Alchemist/swing-graalvm-demo

See especially:

Environment

  • GraalVM: native-image 25.0.3 / GraalVM CE 25.2.4-dev+9.1 (build 25.0.3+9-jvmci-25.1-b19)
  • OS: macOS 26.5.2 (25F84), arm64
  • App: FlatLaf demo (SwingUtilities.invokeLater → show JFrame), also reproducible with a minimal frame

Build (succeeds; artifacts present)

bash
native-image \
  --add-opens=java.desktop/sun.lwawt.macosx=ALL-UNNAMED \
  -H:+UnlockExperimentalVMOptions \
  -H:Preserve=module=java.desktop \
  -Djava.awt.headless=false \
  -jar app.jar demo

Build artifacts include e.g. libawt.dylib, libawt_lwawt.dylib, libjvm.dylib (shim), libjava.dylib (shim) next to the executable.

Run without workaround

bash
./demo
# process stays alive, ~0% CPU, no window

Stacks (sample)

AWT-EventQueue-0 (abbreviated):

Java_sun_java2d_metal_MTLGraphicsConfig_tryLoadMetalLibrary  (libawt_lwawt.dylib)
  -[NSObject performSelector:onThread:withObject:waitUntilDone:modes:]
    -[_NSThreadPerformInfo wait]
      _pthread_cond_wait

(With -Dsun.java2d.metal=false, the same pattern appears on Java_sun_java2d_opengl_CGLGraphicsConfig_getCGLConfigInfo.)

pthread main / com.apple.main-thread:

(demo) …
  _pthread_cond_wait
    __psynch_cvwait

Main is not in CFRunLoop / [NSApplication run].

HotSpot contrast

Same jar under the same JDK: main samples in [NSApplication run] / CFRunLoop; UI appears.

Related

  • #4124 — @petermz noted Cocoa requires the run loop on pthread main and that libjli parks main in the RunLoop while Java runs elsewhere; a prior SVM JavaMainWrapper port was mentioned
  • #13272 / #13282 — Darwin AWT library copy / shims (behavior present in CE nightlies that include that path); this report assumes that part already works
  • Non-headless coverage added around #13282 does not appear to cover the common Swing pattern: invokeLater + return from main

Workaround

After scheduling the UI, keep pthread main inside AWT’s nested run loop:

java
// needs --add-opens=java.desktop/sun.lwawt.macosx=ALL-UNNAMED
Class<?> lwc = Class.forName("sun.lwawt.macosx.LWCToolkit");
var create = lwc.getDeclaredMethod("createAWTRunLoopMediator");
create.setAccessible(true);
var loop = lwc.getDeclaredMethod("doAWTRunLoop", long.class, boolean.class);
loop.setAccessible(true);
while (true) {
  long mediator = (Long) create.invoke(null);
  loop.invoke(null, mediator, true);
}

With that, Metal init completes and the window shows. Note: System.exit can stall while main is inside NSApp run; Runtime.halt(0) was needed for clean quit in our testing.

Minimal repro sketch

java
import javax.swing.*;

public class SwingHang {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
      JFrame f = new JFrame("native-image darwin");
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      f.setSize(400, 300);
      f.setVisible(true);
    });
    // return: SVM parks main without AppKit CFRunLoop → hang
  }
}

Questions

  1. Should SubstrateVM mirror libjli on Darwin (RunLoop on pthread main, Java work on a side thread)?
  2. Or should Darwin GUI native-image docs require apps to pump LWCToolkit.doAWTRunLoop (or equivalent) on main after startup?

Happy to attach fuller sample output or a small public repro if useful.