use std::ffi::{CStr, CString, c_char, c_int, c_ulong, c_void};
use std::mem;
use std::ptr::{self, NonNull};
use std::sync::OnceLock;
use libloading::os::unix::{Library, RTLD_LOCAL, RTLD_NOW};
type Ptr = *mut c_void;
type Gboolean = c_int;
#[repr(C)]
struct GError {
domain: u32,
code: c_int,
message: *mut c_char,
}
const G_BUS_TYPE_SESSION: c_int = 2;
const G_DBUS_CALL_FLAGS_NONE: c_int = 0;
const LIBRARIES: Libraries = Libraries {
glib: "libglib-2.0.so.0",
gobject: "libgobject-2.0.so.0",
gio: "libgio-2.0.so.0",
};
struct Libraries {
glib: &'static str,
gobject: &'static str,
gio: &'static str,
}
pub(crate) struct LoadError {
library: &'static str,
symbol: Option<&'static str>,
message: String,
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.symbol {
Some(symbol) => write!(f, "{} has no {symbol}: {}", self.library, self.message),
None => write!(f, "{} cannot be opened: {}", self.library, self.message),
}
}
}
macro_rules! symbols {
($($lib:ident . $name:ident : fn($($arg:ty),*) $(-> $ret:ty)?;)*) => {
pub(crate) struct Gio {
$($name: unsafe extern "C" fn($($arg),*) $(-> $ret)?,)*
}
impl Gio {
fn load(names: &Libraries) -> Result<Self, LoadError> {
let glib = open(names.glib)?;
let gobject = open(names.gobject)?;
let gio = open(names.gio)?;
let table = Self {
$($name: {
let library = match stringify!($lib) {
"glib" => &glib,
"gobject" => &gobject,
_ => &gio,
};
let symbol = unsafe {
library.get::<unsafe extern "C" fn($($arg),*) $(-> $ret)?>(
concat!(stringify!($name), "\0").as_bytes(),
)
}
.map_err(|error| LoadError {
library: names.$lib,
symbol: Some(stringify!($name)),
message: error.to_string(),
})?;
*symbol
},)*
};
mem::forget(glib);
mem::forget(gobject);
mem::forget(gio);
Ok(table)
}
}
};
}
symbols! {
glib.g_free: fn(Ptr);
glib.g_strfreev: fn(*mut *mut c_char);
glib.g_error_free: fn(*mut GError);
glib.g_variant_ref_sink: fn(Ptr) -> Ptr;
glib.g_variant_unref: fn(Ptr);
glib.g_variant_equal: fn(Ptr, Ptr) -> Gboolean;
glib.g_variant_new_string: fn(*const c_char) -> Ptr;
glib.g_variant_new_tuple: fn(*const Ptr, usize) -> Ptr;
glib.g_variant_get_child_value: fn(Ptr, usize) -> Ptr;
glib.g_variant_get_strv: fn(Ptr, *mut usize) -> *mut *const c_char;
glib.g_variant_type_new: fn(*const c_char) -> Ptr;
glib.g_variant_type_free: fn(Ptr);
glib.g_main_context_new: fn() -> Ptr;
glib.g_main_context_unref: fn(Ptr);
glib.g_main_context_push_thread_default: fn(Ptr);
glib.g_main_context_pop_thread_default: fn(Ptr);
glib.g_main_context_iteration: fn(Ptr, Gboolean) -> Gboolean;
glib.g_main_context_wakeup: fn(Ptr);
gobject.g_object_unref: fn(Ptr);
gobject.g_signal_connect_data:
fn(Ptr, *const c_char, Option<unsafe extern "C" fn()>, Ptr,
Option<unsafe extern "C" fn(Ptr, Ptr)>, c_int) -> c_ulong;
gio.g_settings_schema_source_get_default: fn() -> Ptr;
gio.g_settings_schema_source_lookup: fn(Ptr, *const c_char, Gboolean) -> Ptr;
gio.g_settings_schema_unref: fn(Ptr);
gio.g_settings_schema_list_children: fn(Ptr) -> *mut *mut c_char;
gio.g_settings_schema_has_key: fn(Ptr, *const c_char) -> Gboolean;
gio.g_settings_schema_get_key: fn(Ptr, *const c_char) -> Ptr;
gio.g_settings_schema_key_unref: fn(Ptr);
gio.g_settings_schema_key_get_default_value: fn(Ptr) -> Ptr;
gio.g_settings_new: fn(*const c_char) -> Ptr;
gio.g_settings_get_child: fn(Ptr, *const c_char) -> Ptr;
gio.g_settings_get_string: fn(Ptr, *const c_char) -> *mut c_char;
gio.g_settings_get_boolean: fn(Ptr, *const c_char) -> Gboolean;
gio.g_settings_get_int: fn(Ptr, *const c_char) -> c_int;
gio.g_settings_get_strv: fn(Ptr, *const c_char) -> *mut *mut c_char;
gio.g_settings_get_user_value: fn(Ptr, *const c_char) -> Ptr;
gio.g_settings_get_default_value: fn(Ptr, *const c_char) -> Ptr;
gio.g_settings_is_writable: fn(Ptr, *const c_char) -> Gboolean;
gio.g_bus_get_sync: fn(c_int, Ptr, *mut *mut GError) -> Ptr;
gio.g_dbus_connection_call_sync:
fn(Ptr, *const c_char, *const c_char, *const c_char, *const c_char, Ptr, Ptr,
c_int, c_int, Ptr, *mut *mut GError) -> Ptr;
}
fn open(name: &'static str) -> Result<Library, LoadError> {
unsafe { Library::open(Some(name), RTLD_NOW | RTLD_LOCAL) }.map_err(|error| LoadError {
library: name,
symbol: None,
message: error.to_string(),
})
}
pub(crate) fn gio() -> Option<&'static Gio> {
static GIO: OnceLock<Option<Gio>> = OnceLock::new();
GIO.get_or_init(|| match Gio::load(&LIBRARIES) {
Ok(gio) => Some(gio),
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
Err(error) => {
crate::trace::debug!(error = %error, "GLib is not loadable");
None
}
})
.as_ref()
}
fn c(name: &str) -> CString {
CString::new(name).expect("GSettings names contain no NUL")
}
unsafe fn take_strv(g: &Gio, array: *mut *mut c_char) -> Vec<String> {
let mut out = Vec::new();
if array.is_null() {
return out;
}
let mut cursor = array;
unsafe {
while !(*cursor).is_null() {
out.push(CStr::from_ptr(*cursor).to_string_lossy().into_owned());
cursor = cursor.add(1);
}
(g.g_strfreev)(array);
}
out
}
unsafe fn take_error(g: &Gio, error: *mut GError) -> String {
unsafe {
let message = (*error).message;
let text = if message.is_null() {
String::from("(no message)")
} else {
CStr::from_ptr(message).to_string_lossy().into_owned()
};
(g.g_error_free)(error);
text
}
}
#[derive(Clone, Copy)]
pub(crate) struct SchemaSource {
g: &'static Gio,
ptr: NonNull<c_void>,
}
impl SchemaSource {
pub(crate) fn default(g: &'static Gio) -> Option<Self> {
let ptr = NonNull::new(unsafe { (g.g_settings_schema_source_get_default)() })?;
Some(Self { g, ptr })
}
pub(crate) fn lookup(&self, id: &str) -> Option<Schema> {
let c_id = c(id);
let ptr = NonNull::new(unsafe {
(self.g.g_settings_schema_source_lookup)(self.ptr.as_ptr(), c_id.as_ptr(), 1)
})?;
Some(Schema {
g: self.g,
ptr,
id: id.to_owned(),
})
}
pub(crate) fn gio(&self) -> &'static Gio {
self.g
}
}
pub(crate) struct Schema {
g: &'static Gio,
ptr: NonNull<c_void>,
id: String,
}
impl Schema {
pub(crate) fn settings(&self) -> Settings {
let id = c(&self.id);
let ptr = unsafe { (self.g.g_settings_new)(id.as_ptr()) };
Settings {
g: self.g,
ptr: NonNull::new(ptr).expect("g_settings_new returns an object"),
}
}
pub(crate) fn list_children(&self) -> Vec<String> {
unsafe {
take_strv(
self.g,
(self.g.g_settings_schema_list_children)(self.ptr.as_ptr()),
)
}
}
pub(crate) fn has_key(&self, key: &str) -> bool {
let key = c(key);
unsafe { (self.g.g_settings_schema_has_key)(self.ptr.as_ptr(), key.as_ptr()) != 0 }
}
pub(crate) fn key_default_value(&self, key: &str) -> Variant {
let key = c(key);
unsafe {
let schema_key = (self.g.g_settings_schema_get_key)(self.ptr.as_ptr(), key.as_ptr());
let value = (self.g.g_settings_schema_key_get_default_value)(schema_key);
(self.g.g_settings_schema_key_unref)(schema_key);
Variant::from_full(self.g, value).expect("a schema key has a default value")
}
}
}
impl Drop for Schema {
fn drop(&mut self) {
unsafe { (self.g.g_settings_schema_unref)(self.ptr.as_ptr()) }
}
}
pub(crate) struct Settings {
g: &'static Gio,
ptr: NonNull<c_void>,
}
impl Settings {
pub(crate) fn child(&self, name: &str) -> Self {
let name = c(name);
let ptr = unsafe { (self.g.g_settings_get_child)(self.ptr.as_ptr(), name.as_ptr()) };
Self {
g: self.g,
ptr: NonNull::new(ptr).expect("g_settings_get_child returns an object"),
}
}
pub(crate) fn string(&self, key: &str) -> String {
let key = c(key);
unsafe {
let raw = (self.g.g_settings_get_string)(self.ptr.as_ptr(), key.as_ptr());
let text = CStr::from_ptr(raw).to_string_lossy().into_owned();
(self.g.g_free)(raw.cast());
text
}
}
pub(crate) fn boolean(&self, key: &str) -> bool {
let key = c(key);
unsafe { (self.g.g_settings_get_boolean)(self.ptr.as_ptr(), key.as_ptr()) != 0 }
}
pub(crate) fn int(&self, key: &str) -> i32 {
let key = c(key);
unsafe { (self.g.g_settings_get_int)(self.ptr.as_ptr(), key.as_ptr()) }
}
pub(crate) fn strv(&self, key: &str) -> Vec<String> {
let key = c(key);
unsafe {
take_strv(
self.g,
(self.g.g_settings_get_strv)(self.ptr.as_ptr(), key.as_ptr()),
)
}
}
pub(crate) fn user_value(&self, key: &str) -> Option<Variant> {
let key = c(key);
unsafe {
Variant::from_full(
self.g,
(self.g.g_settings_get_user_value)(self.ptr.as_ptr(), key.as_ptr()),
)
}
}
pub(crate) fn default_value(&self, key: &str) -> Option<Variant> {
let key = c(key);
unsafe {
Variant::from_full(
self.g,
(self.g.g_settings_get_default_value)(self.ptr.as_ptr(), key.as_ptr()),
)
}
}
pub(crate) fn is_writable(&self, key: &str) -> bool {
let key = c(key);
unsafe { (self.g.g_settings_is_writable)(self.ptr.as_ptr(), key.as_ptr()) != 0 }
}
pub(crate) fn connect<F: Fn() + 'static>(&self, signal: &CStr, f: F) {
unsafe extern "C" fn trampoline<F: Fn()>(_: Ptr, _: *const c_char, data: Ptr) {
unsafe { (*data.cast::<F>())() }
}
unsafe extern "C" fn release<F>(data: Ptr, _closure: Ptr) {
drop(unsafe { Box::from_raw(data.cast::<F>()) });
}
let data = Box::into_raw(Box::new(f)).cast::<c_void>();
let handler = unsafe {
mem::transmute::<unsafe extern "C" fn(Ptr, *const c_char, Ptr), unsafe extern "C" fn()>(
trampoline::<F>,
)
};
unsafe {
(self.g.g_signal_connect_data)(
self.ptr.as_ptr(),
signal.as_ptr(),
Some(handler),
data,
Some(release::<F>),
0,
);
}
}
}
impl Drop for Settings {
fn drop(&mut self) {
unsafe { (self.g.g_object_unref)(self.ptr.as_ptr()) }
}
}
pub(crate) struct Variant {
g: &'static Gio,
ptr: NonNull<c_void>,
}
unsafe impl Send for Variant {}
unsafe impl Sync for Variant {}
impl Variant {
unsafe fn from_full(g: &'static Gio, ptr: Ptr) -> Option<Self> {
NonNull::new(ptr).map(|ptr| Self { g, ptr })
}
pub(crate) fn string_tuple(g: &'static Gio, text: &str) -> Self {
let text = CString::new(text).expect("a probe URI contains no NUL");
unsafe {
let child = (g.g_variant_new_string)(text.as_ptr());
let tuple = (g.g_variant_new_tuple)(&child, 1);
let owned = (g.g_variant_ref_sink)(tuple);
Self::from_full(g, owned).expect("g_variant_new_tuple returns a value")
}
}
pub(crate) fn child(&self, index: usize) -> Self {
unsafe {
Self::from_full(
self.g,
(self.g.g_variant_get_child_value)(self.ptr.as_ptr(), index),
)
.expect("g_variant_get_child_value returns a value")
}
}
pub(crate) fn strv(&self) -> Vec<String> {
let mut length = 0usize;
unsafe {
let array = (self.g.g_variant_get_strv)(self.ptr.as_ptr(), &mut length);
let out = (0..length)
.map(|i| CStr::from_ptr(*array.add(i)).to_string_lossy().into_owned())
.collect();
(self.g.g_free)(array.cast());
out
}
}
pub(crate) fn equals(&self, other: &Self) -> bool {
unsafe { (self.g.g_variant_equal)(self.ptr.as_ptr(), other.ptr.as_ptr()) != 0 }
}
}
impl Drop for Variant {
fn drop(&mut self) {
unsafe { (self.g.g_variant_unref)(self.ptr.as_ptr()) }
}
}
pub(crate) struct MainContext {
g: &'static Gio,
ptr: NonNull<c_void>,
}
unsafe impl Send for MainContext {}
unsafe impl Sync for MainContext {}
impl MainContext {
pub(crate) fn new(g: &'static Gio) -> Self {
let ptr = unsafe { (g.g_main_context_new)() };
Self {
g,
ptr: NonNull::new(ptr).expect("g_main_context_new returns a context"),
}
}
pub(crate) fn push_thread_default(&self) {
unsafe { (self.g.g_main_context_push_thread_default)(self.ptr.as_ptr()) }
}
pub(crate) fn pop_thread_default(&self) {
unsafe { (self.g.g_main_context_pop_thread_default)(self.ptr.as_ptr()) }
}
pub(crate) fn iteration(&self) {
unsafe { (self.g.g_main_context_iteration)(self.ptr.as_ptr(), 1) };
}
pub(crate) fn wakeup(&self) {
unsafe { (self.g.g_main_context_wakeup)(self.ptr.as_ptr()) }
}
}
impl Drop for MainContext {
fn drop(&mut self) {
unsafe { (self.g.g_main_context_unref)(self.ptr.as_ptr()) }
}
}
pub(crate) struct DBusConnection {
g: &'static Gio,
ptr: NonNull<c_void>,
}
impl DBusConnection {
pub(crate) fn session(g: &'static Gio) -> Result<Self, String> {
let mut error: *mut GError = ptr::null_mut();
let raw = unsafe { (g.g_bus_get_sync)(G_BUS_TYPE_SESSION, ptr::null_mut(), &mut error) };
match NonNull::new(raw) {
Some(ptr) => Ok(Self { g, ptr }),
None => Err(unsafe { take_error(g, error) }),
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn call_sync(
&self,
bus_name: &str,
object_path: &str,
interface: &str,
method: &str,
parameters: &Variant,
reply_type: &str,
timeout_ms: i32,
) -> Result<Variant, String> {
let (bus_name, object_path, interface, method, reply_type) = (
c(bus_name),
c(object_path),
c(interface),
c(method),
c(reply_type),
);
let mut error: *mut GError = ptr::null_mut();
unsafe {
let reply_type = (self.g.g_variant_type_new)(reply_type.as_ptr());
let reply = (self.g.g_dbus_connection_call_sync)(
self.ptr.as_ptr(),
bus_name.as_ptr(),
object_path.as_ptr(),
interface.as_ptr(),
method.as_ptr(),
parameters.ptr.as_ptr(),
reply_type,
G_DBUS_CALL_FLAGS_NONE,
timeout_ms,
ptr::null_mut(),
&mut error,
);
(self.g.g_variant_type_free)(reply_type);
match Variant::from_full(self.g, reply) {
Some(reply) => Ok(reply),
None => Err(take_error(self.g, error)),
}
}
}
}
impl Drop for DBusConnection {
fn drop(&mut self) {
unsafe { (self.g.g_object_unref)(self.ptr.as_ptr()) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_library_is_a_load_error_naming_it() {
let names = Libraries {
glib: "libproxy-watch-absent-glib.so.0",
gobject: LIBRARIES.gobject,
gio: LIBRARIES.gio,
};
let error = Gio::load(&names).err().expect("the library does not exist");
assert_eq!(error.library, names.glib);
assert_eq!(error.symbol, None);
}
#[test]
fn a_library_without_the_symbols_is_a_load_error_naming_the_symbol() {
let names = Libraries {
glib: "libc.so.6",
gobject: "libc.so.6",
gio: "libc.so.6",
};
let error = Gio::load(&names).err().expect("libc has no GLib symbols");
assert_eq!(error.library, "libc.so.6");
assert_eq!(error.symbol, Some("g_free"));
}
#[test]
fn every_symbol_resolves_against_the_installed_glib() {
if open(LIBRARIES.gio).is_err() {
assert!(
std::env::var_os("CI").is_none(),
"GLib is not installed on a CI runner"
);
eprintln!("GLib is not installed; nothing to resolve against");
return;
}
if let Err(error) = Gio::load(&LIBRARIES) {
panic!("{error}");
}
}
}