From 0977820aff7560cf220f974ce3b7c223c9a4f21f Mon Sep 17 00:00:00 2001 From: fbrouille Date: Wed, 4 Jun 2025 16:07:05 +0000 Subject: [PATCH 1/2] Add glib::CStrV This type represents a borrowed reference to a `NULL`-terminated array of `NULL`-terminated UTF-8 strings. Signed-off-by: fbrouille --- glib/src/collections/mod.rs | 2 +- glib/src/collections/strv.rs | 222 ++++++++++++++++++++++++++++++++++- glib/src/lib.rs | 2 +- 3 files changed, 221 insertions(+), 5 deletions(-) diff --git a/glib/src/collections/mod.rs b/glib/src/collections/mod.rs index 9f775cd9d00f..b2614c9642ff 100644 --- a/glib/src/collections/mod.rs +++ b/glib/src/collections/mod.rs @@ -13,4 +13,4 @@ pub mod slist; pub use slist::SList; pub mod strv; -pub use strv::StrV; +pub use strv::{CStrV, StrV}; diff --git a/glib/src/collections/strv.rs b/glib/src/collections/strv.rs index 6852256908d4..fce4d0be636b 100644 --- a/glib/src/collections/strv.rs +++ b/glib/src/collections/strv.rs @@ -114,12 +114,26 @@ impl std::borrow::Borrow<[GStringPtr]> for StrV { } } +impl AsRef for StrV { + #[inline] + fn as_ref(&self) -> &CStrV { + self.into() + } +} + +impl std::borrow::Borrow for StrV { + #[inline] + fn borrow(&self) -> &CStrV { + self.into() + } +} + impl std::ops::Deref for StrV { - type Target = [GStringPtr]; + type Target = CStrV; #[inline] - fn deref(&self) -> &[GStringPtr] { - self.as_slice() + fn deref(&self) -> &CStrV { + self.into() } } @@ -1359,6 +1373,208 @@ impl IntoStrV for [&'_ String; N] { } } +// rustdoc-stripper-ignore-next +/// Representation of a borrowed `NULL`-terminated C array of `NULL`-terminated UTF-8 strings. +/// +/// It can be constructed safely from a `&StrV` and unsafely from a pointer to a C array. +/// This type is very similar to `[GStringPtr]`, but with one added constraint: the underlying C array must be `NULL`-terminated. +#[repr(transparent)] +pub struct CStrV { + inner: [GStringPtr], +} + +impl CStrV { + // rustdoc-stripper-ignore-next + /// Borrows a C array. + /// # Safety + /// + /// The provided pointer **must** be `NULL`-terminated. It is undefined behavior to + /// pass a pointer that does not uphold this condition. + #[inline] + pub unsafe fn from_glib_borrow<'a>(ptr: *const *const c_char) -> &'a CStrV { + let slice = StrV::from_glib_borrow(ptr); + &*(slice as *const [GStringPtr] as *const CStrV) + } + + // rustdoc-stripper-ignore-next + /// Borrows a C array. + /// # Safety + /// + /// The provided pointer **must** be `NULL`-terminated. It is undefined behavior to + /// pass a pointer that does not uphold this condition. + #[inline] + pub unsafe fn from_glib_borrow_num<'a>(ptr: *const *const c_char, len: usize) -> &'a CStrV { + let slice = StrV::from_glib_borrow_num(ptr, len); + &*(slice as *const [GStringPtr] as *const CStrV) + } + + // rustdoc-stripper-ignore-next + /// Returns the underlying pointer. + /// + /// This is guaranteed to be nul-terminated. + #[inline] + pub const fn as_ptr(&self) -> *const *const c_char { + self.inner.as_ptr() as *const *const _ + } +} + +impl fmt::Debug for CStrV { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +unsafe impl Send for CStrV {} + +unsafe impl Sync for CStrV {} + +impl PartialEq for CStrV { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for CStrV {} + +impl PartialOrd for CStrV { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CStrV { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.inner.cmp(&other.inner) + } +} + +impl std::hash::Hash for CStrV { + #[inline] + fn hash(&self, state: &mut H) { + self.inner.hash(state) + } +} + +impl PartialEq<[&'_ str]> for CStrV { + fn eq(&self, other: &[&'_ str]) -> bool { + for (a, b) in Iterator::zip(self.iter(), other.iter()) { + if a != b { + return false; + } + } + + true + } +} + +impl PartialEq for [&'_ str] { + #[inline] + fn eq(&self, other: &CStrV) -> bool { + other.eq(self) + } +} + +impl Default for &CStrV { + #[inline] + fn default() -> Self { + const SLICE: &[*const c_char] = &[ptr::null()]; + // SAFETY: `SLICE` is indeed a valid nul-terminated array. + unsafe { CStrV::from_glib_borrow(SLICE.as_ptr()) } + } +} + +impl std::ops::Deref for CStrV { + type Target = [GStringPtr]; + + #[inline] + fn deref(&self) -> &[GStringPtr] { + &self.inner + } +} + +impl<'a> std::iter::IntoIterator for &'a CStrV { + type Item = &'a GStringPtr; + type IntoIter = std::slice::Iter<'a, GStringPtr>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.inner.iter() + } +} + +impl<'a> From<&'a StrV> for &'a CStrV { + fn from(value: &'a StrV) -> Self { + let slice = value.as_slice(); + // Safety: `&StrV` is a null-terminated C array of nul-terminated UTF-8 strings, + // therefore `&StrV::as_slice()` return a a null-terminated slice of nul-terminated UTF-8 strings, + // thus it is safe to convert it to `&CStr`. + unsafe { &*(slice as *const [GStringPtr] as *const CStrV) } + } +} + +impl FromGlibContainer<*mut c_char, *const *const c_char> for &CStrV { + unsafe fn from_glib_none_num(ptr: *const *const c_char, num: usize) -> Self { + CStrV::from_glib_borrow_num(ptr, num) + } + + unsafe fn from_glib_container_num(_ptr: *const *const c_char, _num: usize) -> Self { + unimplemented!(); + } + + unsafe fn from_glib_full_num(_ptr: *const *const c_char, _num: usize) -> Self { + unimplemented!(); + } +} + +impl FromGlibPtrContainer<*mut c_char, *const *const c_char> for &CStrV { + #[inline] + unsafe fn from_glib_none(ptr: *const *const c_char) -> Self { + CStrV::from_glib_borrow(ptr) + } + + unsafe fn from_glib_container(_ptr: *const *const c_char) -> Self { + unimplemented!(); + } + + unsafe fn from_glib_full(_ptr: *const *const c_char) -> Self { + unimplemented!(); + } +} + +impl<'a> ToGlibPtr<'a, *const *const c_char> for CStrV { + type Storage = PhantomData<&'a Self>; + + #[inline] + fn to_glib_none(&'a self) -> Stash<'a, *const *const c_char, Self> { + Stash(self.as_ptr(), PhantomData) + } +} + +impl IntoGlibPtr<*const *const c_char> for &CStrV { + #[inline] + fn into_glib_ptr(self) -> *const *const c_char { + self.as_ptr() + } +} + +impl StaticType for CStrV { + #[inline] + fn static_type() -> crate::Type { + >::static_type() + } +} + +unsafe impl<'a> crate::value::FromValue<'a> for &'a CStrV { + type Checker = crate::value::GenericValueTypeChecker; + + unsafe fn from_value(value: &'a crate::value::Value) -> Self { + let ptr = gobject_ffi::g_value_get_boxed(value.to_glib_none().0) as *const *const c_char; + CStrV::from_glib_borrow(ptr) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/glib/src/lib.rs b/glib/src/lib.rs index 827c49b3fd59..af42624bb33b 100644 --- a/glib/src/lib.rs +++ b/glib/src/lib.rs @@ -120,7 +120,7 @@ mod exit_code; pub use exit_code::{ExitCode, InvalidExitCode}; pub mod collections; -pub use collections::{List, PtrSlice, SList, Slice, StrV}; +pub use collections::{CStrV, List, PtrSlice, SList, Slice, StrV}; pub use self::auto::*; #[allow(clippy::too_many_arguments)] From c333df37e4c44519cec12f63913b824c8e84f2ba Mon Sep 17 00:00:00 2001 From: fbrouille Date: Wed, 4 Jun 2025 16:07:26 +0000 Subject: [PATCH 2/2] Add gio::Vfs subclass Signed-off-by: fbrouille --- gio/src/subclass/mod.rs | 2 + gio/src/subclass/vfs.rs | 341 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 gio/src/subclass/vfs.rs diff --git a/gio/src/subclass/mod.rs b/gio/src/subclass/mod.rs index cbf526ef248a..1a02d483fe80 100644 --- a/gio/src/subclass/mod.rs +++ b/gio/src/subclass/mod.rs @@ -12,6 +12,7 @@ mod list_model; mod output_stream; mod seekable; mod socket_control_message; +mod vfs; pub use self::application::ArgumentList; @@ -32,5 +33,6 @@ pub mod prelude { output_stream::{OutputStreamImpl, OutputStreamImplExt}, seekable::{SeekableImpl, SeekableImplExt}, socket_control_message::{SocketControlMessageImpl, SocketControlMessageImplExt}, + vfs::{VfsImpl, VfsImplExt}, }; } diff --git a/gio/src/subclass/vfs.rs b/gio/src/subclass/vfs.rs new file mode 100644 index 000000000000..91aaf292d44f --- /dev/null +++ b/gio/src/subclass/vfs.rs @@ -0,0 +1,341 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use std::path::PathBuf; + +use glib::{prelude::*, subclass::prelude::*, translate::*, CStrV, GString}; + +use libc::c_char; + +use crate::{ffi, File, Vfs}; + +// Support custom implementation of virtual functions defined in `gio::ffi::GVfsClass`. +pub trait VfsImpl: ObjectImpl + ObjectSubclass> { + fn is_active(&self) -> bool { + self.parent_is_active() + } + + fn get_file_for_path(&self, path: &std::path::Path) -> File { + self.parent_get_file_for_path(path) + } + + fn get_file_for_uri(&self, uri: &str) -> File { + self.parent_get_file_for_uri(uri) + } + + fn get_supported_uri_schemes(&self) -> &'static CStrV { + self.parent_get_supported_uri_schemes() + } + + fn parse_name(&self, parse_name: &str) -> File { + self.parent_parse_name(parse_name) + } +} + +// Support parent implementation of virtual functions defined in `gio::ffi::GVfsClass`. +pub trait VfsImplExt: VfsImpl { + fn parent_is_active(&self) -> bool { + unsafe { + let data = Self::type_data(); + let parent_class = data.as_ref().parent_class() as *const ffi::GVfsClass; + + let f = (*parent_class) + .is_active + .expect("No parent class implementation for \"is_active\""); + + let res = f(self.obj().unsafe_cast_ref::().to_glib_none().0); + from_glib(res) + } + } + + fn parent_get_file_for_path(&self, path: &std::path::Path) -> File { + unsafe { + let data = Self::type_data(); + let parent_class = data.as_ref().parent_class() as *const ffi::GVfsClass; + + let f = (*parent_class) + .get_file_for_path + .expect("No parent class implementation for \"get_file_for_path\""); + + let res = f( + self.obj().unsafe_cast_ref::().to_glib_none().0, + path.to_glib_none().0, + ); + from_glib_full(res) + } + } + + fn parent_get_file_for_uri(&self, uri: &str) -> File { + unsafe { + let data = Self::type_data(); + let parent_class = data.as_ref().parent_class() as *const ffi::GVfsClass; + + let f = (*parent_class) + .get_file_for_uri + .expect("No parent class implementation for \"get_file_for_uri\""); + + let res = f( + self.obj().unsafe_cast_ref::().to_glib_none().0, + uri.to_glib_none().0, + ); + from_glib_full(res) + } + } + + fn parent_get_supported_uri_schemes(&self) -> &'static CStrV { + unsafe { + let data = Self::type_data(); + let parent_class = data.as_ref().parent_class() as *const ffi::GVfsClass; + + let f = (*parent_class) + .get_supported_uri_schemes + .expect("No parent class implementation for \"get_supported_uri_schemes\""); + + let res = f(self.obj().unsafe_cast_ref::().to_glib_none().0); + CStrV::from_glib_borrow(res) + } + } + + fn parent_parse_name(&self, parse_name: &str) -> File { + unsafe { + let data = Self::type_data(); + let parent_class = data.as_ref().parent_class() as *const ffi::GVfsClass; + + let f = (*parent_class) + .parse_name + .expect("No parent class implementation for \"parse_name\""); + + let res = f( + self.obj().unsafe_cast_ref::().to_glib_none().0, + parse_name.to_glib_none().0, + ); + from_glib_full(res) + } + } +} + +impl VfsImplExt for T {} + +// Implement virtual functions defined in `gio::ffi::GVfsClass`. +unsafe impl IsSubclassable for Vfs { + fn class_init(class: &mut ::glib::Class) { + Self::parent_class_init::(class); + + let klass = class.as_mut(); + klass.is_active = Some(is_active::); + klass.get_file_for_path = Some(get_file_for_path::); + klass.get_file_for_uri = Some(get_file_for_uri::); + klass.get_supported_uri_schemes = Some(get_supported_uri_schemes::); + klass.parse_name = Some(parse_name::); + } +} + +unsafe extern "C" fn is_active(vfs: *mut ffi::GVfs) -> glib::ffi::gboolean { + let instance = &*(vfs as *mut T::Instance); + let imp = instance.imp(); + + let res = imp.is_active(); + + res.into_glib() +} + +unsafe extern "C" fn get_file_for_path( + vfs: *mut ffi::GVfs, + path: *const c_char, +) -> *mut ffi::GFile { + let instance = &*(vfs as *mut T::Instance); + let imp = instance.imp(); + + let file = imp.get_file_for_path(&PathBuf::from_glib_none(path)); + + file.into_glib_ptr() +} + +unsafe extern "C" fn get_file_for_uri( + vfs: *mut ffi::GVfs, + uri: *const c_char, +) -> *mut ffi::GFile { + let instance = &*(vfs as *mut T::Instance); + let imp = instance.imp(); + + let file = imp.get_file_for_uri(&GString::from_glib_borrow(uri)); + + file.into_glib_ptr() +} + +unsafe extern "C" fn get_supported_uri_schemes( + vfs: *mut ffi::GVfs, +) -> *const *const c_char { + let instance = &*(vfs as *mut T::Instance); + let imp = instance.imp(); + + let supported_uri_schemes = imp.get_supported_uri_schemes(); + + supported_uri_schemes.as_ptr() +} + +unsafe extern "C" fn parse_name( + vfs: *mut ffi::GVfs, + parse_name: *const c_char, +) -> *mut ffi::GFile { + let instance = &*(vfs as *mut T::Instance); + let imp = instance.imp(); + + let file = imp.parse_name(&GString::from_glib_borrow(parse_name)); + + file.into_glib_ptr() +} + +#[cfg(test)] +mod tests { + // The following tests rely on a custom type `MyCustomVfs` that extends another custom type `MyVfs`. + // For each virtual method defined in class `gio::ffi::GVfsClass`, a test checks that `MyCustomVfs` and `MyVfs` return the same results. + + use super::*; + use crate::prelude::*; + + // Define `MyCustomVfs` as a subclass of `MyVfs`. + mod imp { + use std::sync::LazyLock; + + use super::*; + + // Defines `MyVfs` as a subclass of `Vfs`. + #[derive(Default)] + pub struct MyVfs; + + #[glib::object_subclass] + impl ObjectSubclass for MyVfs { + const NAME: &'static str = "MyVfs"; + type Type = super::MyVfs; + type ParentType = Vfs; + } + + impl ObjectImpl for MyVfs {} + + // Implements `VfsImpl` with custom implementation. + impl VfsImpl for MyVfs { + fn is_active(&self) -> bool { + true + } + + fn get_file_for_path(&self, path: &std::path::Path) -> File { + File::for_path(path) + } + + fn get_file_for_uri(&self, uri: &str) -> File { + File::for_uri(uri) + } + + fn get_supported_uri_schemes(&self) -> &'static CStrV { + static SUPPORTED_URI_SCHEMES: LazyLock = + LazyLock::new(|| glib::StrV::from(["file"])); + &SUPPORTED_URI_SCHEMES + } + + fn parse_name(&self, parse_name: &str) -> File { + File::for_parse_name(parse_name) + } + } + + // Defines `MyCustomVfs` as a subclass of `MyVfs`. + #[derive(Default)] + pub struct MyCustomVfs; + + #[glib::object_subclass] + impl ObjectSubclass for MyCustomVfs { + const NAME: &'static str = "MyCustomVfs"; + type Type = super::MyCustomVfs; + type ParentType = super::MyVfs; + } + + impl ObjectImpl for MyCustomVfs {} + + // Implements `VfsImpl` with default implementation, which calls the parent's implementation. + impl VfsImpl for MyCustomVfs {} + + impl MyVfsImpl for MyCustomVfs {} + } + + glib::wrapper! { + pub struct MyVfs(ObjectSubclass) @extends Vfs; + } + + pub trait MyVfsImpl: ObjectImpl + ObjectSubclass + IsA> {} + + // To make this class subclassable we need to implement IsSubclassable + unsafe impl IsSubclassable for MyVfs {} + + glib::wrapper! { + pub struct MyCustomVfs(ObjectSubclass) @extends MyVfs, Vfs; + } + + #[test] + fn vfs_is_active() { + // invoke `MyCustomVfs` implementation of `gio::ffi::GVfsClass::is_active` + let my_custom_vfs = glib::Object::new::(); + let active = my_custom_vfs.is_active(); + + // invoke `MyVfs` implementation of `gio::ffi::GVfsClass::is_active` + let my_vfs = glib::Object::new::(); + let expected = my_vfs.is_active(); + + // both results should equal + assert_eq!(active, expected); + } + + #[test] + fn vfs_get_file_for_path() { + // invoke `MyCustomVfs` implementation of `gio::ffi::GVfsClass::get_file_for_path` + let my_custom_vfs = glib::Object::new::(); + let file = my_custom_vfs.file_for_path("/path"); + + // invoke `MyVfs` implementation of `gio::ffi::GVfsClass::get_file_for_path` + let my_vfs = glib::Object::new::(); + let expected = my_vfs.file_for_path("/path"); + + // both files should equal + assert!(file.equal(&expected)); + } + + #[test] + fn vfs_get_file_for_uri() { + // invoke `MyCustomVfs` implementation of `gio::ffi::GVfsClass::get_file_for_uri` + let my_custom_vfs = glib::Object::new::(); + let file = my_custom_vfs.file_for_uri("file:///path"); + + // invoke `MyVfs` implementation of `gio::ffi::GVfsClass::get_file_for_uri` + let my_vfs = glib::Object::new::(); + let expected = my_vfs.file_for_uri("file:///path"); + + // both files should equal + assert!(file.equal(&expected)); + } + + #[test] + fn vfs_get_supported_uri_schemes() { + // invoke `MyCustomVfs` implementation of `gio::ffi::GVfsClass::supported_uri_schemes` + let my_custom_vfs = glib::Object::new::(); + let schemes = my_custom_vfs.supported_uri_schemes(); + + // invoke `MyVfs` implementation of `gio::ffi::GVfsClass::supported_uri_schemes` + let my_vfs = glib::Object::new::(); + let expected = my_vfs.supported_uri_schemes(); + + // both results should equal + assert_eq!(schemes, expected); + } + + #[test] + fn vfs_parse_name() { + // invoke `MyCustomVfs` implementation of `gio::ffi::GVfsClass::parse_name` + let my_custom_vfs = glib::Object::new::(); + let file = my_custom_vfs.parse_name("file:///path"); + + // invoke `MyVfs` implementation of `gio::ffi::GVfsClass::parse_name` + let my_vfs = glib::Object::new::(); + let expected = my_vfs.parse_name("file:///path"); + + // both files should equal + assert!(file.equal(&expected)); + } +}