From c2cc0488038946fce135d1e5dc95ff719aa89990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Laignel?= Date: Mon, 30 Sep 2024 21:20:02 +0200 Subject: [PATCH] gst: implement IdStr bindings and compatibility versions IdStr represents UTF-8 immutable strings which perform optimizations for short strings (< 16 bytes). The C type `GstIdStr` was introduced in GStreamer 1.26 as a replacement for GQuarks. This commit adds Rust bindings for the C type `GstIdStr`. Since this type will be used in API which previously relied on GQuarks, the commit also adds a compatibility implementation which can be used with GStreamer versions prior to 1.26, which is the first version to implement and use `GstIdStr`. The crate [KString] was used as the inner implementation for the compatibility version as it performs similar optimizations as `GstIdStr` and uses the same threshold to trigger heap allocation. See also: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7432 [KString]: https://crates.io/crates/kstring Part-of: --- Cargo.lock | 16 + gstreamer/Cargo.toml | 1 + gstreamer/src/id_str/bindings.rs | 551 +++++++++++++++++++++++++++++++ gstreamer/src/id_str/compat.rs | 543 ++++++++++++++++++++++++++++++ gstreamer/src/id_str/mod.rs | 435 ++++++++++++++++++++++++ gstreamer/src/id_str/serde.rs | 37 +++ gstreamer/src/lib.rs | 4 + 7 files changed, 1587 insertions(+) create mode 100644 gstreamer/src/id_str/bindings.rs create mode 100644 gstreamer/src/id_str/compat.rs create mode 100644 gstreamer/src/id_str/mod.rs create mode 100644 gstreamer/src/id_str/serde.rs diff --git a/Cargo.lock b/Cargo.lock index 1466440de..154196918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -884,6 +884,7 @@ dependencies = [ "glib", "gstreamer-sys", "itertools", + "kstring", "libc", "log", "muldiv", @@ -1705,6 +1706,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "libc" version = "0.2.161" @@ -2293,6 +2303,12 @@ dependencies = [ "serde", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.87" diff --git a/gstreamer/Cargo.toml b/gstreamer/Cargo.toml index 106e59bdd..d8358b840 100644 --- a/gstreamer/Cargo.toml +++ b/gstreamer/Cargo.toml @@ -23,6 +23,7 @@ num-rational = { version = "0.4", default-features = false, features = [] } futures-core = "0.3" futures-channel = "0.3" futures-util = { version = "0.3", default-features = false } +kstring = "2.0" log = { version = "0.4", optional = true } muldiv = "1" opt-ops = { package = "option-operations", version = "0.5" } diff --git a/gstreamer/src/id_str/bindings.rs b/gstreamer/src/id_str/bindings.rs new file mode 100644 index 000000000..00a5aa988 --- /dev/null +++ b/gstreamer/src/id_str/bindings.rs @@ -0,0 +1,551 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +// rustdoc-stripper-ignore-next +//! The `IdStr` bindings of the C type `GstIdStr`. +//! +//! See the higher level module documentation for details. + +use crate::ffi; +use glib::{translate::*, GStr, GString}; +use std::{ + cmp, + ffi::{c_char, CStr}, + fmt, + hash::{Hash, Hasher}, + mem, + ops::Deref, + ptr::NonNull, +}; + +glib::wrapper! { + // rustdoc-stripper-ignore-next + /// An UTF-8 immutable string type with optimizations for short values (len < 16). + #[derive(Debug)] + #[doc(alias = "GstIdStr")] + pub struct IdStr(BoxedInline); + + match fn { + copy => |ptr| ffi::gst_id_str_copy(ptr), + free => |ptr| ffi::gst_id_str_free(ptr), + init => |ptr| ffi::gst_id_str_init(ptr), + copy_into => |dest, src| ffi::gst_id_str_copy_into(dest, src), + clear => |ptr| ffi::gst_id_str_clear(ptr), + } +} + +impl IdStr { + #[doc(alias = "gst_id_str_new")] + #[inline] + pub const fn new() -> IdStr { + skip_assert_initialized!(); + unsafe { + // Safety: empty inlined string consists in the type being all zeroed + IdStr { + inner: mem::zeroed(), + } + } + } + + // rustdoc-stripper-ignore-next + /// Builds an `IdStr` from the given static `GStr`. + /// + /// This constructor performs optimizations which other constructors can't rely on. + /// + /// To build an `IdStr` from a string literal, use the [`idstr`](crate::idstr) macro. + #[inline] + pub fn from_static + ?Sized>(value: &'static T) -> IdStr { + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set_static(value); + + ret + } + + #[doc(alias = "gst_id_str_new")] + #[inline] + pub fn from>(value: T) -> IdStr { + skip_assert_initialized!(); + let mut id = IdStr::new(); + id.set(value); + + id + } + + #[doc(alias = "gst_id_str_get_len")] + #[inline] + pub fn len(&self) -> usize { + unsafe { ffi::gst_id_str_get_len(self.to_glib_none().0) } + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + // rustdoc-stripper-ignore-next + /// Returns the pointer to the nul terminated string `value` represented by this `IdStr`. + #[inline] + fn as_char_ptr(&self) -> NonNull { + unsafe { + let ptr = ffi::gst_id_str_as_str(self.to_glib_none().0); + debug_assert!(!ptr.is_null()); + let nn = NonNull::::new_unchecked(ptr as *mut _); + + debug_assert_eq!(*nn.as_ptr().add(self.len()), 0, "expecting nul terminator"); + + nn + } + } + + #[inline] + pub fn as_bytes(&self) -> &[u8] { + unsafe { + // Safety: `as_char_ptr()` returns a non-null pointer to a nul terminated string. + std::slice::from_raw_parts(self.as_char_ptr().as_ptr() as *const _, self.len()) + } + } + + #[inline] + fn as_bytes_with_nul(&self) -> &[u8] { + unsafe { + // Safety: `as_char_ptr()` returns a non-null pointer to a nul terminated string. + std::slice::from_raw_parts(self.as_char_ptr().as_ptr() as *const _, self.len() + 1) + } + } + + #[inline] + pub fn as_str(&self) -> &str { + cfg_if::cfg_if! { + if #[cfg(debug_assertions)] { + std::str::from_utf8(self.as_bytes()).unwrap() + } else { + unsafe { + std::str::from_utf8_unchecked(self.as_bytes()) + } + } + } + } + + #[doc(alias = "gst_id_str_as_str")] + #[inline] + pub fn as_gstr(&self) -> &GStr { + cfg_if::cfg_if! { + if #[cfg(debug_assertions)] { + GStr::from_utf8_with_nul(self.as_bytes_with_nul()).unwrap() + } else { + unsafe { + GStr::from_utf8_with_nul_unchecked(self.as_bytes_with_nul()) + } + } + } + } + + #[inline] + pub fn as_cstr(&self) -> &CStr { + cfg_if::cfg_if! { + if #[cfg(debug_assertions)] { + CStr::from_bytes_with_nul(self.as_bytes_with_nul()).unwrap() + } else { + unsafe { + CStr::from_bytes_with_nul_unchecked(self.as_bytes_with_nul()) + } + } + } + } + + #[doc(alias = "gst_id_str_is_equal")] + #[inline] + fn is_equal(&self, s2: &IdStr) -> bool { + unsafe { + from_glib(ffi::gst_id_str_is_equal( + self.to_glib_none().0, + s2.to_glib_none().0, + )) + } + } + + #[doc(alias = "gst_id_str_is_equal_to_str_with_len")] + #[inline] + fn is_equal_to_str(&self, s2: impl AsRef) -> bool { + unsafe { + let s2 = s2.as_ref(); + from_glib(ffi::gst_id_str_is_equal_to_str_with_len( + self.to_glib_none().0, + s2.as_ptr() as *const c_char, + s2.len(), + )) + } + } + + // rustdoc-stripper-ignore-next + /// Sets `self` to the static string `value`. + /// + /// This function performs optimizations which [IdStr::set] can't rely on. + /// + /// To build an `IdStr` from a string literal, use the [`idstr`](crate::idstr) macro. + #[doc(alias = "gst_id_str_set_static_str")] + #[doc(alias = "gst_id_str_set_static_str_with_len")] + #[inline] + pub fn set_static + ?Sized>(&mut self, value: &'static T) { + unsafe { + let v = value.as_ref(); + ffi::gst_id_str_set_static_str_with_len( + self.to_glib_none_mut().0, + v.to_glib_none().0, + v.len(), + ); + } + } + + // rustdoc-stripper-ignore-next + /// Sets `self` to the string `value`. + /// + /// For a static value, use [IdStr::set_static] which can perform optimizations. + /// + /// To build an `IdStr` from a string literal, use the [`idstr`](crate::idstr) macro. + #[doc(alias = "gst_id_str_set")] + #[doc(alias = "gst_id_str_set_with_len")] + #[inline] + pub fn set(&mut self, value: impl AsRef) { + unsafe { + let v = value.as_ref(); + ffi::gst_id_str_set_with_len( + self.to_glib_none_mut().0, + v.as_ptr() as *const c_char, + v.len(), + ); + } + } +} + +impl Default for IdStr { + fn default() -> Self { + Self::new() + } +} + +impl Deref for IdStr { + type Target = GStr; + + fn deref(&self) -> &Self::Target { + self.as_gstr() + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &IdStr { + self + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &GStr { + self.as_gstr() + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &CStr { + self.as_cstr() + } +} + +impl From<&str> for IdStr { + #[inline] + fn from(value: &str) -> IdStr { + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set(value); + + ret + } +} + +impl From<&String> for IdStr { + #[inline] + fn from(value: &String) -> IdStr { + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set(value); + + ret + } +} + +impl From for IdStr { + #[inline] + fn from(value: String) -> IdStr { + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set(&value); + + ret + } +} + +impl From<&GStr> for IdStr { + #[inline] + fn from(value: &GStr) -> IdStr { + // assert checked in new() + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set(value); + + ret + } +} + +impl From<&GString> for IdStr { + #[inline] + fn from(value: &GString) -> IdStr { + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set(value); + + ret + } +} + +impl From for IdStr { + #[inline] + fn from(value: GString) -> IdStr { + skip_assert_initialized!(); + let mut ret = IdStr::new(); + ret.set(&value); + + ret + } +} + +impl fmt::Display for IdStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_gstr()) + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.is_equal(other) + } +} + +impl PartialOrd<&IdStr> for IdStr { + #[inline] + fn partial_cmp(&self, other: &&IdStr) -> Option { + Some(self.cmp(*other)) + } +} + +impl PartialEq<&IdStr> for IdStr { + #[inline] + fn eq(&self, other: &&IdStr) -> bool { + self.is_equal(other) + } +} + +impl Ord for IdStr { + #[inline] + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.as_cstr().cmp(other.as_cstr()) + } +} + +impl Eq for IdStr {} + +impl PartialOrd<&GStr> for IdStr { + #[inline] + fn partial_cmp(&self, other: &&GStr) -> Option { + self.as_str().partial_cmp(*other) + } +} + +impl PartialEq<&GStr> for IdStr { + #[inline] + fn eq(&self, other: &&GStr) -> bool { + self.is_equal_to_str(other) + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &GStr) -> Option { + self.as_str().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &GStr) -> bool { + self.is_equal_to_str(other) + } +} + +impl PartialOrd for &GStr { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + (*self).partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for &GStr { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + other.is_equal_to_str(self) + } +} + +impl PartialOrd for GStr { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for GStr { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + other.is_equal_to_str(self) + } +} + +impl PartialOrd<&str> for IdStr { + #[inline] + fn partial_cmp(&self, other: &&str) -> Option { + self.as_gstr().partial_cmp(*other) + } +} + +impl PartialEq<&str> for IdStr { + #[inline] + fn eq(&self, other: &&str) -> bool { + self.is_equal_to_str(*other) + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &str) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &str) -> bool { + self.is_equal_to_str(other) + } +} + +impl PartialOrd for &str { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + (*self).partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for &str { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + other.is_equal_to_str(self) + } +} + +impl PartialOrd for str { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for str { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + other.is_equal_to_str(self) + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &GString) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &GString) -> bool { + self.is_equal_to_str(other) + } +} + +impl PartialOrd for GString { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for GString { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + other.is_equal_to_str(self) + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &String) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &String) -> bool { + self.is_equal_to_str(other) + } +} + +impl PartialOrd for String { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for String { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + other.is_equal_to_str(self) + } +} + +impl Hash for IdStr { + fn hash(&self, state: &mut H) { + self.as_gstr().hash(state) + } +} + +unsafe impl Send for IdStr {} +unsafe impl Sync for IdStr {} + +// Tests are mutualised between this implementation and the one in id_str_compat +// See gstreamer/id_str/mod.rs diff --git a/gstreamer/src/id_str/compat.rs b/gstreamer/src/id_str/compat.rs new file mode 100644 index 000000000..5f45980ff --- /dev/null +++ b/gstreamer/src/id_str/compat.rs @@ -0,0 +1,543 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +// rustdoc-stripper-ignore-next +//! The `IdStr` compatibility implementation. +//! +//! See the higher level module documentation for details. + +use glib::{GStr, GString, IntoGStr}; +use std::{ + cmp, + ffi::CStr, + fmt, + hash::{Hash, Hasher}, + ops::Deref, +}; + +use kstring::KString; + +// rustdoc-stripper-ignore-next +/// An UTF-8 immutable string type with optimizations for short values (len < 16). +#[derive(Clone, Debug)] +#[doc(alias = "GstIdStr")] +pub struct IdStr(KString); + +impl IdStr { + // In order to keep the same API and usability as `id_str_bindings::IdStr` regarding + // the ability to efficiently deref to `&GStr`, the internal `KString` is always built + // from a string with a nul terminator. + + #[doc(alias = "gst_id_str_new")] + #[inline] + pub const fn new() -> IdStr { + skip_assert_initialized!(); + // Always include the nul terminator in the internal string + IdStr(KString::from_static("\0")) + } + + // rustdoc-stripper-ignore-next + /// Builds an `IdStr` from the given static `GStr`. + /// + /// This constructor performs optimizations which other constructors can't rely on. + /// + /// To build an `IdStr` from a string literal, use the [`idstr`](crate::idstr) macro. + #[inline] + pub fn from_static + ?Sized>(value: &'static T) -> IdStr { + skip_assert_initialized!(); + let gstr = value.as_ref(); + unsafe { + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + gstr.as_ptr() as *const _, + gstr.as_bytes_with_nul().len(), + )); + + IdStr(KString::from_static(str_with_nul)) + } + } + + #[doc(alias = "gst_id_str_new")] + #[inline] + pub fn from(value: impl AsRef) -> IdStr { + skip_assert_initialized!(); + let mut id = IdStr::new(); + id.set(value); + + id + } + + #[doc(alias = "gst_id_str_get_len")] + #[inline] + pub fn len(&self) -> usize { + // The internal string ends with a nul terminator + self.0.len() - 1 + } + + #[inline] + pub fn is_empty(&self) -> bool { + // The internal string ends with a nul terminator + self.0.len() == 1 + } + + #[inline] + pub fn as_bytes(&self) -> &[u8] { + // The internal string ends with a nul terminator + &self.0.as_bytes()[..IdStr::len(self)] + } + + #[inline] + fn as_bytes_with_nul(&self) -> &[u8] { + // The internal string ends with a nul terminator + self.0.as_bytes() + } + + #[inline] + pub fn as_str(&self) -> &str { + unsafe { + // Safety: the internal value is guaranteed to be an utf-8 string. + std::str::from_utf8_unchecked(self.as_bytes()) + } + } + + #[doc(alias = "gst_id_str_as_str")] + #[inline] + pub fn as_gstr(&self) -> &GStr { + unsafe { + // Safety: the internal value is guaranteed to be an utf-8 string. + GStr::from_utf8_with_nul_unchecked(self.as_bytes_with_nul()) + } + } + + #[doc(alias = "gst_id_str_as_str")] + #[inline] + pub fn as_cstr(&self) -> &CStr { + unsafe { + // Safety: the internal value is guaranteed to be an utf-8 string + // thus to not contain any nul bytes except for the terminator. + CStr::from_bytes_with_nul_unchecked(self.as_bytes_with_nul()) + } + } + + // rustdoc-stripper-ignore-next + /// Sets `self` to the static string `value`. + /// + /// This function performs optimizations which [IdStr::set] can't rely on. + /// + /// To build an `IdStr` from a string literal, use the [`idstr`](crate::idstr) macro. + #[doc(alias = "gst_id_str_set_static_str")] + #[doc(alias = "gst_id_str_set_static_str_with_len")] + #[inline] + pub fn set_static + ?Sized>(&mut self, value: &'static T) { + unsafe { + let gstr = value.as_ref(); + // Safety: the `GStr` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + gstr.as_ptr() as *const _, + gstr.as_bytes_with_nul().len(), + )); + + self.0 = KString::from_static(str_with_nul); + } + } + + // rustdoc-stripper-ignore-next + /// Sets `self` to the string `value`. + /// + /// For a static value, use [IdStr::set_static] which can perform optimizations. + /// + /// To build an `IdStr` from a string literal, use the [`idstr`](crate::idstr) macro. + #[doc(alias = "gst_id_str_set")] + #[doc(alias = "gst_id_str_set_with_len")] + #[inline] + pub fn set(&mut self, value: impl AsRef) { + self.0 = value.as_ref().run_with_gstr(|gstr| unsafe { + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + gstr.as_ptr() as *const _, + gstr.as_bytes_with_nul().len(), + )); + + KString::from_ref(str_with_nul) + }); + } +} + +impl Default for IdStr { + fn default() -> Self { + Self::new() + } +} + +impl Deref for IdStr { + type Target = GStr; + + fn deref(&self) -> &Self::Target { + self.as_gstr() + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &IdStr { + self + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &GStr { + self.as_gstr() + } +} + +impl AsRef for IdStr { + #[inline] + fn as_ref(&self) -> &CStr { + self.as_cstr() + } +} + +impl From<&str> for IdStr { + #[inline] + fn from(value: &str) -> IdStr { + skip_assert_initialized!(); + value.run_with_gstr(|gstr| unsafe { + // Safety: the `GStr` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + gstr.as_ptr() as *const _, + gstr.as_bytes_with_nul().len(), + )); + + IdStr(KString::from_ref(str_with_nul)) + }) + } +} + +impl From<&String> for IdStr { + #[inline] + fn from(value: &String) -> IdStr { + skip_assert_initialized!(); + value.run_with_gstr(|gstr| unsafe { + // Safety: the `GStr` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + gstr.as_ptr() as *const _, + gstr.as_bytes_with_nul().len(), + )); + + IdStr(KString::from_ref(str_with_nul)) + }) + } +} + +impl From for IdStr { + #[inline] + fn from(value: String) -> IdStr { + skip_assert_initialized!(); + value.run_with_gstr(|gstr| unsafe { + // Safety: the `GStr` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + gstr.as_ptr() as *const _, + gstr.as_bytes_with_nul().len(), + )); + + IdStr(KString::from_ref(str_with_nul)) + }) + } +} + +impl From<&GStr> for IdStr { + #[inline] + fn from(value: &GStr) -> IdStr { + skip_assert_initialized!(); + unsafe { + // Safety: the `GStr` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + value.as_ptr() as *const _, + value.as_bytes_with_nul().len(), + )); + + IdStr(KString::from_ref(str_with_nul)) + } + } +} + +impl From<&GString> for IdStr { + #[inline] + fn from(value: &GString) -> IdStr { + skip_assert_initialized!(); + unsafe { + // Safety: the `GString` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + value.as_ptr() as *const _, + value.len() + 1, + )); + + IdStr(KString::from_ref(str_with_nul)) + } + } +} + +impl From for IdStr { + #[inline] + fn from(value: GString) -> IdStr { + skip_assert_initialized!(); + unsafe { + // Safety: the `GString` value is guaranteed to be an utf-8 string + // ending with a nul terminator. + let str_with_nul = std::str::from_utf8_unchecked(std::slice::from_raw_parts( + value.as_ptr() as *const _, + value.len() + 1, + )); + + IdStr(KString::from_ref(str_with_nul)) + } + } +} + +impl fmt::Display for IdStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_gstr()) + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl PartialOrd<&IdStr> for IdStr { + #[inline] + fn partial_cmp(&self, other: &&IdStr) -> Option { + Some(self.0.cmp(&other.0)) + } +} + +impl PartialEq<&IdStr> for IdStr { + #[inline] + fn eq(&self, other: &&IdStr) -> bool { + self.0 == other.0 + } +} + +impl PartialOrd for &IdStr { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + Some(self.0.cmp(&other.0)) + } +} + +impl PartialEq for &IdStr { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + self.0 == other.0 + } +} + +impl Ord for IdStr { + #[inline] + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl Eq for IdStr {} + +impl PartialOrd<&GStr> for IdStr { + #[inline] + fn partial_cmp(&self, other: &&GStr) -> Option { + self.as_gstr().partial_cmp(*other) + } +} + +impl PartialEq<&GStr> for IdStr { + #[inline] + fn eq(&self, other: &&GStr) -> bool { + self.as_gstr() == *other + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &GStr) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &GStr) -> bool { + self.as_gstr() == other + } +} + +impl PartialOrd for &GStr { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + (*self).partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for &GStr { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + (*self) == other.as_gstr() + } +} + +impl PartialOrd for GStr { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for GStr { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + self == other.as_gstr() + } +} + +impl PartialOrd<&str> for IdStr { + #[inline] + fn partial_cmp(&self, other: &&str) -> Option { + self.as_gstr().partial_cmp(*other) + } +} + +impl PartialEq<&str> for IdStr { + #[inline] + fn eq(&self, other: &&str) -> bool { + self.as_gstr() == *other + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &str) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &str) -> bool { + self.as_gstr() == other + } +} + +impl PartialOrd for &str { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + (*self).partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for &str { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + (*self) == other.as_gstr() + } +} + +impl PartialOrd for str { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for str { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + self == other.as_gstr() + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &GString) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &GString) -> bool { + self.as_gstr() == other + } +} + +impl PartialOrd for GString { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for GString { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + self == other.as_gstr() + } +} + +impl PartialOrd for IdStr { + #[inline] + fn partial_cmp(&self, other: &String) -> Option { + self.as_gstr().partial_cmp(other) + } +} + +impl PartialEq for IdStr { + #[inline] + fn eq(&self, other: &String) -> bool { + self.as_gstr() == other + } +} + +impl PartialOrd for String { + #[inline] + fn partial_cmp(&self, other: &IdStr) -> Option { + self.partial_cmp(other.as_gstr()) + } +} + +impl PartialEq for String { + #[inline] + fn eq(&self, other: &IdStr) -> bool { + self == other.as_gstr() + } +} + +impl Hash for IdStr { + fn hash(&self, state: &mut H) { + self.as_gstr().hash(state) + } +} + +unsafe impl Send for IdStr {} +unsafe impl Sync for IdStr {} + +// Tests are mutualised between this implementation and the one in id_str_bindings +// See gstreamer/id_str/mod.rs diff --git a/gstreamer/src/id_str/mod.rs b/gstreamer/src/id_str/mod.rs new file mode 100644 index 000000000..e2ebc3a90 --- /dev/null +++ b/gstreamer/src/id_str/mod.rs @@ -0,0 +1,435 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +// rustdoc-stripper-ignore-next +//! This module selects one of the two `IdStr` implementations: +//! +//! * When feature `v1_26` (or later) is activated, `IdStr` implements the bindings +//! for the C type `GstIdStr`. +//! * For earlier feature versions, a compatibility implementation is used. +//! +//! See also: + +cfg_if::cfg_if! { + if #[cfg(feature = "v1_26")] { + mod bindings; + pub use self::bindings::IdStr; + } else { + mod compat; + pub use self::compat::IdStr; + } +} + +#[cfg(feature = "serde")] +mod serde; + +// rustdoc-stripper-ignore-next +/// Builds an [`IdStr`] from a string literal. +/// +/// # Examples +/// +/// ``` +/// # fn main() { +/// use gstreamer::{idstr, IdStr}; +/// use std::sync::LazyLock; +/// +/// static MY_ID_STR: LazyLock = LazyLock::new(|| idstr!("static id")); +/// assert_eq!(*MY_ID_STR, "static id"); +/// +/// let my_id_str: IdStr = idstr!("local id"); +/// assert_eq!(my_id_str, "local id"); +/// # } +/// ``` +/// +/// [`IdStr`]: crate::IdStr +#[macro_export] +macro_rules! idstr { + ($s:literal) => { + IdStr::from_static($crate::glib::gstr!($s)) + }; +} + +#[cfg(test)] +mod tests { + use glib::{gstr, GStr, GString}; + use std::{ffi::CStr, sync::LazyLock}; + + use super::IdStr; + + const STR: &str = "STR"; + static IDSTR: LazyLock = LazyLock::new(|| idstr!("IDSTR")); + static GSTR: &GStr = gstr!("GSTR"); + static GSTRING: LazyLock = LazyLock::new(|| GString::from("GSTRING")); + + const LONG_STR: &str = "An STR longer than 15 bytes"; + static LONG_IDSTR: LazyLock = LazyLock::new(|| idstr!("An IdStr longer than 15 bytes")); + static LONG_GSTR: &GStr = gstr!("A GSTR longer than 15 bytes"); + static LONG_GSTRING: LazyLock = + LazyLock::new(|| GString::from("A GSTRING longer than 15 bytes")); + + #[test] + fn new_set_static() { + assert!(!IDSTR.is_empty()); + assert_eq!(IDSTR.len(), "IDSTR".len()); + assert_eq!(IDSTR.as_str().len(), "IDSTR".len()); + assert_eq!(*IDSTR, "IDSTR"); + // Display impl + assert_eq!(IDSTR.to_string(), "IDSTR"); + assert_eq!(IDSTR.as_str(), "IDSTR"); + assert_eq!(IDSTR.as_gstr().len(), "IDSTR".len()); + assert_eq!(IDSTR.as_gstr(), "IDSTR"); + + let id_str: IdStr = idstr!("id_str"); + assert!(!id_str.is_empty()); + assert_eq!(id_str.len(), "id_str".len()); + assert_eq!(id_str.as_str().len(), "id_str".len()); + assert_eq!(id_str, "id_str"); + + let mut s = IdStr::new(); + assert!(s.is_empty()); + assert_eq!(s.len(), 0); + assert_eq!(s.as_str(), ""); + assert_eq!(s.as_gstr(), ""); + + s.set_static(gstr!("str")); + assert!(!s.is_empty()); + assert_eq!(s.len(), "str".len()); + assert_eq!(s.as_str().len(), "str".len()); + assert_eq!(s, "str"); + // Display impl + assert_eq!(s.to_string(), "str"); + assert_eq!(s.as_str(), "str"); + assert_eq!(s.as_gstr().len(), "str".len()); + assert_eq!(s.as_gstr(), "str"); + + s.set_static(GSTR); + assert_eq!(s.as_str(), "GSTR"); + + s.set_static(&*GSTRING); + assert_eq!(s.as_str(), "GSTRING"); + + assert!(!LONG_IDSTR.is_empty()); + assert_eq!(LONG_IDSTR.len(), "An IdStr longer than 15 bytes".len()); + assert_eq!(*LONG_IDSTR, "An IdStr longer than 15 bytes"); + // Display impl + assert_eq!(LONG_IDSTR.to_string(), "An IdStr longer than 15 bytes"); + assert_eq!( + LONG_IDSTR.as_str().len(), + "An IdStr longer than 15 bytes".len() + ); + assert_eq!(LONG_IDSTR.as_str(), "An IdStr longer than 15 bytes"); + assert_eq!( + LONG_IDSTR.as_gstr().len(), + "An IdStr longer than 15 bytes".len() + ); + assert_eq!(LONG_IDSTR.as_gstr(), "An IdStr longer than 15 bytes"); + + let ls = idstr!("An IdStr longer than 15 bytes"); + assert!(!ls.is_empty()); + assert_eq!(ls.len(), "An IdStr longer than 15 bytes".len()); + assert_eq!(ls, "An IdStr longer than 15 bytes"); + + let mut ls = IdStr::new(); + + ls.set_static(gstr!("An str longer than 15 bytes")); + assert!(!ls.is_empty()); + assert_eq!(ls.len(), "An str longer than 15 bytes".len()); + assert_eq!(ls, "An str longer than 15 bytes"); + + ls.set_static(LONG_GSTR); + assert_eq!(ls.as_str(), "A GSTR longer than 15 bytes"); + + ls.set_static(&*LONG_GSTRING); + assert_eq!(ls.as_str(), "A GSTRING longer than 15 bytes"); + } + + #[test] + fn from_static() { + let s = IdStr::from_static(gstr!("str")); + assert!(!s.is_empty()); + assert_eq!(s.len(), "str".len()); + assert_eq!(s.as_str().len(), "str".len()); + assert_eq!(s, "str"); + // Display impl + assert_eq!(s.to_string(), "str"); + assert_eq!(s.as_str(), "str"); + assert_eq!(s.as_gstr().len(), "str".len()); + assert_eq!(s.as_gstr(), "str"); + + let s = idstr!("str"); + assert!(!s.is_empty()); + assert_eq!(s.len(), "str".len()); + assert_eq!(s.as_str().len(), "str".len()); + assert_eq!(s, "str"); + + let s = IdStr::from_static(GSTR); + assert_eq!(s.as_str(), "GSTR"); + + let s = IdStr::from_static(&*GSTRING); + assert_eq!(s.as_str(), "GSTRING"); + + let ls = IdStr::from_static(gstr!("An str longer than 15 bytes")); + assert!(!ls.is_empty()); + assert_eq!(ls.len(), "An str longer than 15 bytes".len()); + assert_eq!(ls, "An str longer than 15 bytes"); + // Display impl + assert_eq!(ls.to_string(), "An str longer than 15 bytes"); + assert_eq!(ls.as_str().len(), "An str longer than 15 bytes".len()); + assert_eq!(ls.as_str(), "An str longer than 15 bytes"); + assert_eq!(ls.as_gstr().len(), "An str longer than 15 bytes".len()); + assert_eq!(ls.as_gstr(), "An str longer than 15 bytes"); + + let ls = idstr!("An str longer than 15 bytes"); + assert!(!ls.is_empty()); + assert_eq!(ls.len(), "An str longer than 15 bytes".len()); + assert_eq!(ls, "An str longer than 15 bytes"); + + let ls = IdStr::from_static(LONG_GSTR); + assert_eq!(ls.as_str(), "A GSTR longer than 15 bytes"); + + let ls = IdStr::from_static(&*LONG_GSTRING); + assert_eq!(ls.as_str(), "A GSTRING longer than 15 bytes"); + } + + #[test] + fn new_set() { + let d = IdStr::default(); + assert!(d.is_empty()); + assert_eq!(d.len(), 0); + assert_eq!(d.as_str(), ""); + assert_eq!(d.as_gstr(), ""); + + let mut s = IdStr::new(); + assert!(s.is_empty()); + assert_eq!(s.len(), 0); + assert_eq!(s.as_str(), ""); + assert_eq!(s.as_gstr(), ""); + + s.set("str"); + assert!(!s.is_empty()); + assert_eq!(s.len(), "str".len()); + assert_eq!(s.as_str().len(), "str".len()); + assert_eq!(s.as_str(), "str"); + assert_eq!(AsRef::::as_ref(&s), "str"); + // Display impl + assert_eq!(s.to_string(), "str"); + assert_eq!(s.as_gstr().len(), "str".len()); + assert_eq!(s.as_gstr(), "str"); + assert_eq!(AsRef::::as_ref(&s), "str"); + assert_eq!(s.as_cstr().to_bytes(), b"str"); + assert_eq!(AsRef::::as_ref(&s).to_bytes(), b"str"); + assert_eq!(s.as_bytes(), b"str"); + + let string = String::from("String"); + s.set(string.as_str()); + assert_eq!(s.as_str(), "String"); + s.set(&string); + assert_eq!(s.as_str(), "String"); + + s.set(gstr!("gstr")); + assert_eq!(s.as_str(), "gstr"); + + let gstring = GString::from("GString"); + s.set(gstring.as_gstr()); + assert_eq!(s.as_str(), "GString"); + s.set(&gstring); + assert_eq!(s.as_str(), "GString"); + s.set(gstring.as_str()); + assert_eq!(s.as_str(), "GString"); + + let mut ls = IdStr::new(); + + ls.set("An str longer than 15 bytes"); + assert!(!ls.is_empty()); + assert_eq!(ls.len(), "An str longer than 15 bytes".len()); + assert_eq!(ls, "An str longer than 15 bytes"); + // Display impl + assert_eq!(ls.to_string(), "An str longer than 15 bytes"); + assert_eq!(ls.as_str().len(), "An str longer than 15 bytes".len()); + assert_eq!(ls.as_str(), "An str longer than 15 bytes"); + assert_eq!(ls.as_gstr().len(), "An str longer than 15 bytes".len()); + assert_eq!(ls.as_gstr(), "An str longer than 15 bytes"); + assert_eq!(ls.as_cstr().to_bytes(), b"An str longer than 15 bytes"); + assert_eq!( + AsRef::::as_ref(&ls).to_bytes(), + b"An str longer than 15 bytes" + ); + assert_eq!(ls.as_bytes(), b"An str longer than 15 bytes"); + + ls.set(gstr!("A gstr longer than 15 bytes")); + assert_eq!(ls.as_str(), "A gstr longer than 15 bytes"); + } + + #[test] + fn from() { + let s = IdStr::from("str"); + assert_eq!(s.len(), "str".len()); + assert_eq!(s.as_str().len(), "str".len()); + assert_eq!(s.as_str(), "str"); + // Display impl + assert_eq!(s.to_string(), "str"); + assert_eq!(s.as_gstr().len(), "str".len()); + assert_eq!(s.as_gstr(), "str"); + + let string = String::from("String"); + let s = IdStr::from(string.as_str()); + assert_eq!(s.as_str(), "String"); + let s: IdStr = string.as_str().into(); + assert_eq!(s.as_str(), "String"); + let s: IdStr = (&string).into(); + assert_eq!(s.as_str(), "String"); + let s: IdStr = string.into(); + assert_eq!(s.as_str(), "String"); + + let s = IdStr::from(gstr!("str")); + assert_eq!(s.as_str(), "str"); + + let gstring = GString::from("GString"); + let s = IdStr::from(gstring.as_gstr()); + assert_eq!(s.as_str(), "GString"); + let s: IdStr = (&gstring).into(); + assert_eq!(s.as_str(), "GString"); + let s: IdStr = gstring.into(); + assert_eq!(s.as_str(), "GString"); + + let ls = IdStr::from("An str longer than 15 bytes"); + assert!(!ls.is_empty()); + assert_eq!(ls.len(), "An str longer than 15 bytes".len()); + assert_eq!(ls, "An str longer than 15 bytes"); + // Display impl + assert_eq!(ls.to_string(), "An str longer than 15 bytes"); + assert_eq!(ls.as_str().len(), "An str longer than 15 bytes".len()); + assert_eq!(ls.as_str(), "An str longer than 15 bytes"); + assert_eq!(ls.as_gstr().len(), "An str longer than 15 bytes".len()); + assert_eq!(ls.as_gstr(), "An str longer than 15 bytes"); + + let ls = IdStr::from(gstr!("A gstr longer than 15 bytes")); + assert_eq!(ls.as_str(), "A gstr longer than 15 bytes"); + assert_eq!(ls.as_gstr(), "A gstr longer than 15 bytes"); + + let lstring = String::from("A String longer than 15 bytes"); + let ls = IdStr::from(lstring.as_str()); + assert_eq!(ls.as_str(), "A String longer than 15 bytes"); + let ls = IdStr::from(&lstring); + assert_eq!(ls.as_str(), "A String longer than 15 bytes"); + + let lgstring = String::from("A GString longer than 15 bytes"); + let ls = IdStr::from(lgstring.as_str()); + assert_eq!(ls.as_str(), "A GString longer than 15 bytes"); + let ls = IdStr::from(&lgstring); + assert_eq!(ls.as_str(), "A GString longer than 15 bytes"); + } + + #[test] + #[allow(clippy::cmp_owned)] + fn eq_cmp() { + let s1 = IdStr::from(STR); + let s12: IdStr = STR.into(); + assert_eq!(s1, s12); + let s2 = IdStr::from(String::from(STR)); + let s22: IdStr = String::from(STR).into(); + assert_eq!(s2, s22); + let s3 = IdStr::from_static(gstr!("STR")); + assert_eq!(s1, s2); + assert_eq!(s1, s3); + assert_eq!(s2, s3); + + assert!(s1 == gstr!("STR")); + assert_eq!(s1, gstr!("STR")); + assert_eq!(s1, GString::from("STR")); + assert!(s1 == "STR"); + assert_eq!(s1, "STR"); + assert!("STR" == s1); + assert_eq!("STR", s1); + assert_eq!(s1, String::from("STR")); + + assert_eq!(gstr!("STR"), s1); + assert_eq!(GString::from("STR"), s1); + assert_eq!("STR", s1); + assert_eq!(String::from("STR"), s1); + + let ls1 = IdStr::from(LONG_STR); + let ls2: IdStr = String::from(LONG_STR).into(); + let ls3 = IdStr::from_static(gstr!("An STR longer than 15 bytes")); + assert_eq!(ls1, ls2); + assert_eq!(ls1, ls3); + assert_eq!(ls2, ls3); + + assert!(ls1 == gstr!("An STR longer than 15 bytes")); + assert_eq!(ls1, gstr!("An STR longer than 15 bytes")); + assert_eq!(ls1, GString::from(LONG_STR)); + assert_eq!(ls1, LONG_STR); + assert!(ls1 == "An STR longer than 15 bytes"); + assert_eq!(ls1, "An STR longer than 15 bytes"); + assert_eq!(ls1, String::from(LONG_STR)); + + assert_eq!(gstr!("An STR longer than 15 bytes"), ls1); + assert_eq!(GString::from(LONG_STR), ls1); + assert_eq!(LONG_STR, ls1); + assert_eq!("An STR longer than 15 bytes", ls1); + assert_eq!(String::from(LONG_STR), ls1); + + assert_ne!(s1, ls1); + assert_ne!(ls1, s1); + + let s4 = IdStr::from("STR4"); + assert_ne!(s1, s4); + assert!(s1 < s4); + assert!(s4 > s1); + + assert!(s1 < gstr!("STR4")); + assert!(s1 < GString::from("STR4")); + assert!(s1 < "STR4"); + assert!("STR4" > s1); + assert!(s1 < String::from("STR4")); + + assert!(gstr!("STR4") > s1); + assert!(GString::from("STR4") > s1); + assert!("STR4" > s1); + assert!(String::from("STR4") > s1); + + // ls1 starts with an 'A', s4 with an 'S' + assert_ne!(ls1, s4); + assert!(ls1 < s4); + assert!(s4 > s1); + } + + #[test] + fn as_ref_idstr() { + #[allow(clippy::nonminimal_bool)] + fn check(c: &str, v: impl AsRef) { + let v = v.as_ref(); + + assert_eq!(c, v); + assert_eq!(v, c); + + assert!(!(c > v)); + assert!(!(v > c)); + + let i = IdStr::from(c); + assert_eq!(i, v); + assert_eq!(i, IdStr::from(c)); + + assert!(!(c > i)); + assert!(!(i > c)); + } + + let v = IdStr::from(STR); + check(STR, &v); + check(STR, v); + + #[allow(clippy::nonminimal_bool)] + fn check_gstr(c: &GStr, v: impl AsRef) { + let v = v.as_ref(); + + assert_eq!(c, v); + assert_eq!(v, c); + + assert!(!(c > v)); + assert!(!(v > c)); + } + + let v = IdStr::from(GSTR); + check_gstr(GSTR, &v); + check_gstr(GSTR, v); + } +} diff --git a/gstreamer/src/id_str/serde.rs b/gstreamer/src/id_str/serde.rs new file mode 100644 index 000000000..d40e40eb4 --- /dev/null +++ b/gstreamer/src/id_str/serde.rs @@ -0,0 +1,37 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use serde::{ + de::{Deserialize, Deserializer}, + ser::{Serialize, Serializer}, +}; + +use crate::IdStr; + +impl Serialize for IdStr { + fn serialize(&self, serializer: S) -> Result { + self.as_str().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for IdStr { + fn deserialize>(deserializer: D) -> Result { + skip_assert_initialized!(); + <&str>::deserialize(deserializer).map(IdStr::from) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::idstr; + + #[test] + fn ser_de() { + assert_eq!( + ron::ser::to_string(&idstr!("my IdStr")), + Ok("\"my IdStr\"".to_owned()) + ); + + assert_eq!(ron::de::from_str("\"my IdStr\""), Ok(idstr!("my IdStr"))); + } +} diff --git a/gstreamer/src/lib.rs b/gstreamer/src/lib.rs index 73a83b7ff..9d9a61856 100644 --- a/gstreamer/src/lib.rs +++ b/gstreamer/src/lib.rs @@ -82,6 +82,10 @@ pub use crate::value::{ #[macro_use] mod value_serde; +#[macro_use] +mod id_str; +pub use crate::id_str::IdStr; + #[cfg(feature = "serde")] mod flag_serde;