Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
74 changes: 63 additions & 11 deletions src_c/webkit_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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<void **>(&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;
Expand Down
6 changes: 4 additions & 2 deletions src_c/webkit_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
18 changes: 16 additions & 2 deletions src_c/webview_embed.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading