Switching from CEF or Electron to BlinkGTK

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.

Who this is for

If you are coming from WebKitGTK, read the
WebKitGTK migration guide instead. There the
replacements are nearly one to one.

Check these first

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.

How the model differs

  1. The web view is a single GTK widget. blink_web_view_new() returns a
    GtkWidget*. Put it in a GtkWindow or a GtkBox and it is shown. The engine
    never owns the window, unlike CEF's CefWindowInfo or Electron's BrowserWindow
  2. Your application does not assemble the browser's process layout. There is
    no function corresponding to CEF's CefExecuteProcess(), and you do not provide
    a separate executable for subprocesses. The library starts and manages its
    processes itself
  3. Handlers are functions and signals, not classes. Instead of returning handler
    classes from a CefClient, you register functions per web view or connect to
    GObject signals
  4. You run BlinkGTK's main loop. Use blink_gtk_run_main_loop(), not
    g_main_loop_run() or g_application_run() (see below). GLib's g_timeout_add()
    and g_idle_add() still work under this loop
  5. Your application and the page talk through messages. There are two-way
    messages comparable to Electron's ipcMain / ipcRenderer. There is no way to
    expose C functions directly to the page (such as CEF's CefV8Handler)

Mapping tables

The "BlinkGTK" column lists functions and signal names from the public header.
Connect to signals with g_signal_connect().

Initialization and shutdown

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.

Main loop

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

Windows and web views

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.

Loading URLs and navigation

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.

Talking to the page (running JavaScript and messages)

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:

See "JS ↔︎ C messaging"
in the App integration API for details.

Custom schemes and where to put your resources

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:

Developer tools and debugging

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.

Other common features

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

Distribution (packages and dependencies)

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.

Minimal example (C)

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

Run it in a Wayland session. When you press the button, the terminal prints
hello: ping and the page shows pong.

Features without an equivalent, and other differences

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:

If a missing feature stops you from switching, tell us through
bug reports and requests or at
contact@blinkgtk.org.

Change history

Date Change
2026-10-19 First version