How to call a trait implemented for `dyn AsRef<str>`?

Hello everyone.
Today I was poking around with raylib-rs, and one of its function accepted a string in a very peculiar way.
The function I'm talking about is:

fn gui_label(&mut self, bounds: impl Into<ffi::Rectangle>, text: impl IntoCStr) -> bool {
    unsafe { ffi::GuiLabel(bounds.into(), text.as_cstr_ptr()) > 0 }
}

where I noticed the text is an impl IntoCStr, a custom trait defined like this:

pub trait IntoCStr {
    fn as_cstr_ptr(&self) -> *const std::os::raw::c_char;
}

impl IntoCStr for dyn AsRef<str> {
    fn as_cstr_ptr(&self) -> *const std::os::raw::c_char {
        std::ffi::CString::new(self.as_ref())
            .unwrap()
            .as_c_str()
            .as_ptr()
    }
}

impl IntoCStr for dyn AsRef<CStr> {
    fn as_cstr_ptr(&self) -> *const std::os::raw::c_char {
        self.as_ref().as_ptr()
    }
}

impl IntoCStr for Option<&CStr> {
    fn as_cstr_ptr(&self) -> *const std::os::raw::c_char {
        self.map(CStr::as_ptr).unwrap_or(std::ptr::null())
    }
}

What interested me was the first implementation, for dyn AsRef<str>. While I can easily use the third implementation, this first one seems to me targeted to people wanting to "cast rapidly" a str to a CStr/Impl IntoCStr, but after some digging I found out that this implementation works only for type trait variables like let x: &dyn AsRef<str>.

Still, I'm having a lot of difficulty to understand HOW can this implementation can be called and what use-case cover. I tried for 1 hour without founding a way to call that specific implementation.
Can someone help me understand better this impl?

2 Likes

It's a trait implementation for a trait object? I don't think there's a dedicated name for it, trait objects are just normal, concrete types (that happen to be created by type-erasing some other concrete type). So you also just normally implement a trait for them.

As you said, it's a nice way to cast strings as CStr. For example:

pub trait IntoStrCount {
    fn as_str_count(&self) -> usize;
}

impl IntoStrCount for dyn AsRef<str> {
    fn as_str_count(&self) -> usize {
        self.as_ref().len()
    }
}

fn main() {
    let a_str = "Doobedeboo";
    println!("Count: {}", (&a_str as &dyn AsRef<str>).as_str_count());
}

This creates a new CString allocation, gets a pointer to it, drops the CString, and returns a dangling pointer. You should report this as a bug and avoid this method if possible (or avoid the crate) for now.

9 Likes

The problem is the text parameter of gui_label is passed by value, which gives it an implicit Sized bound. That prevents passing in unsized dyn types. Since the three types listed there are the only ones that IntoCStr is implemented for, it looks like only type that can be passed in is Option<&CStr>. That seems like a mistake in the API. It looks like they want to be implemented for anything that implements AsRef<str> or AsRef<CStr>, but that isn't what those implementations are saying. That should be reported to the crate maintainer as a bug.

2 Likes

Also when you implement things for dyn Trait, you generally want dyn Trait + '_:

//                                vvvvv
impl IntoCStr for dyn AsRef<CStr> + '_ {

(And it's odd there's no implementation for T: AsRef<CStr> or similar, and and and... all in all it's a weird approach.)

3 Likes

That whole crate seems super weird just everywhere you look around that trait; and possibly anywhere else, too.

Like… what is this even!??

The method appears in a trait with exclusively default-implemented items. It has a generic implementation based on another trait…

impl<D: RaylibDraw> RaylibDrawGui for D {}

with an empty body; and that one’s just as weird. Same thing, all methods are provided. Also self is used no-where. Most methods are &mut self, but then self is simply ignored. And that one has a handful of implementors, none of which override any of the methods, either. What is going on!?

As mentioned, the IntoCStr trait, which has those weird implementations for dyn AsRef<CStr> and dyn AsRef<str>. Those are barely usable. For instance, there’s no implementation for any pointer-types, so that you can’t use them for the text: impl IntoCStr argument at all in the first place, that’s for certain.

Then I’m noticing (actually that was the very first thing I saw) how IntoCStr’s method produces *const c_char, but where’s the safety contract!?? It’s a safe trait, anyone can implement it! Then gui_label also is a safe function accepting impl IntoCStr, so essentially it’ll just accept any arbitrary raw pointer and then what … pass it over to FFI which will probably dereference it, right!? That’s what we call unsound API.

And last but not least, how does this magical str to C-style *const c_char conversion work?? Don’t the latter require null-termination? Let’s see – oh god, it’s even worse than I thought!

impl IntoCStr for dyn AsRef<str> {
    fn as_cstr_ptr(&self) -> *const std::os::raw::c_char {
        std::ffi::CString::new(self.as_ref())
            .unwrap()
            .as_c_str()
            .as_ptr()
    }
}

Guys! Don’t do drugs program Rust that way!

std::ffi::CString::new creates a new owned CString on the heap, so far so good… then

impl CString { pub fn as_c_str(&self) -> &CStr }

gets a reference to the contained data, and as_ptr, which is indeed a valid pointer to some 0-terminated copy of the string data now. Until the function returns. Then it’s just a pointer to freed memory. This trait implementation isn’t just unsound (i.e. possible to use incorrectly). No, it’s impossible to use correctly! That wouldn’t even be considered acceptable by any C or C++ developer!

16 Likes

Love that the file this is in is named safe.rs.

8 Likes

Ok whoa I was not expecting this level of problems.
I will see to open an Issue about this interface problems.

Do you think there is a way to call that IntoCStr for dyn AsRef<str> trait implementation? To me it seems it's not callable in any way.

Thank you very much!

Just use CStr instead of CString if the original string is already 0-terminated.

If it isn't, then you have to create a new allocation (likely a CString), add a terminating 0 byte, and return an owning raw pointer (into_raw), freeing of which will then be the responsibility of the caller.

1 Like