BlinkGTK Rust (gtk4-rs) Tutorial

Rust Language Bindings


Table of Contents

  1. Introduction
  2. gtk4-rs Setup
  3. Minimal Browser
  4. Signal Handling
  5. Property Monitoring
  6. Navigation Features
  7. Practical Usage
  8. Troubleshooting

Introduction

BlinkGTK supports GObject Introspection, making it accessible from Rust through gtk4-rs. Rust's type safety and zero-cost abstractions enable building safe and fast browser applications.

BlinkGTK Features

Prerequisites


gtk4-rs Setup

Installing Required Packages

# Rust toolchain (if not installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# GTK4 development libraries

# Fedora/RHEL
sudo dnf install gtk4-devel gobject-introspection-devel

# Ubuntu/Debian
sudo apt install libgtk-4-dev libgirepository1.0-dev

# Arch Linux
sudo pacman -S gtk4 gobject-introspection

Creating a Project

# Create new Rust project
cargo new my-blinkgtk-browser
cd my-blinkgtk-browser

Cargo.toml Configuration

[package]
name = "my-blinkgtk-browser"
version = "0.1.0"
edition = "2021"

[dependencies]
gtk4 = "0.9"
glib = "0.20"
gio = "0.20"

[build-dependencies]
pkg-config = "0.3"

Creating build.rs

// build.rs
use std::env;
use std::path::PathBuf;

fn main() {
    // Set BlinkGTK library path
    let blinkgtk_lib = "/path/to/BlinkGTK/lib";

    println!("cargo:rustc-link-search=native={}", blinkgtk_lib);
    println!("cargo:rustc-link-lib=blinkgtk");

    println!("cargo:rerun-if-changed=build.rs");
}

Environment Variables

# Add BlinkGTK library to LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/path/to/BlinkGTK/lib:$LD_LIBRARY_PATH

# Add BlinkGTK typelib to GI_TYPELIB_PATH
export GI_TYPELIB_PATH=/path/to/BlinkGTK/gir:$GI_TYPELIB_PATH

Minimal Browser

Basic Usage

// src/main.rs
use gtk4::prelude::*;
use gtk4::{glib, Application, ApplicationWindow};
use std::ffi::CString;

// BlinkGTK FFI declarations
mod ffi {
    use glib::ffi::{gboolean, gchar, gdouble, GType};
    use gtk4::ffi::GtkWidget;

    extern "C" {
        pub fn blink_gtk_init(argc: *mut i32, argv: *mut *mut *mut gchar);
        pub fn blink_gtk_shutdown();
        pub fn blink_web_view_new() -> *mut GtkWidget;
        pub fn blink_web_view_load_uri(web_view: *mut GtkWidget, uri: *const gchar);
    }
}

const APP_ID: &str = "org.example.MinimalBrowser";

fn main() -> glib::ExitCode {
    // Initialize BlinkGTK
    unsafe {
        let mut argc = std::env::args().len() as i32;
        let mut argv: Vec<_> = std::env::args()
            .map(|arg| CString::new(arg).unwrap().into_raw())
            .collect();
        let mut argv_ptr = argv.as_mut_ptr();

        ffi::blink_gtk_init(&mut argc, &mut argv_ptr);
    }

    // Create GTK4 application
    let app = Application::builder().application_id(APP_ID).build();

    app.connect_activate(build_ui);

    let exit_code = app.run();

    // Shutdown BlinkGTK
    unsafe {
        ffi::blink_gtk_shutdown();
    }

    exit_code
}

fn build_ui(app: &Application) {
    // Create window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("BlinkGTK Minimal Browser")
        .default_width(1024)
        .default_height(768)
        .build();

    // Create BlinkWebView
    let web_view_ptr = unsafe { ffi::blink_web_view_new() };
    let web_view = unsafe { gtk4::Widget::from_glib_none(web_view_ptr) };

    // Add to window
    window.set_child(Some(&web_view));

    // Load URL
    let url = "https://www.example.com";
    unsafe {
        let url_c = CString::new(url).unwrap();
        ffi::blink_web_view_load_uri(web_view.as_ptr() as *mut _, url_c.as_ptr());
    }

    // Show window
    window.present();
}

