Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions cpp-overload-test/examples/stdcpp-stack.rs
Original file line number Diff line number Diff line change
@@ -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<T>);

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() {}
14 changes: 14 additions & 0 deletions cpp-overload-test/examples/stdcpp-stack.stderr
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions cpp-overload-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
291 changes: 291 additions & 0 deletions cpp-overload-test/src/stdcpp/stack.rs
Original file line number Diff line number Diff line change
@@ -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<int>` 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 <stack>
#include <cstddef>
#include <utility>
#include <vector>
#include <ranges>
}}

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<T>);

#[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<int>*"{
return new std::stack<int>();
})
};
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<int>*"] -> 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<int>*", 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<int>*"] -> 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<int>*"] -> 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<int>*"] -> *mut c_void as "std::stack<int>*" {
return new std::stack<int>(*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<int>*"] -> *mut c_void as "std::stack<int>*" {
return new std::stack<int>(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<int>*" {
return new std::stack<int>(items, items + len);
})
};
StdStack(stack, PhantomData)
}

fn from_iter(iter: &mut dyn Iterator<Item = T>) -> StdStack {
let items: Vec<T> = iter.collect();
// An empty Rust Vec is guaranteed to have a dangling pointer, see
// <https://doc.rust-lang.org/std/vec/struct.Vec.html#method.as_ptr>
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<int>*"{
std::vector<int> vec = std::vector<int>(items, items + len);
#ifdef __cpp_lib_containers_ranges
return new std::stack<int>(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<int>`,
// 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 {
Comment thread
teor2345 marked this conversation as resolved.
Resolved
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<Item = _>`
/// 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<Item = T>) -> 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);
Comment thread
teor2345 marked this conversation as resolved.
Resolved
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<T, 3> = [1, 2, 3].into_iter();
let from_iter = StdStack::new(&mut iter as &mut dyn Iterator<Item = _>);
assert_eq!(from_iter.size(), 3);
assert_eq!(from_iter.top_const(), Some(&3));
let mut empty_iter: std::array::IntoIter<T, 0> = [].into_iter();
let from_empty_iter = StdStack::new(&mut empty_iter as &mut dyn Iterator<Item = _>);
assert!(from_empty_iter.is_empty());
}
3 changes: 3 additions & 0 deletions cpp-overloads-in-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] | ✅ | ✅ | | ✅ |
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading