ezffi 0.1.1

Generate C-FFI bindings from Rust types/functions via a single proc-macro attribute
use core::ffi::{c_char, c_void};
use std::ffi::CString;

use crate::{COwnedIntoRust, RustOwnedIntoC};

#[doc(hidden)]
pub struct EzffiError {
    code: i32,
    message: String,
}

impl EzffiError {
    pub fn new(code: i32, message: String) -> Self {
        EzffiError { code, message }
    }
}

#[doc(hidden)]
#[derive(Clone, Copy)]
#[repr(C)]
pub struct EzffiOption {
    value: *mut c_void,
    payload_fn: unsafe extern "C" fn(*mut c_void, *mut c_void),
}

#[doc(hidden)]
#[derive(Clone, Copy)]
#[repr(C)]
pub struct EzffiResult {
    ok: *mut c_void,
    message: *mut c_char,
    payload_fn: unsafe extern "C" fn(*mut c_void, *mut c_void),
    code: i32,
}

/// Consumes a boxed payload. With a non-null `out`, the FFI value is moved into
/// `out` (the caller's stack) and only the heap box is freed. With a null `out`,
/// the payload is destroyed outright.
unsafe extern "C" fn payload_op<R, F: COwnedIntoRust<R>>(value: *mut c_void, out: *mut c_void) {
    if value.is_null() {
        return;
    }

    let ffi = unsafe { *Box::from_raw(value as *mut F) };

    if out.is_null() {
        let _ = unsafe { ffi.into_rust_owned() };
    } else {
        unsafe { (out as *mut F).write(ffi) };
    }
}

unsafe fn free_message(message: *mut c_char) {
    if !message.is_null() {
        let _ = unsafe { CString::from_raw(message) };
    }
}

// `Option` and `Result` only cross FFI by value — no by-ref impls.

impl<R> RustOwnedIntoC<()> for Option<R>
where
    R: RustOwnedIntoC<()>,
{
    type C = EzffiOption;

    unsafe fn owned_into_c(self) -> EzffiOption {
        let payload_fn = payload_op::<R, <R as RustOwnedIntoC<()>>::C>;
        match self {
            Some(inner) => {
                let ffi = unsafe { inner.owned_into_c() };
                EzffiOption {
                    value: Box::into_raw(Box::new(ffi)) as *mut c_void,
                    payload_fn,
                }
            }
            None => EzffiOption {
                value: core::ptr::null_mut(),
                payload_fn,
            },
        }
    }
}

impl<R> COwnedIntoRust<Option<R>> for EzffiOption
where
    R: RustOwnedIntoC<()>,
{
    unsafe fn into_rust_owned(self) -> Option<R> {
        if self.value.is_null() {
            return None;
        }
        let ffi = unsafe { *Box::from_raw(self.value as *mut <R as RustOwnedIntoC<()>>::C) };
        Some(unsafe { ffi.into_rust_owned() })
    }
}

impl<T, E> RustOwnedIntoC<()> for Result<T, E>
where
    T: RustOwnedIntoC<()>,
    E: Into<EzffiError>,
{
    type C = EzffiResult;

    unsafe fn owned_into_c(self) -> EzffiResult {
        let payload_fn = payload_op::<T, <T as RustOwnedIntoC<()>>::C>;
        match self {
            Ok(inner) => {
                let ffi = unsafe { inner.owned_into_c() };
                EzffiResult {
                    ok: Box::into_raw(Box::new(ffi)) as *mut c_void,
                    message: core::ptr::null_mut(),
                    payload_fn,
                    code: 0,
                }
            }
            Err(error) => {
                let error: EzffiError = error.into();
                let cmsg = CString::new(error.message).unwrap_or_else(|_| {
                    CString::new("<error message contained an interior NUL byte>")
                        .expect("fallback message is well-formed")
                });
                EzffiResult {
                    ok: core::ptr::null_mut(),
                    message: cmsg.into_raw(),
                    payload_fn,
                    code: error.code,
                }
            }
        }
    }
}

impl<T, E> COwnedIntoRust<Result<T, E>> for EzffiResult {
    unsafe fn into_rust_owned(self) -> Result<T, E> {
        unimplemented!("`Result` is only supported as a return value across FFI")
    }
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_option_is_some(o: *const EzffiOption) -> bool {
    let o = unsafe { &*o };
    !o.value.is_null()
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_result_is_ok(o: *const EzffiResult) -> bool {
    let o = unsafe { &*o };
    !o.ok.is_null()
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_result_error_code(o: *const EzffiResult) -> i32 {
    let o = unsafe { &*o };
    o.code
}

/// Borrowed pointer to the null-terminated error message, or NULL when the
/// result is `Ok`. The pointer is valid until the result is unwrapped or
/// freed
#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_result_error_message(o: *const EzffiResult) -> *const c_char {
    let o = unsafe { &*o };
    o.message as *const c_char
}

/// Moves the value from the option to the out. Don't call if the Option is None.
/// Call after you check `ezffi_option_is_some`.
/// You don't need to call `ezffi_option_free` after unwrapping
#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_option_unwrap(o: *const EzffiOption, out: *mut c_void) {
    let o = unsafe { &*o };
    unsafe { (o.payload_fn)(o.value, out) };
}

/// Moves the value from the Result to the out. Don't call if the Result is Err.
/// Call after you check `ezffi_result_is_ok`.
/// You don't need to call `ezffi_result_free` after unwrapping
#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_result_unwrap(o: *const EzffiResult, out: *mut c_void) {
    let o = unsafe { &*o };
    unsafe { (o.payload_fn)(o.ok, out) };
    unsafe { free_message(o.message) };
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_option_free(o: *const EzffiOption) {
    let o = unsafe { &*o };
    unsafe { (o.payload_fn)(o.value, core::ptr::null_mut()) };
}

#[doc(hidden)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ezffi_result_free(o: *const EzffiResult) {
    let o = unsafe { &*o };
    unsafe { (o.payload_fn)(o.ok, core::ptr::null_mut()) };
    unsafe { free_message(o.message) };
}