For: developers building their own application with
BlinkGTK (C, Python, Rust, and so on)
Last updated: 2026-07-29
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.
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.
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.
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-configWithout 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:
HasCommitted=0 (navigation never commits)set_dmabuf_pixels=0 (no frame is ever delivered)error=6On 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).
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.debThe 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 runtimeld.so.conf.dfragment or
LD_LIBRARY_PATH, and add the devel.pctoPKG_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_appWhen using BlinkGTK through a Python / Rust FFI binding, the shim is
likewise included as long as
the build metadata references pkg-config.
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/).
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.neededThe 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;
}gboolean blink_gtk_init(int* argc, char*** argv) --
call first, from the main thread.int blink_gtk_run_main_loop(void) -- starts the main
loop.void blink_gtk_quit_main_loop(void) -- quits the main
loop.void blink_gtk_shutdown(void) -- call on close.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.
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.
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.
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.
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.
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.
BlinkWebView reports events via GObject signals. The
available signals are the following seven:
load-changed — load state changes (argument: the
BlinkLoadEvent enum)load-failed — load failure (arguments: error_code (int)
+ failing URI, in this order)title-changed — title changes (no extra
argument — read the value with
blink_web_view_get_title())uri-changed — URI changes (no extra
argument — read the value with
blink_web_view_get_uri()); fires on
blink_web_view_load_uri() and on commits that change the
URInew-window-requested — new-window requests
(window.open() / target="_blank"; notification
only)message-received — messages from page JavaScript (two
arguments: name, data)permission-request — permission requests such as
geolocation and notifications (default deny; respond synchronously
inside the handler)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)
new-window-requested -- new window request
(target="_blank" etc.)message-received -- message from in-page
JavaScriptIn 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);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.
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.
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.
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:
Per the web standard, requestFullscreen() is only
granted from a user gesture (a real click or
key press). It is rejected from synthetic events such as a
programmatic element.click()
(API can only be initiated by a user gesture). Bind your
fullscreen button to a real click.
Call blink_web_view_exit_fullscreen(view) to exit
from the app side, and
blink_web_view_is_fullscreen(view) to query the current
state.
Exiting with the ESC key is not automatic (HTML
fullscreen requires browser-side ESC
coordination). Attach a CAPTURE-phase key controller to the top-level
window and, while in
fullscreen, call blink_web_view_exit_fullscreen() on ESC
(this uses the same coordinated path
as document.exitFullscreen(), so Blink's state is updated
too).
static gboolean on_key(GtkEventControllerKey* c, guint keyval, guint kc,
GdkModifierType st, gpointer ud) {
if (keyval == GDK_KEY_Escape && blink_web_view_is_fullscreen(BLINK_WEB_VIEW(ud))) {
blink_web_view_exit_fullscreen(BLINK_WEB_VIEW(ud));
return TRUE; /* consume */
}
return FALSE; /* propagate when not fullscreen */
}
GtkEventController* kc = gtk_event_controller_key_new();
gtk_event_controller_set_propagation_phase(kc, GTK_PHASE_CAPTURE);
g_signal_connect(kc, "key-pressed", G_CALLBACK(on_key), web_view);
gtk_widget_add_controller(toplevel_window, kc);These APIs are declared in the public header
<blink_gtk/blink_gtk.h>.
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).
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.
During development, passing --no-sandbox in
argv makes things easier. BlinkGTK runs in a
single-process configuration.
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.
BlinkGTK supports Wayland only (X11 is not used). The Ozone platform
is forced to Wayland at
startup.
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.
libv8.sosnapshot_blob.binv8_context_snapshot.binDo not mix files from build trees of different Chromium versions.
The following files are required at run time. Bundle them with your consumer application.
content_shell.pak (UI / default stylesheet; missing it
breaks CSS)icudtl.dat (ICU data; required for Japanese
line-breaking, ruby, etc.)locales/ja.pak, locales/en-US.pak (locale
resources)snapshot_blob.bin, v8_context_snapshot.bin
(V8 snapshots; same as 8.1)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).
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.
# 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 | taildocs/04-user-guides/api-reference/)