diff --git a/cpp-overload-test/examples/stdcpp-u32string-view.rs b/cpp-overload-test/examples/stdcpp-u32string-view.rs new file mode 100644 index 0000000..37adfa9 --- /dev/null +++ b/cpp-overload-test/examples/stdcpp-u32string-view.rs @@ -0,0 +1,175 @@ +//! Test C++ [std::u32string_view][u32string_view] overloads that can't be represented in Rust, and record the macro diagnostics. +//! Error output is in [stdcpp-u32string-view.stderr]. +//! Working overloads are in [u32string_view.rs]. +//! +//! [u32string_view]: https://en.cppreference.com/cpp/string/basic_string_view +//! [stdcpp-u32string-view.stderr]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/examples/stdcpp-u32string-view.stderr +//! [u32string_view.rs]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/u32string_view.rs +//! +//! The `cpp!` macro doesn't work here, because of technical limitations it can only run one build +//! per crate. (Examples and binaries are built separately from `lib.rs`.) +//! +//! ### No receiver overloads +//! +//! The other C++ examples fail on accessor and modifier overloads, such as `begin(&self)` and +//! `begin(&mut self)`. `std::u32string_view` has no such overloads. Every accessor (`operator[]`, +//! `at`, `front`, `back`, `data`, `size`, `begin`, `end`, ...) is `const`, and the few modifiers +//! (`remove_prefix`, `remove_suffix`, `swap`) each have a single overload, so there is no +//! `&self` against `&mut self` overload set to record here. The overloads that do fail are the +//! constructor templates below. +//! +//! ### Inexpressible Overloads +//! +//! Some C++ features have no equivalent in Rust, so their overloads can't be wrapped at all: +//! - The C++23 `basic_string_view(std::nullptr_t) = delete` constructor. It exists only to make +//! `nullptr` a compile error, and a deleted overload has no Rust equivalent. +//! - The negative constraints that choose between the templated constructors. Rust trait bounds +//! can say what a type must implement, but not what it must *not* implement. In C++: +//! - The iterator pair constructor `(It first, End last)` requires a `contiguous_iterator` and +//! a `sized_sentinel_for`, and also that `End` is *not* convertible to `std::size_t`. That last +//! rule is what stops it from taking over the pointer and count constructor +//! `(const CharT* s, size_type count)`. Without it, the two have the same signature when `It` +//! is a pointer and `End` is `size_t`, which is the `E0119` conflicting implementations error +//! recorded for `StdU32StringViewPair` below. +//! - The range constructor requires a `contiguous_range` and `sized_range`, and also that `R` is +//! *not* the same type as `basic_string_view`, and *not* convertible to `const CharT*`. The +//! first rule stops it from taking over the copy constructor, and the second stops it from +//! taking over the null-terminated constructor. +//! +//! The constraints are listed on the constructor page: +//! + +#![feature(splat, tuple_trait, c_size_t)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; +use std::ffi::{c_size_t, c_void}; + +/// A wrapper struct to hold the returned C++ `std::u32string_view` pointer. +#[expect(dead_code, reason = "Incompatible overloads can't be used")] +struct StdU32StringView(*mut c_void); + +/// The C++ `char32_t` type. +type CharT = u32; + +// This overload set has overloads that need generic parameters, which `overload!` can't express. +overload! { + impl StdU32StringView { + /// Construct a view of the first `count` characters at `s`. + /// + /// This overload is compatible. It is here so the set has a working overload next to the + /// failing generic ones, and it is the overload the iterator pair clashes with in + /// `StdU32StringViewPair` below. + /// + /// The working wrapper takes a `&'static [CharT]` and a count instead of a raw pointer, + /// because `overload!` drops `unsafe`, so a raw pointer overload could be called from safe + /// code. + fn new(_s: *const CharT, _count: c_size_t) -> StdU32StringView { + todo!() + } + + /// Construct a view from a C++20 iterator and sentinel pair. + /// + /// ### Incompatibilities + /// + /// In C++ this is `template basic_string_view(It first, End last)`. + /// The Rust parameter is called `Sentinel` instead, because rustc suggests unrelated standard + /// library items for a missing type called `End`, which would make the recorded output + /// depend on the standard library version. + /// + /// The `overload!` macro doesn't support generic parameters: it builds each overload from + /// the argument names and types only, so `It` and `Sentinel` are not declared in the code it + /// generates, and the compiler reports "cannot find type". + /// + /// ### Workarounds + /// + /// - Replace the generic types with concrete types. The working wrapper uses a slice and a + /// `Range` of offsets into it. + /// - Use fake generic types, as in the `forward_list` example: `type It = *const CharT;` + /// and `type Sentinel = *const CharT;` compile, and the overload is then only available + /// for those types. + /// - Add generic parameter and `where` clause support to the `overload!` macro. + fn new(_first: It, _last: Sentinel) -> StdU32StringView { + todo!() + } + + /// Construct a view from a C++23 contiguous range. + /// + /// ### Incompatibilities + /// + /// In C++ this is `template explicit basic_string_view(R&& r)`, which has the + /// same problem as the iterator pair: `R` is not declared in the generated code. + /// + /// ### Workarounds + /// + /// - Replace `R` with a concrete type, such as `&'static [CharT]`. The working wrapper + /// does this. + /// - Use a fake generic type: `type R = &'static [CharT];` compiles. + /// - Add generic parameter support to the `overload!` macro. A fully generic `R` would + /// also need a way to exclude `&StdU32StringView`, so it doesn't overlap the copy + /// constructor. C++ does that with a constraint that `R` is not the same type as + /// `basic_string_view`, which Rust can't express, see the "Inexpressible Overloads" + /// section in the [crate docs](crate#inexpressible-overloads). + fn new(_r: R) -> StdU32StringView { + todo!() + } + } +} + +/// A second wrapper struct for the second overload set. It also needs a different method name, +/// `newer`, because every `overload!` block generates a trait named after its method, and two +/// blocks in one module can't both generate `NewArgs`. +#[expect(dead_code, reason = "Incompatible overloads can't be used")] +struct StdU32StringViewPair(*mut c_void); + +/// Workaround for missing generic support in the `overload!` macro: fake the iterator and +/// sentinel types with concrete types. +/// +/// These are not called `It` and `Sentinel`, because that would declare the names used by the +/// generic overload above, and hide the "cannot find type" errors it records. +/// +/// The sentinel is `c_size_t`, which is exactly what the C++ constraint "`End` is not convertible +/// to `std::size_t`" excludes. +type PairIt = *const CharT; +type PairSentinel = c_size_t; + +// This overload set has overloads that clash when the fake generic types are `*const CharT` and +// `c_size_t`. The method is called `newer` here, to stand in for `new`. +overload! { + impl StdU32StringViewPair { + /// Construct a view of the first `count` characters at `s`. + fn newer(_s: *const CharT, _count: c_size_t) -> StdU32StringViewPair { + todo!() + } + + /// Construct a view from a C++20 iterator and sentinel pair. + /// + /// ### Incompatibilities + /// + /// With `It` faked as `*const CharT` and `Sentinel` faked as `c_size_t`, this overload has + /// the same argument types as `newer(s, count)`, but different semantics: + /// - `newer(s, count)` views `count` characters starting at `s`. + /// - `newer(first, last)` views the characters from `first` up to, but not including, + /// `last`. + /// + /// Rust can't have two overloads with the same argument types, so the macro generates two + /// conflicting trait implementations (error `E0119`). C++ avoids the clash by excluding + /// any `End` that is convertible to `std::size_t`, see the "Inexpressible Overloads" + /// section in the [crate docs](crate#inexpressible-overloads). + /// + /// ### Workarounds + /// + /// - Don't fake `Sentinel` as `c_size_t`. A sentinel of `*const CharT` doesn't clash, and + /// it is the only sentinel type a pointer iterator needs. + /// - Use different argument types for the two overloads. The working wrapper takes a slice + /// and a `Range` for the iterator pair, and a slice and a count for the pointer + /// overload. + /// - Add a way to express negative constraints to the `overload!` macro. + fn newer(_first: PairIt, _last: PairSentinel) -> StdU32StringViewPair { + todo!() + } + } +} + +pub fn main() {} diff --git a/cpp-overload-test/examples/stdcpp-u32string-view.stderr b/cpp-overload-test/examples/stdcpp-u32string-view.stderr new file mode 100644 index 0000000..818112f --- /dev/null +++ b/cpp-overload-test/examples/stdcpp-u32string-view.stderr @@ -0,0 +1,37 @@ +error[E0425]: cannot find type `It` in this scope + --> cpp-overload-test/examples/stdcpp-u32string-view.rs:93:38 + | +93 | fn new(_first: It, _last: Sentinel) -> StdU32StringView { + | ^^ not found in this scope + +error[E0425]: cannot find type `Sentinel` in this scope + --> cpp-overload-test/examples/stdcpp-u32string-view.rs:93:49 + | +93 | fn new(_first: It, _last: Sentinel) -> StdU32StringView { + | ^^^^^^^^ not found in this scope + +error[E0425]: cannot find type `R` in this scope + --> cpp-overload-test/examples/stdcpp-u32string-view.rs:114:23 + | +114 | fn new(_r: R) -> StdU32StringView { + | ^ not found in this scope + +error[E0119]: conflicting implementations of trait `NewerArgs` for type `(*const u32, usize)` + --> cpp-overload-test/examples/stdcpp-u32string-view.rs:139:1 + | +139 | / overload! { +140 | | impl StdU32StringViewPair { +141 | | /// Construct a view of the first `count` characters at `s`. +142 | | fn newer(_s: *const CharT, _count: c_size_t) -> StdU32StringViewPair { +... | +173 | | } + | | ^ + | | | + | |_first implementation here + | conflicting implementation for `(*const u32, usize)` + | + = note: this error originates in the macro `overload` (in Nightly builds, run with -Z macro-backtrace for more info) + +Some errors have detailed explanations: E0119, E0425. +For more information about an error, try `rustc --explain E0119`. +error: could not compile `cpp-overload-test` (example "stdcpp-u32string-view") due to 4 previous errors diff --git a/cpp-overload-test/src/lib.rs b/cpp-overload-test/src/lib.rs index 98274d5..7cf334e 100644 --- a/cpp-overload-test/src/lib.rs +++ b/cpp-overload-test/src/lib.rs @@ -12,6 +12,7 @@ pub mod stdcpp { pub mod forward_list; pub mod hash; pub mod time_point; + pub mod u32string_view; pub mod unordered_map; pub mod unordered_multiset; } diff --git a/cpp-overload-test/src/stdcpp/u32string_view.rs b/cpp-overload-test/src/stdcpp/u32string_view.rs new file mode 100644 index 0000000..612cbb9 --- /dev/null +++ b/cpp-overload-test/src/stdcpp/u32string_view.rs @@ -0,0 +1,906 @@ +//! Test that C++ overloads for [std::u32string_view][u32string_view] can be successfully represented in Rust. +//! +//! [u32string_view]: https://en.cppreference.com/cpp/string/basic_string_view +//! +//! Wrapped overloaded names: constructors, `compare`, `find`, `starts_with`. +//! + +#![allow(unused_braces, reason = "Generated by overload! macro")] + +use cpp::cpp; +use splat_overload::overload; +use std::ffi::{c_char, c_int, c_size_t, c_void}; +use std::marker::PhantomData; +use std::ops::Range; + +// C++ header includes +cpp! {{ + #include + #include + #include +}} + +/// A wrapper struct to hold the returned C++ `std::u32string_view` pointer. +/// +/// ### Invariant +/// +/// Every `StdU32StringView` is either empty, or views `'static` characters. `copy_from` relies on +/// this, so any new constructor must keep it. Pass in `&'static [CharT]` as a workaround. +/// +/// ### Limitations +/// +/// A C++ `u32string_view` doesn't own its characters, so it dangles if they are freed first. +/// The natural Rust type is `StdU32StringView<'a>`, but `overload!` doesn't accept it: it +/// generates `impl StdU32StringView` from a bare type name. +/// +/// Callers can only build a view from static data, such as string-literal constants, constant +/// arrays, or a leaked `Box`. A view of a local array is a compile error. Supporting borrowed +/// data needs generics (and lifetime) support in `overload!`. +/// +/// `std::basic_string_view` is a template, so this wrapper is hardcoded to `char32_t`. +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +pub struct StdU32StringView(*mut c_void, PhantomData); + +/// The Rust type used for C++ `char32_t`. +/// +/// ### Limitations +/// +/// Rust's standard library has no type for `char32_t`, so `u32` is used here. But `char32_t` is +/// only guaranteed to match `uint_least32_t`, which is 32 bits on most platforms, but not all: +/// +/// +/// This is a portability limitation. On a platform where `char32_t` isn't 32 bits, this wrapper +/// should fail to compile, because the `cpp!` macro checks the sizes of the values it passes to +/// C++. Rust could add a matching type to its standard library in future, and this alias could +/// then change to it. +/// +/// Rust's `char` can't be used instead: it excludes surrogates and values above `0x10FFFF`, but +/// `char32_t` can hold any 32-bit value. +pub type CharT = u32; + +/// A slice that contains a zero terminator, standing in for a C++ `const char32_t*` C string. +/// +/// The string ends at the *first* zero, which doesn't have to be the last element. Any characters +/// after the first zero are ignored, because C++ stops scanning there, so it can never read past +/// the end of the slice. This also means a zero can't be part of the string: use the +/// `(slice, count)` overloads for text with embedded zeros. +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +#[derive(Copy, Clone, Debug)] +pub struct NullTerminated<'a>(&'a [CharT]); + +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +impl<'a> NullTerminated<'a> { + /// Returns `None` if `s` has no zero at all, because C++ would read past the end. + /// + /// A zero anywhere in `s` is enough. The string is the characters before the first zero. + pub fn new(s: &'a [CharT]) -> Option> { + s.contains(&0).then_some(NullTerminated(s)) + } + + fn as_ptr(&self) -> *const CharT { + self.0.as_ptr() + } +} + +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +#[allow( + non_upper_case_globals, + reason = "Triggered by cpp! macro in rust-analyzer" +)] +impl StdU32StringView { + fn default() -> StdU32StringView { + let view = unsafe { + cpp!([] -> *mut c_void as "std::u32string_view*" { + return new std::u32string_view(); + }) + }; + StdU32StringView(view, PhantomData) + } + + /// ### Soundness + /// + /// The copy views the same characters as `other`, so it must be valid for as long as `other`'s + /// characters are. That is only guaranteed because every other constructor takes `'static` + /// data (or no data), so `other` always views `'static` characters, and so does the copy. + /// + /// If a constructor is added that takes shorter-lived data, for example once `overload!` + /// supports a lifetime parameter, this must change. Otherwise it would extend the lifetime + /// of the copy beyond the lifetime of its characters. + fn copy_from(other: &StdU32StringView) -> StdU32StringView { + let other_ptr = other.0 as *const c_void; + let view = unsafe { + cpp!([other_ptr as "const std::u32string_view*"] -> *mut c_void as "std::u32string_view*" { + return new std::u32string_view(*other_ptr); + }) + }; + StdU32StringView(view, PhantomData) + } + + fn from_ptr_and_count(s: &'static [CharT], count: c_size_t) -> StdU32StringView { + assert!( + count <= s.len(), + "count {count} is greater than the slice length {}", + s.len() + ); + // An empty Rust slice is guaranteed to have a dangling pointer, see + // + if s.is_empty() { + return StdU32StringView::default(); + } + let ptr = s.as_ptr(); + let view = unsafe { + cpp!([ptr as "const char32_t*", count as "size_t"] -> *mut c_void as "std::u32string_view*" { + return new std::u32string_view(ptr, count); + }) + }; + StdU32StringView(view, PhantomData) + } + + fn from_null_terminated(s: NullTerminated<'static>) -> StdU32StringView { + let ptr = s.as_ptr(); + let view = unsafe { + cpp!([ptr as "const char32_t*"] -> *mut c_void as "std::u32string_view*" { + return new std::u32string_view(ptr); + }) + }; + StdU32StringView(view, PhantomData) + } + + fn from_iterator_pair(s: &'static [CharT], range: Range) -> StdU32StringView { + assert!( + range.start <= range.end && range.end <= s.len(), + "range {range:?} is not within the slice length {}", + s.len() + ); + // An empty Rust slice is guaranteed to have a dangling pointer, see + // + if s.is_empty() { + return StdU32StringView::default(); + } + let sub = &s[range]; + let first = sub.as_ptr(); + let last = unsafe { first.add(sub.len()) }; + let view = unsafe { + cpp!([first as "const char32_t*", last as "const char32_t*"] -> *mut c_void as "std::u32string_view*" { + return new std::u32string_view(first, last); + }) + }; + StdU32StringView(view, PhantomData) + } + + fn from_range(items: &'static [CharT]) -> StdU32StringView { + // An empty Rust slice is guaranteed to have a dangling pointer, see + // + if items.is_empty() { + return StdU32StringView::default(); + } + let len = items.len(); + let ptr = items.as_ptr(); + let view = unsafe { + cpp!([ptr as "const char32_t*", len as "size_t"] -> *mut c_void as "std::u32string_view*" { + // C++23 range constructor, called with a contiguous range of `char32_t`. + return new std::u32string_view(std::span(ptr, len)); + }) + }; + StdU32StringView(view, PhantomData) + } + + // Non-overloaded accessors, only used to check which constructor was called. + + fn size(&self) -> c_size_t { + let slf = self.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*"] -> c_size_t as "size_t" { + return slf->size(); + }) + } + } + + fn data(&self) -> *const CharT { + let slf = self.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*"] -> *const CharT as "const char32_t*" { + return slf->data(); + }) + } + } + + /// The viewed characters. + fn chars(&self) -> &[CharT] { + let len = self.size(); + if len == 0 { + return &[]; + } + // The view always points at `'static` characters, which are valid for the rest of the program. + unsafe { std::slice::from_raw_parts(self.data(), len) } + } +} + +// The `basic_string_view(nullptr_t) = delete` constructor (C++23) is not wrapped. +// It exists only to reject `nullptr` at compile time, and Rust references +// can't be `nullptr_t`. +overload! { + impl StdU32StringView { + /// Construct an empty view. + pub fn new() -> StdU32StringView { + StdU32StringView::default() + } + + /// Construct a view by copying another view. The copy shares the original's characters. + pub fn new(other: &StdU32StringView) -> StdU32StringView { + StdU32StringView::copy_from(other) + } + + /// Construct a view of the first `count` characters of `s`. + /// + /// Panics if `count` is greater than `s.len()`. + /// + /// ### Limitations + /// + /// The C++ constructor takes a `const char32_t*`, and the view doesn't own the characters. + /// Rust can't accept that soundly, because `overload!` drops `unsafe` and lifetimes, so + /// this overload takes a `'static` slice that is checked against `count`. + pub fn new(s: &'static [CharT], count: c_size_t) -> StdU32StringView { + StdU32StringView::from_ptr_and_count(s, count) + } + + /// Construct a view of the characters of `s` before its first zero. + /// + /// ### Ergonomics + /// + /// Callers must wrap the slice with `NullTerminated::new(slice)`, which returns `None` + /// if there is no terminator. Rust slices aren't null-terminated, so a bare slice can't + /// select this overload. + pub fn new(s: NullTerminated<'static>) -> StdU32StringView { + StdU32StringView::from_null_terminated(s) + } + + /// Construct a view from the `range` of offsets into `s`. + /// + /// Panics if `range` is reversed or ends after `s.len()`. + /// + /// ### Limitations + /// + /// C++ accepts any `contiguous_iterator` and `sized_sentinel_for` pair. Rust has no + /// matching trait for iterators across the FFI boundary, and raw pointers can't be + /// checked, so the pair is expressed as offsets into one slice. + pub fn new(s: &'static [CharT], range: Range) -> StdU32StringView { + StdU32StringView::from_iterator_pair(s, range) + } + + /// Construct a view from a contiguous range (C++23). + /// + /// ### Limitations + /// + /// C++ accepts any `contiguous_range` with a matching element type. Only slices of + /// `char32_t` are supported here. + /// + /// ### Ergonomics + /// + /// `as_slice()` is required to match the slice overload, as arrays don't + /// automatically coerce. + pub fn new(items: &'static [CharT]) -> StdU32StringView { + StdU32StringView::from_range(items) + } + } +} + +// ### compare +// +// All six C++ `compare` overloads. They return negative, zero or positive, like `strcmp`. +// +// ### Limitations +// +// The `pos` overloads throw `std::out_of_range` in C++ when `pos > size()`. C++ exceptions can't +// unwind through Rust frames, so the process would abort. These wrappers check the positions +// first and panic instead. `count` arguments larger than the remaining length are clamped, as +// in C++. +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +impl StdU32StringView { + fn check_pos(&self, pos: c_size_t) { + assert!( + pos <= self.size(), + "pos {pos} is greater than the view size {}", + self.size() + ); + } + + fn compare_view(&self, v: &StdU32StringView) -> c_int { + let slf = self.0 as *const c_void; + let v = v.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", v as "const std::u32string_view*"] -> c_int as "int" { + return slf->compare(*v); + }) + } + } + + fn compare_substr_view(&self, pos1: c_size_t, count1: c_size_t, v: &StdU32StringView) -> c_int { + self.check_pos(pos1); + let slf = self.0 as *const c_void; + let v = v.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", pos1 as "size_t", count1 as "size_t", v as "const std::u32string_view*"] -> c_int as "int" { + return slf->compare(pos1, count1, *v); + }) + } + } + + fn compare_substr_substr( + &self, + pos1: c_size_t, + count1: c_size_t, + v: &StdU32StringView, + pos2: c_size_t, + count2: c_size_t, + ) -> c_int { + self.check_pos(pos1); + v.check_pos(pos2); + let slf = self.0 as *const c_void; + let v = v.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", pos1 as "size_t", count1 as "size_t", v as "const std::u32string_view*", pos2 as "size_t", count2 as "size_t"] -> c_int as "int" { + return slf->compare(pos1, count1, *v, pos2, count2); + }) + } + } + + fn compare_ptr(&self, s: NullTerminated<'_>) -> c_int { + let slf = self.0 as *const c_void; + let s = s.as_ptr(); + unsafe { + cpp!([slf as "const std::u32string_view*", s as "const char32_t*"] -> c_int as "int" { + return slf->compare(s); + }) + } + } + + fn compare_substr_ptr(&self, pos1: c_size_t, count1: c_size_t, s: NullTerminated<'_>) -> c_int { + self.check_pos(pos1); + let slf = self.0 as *const c_void; + let s = s.as_ptr(); + unsafe { + cpp!([slf as "const std::u32string_view*", pos1 as "size_t", count1 as "size_t", s as "const char32_t*"] -> c_int as "int" { + return slf->compare(pos1, count1, s); + }) + } + } + + fn compare_substr_ptr_count( + &self, + pos1: c_size_t, + count1: c_size_t, + s: &[CharT], + count2: c_size_t, + ) -> c_int { + self.check_pos(pos1); + assert!( + count2 <= s.len(), + "count2 {count2} is greater than the slice length {}", + s.len() + ); + // An empty Rust slice has a dangling (non-null) pointer, see + // + // C++ accepts a null pointer with a zero count as an empty range. + let s: *const CharT = if s.is_empty() { + std::ptr::null() + } else { + s.as_ptr() + }; + let slf = self.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", pos1 as "size_t", count1 as "size_t", s as "const char32_t*", count2 as "size_t"] -> c_int as "int" { + return slf->compare(pos1, count1, s, count2); + }) + } + } +} + +overload! { + impl StdU32StringView { + /// Compare the whole view with another view. + pub fn compare(&self, v: &StdU32StringView) -> c_int { + self.compare_view(v) + } + + /// Compare `self[pos1..pos1 + count1]` with another view. + /// + /// Panics if `pos1 > self.size()`. + pub fn compare(&self, pos1: c_size_t, count1: c_size_t, v: &StdU32StringView) -> c_int { + self.compare_substr_view(pos1, count1, v) + } + + /// Compare `self[pos1..pos1 + count1]` with `v[pos2..pos2 + count2]`. + /// + /// Panics if `pos1 > self.size()` or `pos2 > v.size()`. + /// + /// ### Ergonomics + /// + /// Five positional arguments of mostly the same type are easy to mix up. + pub fn compare( + &self, + pos1: c_size_t, + count1: c_size_t, + v: &StdU32StringView, + pos2: c_size_t, + count2: c_size_t, + ) -> c_int { + self.compare_substr_substr(pos1, count1, v, pos2, count2) + } + + /// Compare the whole view with a null-terminated string. + pub fn compare(&self, s: NullTerminated<'_>) -> c_int { + self.compare_ptr(s) + } + + /// Compare `self[pos1..pos1 + count1]` with a null-terminated string. + /// + /// Panics if `pos1 > self.size()`. + pub fn compare(&self, pos1: c_size_t, count1: c_size_t, s: NullTerminated<'_>) -> c_int { + self.compare_substr_ptr(pos1, count1, s) + } + + /// Compare `self[pos1..pos1 + count1]` with the first `count2` characters of `s`. + /// + /// Panics if `pos1 > self.size()` or `count2 > s.len()`. + pub fn compare( + &self, + pos1: c_size_t, + count1: c_size_t, + s: &[CharT], + count2: c_size_t, + ) -> c_int { + self.compare_substr_ptr_count(pos1, count1, s, count2) + } + } +} + +// ### find +// +// The four C++ `find` overloads, with the default `pos = 0` argument expanded into separate Rust +// overloads, so there are seven in total. `rfind` is similar to this so it was not wrapped. +// +// ### Limitations +// +// The C++ "not found" result is `std::u32string_view::npos`, which is `usize::MAX` here. +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +impl StdU32StringView { + fn find_view(&self, v: &StdU32StringView, pos: c_size_t) -> c_size_t { + let slf = self.0 as *const c_void; + let v = v.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", v as "const std::u32string_view*", pos as "size_t"] -> c_size_t as "size_t" { + return slf->find(*v, pos); + }) + } + } + + fn find_char(&self, ch: CharT, pos: c_size_t) -> c_size_t { + let slf = self.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", ch as "char32_t", pos as "size_t"] -> c_size_t as "size_t" { + return slf->find(ch, pos); + }) + } + } + + fn find_ptr_count(&self, s: &[CharT], pos: c_size_t, count: c_size_t) -> c_size_t { + assert!( + count <= s.len(), + "count {count} is greater than the slice length {}", + s.len() + ); + // An empty Rust slice has a dangling (non-null) pointer, see + // + // C++ accepts a null pointer with a zero count as an empty range. + let s: *const CharT = if s.is_empty() { + std::ptr::null() + } else { + s.as_ptr() + }; + let slf = self.0 as *const c_void; + unsafe { + cpp!([slf as "const std::u32string_view*", s as "const char32_t*", pos as "size_t", count as "size_t"] -> c_size_t as "size_t" { + return slf->find(s, pos, count); + }) + } + } + + fn find_ptr(&self, s: NullTerminated<'_>, pos: c_size_t) -> c_size_t { + let slf = self.0 as *const c_void; + let s = s.as_ptr(); + unsafe { + cpp!([slf as "const std::u32string_view*", s as "const char32_t*", pos as "size_t"] -> c_size_t as "size_t" { + return slf->find(s, pos); + }) + } + } +} + +overload! { + impl StdU32StringView { + /// Find the first occurrence of a view, starting at `pos`. + pub fn find(&self, v: &StdU32StringView, pos: c_size_t) -> c_size_t { + self.find_view(v, pos) + } + + /// Find the first occurrence of a view. + /// + /// ### Ergonomics + /// + /// Rust has no default arguments, so each C++ default needs its own overload. + pub fn find(&self, v: &StdU32StringView) -> c_size_t { + self.find_view(v, 0) + } + + /// Find the first occurrence of a character, starting at `pos`. + pub fn find(&self, ch: CharT, pos: c_size_t) -> c_size_t { + self.find_char(ch, pos) + } + + /// Find the first occurrence of a character. + pub fn find(&self, ch: CharT) -> c_size_t { + self.find_char(ch, 0) + } + + /// Find the first occurrence of the first `count` characters of `s`, starting at `pos`. + /// + /// Panics if `count > s.len()`. + pub fn find(&self, s: &[CharT], pos: c_size_t, count: c_size_t) -> c_size_t { + self.find_ptr_count(s, pos, count) + } + + /// Find the first occurrence of a null-terminated string, starting at `pos`. + pub fn find(&self, s: NullTerminated<'_>, pos: c_size_t) -> c_size_t { + self.find_ptr(s, pos) + } + + /// Find the first occurrence of a null-terminated string. + pub fn find(&self, s: NullTerminated<'_>) -> c_size_t { + self.find_ptr(s, 0) + } + } +} + +// ### starts_with +// +// ### Limitations +// +// C++ returns `bool`. Rust has no `c_bool`, so the wrapper returns a `char`-sized value across +// the FFI boundary and converts it. +#[cfg_attr(not(test), expect(dead_code, reason = "Only used in tests"))] +impl StdU32StringView { + fn starts_with_view(&self, v: &StdU32StringView) -> bool { + let slf = self.0 as *const c_void; + let v = v.0 as *const c_void; + let result = unsafe { + cpp!([slf as "const std::u32string_view*", v as "const std::u32string_view*"] -> c_char as "char" { + return slf->starts_with(*v); + }) + }; + result != 0 + } + + fn starts_with_char(&self, ch: CharT) -> bool { + let slf = self.0 as *const c_void; + let result = unsafe { + cpp!([slf as "const std::u32string_view*", ch as "char32_t"] -> c_char as "char" { + return slf->starts_with(ch); + }) + }; + result != 0 + } + + fn starts_with_ptr(&self, s: NullTerminated<'_>) -> bool { + let slf = self.0 as *const c_void; + let s = s.as_ptr(); + let result = unsafe { + cpp!([slf as "const std::u32string_view*", s as "const char32_t*"] -> c_char as "char" { + return slf->starts_with(s); + }) + }; + result != 0 + } +} + +overload! { + impl StdU32StringView { + /// Check if the view starts with another view. + pub fn starts_with(&self, v: &StdU32StringView) -> bool { + self.starts_with_view(v) + } + + /// Check if the view starts with a character. + pub fn starts_with(&self, ch: CharT) -> bool { + self.starts_with_char(ch) + } + + /// Check if the view starts with a null-terminated string. + pub fn starts_with(&self, s: NullTerminated<'_>) -> bool { + self.starts_with_ptr(s) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const NPOS: c_size_t = c_size_t::MAX; + + /// Convert text to `char32_t` values. `\0` in the text becomes a zero character. + /// The constructors need `'static` characters, so the buffer is leaked. + fn cs(text: &str) -> &'static [CharT] { + Box::leak( + text.chars() + .map(|c| c as CharT) + .collect::>() + .into_boxed_slice(), + ) + } + + /// A view of all of `text`. + fn view(text: &str) -> StdU32StringView { + StdU32StringView::new(cs(text)) + } + + /// Coerce a constant array reference to a `'static` slice, which overload selection won't do. + fn st(items: &'static [CharT]) -> &'static [CharT] { + items + } + + fn nt(buf: &'static [CharT]) -> NullTerminated<'static> { + NullTerminated::new(buf).expect("test buffer has a terminator") + } + + #[test] + fn constructor_default() { + let v = StdU32StringView::new(); + assert_eq!(v.size(), 0); + assert!(v.data().is_null()); + assert!(v.chars().is_empty()); + } + + #[test] + fn constructor_pointer_and_count() { + // The count is used, and the embedded terminator is ignored. + let text = cs("hello\0xy"); + let v = StdU32StringView::new(text, 8); + assert_eq!(v.chars(), text); + + let prefix = StdU32StringView::new(text, 3); + assert_eq!(prefix.chars(), cs("hel")); + + assert_eq!(StdU32StringView::new(text, 0).size(), 0); + assert_eq!(StdU32StringView::new(st(&[]), 0).size(), 0); + } + + #[test] + #[should_panic(expected = "greater than the slice length")] + fn constructor_pointer_and_count_too_long() { + StdU32StringView::new(cs("ab"), 3); + } + + #[test] + fn constructor_null_terminated() { + // Scans for the terminator, so only "hello". + let text = cs("hello\0xy"); + let v = StdU32StringView::new(nt(text)); + assert_eq!(v.chars(), cs("hello")); + + // A leading terminator is an empty string. + assert_eq!(StdU32StringView::new(nt(cs("\0a"))).size(), 0); + + // The string ends at the first zero, not the last: later zeros and characters are ignored. + let v = StdU32StringView::new(nt(cs("ab\0cd\0"))); + assert_eq!(v.chars(), cs("ab")); + assert_eq!(StdU32StringView::new(nt(cs("\0\0\0"))).size(), 0); + + // No terminator: C++ would read past the end, so it can't be constructed. + assert!(NullTerminated::new(cs("abc")).is_none()); + assert!(NullTerminated::new(st(&[])).is_none()); + } + + #[test] + fn constructor_iterator_pair() { + let text = cs("abcdef"); + + // A sub-range of offsets, like (begin + 1, begin + 4). + let mid = StdU32StringView::new(text, 1..4); + assert_eq!(mid.chars(), cs("bcd")); + + // Full and empty range. + assert_eq!(StdU32StringView::new(text, 0..6).chars(), text); + assert_eq!(StdU32StringView::new(text, 0..0).size(), 0); + + // The range may contain zeros. + let zeros = cs("\0a\0b"); + assert_eq!(StdU32StringView::new(zeros, 0..4).chars(), zeros); + } + + #[test] + #[should_panic(expected = "is not within the slice length")] + fn constructor_iterator_pair_end_out_of_bounds() { + StdU32StringView::new(cs("abc"), 1..4); + } + + #[test] + fn constructor_range() { + // The whole slice is used, including embedded zeros, emoji and the largest `char32_t`. + let text: &'static [CharT] = &[0, u32::MAX, '😀' as CharT, 55296]; // 55296 is a surrogate + let v = StdU32StringView::new(text); + assert_eq!(v.chars(), text); + + assert_eq!(StdU32StringView::new(st(&[])).size(), 0); + } + + #[test] + fn constructor_copy() { + let original = view("hello"); + let copy = StdU32StringView::new(&original); + assert_eq!(copy.chars(), original.chars()); + // A C++ copy shares the characters rather than duplicating them. + assert_eq!(copy.data(), original.data()); + + assert_eq!(StdU32StringView::new(&StdU32StringView::new()).size(), 0); + } + + #[test] + fn constructors_pick_different_cpp_constructors() { + // The same slice gives different sizes depending on which C++ constructor ran. + let text = cs("ab\0cde"); + assert_eq!(StdU32StringView::new(text).size(), 6); // range + assert_eq!(StdU32StringView::new(nt(text)).size(), 2); // null-terminated + assert_eq!(StdU32StringView::new(text, 4).size(), 4); // (s, count) + assert_eq!(StdU32StringView::new(text, 1..3).size(), 2); // (first, last) + } + + // compare + + #[test] + fn compare_view() { + let (a, b) = (view("abc"), view("abd")); + assert!(a.compare(&b) < 0); + assert!(b.compare(&a) > 0); + assert_eq!(a.compare(&a), 0); + + // A prefix sorts before the longer string, and empty sorts first. + assert!(view("ab").compare(&view("abc")) < 0); + assert!(view("abc").compare(&view("ab")) > 0); + assert!(StdU32StringView::new().compare(&view("a")) < 0); + assert_eq!(StdU32StringView::new().compare(&StdU32StringView::new()), 0); + } + + #[test] + fn compare_substr_view() { + let hay = view("abcdef"); + assert_eq!(hay.compare(1, 2, &view("bc")), 0); + assert!(hay.compare(0, 2, &view("bc")) < 0); + // A zero count is an empty substring. + assert_eq!(hay.compare(2, 0, &StdU32StringView::new()), 0); + } + + #[test] + #[should_panic(expected = "pos 7 is greater than the view size 6")] + fn compare_substr_view_pos_out_of_range() { + view("abcdef").compare(7, 1, &view("a")); + } + + #[test] + fn compare_substr_substr() { + let (a, b) = (view("abcdef"), view("xxcdyy")); + assert_eq!(a.compare(2, 2, &b, 2, 2), 0); + assert!(a.compare(1, 2, &b, 2, 2) < 0); + } + + #[test] + fn compare_null_terminated() { + let a = view("abc"); + assert_eq!(a.compare(nt(cs("abc\0"))), 0); + assert!(a.compare(nt(cs("abd\0"))) < 0); + // Only characters before the terminator count. + assert_eq!(a.compare(nt(cs("abc\0z"))), 0); + assert!(a.compare(nt(cs("\0"))) > 0); + } + + #[test] + fn compare_substr_null_terminated() { + let a = view("abcdef"); + assert_eq!(a.compare(1, 2, nt(cs("bc\0"))), 0); + assert!(a.compare(1, 3, nt(cs("bc\0"))) > 0); + assert_eq!(a.compare(6, 1, nt(cs("\0"))), 0); + } + + #[test] + fn compare_substr_pointer_and_count() { + let a = view("abcdef"); + let bcd = cs("bcd"); + assert_eq!(a.compare(1, 2, bcd, 2), 0); + assert!(a.compare(1, 2, bcd, 1) > 0); + assert!(a.compare(1, 2, bcd, 3) < 0); + // Zero count2 compares against the empty string. + assert!(a.compare(1, 2, bcd, 0) > 0); + } + + // find + + #[test] + fn find_view() { + let hay = view("abcabc"); + let needle = view("bc"); + assert_eq!(hay.find(&needle), 1); + assert_eq!(hay.find(&needle, 1), 1); + assert_eq!(hay.find(&needle, 2), 4); + assert_eq!(hay.find(&needle, 5), NPOS); + + // Needle longer than the haystack, and a needle that is the whole haystack. + assert_eq!(view("bc").find(&view("abc")), NPOS); + assert_eq!(hay.find(&hay), 0); + } + + #[test] + fn find_char() { + let hay = view("abcabc"); + assert_eq!(hay.find('c' as CharT), 2); + assert_eq!(hay.find('c' as CharT, 3), 5); + assert_eq!(hay.find('c' as CharT, 6), NPOS); + assert_eq!(hay.find('z' as CharT), NPOS); + } + + #[test] + fn find_pointer_and_count() { + let hay = view("abcabc"); + let bc = cs("bcx"); + // Only the first `count` characters of `s` are the needle. + assert_eq!(hay.find(bc, 0, 2), 1); + assert_eq!(hay.find(bc, 2, 2), 4); + assert_eq!(hay.find(bc, 0, 3), NPOS); + } + + #[test] + fn find_null_terminated() { + let hay = view("abcabc"); + let bc = cs("bc\0"); + assert_eq!(hay.find(nt(bc)), 1); + assert_eq!(hay.find(nt(bc), 2), 4); + assert_eq!(hay.find(nt(bc), 5), NPOS); + // Characters after the terminator are not part of the needle. + assert_eq!(hay.find(nt(cs("bc\0z"))), 1); + // The empty string matches at `pos`. + assert_eq!(hay.find(nt(cs("\0")), 3), 3); + } + + // starts_with + + #[test] + fn starts_with_view() { + let hay = view("abc"); + assert!(hay.starts_with(&view("ab"))); + assert!(hay.starts_with(&view("abc"))); + assert!(!hay.starts_with(&view("bc"))); + assert!(!hay.starts_with(&view("abcd"))); + // The empty view is a prefix of everything, including the empty view. + assert!(hay.starts_with(&StdU32StringView::new())); + assert!(StdU32StringView::new().starts_with(&StdU32StringView::new())); + assert!(!StdU32StringView::new().starts_with(&view("a"))); + } + + #[test] + fn starts_with_char() { + let hay = view("abc"); + assert!(hay.starts_with('a' as CharT)); + assert!(!hay.starts_with('b' as CharT)); + assert!(!StdU32StringView::new().starts_with('a' as CharT)); + } + + #[test] + fn starts_with_null_terminated() { + let hay = view("abc"); + assert!(hay.starts_with(nt(cs("ab\0")))); + assert!(!hay.starts_with(nt(cs("bc\0")))); + assert!(hay.starts_with(nt(cs("\0")))); + // Characters after the terminator are not part of the prefix. + assert!(hay.starts_with(nt(cs("ab\0z")))); + assert!(!hay.starts_with(nt(cs("abcd\0")))); + } +} diff --git a/cpp-overloads-in-rust.md b/cpp-overloads-in-rust.md index 7c5d0b4..f5df808 100644 --- a/cpp-overloads-in-rust.md +++ b/cpp-overloads-in-rust.md @@ -28,6 +28,7 @@ See the Limitations section below the table for more details. | *Template Classes* | Constructors | Accessors | Modifiers | Transforms | | [forward_list] | ✅ | 🌗⚙️[^1] | 🌗⚙️?[^2] | ✅ | | [hash] | ✅ | ✅ | | | +| [u32string_view] | 🌗⚙️⛔[^6] | 🌗[^7] | | | | [unordered_map] | ✅ | 🌗⚙️[^5] | 🌗⚙️ | | | [unordered_multiset] | ✅ | ✅⚙️[^3] | ✅ | ?[^4] | | [time_point] | ✅ | ✅ | | ✅ | @@ -41,6 +42,7 @@ Transforms: Rearrange the items in a list class, called as `instance->sort(...)` [forward_list]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/forward_list.rs [hash]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/hash.rs [time_point]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/time_point.rs +[u32string_view]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/u32string_view.rs [unordered_map]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/unordered_map.rs [unordered_multiset]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/unordered_multiset.rs @@ -49,6 +51,8 @@ Transforms: Rearrange the items in a list class, called as `instance->sort(...)` [^3]: The overload macro can't overload on `&self` vs `&mut self`, which limits representing some accessor overloads. [^4]: `extract`, `merge`, and `erase_if` still need analysis. These are C++17/C++20 node handle and non-member function overloads. [^5]: `find` and `max_load_factor` use associated functions instead of methods due to macro limitations with `&self` vs `&mut self`. +[^6]: All six constructors are wrapped, but iterator/range templates require concrete Rust types; views are limited to `'static` data, and the iterator-pair/pointer-count signature conflict requires a workaround (see example file). +[^7]: `compare`, `find`, and `starts_with` are wrapped; C++ default arguments and null-terminated strings require ergonomic workarounds, while some equivalent overloads were skipped under the 'Equivalences & Supersets' rule. ## Limitations