Build and Run

# Build
cargo build --release

# Run
LD_LIBRARY_PATH=/path/to/BlinkGTK/lib:$LD_LIBRARY_PATH \
  ./target/release/my-blinkgtk-browser

Signal Handling

BlinkLoadEvent Enumeration

#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlinkLoadEvent {
    Started = 0,
    Redirected = 1,
    Committed = 2,
    Finished = 3,
}

impl BlinkLoadEvent {
    pub fn from_i32(value: i32) -> Option<Self> {
        match value {
            0 => Some(BlinkLoadEvent::Started),
            1 => Some(BlinkLoadEvent::Redirected),
            2 => Some(BlinkLoadEvent::Committed),
            3 => Some(BlinkLoadEvent::Finished),
            _ => None,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            BlinkLoadEvent::Started => "STARTED",
            BlinkLoadEvent::Redirected => "REDIRECTED",
            BlinkLoadEvent::Committed => "COMMITTED",
            BlinkLoadEvent::Finished => "FINISHED",
        }
    }
}

Connecting Signals

use gtk4::{glib, prelude::*};

fn build_ui(app: &Application) {
    // ... Create window and WebView ...

    // Connect load-changed signal
    web_view.connect_closure(
        "load-changed",
        false,
        glib::closure_local!(|_web_view: gtk4::Widget, load_event: i32| {
            if let Some(event) = BlinkLoadEvent::from_i32(load_event) {
                println!("Load state: {}", event.as_str());

                if event == BlinkLoadEvent::Finished {
                    unsafe {
                        let uri_ptr = ffi::blink_web_view_get_uri(
                            _web_view.as_ptr() as *mut _
                        );
                        let title_ptr = ffi::blink_web_view_get_title(
                            _web_view.as_ptr() as *mut _
                        );

                        let uri = if !uri_ptr.is_null() {
                            glib::GString::from_glib_none(uri_ptr).to_string()
                        } else {
                            String::from("(none)")
                        };

                        let title = if !title_ptr.is_null() {
                            glib::GString::from_glib_none(title_ptr).to_string()
                        } else {
                            String::from("(none)")
                        };

                        println!("Page loaded: {} ({})", title, uri);
                    }
                }
            }
        }),
    );

    // Connect title-changed signal
    web_view.connect_closure(
        "title-changed",
        false,
        glib::closure_local!(|_web_view: gtk4::Widget| {
            unsafe {
                let title_ptr = ffi::blink_web_view_get_title(
                    _web_view.as_ptr() as *mut _
                );
                if !title_ptr.is_null() {
                    let title = glib::GString::from_glib_none(title_ptr).to_string();
                    println!("Title changed: {}", title);
                }
            }
        }),
    );

    // Connect load-failed signal
    web_view.connect_closure(
        "load-failed",
        false,
        glib::closure_local!(
            |_web_view: gtk4::Widget,
             load_event: i32,
             failing_uri: String,
             error: glib::Error| -> bool {
                if let Some(event) = BlinkLoadEvent::from_i32(load_event) {
                    println!("Load failed: {}", event.as_str());
                    println!("  URI: {}", failing_uri);
                    println!("  Error: {}", error);
                }
                true // Signal handled
            }
        ),
    );
}

Property Monitoring

notify::property-name Signals

use std::cell::RefCell;
use std::rc::Rc;

