Embedding BlinkGTK (linking and runtime requirements)

For: developers building their own application with BlinkGTK (C, Python, Rust, and so on)
Last updated: 2026-07-29

Just this, first

Link through pkg-config. That alone avoids almost every pitfall described below.

cc -o myapp myapp.c $(pkg-config --cflags --libs blinkgtk-0.1)

In a Makefile:

CFLAGS  += $(shell pkg-config --cflags blinkgtk-0.1)
LDLIBS  += $(shell pkg-config --libs blinkgtk-0.1)

If something does not work, check these three first.

Symptom Check
It will not build Does pkg-config --cflags --libs blinkgtk-0.1 print anything? Is the devel package installed?
It starts, but the window is blank Does readelf -d ./myapp | grep allocator_shim print at least one line? (chapter 2)
It dies with Invalid file descriptor to ICU data Where the resources are (chapter 3)

The rest explains why, and what to do when you cannot use pkg-config. Read it when you need it.


1. How BlinkGTK differs from an ordinary GTK library

BlinkGTK carries a whole copy of Chromium inside it. That brings linking and runtime
requirements an ordinary GTK library does not have
.

Miss one, and the build and the startup both appear to succeed — but the page never
appears
(a blank or black window). No error is printed, which is what makes it hard to
diagnose, and why this guide exists.

The bundled blinkgtk_browser sample links through pkg-config, so it satisfies these
requirements.


2.1 What is happening

Chromium uses its own memory allocator (PartitionAlloc). For it to take effect, a component that
intercepts malloc and friends must be loaded at process start. That component is the
allocator shim.

If the shim loads late, Chromium's memory bookkeeping does not line up and page loads never
finish
.

2.2 Why pkg-config is needed

Writing just -lblinkgtk means the shim is loaded indirectly, through libblinkgtk.so.
Indirect is too late for the interception to take hold.

The shim has to be a direct dependency of the executable. The Libs line of
blinkgtk-0.1.pc includes it, so going through pkg-config gets this right automatically.

To check:

readelf -d ./myapp | grep allocator_shim
# One or more lines: good. Nothing: you are not going through pkg-config

2.3 Symptoms when not linked

Without direct linking, the renderer's navigation commit fails on Chromium 148 and later,
producing a black screen at startup. The logs show signs such as:

2.4 Version dependency

On Chromium 147, transitive loading of the shim still worked. On Chromium 148, PartitionAlloc
became more deeply integrated into the renderer commit path, making early interposition mandatory.
This requirement continues on Chromium 151 (verified on the 151 build: simple_browser carries the shim as a direct NEEDED entry).


3.0 Package layout (runtime / devel / gir, v1.1.0+)

From v1.1.0, BlinkGTK ships as three split packages like WebKitGTK
(binary-only, GitHub Releases). For consumer development, install the runtime
plus devel (and gir if needed).

Package RPM DEB Contents Consumer role
runtime blinkgtk-bin libblinkgtk-0.1-0 Shared library (libblinkgtk-0.1.so.0) + Chromium runtime (private dir /usr/lib64/blinkgtk-0.1/) + resources Required to run apps
devel blinkgtk-bin-devel libblinkgtk-0.1-dev Headers + blinkgtk-0.1.pc + .so symlink Required to build apps
gir blinkgtk-bin-gir (with devel) GObject Introspection typelib For Python / GJS bindings
# Fedora/RHEL (development: runtime + devel + gir)
sudo dnf install ./blinkgtk-bin-<VER>-1.fc44.x86_64.rpm \
                 ./blinkgtk-bin-devel-<VER>-1.fc44.x86_64.rpm \
                 ./blinkgtk-bin-gir-<VER>-1.fc44.x86_64.rpm

# Debian/Ubuntu
sudo apt install ./libblinkgtk-0.1-0_<VER>-1_amd64.deb \
                 ./libblinkgtk-0.1-dev_<VER>-1_amd64.deb

