Rust Language Bindings
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.
# 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# Create new Rust project
cargo new my-blinkgtk-browser
cd my-blinkgtk-browser[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"// 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");
}# 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// 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
cargo build --release
# Run
LD_LIBRARY_PATH=/path/to/BlinkGTK/lib:$LD_LIBRARY_PATH \
./target/release/my-blinkgtk-browser#[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",
}
}
}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
}
),
);
}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));
}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();
}),
);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) -> >k4::Widget {
&self.widget
}
}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();
}blink_web_view_newCause: 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_PATHblink_gtk_initCause: 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;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();
// ...
}# Enable Rust backtraces
export RUST_BACKTRACE=1
# GTK4 debug logs
export G_MESSAGES_DEBUG=all
# Run
cargo runComplete sample code is available at:
examples/blinkgtk_browser.c - Minimal browser in C
(official sample)Build and run (C version):
cd $BLINKGTK_ROOT
gcc -o blinkgtk_browser examples/blinkgtk_browser.c $(pkg-config --cflags --libs blinkgtk-0.1)
./blinkgtk_browserUsing BlinkGTK from Rust offers the following benefits:
Next Steps:
BlinkGTK Project | Copyright 2025 | BSD-3-Clause License