fn build_ui(app: &Application) {
    // ... Create window and WebView ...

    let progress_bar = Rc::new(RefCell::new(ProgressBar::new()));
    progress_bar.borrow().set_hexpand(true);
    progress_bar.borrow().set_show_text(true);

    // notify::estimated-load-progress
    {
        let progress = progress_bar.clone();
        let web_view_clone = web_view.clone();

        web_view.connect_notify_local(
            Some("estimated-load-progress"),
            move |_web_view, _pspec| unsafe {
                let prog = ffi::blink_web_view_get_estimated_load_progress(
                    web_view_clone.as_ptr() as *mut _
                );
                println!("Progress: {:.0}%", prog * 100.0);

                progress.borrow().set_fraction(prog);
                progress.borrow().set_text(Some(&format!("{:.0}%", prog * 100.0)));
            },
        );
    }

    // notify::uri
    {
        let web_view_clone = web_view.clone();

        web_view.connect_notify_local(
            Some("uri"),
            move |_web_view, _pspec| unsafe {
                let uri_ptr = ffi::blink_web_view_get_uri(
                    web_view_clone.as_ptr() as *mut _
                );
                if !uri_ptr.is_null() {
                    let uri = glib::GString::from_glib_none(uri_ptr).to_string();
                    println!("URI changed: {}", uri);
                }
            },
        );
    }

    // notify::title
    {
        let window = window.clone();
        let web_view_clone = web_view.clone();

        web_view.connect_notify_local(
            Some("title"),
            move |_web_view, _pspec| unsafe {
                let title_ptr = ffi::blink_web_view_get_title(
                    web_view_clone.as_ptr() as *mut _
                );
                if !title_ptr.is_null() {
                    let title = glib::GString::from_glib_none(title_ptr).to_string();
                    window.set_title(Some(&format!("{} - Browser", title)));
                }
            },
        );
    }
}

use gtk4::{Box, Button, Entry, Orientation};

fn build_ui(app: &Application) {
    // ... Create window ...

    let main_box = Box::new(Orientation::Vertical, 0);

    // Toolbar
    let toolbar = Box::new(Orientation::Horizontal, 5);

    // Back button
    let back_button = Button::with_label("◀");
    {
        let web_view = web_view.clone();
        back_button.connect_clicked(move |_| {
            unsafe {
                ffi::blink_web_view_go_back(web_view.as_ptr() as *mut _);
            }
        });
    }
    toolbar.append(&back_button);

    // Forward button
    let forward_button = Button::with_label("▶");
    {
        let web_view = web_view.clone();
        forward_button.connect_clicked(move |_| {
            unsafe {
                ffi::blink_web_view_go_forward(web_view.as_ptr() as *mut _);
            }
        });
    }
    toolbar.append(&forward_button);

    // Reload button
    let reload_button = Button::with_label("↻");
    {
        let web_view = web_view.clone();
        reload_button.connect_clicked(move |_| {
            unsafe {
                ffi::blink_web_view_reload(web_view.as_ptr() as *mut _);
            }
        });
    }
    toolbar.append(&reload_button);

    // URL Entry
    let url_entry = Entry::new();
    url_entry.set_hexpand(true);
    {
        let web_view = web_view.clone();
        url_entry.connect_activate(move |entry| {
            let url = entry.text().to_string();
            unsafe {
                let url_c = CString::new(url).unwrap();
                ffi::blink_web_view_load_uri(
                    web_view.as_ptr() as *mut _,
                    url_c.as_ptr()
                );
            }
        });
    }
    toolbar.append(&url_entry);

    main_box.append(&toolbar);
    main_box.append(&web_view);

    window.set_child(Some(&main_box));
}

Checking Navigation State

use std::cell::RefCell;
use std::rc::Rc;

// Navigation button update function
let back_button_rc = Rc::new(RefCell::new(back_button.clone()));
let forward_button_rc = Rc::new(RefCell::new(forward_button.clone()));

let update_navigation_buttons = {
    let web_view = web_view.clone();
    let back_btn = back_button_rc.clone();
    let forward_btn = forward_button_rc.clone();

    move || {
        unsafe {
            let can_go_back = ffi::blink_web_view_can_go_back(
                web_view.as_ptr() as *mut _
            ) != 0;
            let can_go_forward = ffi::blink_web_view_can_go_forward(
                web_view.as_ptr() as *mut _
            ) != 0;

            back_btn.borrow().set_sensitive(can_go_back);
            forward_btn.borrow().set_sensitive(can_go_forward);
        }
    }
};

// Call from load-changed
web_view.connect_closure(
    "load-changed",
    false,
    glib::closure_local!(move |_web_view: gtk4::Widget, _load_event: i32| {
        update_navigation_buttons();
    }),
);

Practical Usage

Type-Safe Wrapper Class

use gtk4::glib;
use std::ffi::CString;

pub struct BlinkWebView {
    widget: gtk4::Widget,
}

