diff --git a/cpp-overload-test/examples/stdcpp-stack.rs b/cpp-overload-test/examples/stdcpp-stack.rs new file mode 100644 index 0000000..aea9c1f --- /dev/null +++ b/cpp-overload-test/examples/stdcpp-stack.rs @@ -0,0 +1,59 @@ +//! Test C++ [std::stack][stack] overloads that can't be represented in Rust, and record the macro diagnostics. +//! Working overloads are in [stack.rs]. +//! +//! [stack]: https://en.cppreference.com/cpp/container/stack +//! [stack.rs]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/stack.rs +//! +//! The `cpp!` macro doesn't work here. + +#![feature(splat, tuple_trait, c_size_t)] +#![allow(incomplete_features)] +#![allow(unused_braces)] + +use splat_overload::overload; +use std::ffi::c_void; +use std::marker::PhantomData; + +#[expect(dead_code, reason = "Incompatible overloads can't be used")] +struct StdStack(*mut c_void, PhantomData); + +type T = std::ffi::c_int; + +overload! { + impl StdStack { + /// ### Incompatibilities + /// + /// cpp has two `two` top functions with the same name, `T& top()` return an item you can change, + /// and `const T& top() const` return a read only one. they only differ by const, + /// which in Rust is `&mut self` reciever vs a `&self ` receiver. + /// + /// the overload! cant express this. it turns all overloads with the same name into one + /// real function, that sends each call to the right version. and one function can + /// only have one kind of receiver , never `&self` and `&mut self` at the same time. + /// + /// ### Workarounds + /// + /// The simplest fix would be to not overload at all and use the two names + /// `top()` for read only version and `top_mut()` for the changeable one. + /// Different names means theres no overloading is needed , so macro isn't + /// involved. It will not be same as C++, where both functions are + /// called top. But its how Rust normally does it, Vec has first() and first_mut() + /// ANd Rust users would find it natural. + /// + /// The other option is like in the `forward_list::front` , take the stack as a normal + /// argument and not as self, like `top(this: &'static StdStack)` and + /// `top(this: &'static mut StdStack)` . but the thing is the macro, doesnt support + /// lifetimes , so the reference has to be static, and the stack has to be leaked , + /// which is very awkward to use and the callers write `StdStack::top(&stack)` + /// and not `stack.top()` + fn top(&self) where T:Sized { + todo!() + } + + fn top(&mut self) where T: Sized { + todo!() + } + } +} + +pub fn main() {} diff --git a/cpp-overload-test/examples/stdcpp-stack.stderr b/cpp-overload-test/examples/stdcpp-stack.stderr new file mode 100644 index 0000000..db4a0ec --- /dev/null +++ b/cpp-overload-test/examples/stdcpp-stack.stderr @@ -0,0 +1,14 @@ +error: proc macro panicked + --> cpp-overload-test/examples/stdcpp-stack.rs:22:1 + | +22 | / overload! { +23 | | impl StdStack { +24 | | /// ### Incompatibilities +25 | | /// +... | +57 | | } + | |_^ + | + = help: message: all overloads must have the same receiver kind (&self, &mut self, self, or no receiver), got top: &self and top: &mut self + +error: could not compile `cpp-overload-test` (example "stdcpp-stack") due to 1 previous error diff --git a/cpp-overload-test/src/lib.rs b/cpp-overload-test/src/lib.rs index 98274d5..416ace8 100644 --- a/cpp-overload-test/src/lib.rs +++ b/cpp-overload-test/src/lib.rs @@ -11,6 +11,7 @@ pub mod stdcpp { // Add new modules for each type here, in alphabetical order. pub mod forward_list; pub mod hash; + pub mod stack; pub mod time_point; pub mod unordered_map; pub mod unordered_multiset; diff --git a/cpp-overload-test/src/stdcpp/stack.rs b/cpp-overload-test/src/stdcpp/stack.rs new file mode 100644 index 0000000..ea5ccfd --- /dev/null +++ b/cpp-overload-test/src/stdcpp/stack.rs @@ -0,0 +1,291 @@ +//! Test that C++ overloads for [std::stack][stack] can be successfully represented in Rust. +//! +//! `std::stack` is a container adapter: it wraps a container (`std::deque` by default ) +//! [stack]: https://en.cppreference.com/cpp/container/stack + +#![allow(unused_braces, reason = "Generated by overload! macro")] + +use cpp::cpp; +use splat_overload::overload; +use std::ffi::{c_char, c_size_t, c_void}; +use std::marker::PhantomData; + +// C++ header includes +cpp! {{ + #include + #include + #include + #include + #include +}} + +pub type T = std::ffi::c_int; + +/// A wrapper struct named StdStack that holds the returned c++ pointer. +#[cfg_attr(not(test), expect(dead_code, reason = "only used in tests"))] +pub struct StdStack(*mut c_void, PhantomData); + +#[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 StdStack { + fn default() -> StdStack { + let stack = unsafe { + cpp!([] -> *mut c_void as "std::stack*"{ + return new std::stack(); + }) + }; + StdStack(stack, PhantomData) + } + + // ### Limitations + // + // We want `c_bool` here, but rust only has `c_char` , so `is_empty` converts the + // `char` back to bool . char is FFI safe, but rust bool is not + fn is_empty(&self) -> bool { + let slf = self.0 as *const c_void; + let empty = unsafe { + cpp!([slf as "const std::stack*"] -> c_char as "char" { + return slf->empty(); + }) + }; + empty != 0 + } + + // ### Limitations + // + // c++ `push` has two overloads; `push(const T&)` copies the values in and + // `push(T&&)` moves it in. They only differ by `&` and `&&`. + // + // Only one is wrapped here, as a helper outside `overload!` . + // The `&`/`&&` difference is covered by copy and move constructors (`new(&StdStack)` and + // `new(&mut StdStack)`). + fn push(&mut self, value: T) { + let slf: *mut c_void = self.0; + unsafe { + cpp!([slf as "std::stack*", value as "int"]{ + slf->push(value); + }) + } + } + + fn size(&self) -> c_size_t { + let slf = self.0 as *const c_void; + + unsafe { + cpp!([slf as "const std::stack*"] -> c_size_t as "size_t"{ + return slf->size(); + }) + } + } + + fn top_const(&self) -> Option<&T> { + let slf = self.0 as *const c_void; + + unsafe { + cpp!([slf as "const std::stack*"] -> Option<&T> as "const int*"{ + //calling top() on an empty stack is undefined behaviour in cpp + if(slf->empty()){ + return nullptr; + }else{ + return &slf->top(); + } + }) + } + } + + // *other is const lvalue (from &StdStack) , so only the copy constructor + // stack(const stack&) matched, The move constructor cant take a + // const object, so moving isnot possible here. + fn copy_from(other: &StdStack) -> StdStack { + let other = other.0 as *const c_void; + let stack = unsafe { + cpp!([other as "const std::stack*"] -> *mut c_void as "std::stack*" { + return new std::stack(*other); + }) + }; + StdStack(stack, PhantomData) + } + + // std::move(*other) marks the stack as an rvalue + // so overload picks the move constructor stack (stack&&) instead + // of copy constructor + // Do it in one steps: a named variable will be lvalue again and get + // copied, other is non-const , which is what make the move possible + fn move_from(other: &mut StdStack) -> StdStack { + let other = other.0; + let stack = unsafe { + cpp!([other as "std::stack*"] -> *mut c_void as "std::stack*" { + return new std::stack(std::move(*other)); + }) + }; + StdStack(stack, PhantomData) + } + + fn from_slice(items: &[T]) -> StdStack { + if items.is_empty() { + return StdStack::default(); + } + let len = items.len(); + + let items: *const T = items.as_ptr(); + let stack = unsafe { + cpp!([items as "const int*", len as "size_t"] -> *mut c_void as "std::stack*" { + return new std::stack(items, items + len); + }) + }; + StdStack(stack, PhantomData) + } + + fn from_iter(iter: &mut dyn Iterator) -> StdStack { + let items: Vec = iter.collect(); + // An empty Rust Vec is guaranteed to have a dangling pointer, see + // + if items.is_empty() { + return StdStack::default(); + } + let len = items.len(); + let items: *const T = items.as_ptr(); + // SAFETY: items is a non-null pointer to len properly initialized values of T + let stack = unsafe { + cpp!([items as "const int*", len as "size_t"] -> *mut c_void as "std::stack*"{ + std::vector vec = std::vector(items, items + len); + #ifdef __cpp_lib_containers_ranges + return new std::stack(std::from_range, vec); + #else + #error "std::from_range is not available, try using `-std=c++23` or a newer compiler" + #endif + }) + }; + StdStack(stack, PhantomData) + } +} + +// Constructors: `new(...)` +// +// ### Limitations +// +// These contructors are not wrapped yet: +// - `stack(const Container&)` and `stack(Container&&)`: they take a cpp `std::deque`, +// so they need a rust wrapper for `deque` first, which is not done yet. +// - constructors that differ by allocator argument : ignored, because they are not interesting overloads +// to cover. +overload! { + impl StdStack { + /// an empty stack + pub fn new() -> StdStack { + StdStack::default() + } + /// Clone stack items into a new stack from a shared stack reference + /// + /// ### Ergonomics + /// + /// `new(&s)` copies and `new(&mut s)` moves, but at the call site they look almost + /// same and do very different things, + /// it will be very easy to mix them up. + pub fn new(other: &StdStack) -> StdStack { + StdStack::copy_from(other) + } + + /// Move stack items into a new stack from a mutable stack reference. + /// + /// ### Limitations + /// + /// After a cpp move, the old stack still exists in a "valid but unspecified" state, + /// which is empty in practice. A Rust moves makes the old variable unusauable , + /// so idiomatic rust would take the stack by value instead of `&mut` + pub fn new(other: &mut StdStack) -> StdStack { + StdStack::move_from(other) + } + + /// constructs a stack by copying items from a slice. + pub fn new(items: &[T]) -> StdStack { + StdStack::from_slice(items) + } + + /// constructs a stack from an iterator + /// + /// ### Limitations + /// The caller has to cast the iterator, like `&mut iter as &mut dyn Iterator` + /// because rust doesnot automatically convert it to `dyn Iterator` automatically + /// + /// It used the dyn Iterator and not a generic `impl Iterator` because overload! + /// doesn't support generics in this position yet and a `impl Iterator` + /// could overlap with the `&[T]` overload. + /// + /// the items are basically copied twice, first into a Rust `Vec` and + /// then into a cpp `std::vector`. + /// its simple, but it do the extra work. + /// In c++, we need a `std::from_range` tag to tell this constructor apart from the others. + /// Rust doesnot need a tag because argu,ment types are already there. + pub fn new(iter: &mut dyn Iterator) -> StdStack { + StdStack::from_iter(iter) + } + } +} + +/// These tests show that the correct Rust and C++ overloads are being called. +#[test] +pub fn test_stack() { + let mut default_stack = StdStack::new(); + assert!(default_stack.is_empty()); + + //push, size, top_const + let mut stack = StdStack::new(); + assert_eq!(stack.size(), 0); + assert_eq!(stack.top_const(), None); + + stack.push(10); + stack.push(20); + assert!(!stack.is_empty()); + assert_eq!(stack.size(), 2); + assert_eq!(stack.top_const(), Some(&20)); + + //copy constructor + let mut original = StdStack::new(); + original.push(13); + original.push(09); + + let copy = StdStack::new(&original); + assert_eq!(copy.size(), 2); + assert_eq!(copy.top_const(), Some(&09)); + + //changing original will not change copy, because copy is independent + original.push(10); + assert_eq!(copy.size(), 2); + assert_eq!(original.size(), 3); + + // move constructor + let mut real = StdStack::new(); + real.push(10); + real.push(20); + real.push(30); + + let moved_stack = StdStack::new(&mut real); + assert_eq!(moved_stack.size(), 3); + assert_eq!(moved_stack.top_const(), Some(&30)); + assert!(real.is_empty()); + assert_eq!(real.size(), 0); + assert_eq!(real.top_const(), None); + //moving an empty stack is fine too + let moved_empty = StdStack::new(&mut default_stack); + assert!(moved_empty.is_empty()); + + // ### Limitations: with overload!, caller has to call .as_slice(), because array reference doesnot convert into slice automatically. + // no mut since, from_slice is never changed. + let from_slice = StdStack::new([1, 2, 3].as_slice()); + assert_eq!(from_slice.size(), 3); + assert_eq!(from_slice.top_const(), Some(&3)); + let from_empty_slice = StdStack::new([].as_slice()); + assert!(from_empty_slice.is_empty()); + + let mut iter: std::array::IntoIter = [1, 2, 3].into_iter(); + let from_iter = StdStack::new(&mut iter as &mut dyn Iterator); + assert_eq!(from_iter.size(), 3); + assert_eq!(from_iter.top_const(), Some(&3)); + let mut empty_iter: std::array::IntoIter = [].into_iter(); + let from_empty_iter = StdStack::new(&mut empty_iter as &mut dyn Iterator); + assert!(from_empty_iter.is_empty()); +} diff --git a/cpp-overloads-in-rust.md b/cpp-overloads-in-rust.md index 7c5d0b4..fa05f5d 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] | ✅ | ✅ | | | +| [stack] | ✅ | ⚙️[^6] | ? | | | [unordered_map] | ✅ | 🌗⚙️[^5] | 🌗⚙️ | | | [unordered_multiset] | ✅ | ✅⚙️[^3] | ✅ | ?[^4] | | [time_point] | ✅ | ✅ | | ✅ | @@ -40,6 +41,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 +[stack]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/stack.rs [time_point]: https://github.com/rustfoundation/overloading-macros/blob/main/cpp-overload-test/src/stdcpp/time_point.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,7 @@ 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]: `top` overloads differ only by `const` (`T& top()` and `const T& top() const`) , `&self` vs `&mut self` in Rust, which the overload macro can't represent. ## Limitations