t348575
t348575

Reputation: 753

Rust & FFI lib share string & free from both

I have a library that is used through its rust interface by rust programs, as well as through C/C++ programs through generated cbindgen bindings, so I implemented a free function to free the string once the ffi function has used the string. However I want rust also to control the memory when it is used as a rust lib. How do I achieve this? is it even possible? or is calling the free function manually in rust the only option?

I also tried implementing drop, but that lead to this:

free(): double free detected in tcache 2 [1] 11097 IOT instruction cargo run

This block allows the string to be freed from C/C++, but the string is not freed in rust (valgrind shows definitely lost block). data is assigned using CString::into_raw()

use std::{ffi::CString, os::raw::c_char};

pub struct SomeData {
    pub data: *const c_char
}

impl SomeData {
   #[no_mangle] pub extern fn free_shared_string(&mut self) {
        if !self.data.is_null() {
            unsafe { CString::from_raw(self.data.cast_mut()); }
        }
    }
}

Upvotes: 0

Views: 1161

Answers (2)

t348575
t348575

Reputation: 753

The best solution for me was to have a separate feature, used when building the library to be used through C/C++ applications (ie. .a/.so) vs .rlib which cargo will build when included in a rust project through Cargo.toml.

This lets me use the same API from both possible application languages, call free from C/C++ on my string, and drop will free it in rust.

Note: the null character at the end is because the majority of the time my lib is used with C apps, hence storing with null for faster returns for them.

Add default-features = false when adding in Cargo.toml of a rust app.

lib.rs

use std::{ffi::{c_char, CStr, FromBytesWithNulError, CString}, mem::forget, str::Utf8Error, string::FromUtf8Error};

#[cfg(feature = "c-str")]
#[repr(C)]
pub struct SharedString {
    str: *const c_char
}

#[cfg(not(feature = "c-str"))]
pub struct SharedString {
    str: Vec<u8>
}

#[cfg(feature = "c-str")]
impl SharedString {
    pub fn from_bytes(buf: &[u8]) -> Self {
        let mut buf = buf.to_vec();
        if let Some(c) = buf.last() {
            if *c != 0 {
                buf.push(0);
            }
        }
        let s = Self { str: buf.as_ptr().cast() };
        forget(buf);
        s
    }

    pub unsafe fn get_string(&self) -> Result<String, SharedStringError> {
        Ok(CStr::from_ptr(self.str).to_str()?.to_owned())
    }

    pub unsafe fn free(&self) {
        if !self.str.is_null() {
            let _ = CString::from_raw(self.str.cast_mut());
        }
    }
}

#[cfg(not(feature = "c-str"))]
impl SharedString {
    pub fn from_bytes(buf: &[u8]) -> Self {
        let mut buf = buf.to_vec();
        if let Some(c) = buf.last() {
            if *c != 0 {
                buf.push(0);
            }
        }
        Self { str: buf }
    }

    pub fn get_string(&self) -> Result<String, SharedStringError> {
        let mut s = self.str.clone();
        if let Some(c) = s.last() {
            if *c == 0 {
                s.pop();
            }
        }
        String::from_utf8(s).map_err(|e| e.into())
    }

    // do nothing because rust vec will get dropped automatically
    pub fn free(&self) {}
}

// Just for proof of concept
#[derive(Debug)]
pub enum SharedStringError {
    NullError,
    Utf8Error
}

impl From<FromBytesWithNulError> for SharedStringError {
    fn from(_: FromBytesWithNulError) -> Self {
        Self::NullError
    }
}

impl From<Utf8Error> for SharedStringError {
    fn from(_: Utf8Error) -> Self {
        Self::Utf8Error
    }
}

impl From<FromUtf8Error> for SharedStringError {
    fn from(_: FromUtf8Error) -> Self {
        Self::Utf8Error
    }
}

Cargo.toml

[package]
name = "mylib"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

[features]
default = ["c-str"]
c-str = []

Upvotes: 0

Silvio Mayolo
Silvio Mayolo

Reputation: 70277

The docs for from_raw warn against doing exactly this.

Safety

This should only ever be called with a pointer that was earlier obtained by calling CString::into_raw. Other usage (e.g., trying to take ownership of a string that was allocated by foreign code) is likely to lead to undefined behavior or allocator corruption.

So do not use from_raw to pretend that a foreign string was allocated using Rust. If you just need to borrow it and let C free it, you should use the CStr type for borrowed strings. If you want to take ownership, you should copy it into a new string, or wrap it in a custom structure that has a Drop implementation capable of freeing the original memory.

You cannot have two different languages owning that memory. Rust is fundamentally built on a single-ownership model, so every piece of memory has a unique owner. There are some (intra-Rust) workarounds for that like Rc, but none of that will translate to C. So pick an owner, and make that language responsible for freeing the data.

Upvotes: 2

Related Questions