| Author | BlinkGTK-Readium project (consumer/embedder perspective, #121) |
| Target version | BlinkGTK 1.2.1 (Chromium 152.0.7977.64) |
| Scope | A user manual that includes what an embedder learned running BlinkGTK in production |
The authority on API semantics is the public header
blink_gtk/blink_gtk.h and
upstream review.
BlinkGTK is a library for embedding Chromium's Blink rendering engine
into an
application as a GTK4 widget. It offers a C API
(BlinkWebView) with the same
design philosophy as WebKitGTK. A GTK4 application can display web
content,
navigate, talk to JavaScript, and serve custom URI schemes.
You do not need to build Chromium yourself — link
against the prebuilt
package (shared libraries + headers + pkg-config). This manual is
written from
the perspective of an embedder running BlinkGTK in production as an EPUB
viewer
(BlinkGTK-Readium).
Top to bottom:
BlinkWebView inside
it as a childGtkWidget subclassThe embedder only touches layers 1 and 2; Chromium's process model
stays behind
the curtain. That said, the pitfalls in §1.5 (sandbox, profile, RUNPATH)
all
stem from this multi-process structure, so being aware it exists makes
trouble
much faster to diagnose.
#include <blink_gtk/blink_gtk.h>pkg-config --cflags --libs blinkgtk-0.1Include only
blink_gtk.h.On v1.2.0-build5 and earlier, the pre-GObject internal header
blink_web_view.h(2025)
was also bundled. Including it alongsideblink_gtk.hmakes the same typedef
name refer to two different types, which fails to compile every time:error: conflicting types for 'BlinkWebView'; have 'struct BlinkWebView'There is no reason to ship a header that cannot be used, so it has not been
bundled since then. If you need somethingblink_gtk.hdoes not offer,
please raise an issue.Your application should include
blink_gtk/blink_gtk.hand nothing else.The header is gone since v1.2.0-build6 (verified 2026-09-03 by unpacking
every devel package published at that time). The
headers shipped today areblink_gtk.h,blinkgtk_export.hand
blinkgtk_version.h.
Every public BlinkGTK API may be called only from the GTK
main thread
(see the Thread Safety Policy at the top of the header).
Callbacks — JavaScript results, cookies, printing — are likewise always
invoked
on the GTK main thread. From a worker thread, dispatch to the main
thread with
g_idle_add().
An embedder process moves through these phases in one direction.
| Phase | Main API | Notes |
|---|---|---|
| (0) Pre-init configuration | blink_gtk_set_resources_path() and friends |
Must be called before
blink_gtk_init() |
| (1) Initialization | blink_gtk_init() |
Once, on the main thread, before any GTK/GLib call |
| (2) Creating the WebView | blink_web_view_new() /
_new_with_gpu_mode() / _new_container() |
Returns a GtkWidget |
| (3) Registration before load | blink_web_view_register_custom_scheme_full(),
register_message_handler(),
g_signal_connect(load-changed) |
Finish this before load_uri() |
| (4) Placement and display | gtk_window_set_child(),
gtk_window_present() |
Treat it as an ordinary GTK4 widget |
| (5) Load | blink_web_view_load_uri() |
Progress arrives on the load-changed signal |
| (6) Main loop | blink_gtk_run_main_loop() /
blink_gtk_quit_main_loop() |
Chromium's RunLoop, not GTK's |
| (7) Shutdown | blink_gtk_shutdown() |
After run_main_loop() returns (calling it
explicitly is recommended; see below) |
Whether and when to call
blink_gtk_shutdown()(the official position from
the upstream#123review)
How you call it Assessment After run_main_loop()returnsRecommended. Teardown order becomes deterministic (the complete example below takes this form) While the RunLoop is running, e.g. from a window close handler Safe. It does not deadlock; it switches to a deferred shutdown that runs once run_main_loop()returnsNot at all It works (the atexitsafety net catches it), but teardown is left to atexit and the order is non-deterministic. Not recommendedNothing breaks if you skip it because an
atexithandler is registered once
ContentMainRunner::Runsucceeds, so teardown happens automatically while the
AtExitManager is alive (issue#102-C). That is also why embedders that never
called it historically kept working. Even so, call it explicitly so the order
is deterministic.
The header states explicitly that these must be called
before
blink_gtk_init().
void blink_gtk_set_devtools_locale(const char* locale); /* NULL = follow the system locale */
const char* blink_gtk_get_devtools_locale(void);
void blink_gtk_set_icu_data_path(const char* path); /* absolute path to icudtl.dat */
void blink_gtk_set_resources_path(const char* path); /* root of the pak/snapshot files */You normally do not need any of them: the installed location
<prefix>/lib/chromium/ is detected automatically. Use
set_resources_path()
only for a non-standard layout, such as bundling the runtime inside your
own
application. If both are given, resources_path takes
precedence over
set_icu_data_path().
gboolean blink_gtk_init(int* argc, char*** argv);Passing argc/argv lets Chromium-style
command-line flags (--no-sandbox and
so on) be parsed here. FALSE means initialization failed.
Call it exactly once,
on the main thread, before any other GTK/GLib call.
GtkWidget* blink_web_view_new(void);
GtkWidget* blink_web_view_new_with_gpu_mode(BlinkGpuMode mode);
GtkWidget* blink_web_view_new_container(void);
BlinkGpuMode blink_web_view_get_gpu_mode(BlinkWebView* web_view);typedef enum {
BLINK_GPU_MODE_SOFTWARE = 0, /* CPU rendering only (default; no GPU needed) */
BLINK_GPU_MODE_SWIFTSHADER = 1, /* GL emulation via SwiftShader */
BLINK_GPU_MODE_EGL = 2, /* native EGL, hardware GPU */
} BlinkGpuMode;GtkWidget*. Use the
BLINK_WEB_VIEW() /BLINK_IS_WEB_VIEW() macros for type-safe access.new_container() is a convenience that builds a
GtkOverlay + GtkPictureBlinkWebView* withg_object_get_data(overlay, "blinkgtk-webview").BLINK_GPU_MODE_SOFTWARE (it is what we use).BLINKGTK_GPU_MODE in Chapter 9 and Chapter 5
(rendering) for detail.Before calling load_uri(), put in place everything that
receives events
originating from the content.
/* Custom URI scheme (binary-capable variant; issue #103) */
void blink_web_view_register_custom_scheme_full(
BlinkWebView* web_view, const char* scheme,
BlinkCustomSchemeBytesCallback callback, gpointer user_data);
/* JS -> C messages (window.blinkgtk.postMessage) */
void blink_web_view_register_message_handler(
BlinkWebView* web_view, const char* name,
BlinkMessageCallback callback, gpointer user_data);Load events arrive on the GObject signal
"load-changed".
typedef enum {
BLINK_LOAD_STARTED = 0,
BLINK_LOAD_COMMITTED = 1,
BLINK_LOAD_FINISHED = 2,
BLINK_LOAD_REDIRECTED = 3 /* reserved; the current runtime never emits it */
} BlinkLoadEvent;From the consumer's experience:
BLINK_LOAD_REDIRECTED is reserved for the
future and is not emitted today. Also, the integer values of the enum
were made
explicit in v1.0.10 iter14 to match the values actually
emitted — before
that, a build existed in which ev == BLINK_LOAD_FINISHED
silently evaluated to
false. Do not mix old packages with new ones.
BlinkWebView is an ordinary GTK4 widget: place it
with
gtk_window_set_child(), and call
blink_web_view_load_uri() after
gtk_window_present().
void blink_web_view_load_uri(BlinkWebView* web_view, const char* uri);
void blink_web_view_load_html(BlinkWebView* web_view, const char* html, const char* base_uri);int blink_gtk_run_main_loop(void); /* 0 = success. Blocks until it returns */
void blink_gtk_quit_main_loop(void);This runs Chromium's base::RunLoop, not
g_application_run(). The design
avoids a collision between the GTK and Chromium lifecycles and keeps
the
shutdown order under control. GLib's g_timeout_add() /
g_idle_add() still
fire under this loop (we use GLib timeouts routinely for recording
and
diagnostics). Call blink_gtk_quit_main_loop() from wherever
you want to end
it, such as a window's close-request.
void blink_gtk_shutdown(void);Call it after run_main_loop() returns, just before the
process exits.
A compilable example, boiled down from the real flow of a production
embedder
(BlinkGTK-Readium's readium-launcher). It serves a local directory over
the
custom scheme app:// and receives load completion through a
signal.
/* minimal-embedder.c — a minimal BlinkGTK embedder (GTK4) */
#include <blink_gtk/blink_gtk.h>
#include <gtk/gtk.h>
#include <string.h>
/* Serve app://host/<path> from the current directory (binary is fine). NULL = 404 */
static GBytes* scheme_cb(BlinkWebView* wv, const char* uri,
char** out_mime, gpointer ud) {
(void)wv; (void)ud;
const char* p = strstr(uri, "://");
if (!p) return NULL;
const char* slash = strchr(p + 3, '/'); /* the '/' after the host */
const char* rel = slash ? slash + 1 : "index.html";
if (*rel == '\0') rel = "index.html";
if (strstr(rel, "..")) return NULL; /* path-traversal guard */
char* contents = NULL; gsize len = 0;
if (!g_file_get_contents(rel, &contents, &len, NULL)) return NULL;
if (out_mime && g_str_has_suffix(rel, ".html"))
*out_mime = g_strdup("text/html"); /* NULL = guess from the URI */
return g_bytes_new_take(contents, len);
}
static void on_load_changed(BlinkWebView* wv, BlinkLoadEvent ev, gpointer ud) {
(void)ud;
if (ev == BLINK_LOAD_FINISHED)
g_print("loaded: %s (title=%s)\n",
blink_web_view_get_uri(wv), blink_web_view_get_title(wv));
}
static gboolean on_close(GtkWindow* w, gpointer ud) {
(void)w; (void)ud;
blink_gtk_quit_main_loop();
return FALSE;
}
int main(int argc, char* argv[]) {
/* (0) For a non-standard layout, call blink_gtk_set_resources_path() here. */
/* (1) Initialize (before any GTK/GLib call) */
if (!blink_gtk_init(&argc, &argv)) return 1;
/* (2) Window + WebView (software rendering recommended) */
GtkWidget* window = gtk_window_new();
gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
GtkWidget* webview = blink_web_view_new_with_gpu_mode(BLINK_GPU_MODE_SOFTWARE);
/* (3) Register before loading */
blink_web_view_register_custom_scheme_full(
BLINK_WEB_VIEW(webview), "app", scheme_cb, NULL);
g_signal_connect(webview, "load-changed", G_CALLBACK(on_load_changed), NULL);
g_signal_connect(window, "close-request", G_CALLBACK(on_close), NULL);
/* (4)-(5) Place, present, load */
gtk_window_set_child(GTK_WINDOW(window), webview);
gtk_window_present(GTK_WINDOW(window));
blink_web_view_load_uri(BLINK_WEB_VIEW(webview),
argc > 1 ? argv[1] : "app://local/index.html");
/* (6)-(7) Main loop, then shutdown */
int rc = blink_gtk_run_main_loop();
blink_gtk_shutdown();
return rc;
}Build and run:
export PKG_CONFIG_PATH="$BLINKGTK_PKG/lib/pkgconfig:$PKG_CONFIG_PATH"
cc -O2 -Wall -o minimal-embedder minimal-embedder.c \
-Wl,-rpath,'$ORIGIN' \
$(pkg-config --cflags --libs blinkgtk-0.1)
# The surest arrangement is to run it from the same directory as the runtime
# (the .so files and the Chromium resources)
cp minimal-embedder "$BLINKGTK_PKG/lib/chromium/"
"$BLINKGTK_PKG/lib/chromium/minimal-embedder" --no-sandbox --no-zygoteThe next section explains why -Wl,-rpath,'$ORIGIN' and
"place it in
lib/chromium and launch from there".
So that a new embedder starting from zero can get running in a single
round
trip, here are the ones we actually walked into.
libblinkgtk.so must be paired strictly with the Chromium
resources of the same
package (v8_context_snapshot.bin and so on). Do not build a
binary with only
the absolute rpath that comes from the .pc file and then
copy it into the
lib/chromium/ of a different package and
run it: it will load the .so
from the package it was built against, and the renderer dies
instantly on a V8
snapshot mismatch (we demonstrated this on 2026-07-07). Either
of these avoids
it:
-Wl,-rpath,'$ORIGIN'
in first, then place the binary inlib/chromium/ of the package you want to use and launch
it there. The.so files next to it always win, which makes the binary
position-independentLD_LIBRARY_PATH=<pkg>/lib/chromium
explicitly at launch.Sample binaries as distributed may have a build-time path baked into
RUNPATH, so
if you intend to move or swap packages, building your own is the safe
route.
~/.blink_gtk/ is shared by every instanceIn our measurements, passing --user-data-dir does not
separate profiles;
~/.blink_gtk/ is shared by every BlinkGTK process belonging
to the same user.
Furthermore, persistence to disk (localStorage and the like) happens
only on a
clean exit path such as SIGTERM — with SIGKILL or _exit()
it is lost. If a test
dirties the state, the practical remedy is to delete the relevant data
under
~/.blink_gtk/ by hand. If running several applications at
once or separating
profiles is a requirement, design around this behaviour.
--no-sandbox --no-zygoteWhere the Chromium sandbox cannot be set up — containers, restricted
user
namespaces, some development environments — the child processes will not
start
unless the argv you hand to blink_gtk_init() includes
--no-sandbox --no-zygote. Our production launch script
always passes both. For the
equivalent through the environment, see BLINKGTK_NO_SANDBOX
in Chapter 9. Use
it understanding the security implication (the sandbox is switched
off).
The EGL (hardware GPU) path is experimental; having evaluated it, we
decided to
run on software for the time being (both rendering
vertical-writing EPUB body
text and taking screenshots are stable on software). And, as noted
above, the
mode is per process, so it cannot be switched per WebView.
The blink_web_view_capture_screenshot() family saves a
frame re-rendered on the
renderer side, so a fault in the compositor/display stage — the display
going
blank while the content is fine — may not appear in it. Do not accept
a
display-side verification on the strength of a captured PNG alone. We
hit a false
pass this way, and now also run a check that reads the GTK composition
result
directly.
const char* blink_gtk_get_version(void); /* e.g. "1.2.1" */
const char* blink_gtk_get_chromium_version(void); /* e.g. "152.0.7977.64" */
/* Build number (since 1.2.2). The build number counts how many times the same
* version was rebuilt. Behaviour can differ between builds of one version. */
const char* blink_gtk_get_build(void); /* e.g. "2". NULL if unknown */
const char* blink_gtk_get_version_full(void); /* e.g. "1.2.1-build3". Never NULL */These come from the .so linked at run time, not from
compile-time macros
(BLINKGTK_VERSION / CHROMIUM_VERSION), so what
you display follows the
library without rebuilding the consumer. We recommend always recording
these
run-time values in logs and bug reports.
load_uri, history, the detail of load-changed
and load-failedregister_custom_scheme_full(), serving binary data, the
relationship with fetch()capture_screenshot familyBLINKGTK_GPU_MODE, with practical examples(Chapter titles are as planned at the time of the first instalment of
the agreed
table of contents in #121; the final form follows upstream review.)
The first draft was written from the sources below. It was then
re-verified
against the then-current distribution on 2026-09-03 and again on
2026-09-08,
bringing the statements about bundled headers and the version examples
in line
with the artifacts.
include/blinkgtk-0.1/blink_gtk/blink_gtk.h. Functiondocs/04-user-guides/api-reference/c-api-reference-en.mdapp/native/readium-launcher.c (a real