From bce20af03f646997c6dca8894d7058ab7e0a1bf8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 21:12:22 +0000 Subject: [PATCH 1/2] Report both WebKitGTK load errors and publish them to Java (Canvas 8) The Linux loader kept only the last dlerror(), so a 4.1 library that was installed but failed to load read exactly like a host with no WebKitGTK. Each attempt's reason is now kept, for WebKit and JavaScriptCore alike. JNI_OnLoad's JNI_ERR makes the JVM throw a generic "unsupported JNI version 0xFFFFFFFF", so the reason only ever reached stderr. The loader now also sets the system property ca.weblite.webview.nativeLoadError before failing, so an embedder can tell the user why the WebView is unavailable. Publishing is best effort and never changes the outcome. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KJGbJfSqGDbZWCbUnoKYVx --- ...t]-Native-Library-Loading-And-Packaging.md | 29 ++++++-- src_c/webkit_loader.cpp | 74 ++++++++++++++++--- src_c/webkit_loader.h | 6 +- 3 files changed, 91 insertions(+), 18 deletions(-) diff --git a/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md b/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md index ddac28e..15064eb 100644 --- a/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md +++ b/spdd/prompt/8-20260516-0719-[Feat]-Native-Library-Loading-And-Packaging.md @@ -498,9 +498,24 @@ Files: `src_c/webkit_loader.h`, `src_c/webkit_loader.cpp`, - On any failure — either library not `dlopen`able, or a required symbol missing — fail with a clear message naming **both** candidate SONAMEs of the library that could not be - satisfied. Runs from `JNI_OnLoad`; returning `JNI_ERR` makes - `System.load` throw with that message (same load-time failure - point as before, now diagnosable on both 4.0 and 4.1 hosts). + satisfied, **each with its own `dlerror()` text** (the 4.1 + attempt's reason is captured before the 4.0 attempt overwrites + it). Without the first reason, a host whose 4.1 library is + installed but fails to load (a missing dependency, a broken + install) reads exactly like a host with no WebKitGTK at all. + The message buffer is large enough (1024 bytes) to hold both + reasons. + - Runs from `JNI_OnLoad`, which returns `JNI_ERR`. The JVM then + throws a **generic** `UnsatisfiedLinkError` ("unsupported JNI + version 0xFFFFFFFF") — it does not carry the loader's message. + So, before returning `JNI_ERR`, the loader both prints the + message to stderr and publishes it to Java as the system + property `ca.weblite.webview.nativeLoadError` (via + `vm->GetEnv` and `System.setProperty`; any JNI failure while + publishing is cleared and ignored, so publishing can never + change the outcome). On a successful load the property is never + set. This is the one way an embedder can tell the user *why* + the WebView is unavailable rather than only *that* it is. 3. Call-site routing (`webkit_shim.h`): included by `webview.h` and `webview_embed.cpp` immediately after the WebKit/JSC/GTK headers, @@ -749,8 +764,12 @@ Files: `pom.xml`, `.github/workflows/maven-release.yml` neither `libwebkit2gtk-4.1.so.0` nor `libwebkit2gtk-4.0.so.37` (respectively the two JavaScriptCore SONAMEs) can be `dlopen`ed, or a required symbol is missing, `JNI_OnLoad` returns `JNI_ERR` - and `System.load` throws `UnsatisfiedLinkError` with a message - naming both candidate SONAMEs. This replaces the old + and `System.load` throws `UnsatisfiedLinkError`. The loader's own + message — naming both candidate SONAMEs, each with its own + `dlerror()` reason — goes to stderr and to the system property + `ca.weblite.webview.nativeLoadError`, because the JVM's exception + text is generic. The property name is public contract: embedders + read it to explain the failure, so it is never renamed. This replaces the old dynamic-linker error — which named only the 4.1 SONAME and made a 4.0 host look broken — with a message that makes the missing-runtime case obvious on either host. diff --git a/src_c/webkit_loader.cpp b/src_c/webkit_loader.cpp index b0367f4..1f19a7e 100644 --- a/src_c/webkit_loader.cpp +++ b/src_c/webkit_loader.cpp @@ -29,25 +29,35 @@ const char *const kJsc40 = "libjavascriptcoregtk-4.0.so.18"; void *g_webkit_handle = nullptr; void *g_jsc_handle = nullptr; -char g_err[512] = {0}; +char g_err[1024] = {0}; // Resolve everything exactly once. Returns true on success; on failure writes // a diagnostic to g_err and returns false. Called only through the C++11 // thread-safe function-local static in webkit_loader_ensure(). bool do_init() { + // dlerror() only reports the most recent failure, so each attempt's reason + // is copied out before the next dlopen overwrites it. Without the first + // reason, a 4.1 library that is installed but fails to load reads exactly + // like a host with no WebKitGTK at all. + char first_err[384] = {0}; + const char *e = nullptr; + // 1. WebKit: prefer 4.1, fall back to 4.0. bool is_41 = true; g_webkit_handle = dlopen(kWebKit41, RTLD_NOW | RTLD_GLOBAL); if (!g_webkit_handle) { + e = dlerror(); + std::snprintf(first_err, sizeof(first_err), "%s", e ? e : "unknown error"); g_webkit_handle = dlopen(kWebKit40, RTLD_NOW | RTLD_GLOBAL); is_41 = false; } if (!g_webkit_handle) { + e = dlerror(); std::snprintf(g_err, sizeof(g_err), "swingwebview: unable to load the WebKitGTK runtime — tried " - "'%s' then '%s' (%s). Install libwebkit2gtk-4.1-0 (Ubuntu " - "22.04+) or libwebkit2gtk-4.0-37 (Ubuntu 20.04).", - kWebKit41, kWebKit40, dlerror()); + "'%s' (%s) then '%s' (%s). Install libwebkit2gtk-4.1-0 " + "(Ubuntu 22.04+) or libwebkit2gtk-4.0-37 (Ubuntu 20.04).", + kWebKit41, first_err, kWebKit40, e ? e : "unknown error"); return false; } @@ -56,13 +66,17 @@ bool do_init() { const char *jsc_secondary = is_41 ? kJsc40 : kJsc41; g_jsc_handle = dlopen(jsc_primary, RTLD_NOW | RTLD_GLOBAL); if (!g_jsc_handle) { + e = dlerror(); + std::snprintf(first_err, sizeof(first_err), "%s", e ? e : "unknown error"); g_jsc_handle = dlopen(jsc_secondary, RTLD_NOW | RTLD_GLOBAL); } if (!g_jsc_handle) { + e = dlerror(); std::snprintf(g_err, sizeof(g_err), "swingwebview: unable to load the JavaScriptCore runtime — " - "tried '%s' then '%s' (%s).", - kJsc41, kJsc40, dlerror()); + "tried '%s' (%s) then '%s' (%s).", + jsc_primary, first_err, jsc_secondary, + e ? e : "unknown error"); return false; } @@ -114,14 +128,52 @@ bool webkit_loader_ensure(char *errbuf, size_t errlen) { return ok; } -// Resolve at library load time. Returning JNI_ERR makes System.load throw an -// UnsatisfiedLinkError carrying our diagnostic — the same load-time failure -// point as the old hard DT_NEEDED, but now naming both candidate SONAMEs. +namespace { + +// Publish the loader's diagnostic as the system property +// ca.weblite.webview.nativeLoadError. Returning JNI_ERR makes the JVM throw a +// generic UnsatisfiedLinkError ("unsupported JNI version 0xFFFFFFFF") that +// does not carry our message, so this property is how an embedder learns WHY +// the WebView is unavailable. Best effort: any JNI failure here is cleared and +// ignored, so publishing can never change the outcome of the load. +void publish_load_error(JavaVM *vm, const char *msg) { + JNIEnv *env = nullptr; + if (!vm || vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != + JNI_OK || !env) { + return; + } + jclass system = env->FindClass("java/lang/System"); + jmethodID set_property = + system ? env->GetStaticMethodID( + system, "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;") + : nullptr; + jstring key = + set_property ? env->NewStringUTF("ca.weblite.webview.nativeLoadError") + : nullptr; + jstring value = key ? env->NewStringUTF(msg) : nullptr; + if (value) { + jobject previous = + env->CallStaticObjectMethod(system, set_property, key, value); + if (previous) env->DeleteLocalRef(previous); + } + if (env->ExceptionCheck()) env->ExceptionClear(); + if (value) env->DeleteLocalRef(value); + if (key) env->DeleteLocalRef(key); + if (system) env->DeleteLocalRef(system); +} + +} // namespace + +// Resolve at library load time. Returning JNI_ERR makes System.load fail at +// the same load-time point as the old hard DT_NEEDED. The JVM's exception text +// is generic, so the diagnostic — both candidate SONAMEs, each with its own +// dlerror() reason — goes to stderr and to ca.weblite.webview.nativeLoadError. extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) { - (void)vm; - char err[512]; + char err[1024]; if (!webkit_loader_ensure(err, sizeof(err))) { std::fprintf(stderr, "%s\n", err); + publish_load_error(vm, err); return JNI_ERR; } return JNI_VERSION_1_6; diff --git a/src_c/webkit_loader.h b/src_c/webkit_loader.h index 6be224b..e687ee7 100644 --- a/src_c/webkit_loader.h +++ b/src_c/webkit_loader.h @@ -176,8 +176,10 @@ extern WkFns g_wk; // Resolve the WebKit/JSC runtime once (idempotent, thread-safe). Returns true // on success. On failure, if errbuf/errlen are non-null, copies a diagnostic -// message naming the candidate SONAMEs. Invoked from JNI_OnLoad; callers do -// not normally need it directly. +// message naming the candidate SONAMEs, each with its own dlerror() reason +// (pass a buffer of at least 1024 bytes). Invoked from JNI_OnLoad, which also +// publishes the message as the system property +// ca.weblite.webview.nativeLoadError; callers do not normally need it directly. bool webkit_loader_ensure(char *errbuf, size_t errlen); #endif // WEBVIEW_GTK || __linux__ From f8ab9194dc097f2635ea3aadbea7678e4c4db9fe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 22:56:47 +0000 Subject: [PATCH 2/2] Never switch on Xlib threads after a GDK display exists (Canvas 7) Xlib needs XInitThreads() before any display is opened. The jDeploy launcher initialises GTK for its splash before the JVM starts, so the pump's gtk_init reuses a display that has no locks; the pump's late XInitThreads() then makes Xlib lock it, and the first cursor WebKit loads (Xcursor -> XGetDefault -> XrmQGetResource) calls pthread_mutex_lock(NULL) on libX11 1.7.x (Ubuntu 22.04): a SIGSEGV on focusing any text field, seen with a core dump. GtkPump::ensure_started now calls XInitThreads() only when gdk_display_get_default() is NULL, i.e. when nothing has opened a GDK display yet. Reproduced on Ubuntu 22.04 / libX11 1.7.5 with a minimal GTK program: late XInitThreads -> SIGSEGV; skipped -> survives. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KJGbJfSqGDbZWCbUnoKYVx --- ...at]-Swing-Lightweight-Webview-Embedding.md | 33 ++++++++++++++++++- src_c/webview_embed.cpp | 18 ++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md b/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md index ffb11fe..1b08764 100644 --- a/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md +++ b/spdd/prompt/7-20260516-0719-[Feat]-Swing-Lightweight-Webview-Embedding.md @@ -315,6 +315,26 @@ generated_at: 2026-05-16T07:19:13-07:00 also makes the pump thread the one that creates the default context. WebKit symbols are already resolved at `JNI_OnLoad`, so the call is always safe there. +- **Xlib threads are never switched on late.** Xlib requires + `XInitThreads()` to be the first Xlib call in a process: on + libX11 1.7.x (Ubuntu 22.04) a display — and its resource + database — opened before it has no locks, and once the call is + made Xlib starts locking every display, so the lock-less one is + locked through a NULL mutex. The jDeploy launcher initialises + GTK in-process for its splash before the JVM starts; the pump's + `gtk_init` then reuses that display, and the first cursor WebKit + loads (the I-beam on a text-field focus: Xcursor → `XGetDefault` + → `XrmQGetResource`) kills the process with SIGSEGV. So + `GtkPump::ensure_started` calls `XInitThreads()` only when no + GDK display is open yet in the process — checked with + `gdk_display_get_default()` before `gtk_init`. When the host has + already initialised GTK, the pump leaves Xlib exactly as the + host set it up and reuses the host's display (if the host made + Xlib thread-safe first, as the jDeploy launcher now does, the + display is already locked; if not, it stays lock-less, which is + the state the process already ran in). On libX11 ≥ 1.8 every + display is thread-safe by default and both branches are + equivalent. - **DevTools call is single-shot.** No state added on the Java side. The native `webview_offscreen_open_devtools` returns 0 if `enable_developer_extras` is FALSE on the @@ -451,7 +471,9 @@ generated_at: 2026-05-16T07:19:13-07:00 exported via `Java_..._webview_1offscreen_1execute_1editing_1command`. `embed::GtkPump::ensure_started` holds the never-released reference on the default `WebKitWebContext` (see Approach, - issue #64). + issue #64), and calls `XInitThreads()` only when no GDK display + is open yet (see Approach, "Xlib threads are never switched on + late"). - `demos/WebViewExitSmokeTest/src/ca/weblite/webview/demos/WebViewExitSmokeTest.java` and `run-linux-exit-smoketest.sh` — regression smoke test for issue #64: creates a LIGHTWEIGHT component, loads a `data:` page, @@ -1197,6 +1219,15 @@ Files: messages (e.g. WebKit `load-failed`) still print unconditionally. ## S · Safeguards +- **Never call `XInitThreads()` after a GDK display exists + (never-relax).** `GtkPump::ensure_started` makes the call only + when `gdk_display_get_default()` is NULL before `gtk_init`. + Calling it unconditionally re-introduces the SIGSEGV in + `pthread_mutex_lock` (Xcursor → `XGetDefault`) on libX11 1.7.x + whenever the host process initialised GTK first — every + jDeploy-launched app with a launcher older than the fix. + *Added 2026-10-02, from a defect: Ubuntu 22.04 / libX11 1.7.5 / + WebKitGTK 2.50.4, core dump from the affected VM.* - **The default `WebKitWebContext` is never finalized.** The pump-thread reference taken in `GtkPump::ensure_started` is intentionally leaked — never unref it, and never route views diff --git a/src_c/webview_embed.cpp b/src_c/webview_embed.cpp index 141578e..23449d2 100644 --- a/src_c/webview_embed.cpp +++ b/src_c/webview_embed.cpp @@ -526,13 +526,27 @@ class GtkPump { std::condition_variable ready_cv; bool ready = false; thread = std::thread([&] { - // X11 must be told we're going to use it from multiple threads. - XInitThreads(); // Embedding via XReparentWindow requires a real X11 GdkDisplay // on both sides. Force the X11 backend in case GTK would // otherwise pick Wayland or some other backend (e.g. in a // Parallels / virtual desktop session that exposes both). + // Must precede the first gdk_display_manager_get(), which the + // gdk_display_get_default() check below performs; it makes no + // Xlib call. gdk_set_allowed_backends("x11"); + // X11 must be told we're going to use it from multiple threads -- + // but only if no display exists yet (Canvas 7, "Xlib threads are + // never switched on late"). Xlib requires XInitThreads() to be + // the first Xlib call: on libX11 1.7.x a display opened before it + // has no locks, and calling it afterwards makes Xlib lock that + // display through a NULL mutex. The jDeploy launcher initialises + // GTK for its splash before the JVM starts, and gtk_init below + // would reuse that display -- the first cursor WebKit then loads + // (Xcursor -> XGetDefault) crashes the process. When the host + // already opened a GDK display, leave Xlib as the host set it up. + if (gdk_display_get_default() == nullptr) { + XInitThreads(); + } // Use the simple input-method module rather than the system // default (ibus / fcitx / etc.). On the offscreen embed // path the system IMs were observed to commit special-key