impl BlinkWebView {
    pub fn new() -> Self {
        let widget_ptr = unsafe { ffi::blink_web_view_new() };
        let widget = unsafe { gtk4::Widget::from_glib_none(widget_ptr) };

        Self { widget }
    }

    pub fn load_uri(&self, uri: &str) {
        unsafe {
            let uri_c = CString::new(uri).unwrap();
            ffi::blink_web_view_load_uri(
                self.widget.as_ptr() as *mut _,
                uri_c.as_ptr()
            );
        }
    }

    pub fn reload(&self) {
        unsafe {
            ffi::blink_web_view_reload(self.widget.as_ptr() as *mut _);
        }
    }

    pub fn go_back(&self) {
        unsafe {
            ffi::blink_web_view_go_back(self.widget.as_ptr() as *mut _);
        }
    }

    pub fn go_forward(&self) {
        unsafe {
            ffi::blink_web_view_go_forward(self.widget.as_ptr() as *mut _);
        }
    }

    pub fn can_go_back(&self) -> bool {
        unsafe {
            ffi::blink_web_view_can_go_back(self.widget.as_ptr() as *mut _) != 0
        }
    }

    pub fn can_go_forward(&self) -> bool {
        unsafe {
            ffi::blink_web_view_can_go_forward(self.widget.as_ptr() as *mut _) != 0
        }
    }

    pub fn get_uri(&self) -> Option<String> {
        unsafe {
            let uri_ptr = ffi::blink_web_view_get_uri(self.widget.as_ptr() as *mut _);
            if !uri_ptr.is_null() {
                Some(glib::GString::from_glib_none(uri_ptr).to_string())
            } else {
                None
            }
        }
    }

    pub fn get_title(&self) -> Option<String> {
        unsafe {
            let title_ptr = ffi::blink_web_view_get_title(self.widget.as_ptr() as *mut _);
            if !title_ptr.is_null() {
                Some(glib::GString::from_glib_none(title_ptr).to_string())
            } else {
                None
            }
        }
    }

    pub fn as_widget(&self) -> &gtk4::Widget {
        &self.widget
    }
}

Usage Example

fn build_ui(app: &Application) {
    let window = ApplicationWindow::builder()
        .application(app)
        .title("Modern Browser")
        .build();

    // Use type-safe wrapper
    let web_view = BlinkWebView::new();

    web_view.load_uri("https://www.example.com");

    window.set_child(Some(web_view.as_widget()));
    window.present();
}

Troubleshooting

Cause: Failed to link with libblinkgtk.so

Solution:

# Check build.rs
# Add to Cargo.toml
[build-dependencies]
pkg-config = "0.3"

# Set LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/path/to/BlinkGTK/lib:$LD_LIBRARY_PATH

Cause: FFI declarations duplicated across multiple source files

Solution:

// Separate as ffi.rs
pub mod ffi {
    // Consolidate FFI declarations here
}

// Use from other files
use crate::ffi;

Issue: Segmentation fault at runtime

Cause: Calling API before BlinkGTK initialization

Solution:

fn main() -> glib::ExitCode {
    // Always call blink_gtk_init(&argc, &argv) first
    unsafe {
        let mut argc = std::env::args().len() as i32;
        let mut argv: Vec<_> = std::env::args()
            .map(|arg| CString::new(arg).unwrap().into_raw())
            .collect();
        let mut argv_ptr = argv.as_mut_ptr();

        ffi::blink_gtk_init(&mut argc, &mut argv_ptr);
    }

    // Create GTK4 application
    let app = Application::builder().application_id(APP_ID).build();
    // ...
}

Enabling Debug Logs

# Enable Rust backtraces
export RUST_BACKTRACE=1

# GTK4 debug logs
export G_MESSAGES_DEBUG=all

# Run
cargo run

Sample Code

Complete sample code is available at:

Build and run (C version):

cd $BLINKGTK_ROOT
gcc -o blinkgtk_browser examples/blinkgtk_browser.c $(pkg-config --cflags --libs blinkgtk-0.1)
./blinkgtk_browser

Summary

Using BlinkGTK from Rust offers the following benefits:

Next Steps:


BlinkGTK Project | Copyright 2025 | BSD-3-Clause License