C APIs: How to distinguish using a pointer from freeing it?

I'm trying to create a C API wrapping my Rust library. The main entrypoint is a struct called Perspective:

use crate::Perspective;

#[derive(Debug)]
#[repr(C)]
pub struct FFIPerspective(Perspective);

I have code to create and free a FFIPerspective:

#[no_mangle]
pub extern "C" fn perspective_new() -> *const FFIPerspective {
    let perspective = Perspective::default();
    let perspective = FFIPerspective(perspective);
    let perspective = Box::new(perspective);
    Box::into_raw(perspective)
}

#[no_mangle]
pub extern "C" fn perspective_free(ptr: *const FFIPerspective) {
    if ptr.is_null() {
        return;
    }
    let ptr = ptr as *mut FFIPerspective;
    unsafe {
        let _: Box<FFIPerspective> = Box::from_raw(ptr);
    }
}

I assume my free code succeeds by creating the FFIPerspective and dropping the reference. Assuming both my above functions look correct, I have this function which exposes a perspective.load:

#[no_mangle]
pub extern "C" fn perspective_load(ptr: *const FFIPerspective, key: *const c_char, path: *const c_char) -> i8 {
    if ptr.is_null() {
        return -1;
    }
    if key.is_null() {
        return -1;
    }
    if path.is_null() {
        return -1;
    }
    let ptr = ptr as *mut FFIPerspective;
    let perspective = unsafe {
        let perspective: Box<FFIPerspective> = Box::from_raw(ptr);
        perspective
    };
    let mut perspective = perspective.0;
    let key = unsafe {
        CStr::from_ptr(key)
    };
    let key = key.to_string_lossy().into_owned();
    let path = unsafe {
        CStr::from_ptr(path)
    };
    let path = path.to_string_lossy().into_owned();
    let path = PathBuf::from(path);
    let path = path.into_boxed_path();
    if let Ok(_) = perspective.load(key, path) {
        0
    } else {
        -1
    }
}

I'm worried that my load function isn't correct. How do I distinguish between taking ownership of a variable in a function so that the reference is lost and the memory freed, from taking ownership of a variable so I can modify its Rust representation and have it remain intact?

And as an aside, is there a better C error-handling mechanism than returning -1? Can I somehow pass my nice Rust error messages back to C consumers?

Thanks.

Box::from_raw is for freeing. ptr.as_ref().unwrap() is for using the pointer as borrowed. &*ptr is an unchecked way to get a borrow from a raw pointer.

If you make the C side promise not to pass NULL ever, then you can use &FFIPerspective in your arguments directly.

An easy way is to return an enum with error codes.

If you want to pass arbitrary strings, that requires memory management, and it's going to be a drag for C. You'd need to copy them to mallocated memory, and instruct C user to free() them.

Thanks, so something like this for the case where I want to use the
value and not drop it?


#[no_mangle]
pub extern "C" fn perspective_load(ptr: *mut FFIPerspective, key: *const 
c_char, path: *const c_char) -> i8 {
     if ptr.is_null() {
         return -1;
     }
     if key.is_null() {
         return -1;
     }
     if path.is_null() {
         return -1;
     }
     let perspective = unsafe {
         ptr.as_mut()
     };
     if perspective.is_none() {
         return -1;
     }
     let perspective = perspective.unwrap();
     let key = unsafe {
         CStr::from_ptr(key)
     };
     let key = key.to_string_lossy().into_owned();
     let path = unsafe {
         CStr::from_ptr(path)
     };
     let path = path.to_string_lossy().into_owned();
     let path = PathBuf::from(path);
     let path = path.into_boxed_path();
     if let Ok(_) = perspective.0.load(key, path) {
         0
     } else {
         -1
     }
}

My hope is that ptr will never be null, but I'd like to handle the
case where it is without crashing.

Thanks for the note about enum errors--will definitely add those once
this API becomes useful.

As an example, you could return an const FfiError* which is non-null when an error has occurred. The type could contain a String and be stored in the FfiPerspective so that it does not need to be independently deallocated.

Or you could define a type FfiErrors with alloc and free and has_error functions, and take FfiErrors* arguments in your functions.

In any case, you can have a function defined on it that takes (const *FfiError, *char buf, int buf_len) and does a strncpy. And maybe a function to get the message length. Or you can expose a constant max message size.

Yes. as_ref() already checks for NULL, so you don't have to do it twice.

Also, make yourself a macro to avoid repeating -1 over and over again:

macro_rules! try1 {
    ($e:expr) => { match $e {
        Some(e) => e,
        None => return -1,
    }
}}

unsafe fn f(ptr: *const i32) -> i32 {
    let ptr = try1!(ptr.as_ref());
    0
}

By the way, there is need for FFIPerspective wrapper if you work with pointer only.

In my project (rust_swig) I automatically generate something like this for return result:

#[repr(C)]
union ResultData {
   ok: T1,
   err: T2,
}
#[repr(C)]
struct CResult {
   data: ResultData,
   is_ok: u8,
}