Philipp Ludwig
Philipp Ludwig

Reputation: 4200

If an ffi function modifies a pointer, should the owning struct be referenced mutable?

I am currently experimenting with the FFI functionality of Rust and implemented a simble HTTP request using libcurl as an exercise. Consider the following self-contained example:

use std::ffi::c_void;

#[repr(C)]
struct CURL {
    _private: [u8; 0],
}

// Global CURL codes
const CURL_GLOBAL_DEFAULT: i64 = 3;
const CURLOPT_WRITEDATA: i64 = 10001;
const CURLOPT_URL: i64 = 10002;
const CURLOPT_WRITEFUNCTION: i64 = 20011;

// Curl types
type CURLcode = i64;
type CURLoption = i64;

// Curl function bindings
#[link(name = "curl")]
extern "C" {
    fn curl_easy_init() -> *mut CURL;
    fn curl_easy_setopt(handle: *mut CURL, option: CURLoption, value: *mut c_void) -> CURLcode;
    fn curl_easy_perform(handle: *mut CURL) -> CURLcode;
    fn curl_global_init(flags: i64) -> CURLcode;
}

// Curl callback for data retrieving
extern "C" fn callback_writefunction(
    data: *mut u8,
    size: usize,
    nmemb: usize,
    user_data: *mut c_void,
) -> usize {
    let slice = unsafe { std::slice::from_raw_parts(data, size * nmemb) };

    let mut vec = unsafe { Box::from_raw(user_data as *mut Vec<u8>) };
    vec.extend_from_slice(slice);
    Box::into_raw(vec);
    nmemb * size
}

type Result<T> = std::result::Result<T, CURLcode>;

// Our own curl handle
pub struct Curl {
    handle: *mut CURL,
    data_ptr: *mut Vec<u8>,
}

impl Curl {
    pub fn new() -> std::result::Result<Curl, CURLcode> {
        let ret = unsafe { curl_global_init(CURL_GLOBAL_DEFAULT) };
        if ret != 0 {
            return Err(ret);
        }

        let handle = unsafe { curl_easy_init() };
        if handle.is_null() {
            return Err(2); // CURLE_FAILED_INIT according to libcurl-errors(3)
        }

        // Set data callback
        let ret = unsafe {
            curl_easy_setopt(
                handle,
                CURLOPT_WRITEFUNCTION,
                callback_writefunction as *mut c_void,
            )
        };
        if ret != 0 {
            return Err(2);
        }

        // Set data pointer
        let data_buf = Box::new(Vec::new());
        let data_ptr = Box::into_raw(data_buf);
        let ret = unsafe {
            curl_easy_setopt(handle, CURLOPT_WRITEDATA, data_ptr as *mut std::ffi::c_void)
        };
        match ret {
            0 => Ok(Curl { handle, data_ptr }),
            _ => Err(2),
        }
    }

    pub fn set_url(&self, url: &str) -> Result<()> {
        let url_cstr = std::ffi::CString::new(url.as_bytes()).unwrap();
        let ret = unsafe {
            curl_easy_setopt(
                self.handle,
                CURLOPT_URL,
                url_cstr.as_ptr() as *mut std::ffi::c_void,
            )
        };
        match ret {
            0 => Ok(()),
            x => Err(x),
        }
    }

    pub fn perform(&self) -> Result<String> {
        let ret = unsafe { curl_easy_perform(self.handle) };
        if ret == 0 {
            let b = unsafe { Box::from_raw(self.data_ptr) };
            let data = (*b).clone();
            Box::into_raw(b);
            Ok(String::from_utf8(data).unwrap())
        } else {
            Err(ret)
        }
    }
}

fn main() -> Result<()> {
    let my_curl = Curl::new().unwrap();
    my_curl.set_url("https://www.example.com")?;
    my_curl.perform().and_then(|data| Ok(println!("{}", data)))
    // No cleanup code in this example for the sake of brevity.
}

While this works, I found it surprising that my_curl does not need to be declared mut, since none of the methods use &mut self, even though they pass a mut* pointer to the FFI function s.

Should I change the declaration of perform to use &mut self instead of &self (for safety), since the internal buffer gets modified? Rust does not enforce this, but of course Rust does not know that the buffer gets modified by libcurl.

This small example runs fine, but I am unsure if I would be facing any kind of issues in larger programs, when the compiler might optimize for non-mutable access on the Curl struct, even though the instance of the struct is getting modified - or at least the data the pointer is pointing to.

Upvotes: 0

Views: 668

Answers (1)

L. Riemer
L. Riemer

Reputation: 654

Contrary to popular belief, there is absolutely no borrowchecker-induced restriction in Rust on passing *const/*mut pointers. There doesn't need to be, because dereferencing pointers is inherently unsafe, and can only be done in such blocks, with the programmer verifying all necessary invariants manually. In your case, you need to tell the compiler that is a mutable reference, as you already suspected.

The interested reader should definitely give the ffi section of the nomicon a read, to find out about some interesting ways to shoot yourself in the foot with it.

Upvotes: 1

Related Questions