Author: BlinkGTK Project
Applies to: BlinkGTK 1.2.5-build2 (Chromium
156.0.8078.47)
This guide helps you decide whether to move an application built with
CEF
(Chromium Embedded Framework) or Electron to BlinkGTK. For each thing
your
current setup does, the tables show what to use in BlinkGTK. Where
BlinkGTK has
no equivalent, the tables and the section "Features without an
equivalent, and
other differences" say so plainly.
Every BlinkGTK function in the tables is declared in the public
header shipped
with the packages, blink_gtk/blink_gtk.h.
If you are coming from WebKitGTK, read the
WebKitGTK migration guide
instead. There the
replacements are nearly one to one.
Before switching, check that the following four points fit your application.
| Item | What BlinkGTK requires |
|---|---|
| Operating system | Linux, Wayland sessions only. It does not run on X11. For Windows, BlinkGTK for Windows is distributed separately. There is no macOS version |
| Toolkit | You build a GTK4 application. Windows, menus and dialogs are GTK's |
| Application language | C. Through GObject Introspection, Python and other languages can use it too |
| Node.js | Not included. Code you wrote in Electron's main process is rewritten in C, Python or similar |
If you need one code base that ships to Windows, macOS and Linux, as
CEF and
Electron allow, BlinkGTK is not a good fit.
blink_web_view_new() returns aGtkWidget*. Put it in a GtkWindow or a
GtkBox and it is shown. The engineCefWindowInfo or
Electron's BrowserWindowCefExecuteProcess(), and
you do not provideCefClient, you register functions per web
view or connect toblink_gtk_run_main_loop(), notg_main_loop_run() or g_application_run() (see
below). GLib's g_timeout_add()g_idle_add() still work under this loopipcMain /
ipcRenderer. There is no way toCefV8Handler)The "BlinkGTK" column lists functions and signal names from the
public header.
Connect to signals with g_signal_connect().
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Subprocess entry point | CefExecuteProcess() |
(not needed) | Not needed. There is no corresponding function |
| Initialize the engine | CefInitialize() |
wait for app.whenReady() |
blink_gtk_init(&argc, &argv). Call it on the
main thread before any GTK or GLib call.
FALSE means it failed |
| Tell it where the resources are | resources_dir_path / locales_dir_path in
CefSettings |
(Electron carries its own; nothing to set) | blink_gtk_set_resources_path(). To place the ICU data
elsewhere, also blink_gtk_set_icu_data_path(). Call both
before blink_gtk_init() |
| Choose the rendering mode | command-line switches | app.disableHardwareAcceleration() and similar |
blink_gtk_set_gpu_mode(). Call it before
blink_gtk_init() |
| Shut down | CefShutdown() |
app.quit() |
Leave the loop with blink_gtk_quit_main_loop(), then
call blink_gtk_shutdown() |
| Check the version | cef_version_info() |
process.versions |
blink_gtk_get_version() /
blink_gtk_get_chromium_version() |
If you do not set the resource location, BlinkGTK looks next to the
executable
first, then near the library. With the RPM or DEB packages no setting is
needed.
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Run the loop | CefRunMessageLoop() |
(Electron runs it) | blink_gtk_run_main_loop() |
| Leave the loop | CefQuitMessageLoop() |
app.quit() |
blink_gtk_quit_main_loop() |
| Pump the engine from your own loop | CefDoMessageLoopWork() |
— | Not available. Leave the loop to
blink_gtk_run_main_loop() and do your work from GLib timers
and signals |
Build on GtkApplication |
— | — | blink_gtk_application_run(), used instead of
g_application_run(). The loop ends when the last window is
closed |
| Prevent a second instance | — | app.requestSingleInstanceLock() |
blink_gtk_application_run() follows
GApplication's single-instance behaviour. A second launch
is forwarded to the first |
If you run the loop with g_application_run(), the
application builds and starts
but pages never load. When you use GtkApplication, always
use
blink_gtk_application_run().
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Create a web view | CefBrowserHost::CreateBrowser() |
new BrowserWindow() |
blink_web_view_new(). Put the returned
GtkWidget* in a window with
gtk_window_set_child() or similar |
| Create a window | CefWindowInfo / Views |
BrowserWindow |
GTK's gtk_window_new() |
| Several web views in one window | several browsers | WebContentsView |
Create several web views and lay them out in a GtkBox
or similar |
| Separate storage (cookies, cache) | CefRequestContext |
session.fromPartition() |
blink_web_view_new_with_profile("name") |
| Answer a request to open a new window | CefLifeSpanHandler::OnBeforePopup() |
webContents.setWindowOpenHandler() |
The decide-navigation signal. For a new window, take
the web view with blink_navigation_decision_create_view()
and put it in a window of your own |
| Answer a fullscreen request | CefDisplayHandler::OnFullscreenModeChange() |
enter-html-full-screen |
blink_web_view_set_fullscreen_handler() |
Convert to BlinkWebView* with
BLINK_WEB_VIEW() before passing the view to
BlinkGTK functions. GTK functions take it as it is.
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Open a URL | CefFrame::LoadURL() |
loadURL() |
blink_web_view_load_uri() |
| Show an HTML string | data URL | loadURL('data:...') |
blink_web_view_load_html(view, html, base_uri) |
| Back, forward, reload, stop | GoBack() / GoForward() /
Reload() / StopLoad() |
navigationHistory.goBack() and similar |
blink_web_view_go_back() / go_forward() /
reload() / stop() |
| Can it go back or forward | CanGoBack() / CanGoForward() |
navigationHistory.canGoBack() and similar |
blink_web_view_can_go_back() /
can_go_forward() |
| Follow the load state | CefLoadHandler |
did-finish-load and similar |
The load-changed signal
(BLINK_LOAD_STARTED / COMMITTED /
FINISHED); failures arrive as load-failed |
| Follow title and URL changes | CefDisplayHandler::OnTitleChange() /
OnAddressChange() |
page-title-updated / did-navigate |
The title-changed / uri-changed signals.
Read the values with blink_web_view_get_title() /
get_uri() |
| Block, or open externally, a navigation the page started | CefRequestHandler::OnBeforeBrowse() |
will-navigate |
The decide-navigation signal, answered with
blink_navigation_decision_use() / ignore() /
open_external() |
BLINK_LOAD_STARTED from load-changed
arrives only when you call
blink_web_view_load_uri() or load_html(). It
does not arrive for link clicks
or back and forward. See the Signals API for
details.
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Run JavaScript in the page | CefFrame::ExecuteJavaScript() |
webContents.executeJavaScript() |
blink_web_view_execute_javascript(view, script, callback, user_data).
The result reaches the callback as a JSON string. It is not a
Promise |
| Insert a script before the page's own | CefRenderProcessHandler::OnContextCreated() |
preload script | blink_web_view_inject_user_script(view, script, TRUE).
Register it before the first blink_web_view_load_uri() |
| Insert CSS | insert through JavaScript | webContents.insertCSS() |
blink_web_view_inject_user_stylesheet() |
| Page → application | CefMessageRouter (window.cefQuery) |
ipcRenderer.send() → ipcMain.on() |
window.blinkgtk.postMessage('name', data) in the page,
blink_web_view_register_message_handler() in the
application |
| Application → page | call a function with ExecuteJavaScript() |
webContents.send() → ipcRenderer.on() |
blink_web_view_send_message_to_page() in the
application, window.blinkgtk.addMessageHandler('name', fn)
in the page |
| Decide which API the page sees | register functions with CefV8Handler |
contextBridge.exposeInMainWorld() |
Not available. Exchange data through the two
window.blinkgtk functions, and insert wrapper functions on
the page side with inject_user_script() |
| Ask and wait for an answer | cefQuery success and failure callbacks |
ipcRenderer.invoke() /
ipcMain.handle() |
Not available. Send with postMessage
from the page and answer with send_message_to_page() from
the application. Attach an ID that pairs requests and answers on the
page side |
Points to watch with messages:
data sent from the page reaches the application as
Base64 bytes. Decode itg_base64_decode(). From the application to the page,
the string is passed as it issend_message_to_page() silently drops the message when
nobody listens yet. TheaddMessageHandler() registrations in the page are lost
on every navigation. ThepostMessage. Validate
what arrives in the applicationSee "JS ↔︎
C messaging"
in the App integration API for details.
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Register a scheme name | CefApp::OnRegisterCustomSchemes() |
protocol.registerSchemesAsPrivileged() |
Call blink_gtk_register_scheme("myapp") before
blink_gtk_init(). app, res,
resource, ebook and blinkgtk are
registered from the start |
| Answer requests for the scheme | CefRegisterSchemeHandlerFactory() +
CefResourceHandler |
protocol.handle() |
blink_web_view_register_custom_scheme_full(). The
callback receives the URI and returns the body as GBytes
and the type in out_mime |
| Show your application's own HTML | a custom scheme or file:// |
loadFile() |
Serving it from a custom scheme is the reliable way. Pages from a
custom scheme are treated like HTTPS, and fetch()
works |
| Intercept http / https requests | CefResourceRequestHandler |
session.webRequest /
protocol.handle('https') |
Not available |
Points to watch with scheme handlers:
app://reader/... and
app://settings/...realpath() or similar| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Open the developer tools | CefBrowserHost::ShowDevTools() |
webContents.openDevTools() |
Open remote debugging with
blink_web_view_open_devtools(view, 0), then open the URL
from blink_web_view_get_devtools_url() in Chrome or
Chromium, or connect from chrome://inspect |
| Listen on a fixed port | remote_debugging_port in CefSettings |
--remote-debugging-port |
blink_web_view_open_devtools(view, port) |
| Close the developer tools | CloseDevTools() |
closeDevTools() |
blink_web_view_close_devtools() |
| Do not allow the developer tools | — | devTools: false |
BLINK_CONTENT_POLICY_NO_DEVTOOLS with
blink_web_view_set_content_policy() |
There is no way to show the developer tools inside your application's
window.
They open in a separate Chrome or Chromium window.
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| alert / confirm / prompt | CefJSDialogHandler |
(shown by Electron) | blink_web_view_set_javascript_dialog_handler() |
| Choose a file | CefDialogHandler |
dialog.showOpenDialog() |
For <input type="file"> in the page,
blink_web_view_set_file_chooser_handler(). From the
application, GTK's GtkFileDialog |
| Downloads | CefDownloadHandler |
will-download |
blink_web_view_set_download_handler(). Follow progress
through the BlinkDownload passed with the
download-started signal |
| HTTP authentication | CefRequestHandler::GetAuthCredentials() |
the login event |
blink_web_view_set_auth_handler() |
| Certificate errors | CefRequestHandler::OnCertificateError() |
certificate-error |
blink_web_view_set_certificate_error_handler() |
| Permission requests (location, camera, ...) | CefPermissionHandler |
session.setPermissionRequestHandler() |
The permission-request signal, answered with
blink_permission_request_allow() / deny().
Requests are denied if you do nothing |
| Cookies | CefCookieManager |
session.cookies |
blink_web_view_get_cookies() /
set_cookie() / delete_all_cookies() |
| User-Agent | user_agent in CefSettings |
setUserAgent() |
blink_web_view_set_user_agent() |
| Find in page | CefBrowserHost::Find() |
findInPage() |
blink_web_view_find_in_page() /
stop_finding() |
| Zoom | SetZoomLevel() |
setZoomFactor() |
blink_web_view_set_zoom_level() |
| Write a PDF | PrintToPDF() |
printToPDF() |
blink_web_view_print_to_pdf_with_options() |
| Capture the view as an image | — | capturePage() |
blink_web_view_capture_screenshot() /
_async() |
| Task | CEF | Electron | BlinkGTK |
|---|---|---|---|
| Build | CEF binary distribution and CMake | npm | Compile and link with
pkg-config --cflags --libs blinkgtk-0.1 |
| What is needed to run | ship CEF's shared libraries and resources with the application | ship the Electron executable | The runtime package (RPM: blinkgtk-bin, DEB:
libblinkgtk-0.1-0). GTK4 comes from the operating
system |
| What is needed to build | — | — | The devel package (RPM: blinkgtk-bin-devel, DEB:
libblinkgtk-0.1-dev). For Python and other languages, also
the gir package (RPM: blinkgtk-bin-gir) |
| Ship together with the application | ship it in the application directory | electron-builder and similar | Pack a tar.gz following the portable application guide |
Link through pkg-config. If you only write
-lblinkgtk by hand, the
application can build and start but show no page. The reason and how to
check
are in the integration
guide.
When you ship BlinkGTK together with your application, you must also
include the
license notices such as THIRD_PARTY_NOTICES.html. See
"License
obligations"
in the portable application guide.
This example serves the application's own HTML over the
app:// scheme,
sends a message from a button in the page to the application, and sends
an answer
back. It corresponds to Electron's ipcRenderer.send()
combined with webContents.send().
/* cef-electron-migration.c */
#include <blink_gtk/blink_gtk.h>
#include <gtk/gtk.h>
#include <string.h>
static const char page_html[] =
"<!doctype html><meta charset='utf-8'><title>sample</title>"
"<button id='b'>Send to the application</button><p id='out'></p>"
"<script>"
"window.blinkgtk.addMessageHandler('reply', function (data) {"
" document.getElementById('out').textContent = data;"
"});"
"document.getElementById('b').onclick = function () {"
" window.blinkgtk.postMessage('hello', 'ping');"
"};"
"</script>";
/* Called for each request to app://main/... Only the URI is passed in */
static GBytes* on_app_scheme(BlinkWebView* view, const char* uri,
char** out_mime, gpointer user_data) {
(void)view; (void)uri; (void)user_data;
*out_mime = g_strdup("text/html");
return g_bytes_new_static(page_html, strlen(page_html));
}
/* Receives postMessage('hello', ...) from the page. data is Base64 */
static void on_hello(const char* name, const guint8* data, gsize length,
gpointer user_data) {
BlinkWebView* view = BLINK_WEB_VIEW(user_data);
gchar* encoded = g_strndup((const gchar*)data, length);
gsize decoded_len = 0;
guchar* decoded = g_base64_decode(encoded, &decoded_len);
g_print("%s: %.*s\n", name, (int)decoded_len, (const char*)decoded);
blink_web_view_send_message_to_page(view, "reply", "pong");
g_free(decoded);
g_free(encoded);
}
/* Result of execute_javascript: a JSON string, or empty if it could not run */
static void on_title(const char* result, gpointer user_data) {
(void)user_data;
g_print("document.title = %s\n", result[0] ? result : "(not run)");
}
static void on_load_changed(BlinkWebView* view, BlinkLoadEvent event,
gpointer user_data) {
(void)user_data;
if (event == BLINK_LOAD_FINISHED)
blink_web_view_execute_javascript(view, "document.title", on_title, NULL);
}
static gboolean on_close(GtkWindow* window, gpointer user_data) {
(void)window; (void)user_data;
blink_gtk_quit_main_loop();
return FALSE;
}
int main(int argc, char* argv[]) {
/* Corresponds to CefInitialize(). Call it before GTK */
if (!blink_gtk_init(&argc, &argv)) {
g_printerr("blink_gtk_init() failed\n");
return 1;
}
GtkWidget* window = gtk_window_new();
gtk_window_set_title(GTK_WINDOW(window), "BlinkGTK");
gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);
g_signal_connect(window, "close-request", G_CALLBACK(on_close), NULL);
/* Corresponds to CreateBrowser() / new BrowserWindow() */
GtkWidget* web_view = blink_web_view_new();
gtk_window_set_child(GTK_WINDOW(window), web_view);
BlinkWebView* view = BLINK_WEB_VIEW(web_view);
blink_web_view_register_custom_scheme_full(view, "app", on_app_scheme, NULL);
blink_web_view_register_message_handler(view, "hello", on_hello, view);
g_signal_connect(web_view, "load-changed", G_CALLBACK(on_load_changed), NULL);
blink_web_view_load_uri(view, "app://main/index.html");
gtk_window_present(GTK_WINDOW(window));
/* Corresponds to CefRunMessageLoop() */
int status = blink_gtk_run_main_loop();
/* Corresponds to CefShutdown() */
blink_gtk_shutdown();
return status;
}Build and run:
cc cef-electron-migration.c -o cef-electron-migration $(pkg-config --cflags --libs blinkgtk-0.1)
./cef-electron-migrationRun it in a Wayland session. When you press the button, the terminal
prints
hello: ping and the page shows pong.
BlinkGTK's public API has no equivalent for the following.
| Feature | Name in CEF / Electron | What to do instead |
|---|---|---|
| macOS, running on X11 | (both supported) | Not available |
| Node.js | Electron's main process and preload | Write the code in C or a language that uses GObject Introspection |
| Pump the engine from your own loop | CefDoMessageLoopWork() |
Leave the loop to blink_gtk_run_main_loop() and use
GLib timers |
| Render off screen and receive the pixels | CEF windowless rendering (OnPaint) |
Not available. For single images,
blink_web_view_capture_screenshot() |
| Expose C functions directly to the page | CefV8Handler / contextBridge |
window.blinkgtk messages plus wrapper functions
inserted with inject_user_script() |
| Request and answer as a pair | cefQuery / ipcRenderer.invoke() |
Combine two messages |
| Intercept or rewrite http / https requests | CefResourceRequestHandler /
session.webRequest |
Not available. Serve your own content over a custom scheme |
| Status codes, headers and redirects from a scheme handler | CefResourceHandler / the Response from
protocol.handle() |
Not available. Only the body and its type can be returned |
| Print dialog | CefBrowserHost::Print() /
webContents.print() |
Not available. Write a PDF with
blink_web_view_print_to_pdf_with_options() |
| Developer tools inside your window | ShowDevTools() / openDevTools() |
Open remote debugging in a separate Chrome or Chromium |
| Extensions | loading extensions in CEF or Electron | Not available |
| Application auto-update | Electron's autoUpdater |
Not available. Update through the operating system's package manager |
| Menus, tray, notifications | Electron's Menu / Tray /
Notification |
Use GTK and GLib (GMenu, GNotification and
so on) |
Other differences:
blink_gtk_init()blink_web_view_get_uri() and
get_title() return. The nextIf a missing feature stops you from switching, tell us through
bug reports
and requests or at
contact@blinkgtk.org.
| Date | Change |
|---|---|
| 2026-10-19 | First version |