BlinkGTK API Reference Manual — Chapter 1: Overview and Lifecycle

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.


1.1 What BlinkGTK is

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).

1.2 Architecture at a glance

Top to bottom:

  1. Embedder (your application) — a GTK4 application. You build the chrome
    (windows, menus) with GTK and place one BlinkWebView inside it as a child
    widget.
  2. BlinkWebView (BlinkGTK's public API layer) — a GtkWidget subclass
    (GObject). Load-event signals, custom schemes and the JS bridge are all
    handled here through the C API.
  3. Chromium (multi-process) — under the browser process (which lives in your
    process) child processes for the renderer, GPU and so on are started; they do
    the actual layout and painting.

The 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.

Header and linking

#include <blink_gtk/blink_gtk.h>
pkg-config --cflags --libs blinkgtk-0.1

Include 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 alongside blink_gtk.h makes 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 something blink_gtk.h does not offer,
please raise an issue.

Your application should include blink_gtk/blink_gtk.h and 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 are blink_gtk.h, blinkgtk_export.h and
blinkgtk_version.h.

Thread-safety policy

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().

1.3 The lifecycle as a whole

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 #123 review)

How you call it Assessment
After run_main_loop() returns Recommended. 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() returns
Not at all It works (the atexit safety net catches it), but teardown is left to atexit and the order is non-deterministic. Not recommended

Nothing breaks if you skip it because an atexit handler is registered once
ContentMainRunner::Run succeeds, 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
.

(0) Pre-init configuration

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().

(1) Initialization

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.

(2) Creating the WebView

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;

(3) Registration before load

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.

(4)-(5) Placement and load

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);

(6) Main loop

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.

(7) Shutdown

void blink_gtk_shutdown(void);

Call it after run_main_loop() returns, just before the process exits.

1.4 A minimal complete example (putting it on GTK4)

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-zygote

The next section explains why -Wl,-rpath,'$ORIGIN' and "place it in
lib/chromium and launch from there".

1.5 Pitfalls in production (from the consumer)

So that a new embedder starting from zero can get running in a single round
trip, here are the ones we actually walked into.

(a) RUNPATH / library resolution — instant death from a V8 snapshot mismatch

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:

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.

In 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.

(c) Some environments need --no-sandbox --no-zygote

Where 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.

(e) An offscreen capture is not "what you see"

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.

1.6 Checking the version

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.

1.7 Forward references to other chapters

(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.)

Sources for this chapter

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.