The devel package provides blinkgtk-0.1.pc, so pkg-config (section 3.1) works
out of the box after installation. Its Libs: keeps the direct allocator shim
link even after the FHS path rewrite (Issue #90). A runtime-only install can run
apps but cannot build them (no pkg-config / headers).

For tarball (FHS) installs, extract both runtime and devel into the same
prefix, resolve the private dir via the runtime ld.so.conf.d fragment or
LD_LIBRARY_PATH, and add the devel .pc to PKG_CONFIG_PATH. See the
Installation Guide for details.

The official SDK blinkgtk-0.1.pc already includes the allocator shim in Libs:. Linking via
pkg-config therefore makes the shim a direct dependency of the executable automatically.

cc your_app.c $(pkg-config --cflags --libs blinkgtk-0.1) -o your_app

When using BlinkGTK through a Python / Rust FFI binding, the shim is likewise included as long as
the build metadata references pkg-config.

3.2 Manual linking (when hardcoding in a Makefile, etc.)

If you hardcode the link line instead of using pkg-config, add the shim explicitly after
-lblinkgtk.

BLINKGTK_LIBS = -L$(BLINKGTK_LIBDIR) \
                -lblinkgtk \
                -lbase_allocator_partition_allocator_src_partition_alloc_allocator_shim

The shim library is bundled in the BlinkGTK library directory (e.g. the SDK's lib/chromium/).

3.3 Verification: check the NEEDED difference

After linking, verify that the executable lists the shim as a direct NEEDED. This is also the
first step for diagnosing rendering failures.

readelf -d ./your_app | grep NEEDED | grep allocator_shim
# -> one line means it is a direct NEEDED. No line means it is not linked (cause of black screen).

Comparing against the bundled sample is also effective.

readelf -d ./your_app        | grep NEEDED | sort > /tmp/app.needed
readelf -d ./blinkgtk_browser  | grep NEEDED | sort > /tmp/ref.needed
diff /tmp/ref.needed /tmp/app.needed

4. Lifecycle and embedding the WebView

4.1 Initialization, main loop, shutdown

The BlinkGTK lifecycle is as follows.

int main(int argc, char** argv) {
    if (!blink_gtk_init(&argc, &argv)) {   // call first (from the main thread)
        return 1;
    }
    // ... build window / WebView, load_uri ...
    int rc = blink_gtk_run_main_loop();    // start the main loop
    blink_gtk_shutdown();                  // shutdown (required)
    return rc;
}

Always call shutdown. If you end the process without calling blink_gtk_shutdown(), Chromium's
teardown runs after AtExitManager is destroyed and may abort with NOTREACHED at exit (observed in
a past issue). An auto-shutdown safety net is registered for the not-shut-down case, but consumers
are advised to call blink_gtk_shutdown() explicitly.

4.2 Creating and embedding the WebView

Create a WebView with blink_web_view_new(). It returns a GtkWidget*, which you can cast to
BlinkWebView* with BLINK_WEB_VIEW().

GtkWidget* web_view = blink_web_view_new();          // self-rendering WebView
blink_web_view_load_uri(BLINK_WEB_VIEW(web_view), "https://example.com");

// embed in your own layout (e.g. GtkOverlay)
GtkWidget* overlay = gtk_overlay_new();
gtk_overlay_set_child(GTK_OVERLAY(overlay), web_view);

blink_web_view_new_container() is deprecated. The old container-creating API does not render
on Chromium 148 and later (related to #90). Use blink_web_view_new() (self-rendering) and, if you
need a container, put it in your own GtkOverlay / GtkBox.

4.3 Repeated load_uri

Calling blink_web_view_load_uri() repeatedly on the same WebView is safe. The internal renderer
view is reused, and repeated navigations do not accumulate resources (a past leak has been
resolved). There is no need to recreate the WebView each time.


5. Rendering and display pitfalls

5.1 Choosing the GPU mode

There are three rendering backends.

Mode enum Use Requirement
software BLINK_GPU_MODE_SOFTWARE(0) Default. CPU rendering. Most stable No GPU
swiftshader BLINK_GPU_MODE_SWIFTSHADER(1) CPU-based GL emulation No GPU
egl BLINK_GPU_MODE_EGL(2) Native GPU rendering (WebGL/WebGPU) GPU / DRM required
GtkWidget* web_view = blink_web_view_new_with_gpu_mode(BLINK_GPU_MODE_SOFTWARE);

You can also select via the environment variable BLINKGTK_GPU_MODE=software|swiftshader|egl. We
recommend verifying with software first. egl needs a real GPU and DRM and will not render in
headless or GPU-less environments.

5.2 HiDPI and device-scale-factor

On high-DPI environments (scale=2, etc.), a device-scale-factor mismatch can cause "rendering
appears small in the top-left
". In software mode this is handled automatically following the GTK
scale. If you see a scale mismatch on an experimental path such as egl, you can pin the scale by
including the Chromium command-line flag --force-device-scale-factor=<n> in the argv passed to
blink_gtk_init(&argc, &argv). When embedding the WebView deep inside nested containers, watch for scale
propagation.

5.3 Japanese rendering and fonts

To render Japanese pages correctly, a CJK font (Harano Aji, Noto CJK, etc.) must be installed on
the system. Without one, text shows as tofu (boxes). If the deployment environment may lack fonts,
include a CJK font package in your dependencies.


6. Application integration

6.1 Signals (load state, title, and more)

BlinkWebView reports events via GObject signals. The available signals are the following seven:

See the Signals API reference for exact firing conditions and pitfalls.
blink_web_view_load_uri() is called (it does not fire for in-page link
navigation or redirects; full navigation tracking is planned for a future version)

In addition, the uri / title / is-loading / zoom-level properties emit
notify:: signals when their values change (subscribe with
g_signal_connect(view, "notify::uri", ...)).

g_signal_connect(web_view, "load-changed", G_CALLBACK(on_load_changed), NULL);
g_signal_connect(web_view, "title-changed", G_CALLBACK(on_title_changed), NULL);

6.2 Injecting user scripts / stylesheets

blink_web_view_inject_user_script(BLINK_WEB_VIEW(web_view), js_source,
                                  TRUE /* inject_at_document_start */);
blink_web_view_inject_user_stylesheet(BLINK_WEB_VIEW(web_view), css_source);

A script with inject_at_document_start=TRUE runs at the document_start timing after the
navigation commits
. If the allocator shim from Section 2 is not linked and the commit never
succeeds, the injected script will not run even though a 'Stored' log appears (a past issue). If
injection does not run, check the linking requirement in Section 2 first.

6.3 Getting the version (do not hardcode)

The BlinkGTK / Chromium version shown in a subtitle, etc., should be obtained via the runtime API
rather than hardcoded, so it automatically tracks updates to libblinkgtk.so.

const char* blinkgtk_ver = blink_gtk_get_version();           /* BlinkGTK version */
const char* chromium_ver = blink_gtk_get_chromium_version();  /* Chromium version */

These APIs are declared in the public header <blink_gtk/blink_gtk.h> (no extern declaration
needed). The return value is an internal static string -- do not free it. You must link against a
libblinkgtk.so that exports the symbols.

6.4 Content protection policy

For e-book or kiosk terminals, you can restrict copy / save / print, etc.

blink_web_view_set_content_policy(BLINK_WEB_VIEW(web_view),
                                  BLINK_CONTENT_POLICY_EBOOK_READER);

Presets: BLINK_CONTENT_POLICY_ALLOW_ALL (default), BLINK_CONTENT_POLICY_EBOOK_READER,
BLINK_CONTENT_POLICY_KIOSK. You can also combine individual flags (NO_COPY / NO_SAVE /
NO_PRINT / NO_CONTEXT_MENU / NO_DEVTOOLS, etc.) as a bitmask. The policy applies immediately
and can be changed at any time.

6.5 Web Fullscreen API (requestFullscreen)

To make web content's element.requestFullscreen() / document.exitFullscreen() work, register a
fullscreen handler
that reflects the request onto the top-level window. If you do not register one,
the fullscreen element only expands within the WebView's viewport and the window itself does not go
fullscreen (this is the main reason an e-reader's fullscreen button appears to "do nothing").

static void on_fullscreen(BlinkWebView* view, gboolean enter, gpointer user_data) {
  (void)user_data;
  GtkRoot* root = gtk_widget_get_root(GTK_WIDGET(view));
  if (!root || !GTK_IS_WINDOW(root)) return;
  if (enter)
    gtk_window_fullscreen(GTK_WINDOW(root));
  else
    gtk_window_unfullscreen(GTK_WINDOW(root));
}

/* Register once after creating the WebView */
blink_web_view_set_fullscreen_handler(BLINK_WEB_VIEW(web_view), on_fullscreen, NULL);

Notes:


7. Startup and runtime environment

7.1 Shared library search path (LD_LIBRARY_PATH / rpath)

Set rpath or LD_LIBRARY_PATH so that libblinkgtk.so and the Chromium shared libraries can be
resolved. The pkg-config Libs: already contains an -rpath setting.

In configurations where LD_LIBRARY_PATH must be set at run time, the path must be settled
before the Chromium libraries are loaded. A pattern that works is to reset LD_LIBRARY_PATH at
the very start of the process and re-exec yourself (set the environment variable, then execv
your own binary; from then on it loads with the correct search path).

7.1.1 Telling BlinkGTK where the resources are

At startup Chromium loads icudtl.dat, content_shell.pak, and others. It looks for them in
the directory containing the executable — not the current working directory.

Put your application somewhere else and they will not be found, so startup fails:

ERROR:base/i18n/icu_util.cc: Invalid file descriptor to ICU data received.
FATAL:base/i18n/icu_util.cc: Check failed: result.

When that happens, say where they are before calling blink_gtk_init().

blink_gtk_set_resources_path("/usr/lib64/blinkgtk-0.1/chromium");
blink_gtk_set_icu_data_path("/usr/lib64/blinkgtk-0.1/chromium");

blink_gtk_init(&argc, &argv);

With an RPM or DEB installation the resources are in
/usr/lib64/blinkgtk-0.1/chromium/.

The bundled blinkgtk_browser does exactly this; see examples/blinkgtk_browser.c.

7.2 Sandbox and single process

During development, passing --no-sandbox in argv makes things easier. BlinkGTK runs in a
single-process configuration.

7.3 Thread affinity

The BlinkGTK / GTK APIs, including blink_gtk_init(&argc, &argv), should as a rule be called from the main
thread
(the GTK main loop thread). Avoid UI operations from other threads.

7.4 Wayland only

BlinkGTK supports Wayland only (X11 is not used). The Ozone platform is forced to Wayland at
startup.


8. Runtime data files

8.1 V8 three-file version consistency

The three V8-related files must come from the same build and have matching versions. Mixing
them causes a fatal V8 error at startup that prevents launch.

Do not mix files from build trees of different Chromium versions.

8.2 Bundling runtime data files

The following files are required at run time. Bundle them with your consumer application.

8.3 Data file search path and working directory

Chromium data files are resolved relative to the executable. Launching with data files in a
directory different from the executable can fail with errors such as
Invalid file descriptor to ICU data. Place data files in the same directory as the executable
(or the expected relative path).


9. Troubleshooting navigation / rendering failures

When nothing is rendered (black or white screen), check the following in order before forming more
complex hypotheses.

Check Command / sign Action if abnormal
Shim direct link readelf -d <app> | grep allocator_shim If absent, add the link per Section 3
NEEDED difference diff against the bundled sample Supply the missing libraries
Navigation commit HasCommitted in the log If still 0, suspect the shim
Frame delivery set_dmabuf_pixels in the log If still 0, suspect the shim / commit
Crash at exit NOTREACHED at exit Call blink_gtk_shutdown()
Injection not running 'Stored' appears but does not run Confirm commit (shim)
Rendering too small shrunk to top-left at scale=2 device-scale-factor / software mode
V8 consistency origin of libv8.so and snapshots Align to a single build
Data files content_shell.pak / icudtl.dat bundled Place next to the executable

The first step is always to check the NEEDED difference. When the build and launch succeed but
nothing renders, the cause is usually a missing link or missing data file -- a fundamental rather
than a deep issue.


10. Appendix: minimal check commands

# 1) Confirm the shim is directly linked
readelf -d ./your_app | grep -c allocator_shim    # expect 1

# 2) Confirm required runtime files exist
for f in libblinkgtk.so libv8.so content_shell.pak \
         snapshot_blob.bin v8_context_snapshot.bin \
         icudtl.dat locales/ja.pak locales/en-US.pak; do
  [ -e "$f" ] && echo "OK  $f" || echo "MISSING  $f"
done

# 3) Startup log health (when nothing renders)
grep -E "HasCommitted|set_dmabuf_pixels|error=6" your_app.log | tail