gc_plugin_abi 0.5.0

Gridcore Plugin API
Documentation
use gc_abi::{GCAbiConversion, GCDatapointValue, raw};
use std::fmt::{self, Debug, Formatter};
use std::ops::Deref;
use std::sync::OnceLock;

//TODO: Make a test to ensure that a plugin can still call the release function after a plugin restart
static RELEASE_DATAPOINT: OnceLock<unsafe extern "C" fn(arg1: raw::GCDatapointValue) -> bool> = OnceLock::new();

/// This function sets the release function for datapoint values, it must be called before the control flow is handled to the plugin.
/// Note: This function is for internal use only and should not be called by plugins directly.
pub fn set_datapoint_release_fn(release_fn: Option<unsafe extern "C" fn(arg1: raw::GCDatapointValue) -> bool>) -> bool {
    if let Some(release_fn) = release_fn {
        RELEASE_DATAPOINT.get_or_init(|| release_fn);
        true
    } else {
        false
    }
}

/// A wrapper around a GCDatapointValue that takes ownership of a datapoint value across the C ABI boundary and ensures the memory is deallocated when the object is dropped
/// This wraps the raw C struct returned by the [raw::gc_plugin_receive_datapoint] function and ensures that the appropriate deallocation function is called when the object is dropped.
///
/// It defers from [GCDatapointValue] in that it ensures that the memory is deallocated when the object is dropped.
///
/// NOTE:
/// This object must not be created directly by plugins, it should only be created by the gateway core!
/// For this purpose, the [GCDatapointValue] object should be used.
#[repr(transparent)]
pub struct GCBorrowedDatapointValue(gc_abi::raw::GCDatapointValue);

impl GCBorrowedDatapointValue {
    /// Created a new borrowed datapoint value from a owned datapoint value
    ///
    /// This function is intended only for testing purposes, it will override the global release function for datapoint values
    #[cfg(feature = "testing")]
    pub fn new(dpv: &GCDatapointValue) -> Self {
        let dpv = Box::new(dpv.clone());
        let dpv_ptr = Box::into_raw(dpv) as raw::GCDatapointValue;
        unsafe extern "C" fn dealloc(dp: raw::GCDatapointValue) -> bool {
            unsafe {
                drop(Box::from_raw(dp));
                true
            }
        }
        RELEASE_DATAPOINT.get_or_init(|| dealloc);
        GCBorrowedDatapointValue(dpv_ptr)
    }

    /// Copies the borrowed datapoint value into a new owned datapoint value
    pub fn copy(&self) -> GCDatapointValue {
        let copy: &GCDatapointValue = unsafe { GCDatapointValue::from_c_ptr(self.0) };
        copy.clone()
    }
}

impl Drop for GCBorrowedDatapointValue {
    fn drop(&mut self) {
        let dealloc = RELEASE_DATAPOINT.get();
        if let Some(dealloc) = dealloc {
            // Safety:
            // The release function is guaranteed to be valid for the entire lifetime of the process (even after [raw::gc_plugin_shutdown] is called)
            unsafe {
                dealloc(self.0);
            }
        } else {
            panic!("The release function for datapoint values was not set, this should never happen");
        }
    }
}

impl Deref for GCBorrowedDatapointValue {
    type Target = GCDatapointValue;

    fn deref(&self) -> &Self::Target {
        unsafe { GCDatapointValue::from_c_ptr(self.0) }
    }
}

impl From<raw::GCDatapointValue> for GCBorrowedDatapointValue {
    fn from(datapoint_value: raw::GCDatapointValue) -> Self {
        GCBorrowedDatapointValue(datapoint_value)
    }
}

impl Debug for GCBorrowedDatapointValue {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("GCBorrowedDatapointValue")
            .field("value", &self.get_value())
            .field("timestamp", &self.get_timestamp())
            .field("id", &self.get_id())
            .field("quality", &self.get_quality())
            .finish()
    }
}

unsafe impl Send for GCBorrowedDatapointValue {}
unsafe impl Sync for GCBorrowedDatapointValue {}

#[cfg(test)]
mod tests {
    use super::*;
    use gc_abi::GCDatapointValueType;
    use raw::sGCDatapointValue__bindgen_ty_1 as BindagenValue;

    unsafe extern "C" fn dealloc(dp: raw::GCDatapointValue) -> bool {
        unsafe {
            drop(Box::from_raw(dp));
            true
        }
    }

    #[test]
    fn test_conversion_from_c() {
        set_datapoint_release_fn(Some(dealloc));
        let raw_dp = Box::new(raw::sGCDatapointValue {
            type_: raw::eGCDatapointValueType_GCOS_DATAPOINT_TYPE_INT32,
            value: BindagenValue { i32_: 1234 },
            timestamp: 10,
            datapoint_id: 0,
            quality: 0x0,
        });

        let raw_dp_ptr = Box::into_raw(raw_dp) as raw::GCDatapointValue;

        let dp_wrapper: GCBorrowedDatapointValue = raw_dp_ptr.into();
        assert_eq!(
            dp_wrapper.0, raw_dp_ptr,
            "The conversion should not allocate new memory, the address of both structs should be the same"
        );
        assert_eq!(GCDatapointValueType::Int32(1234), dp_wrapper.get_value().unwrap());
        assert_eq!(10, dp_wrapper.get_timestamp());
        assert_eq!(0, dp_wrapper.get_id());
    }

    #[test]
    fn test_copy() {
        set_datapoint_release_fn(Some(dealloc));
        let alloc_info = allocation_counter::measure(|| {
            let raw_dp = Box::new(raw::sGCDatapointValue {
                type_: raw::eGCDatapointValueType_GCOS_DATAPOINT_TYPE_INT32,
                value: BindagenValue { i32_: 1234 },
                timestamp: 10,
                datapoint_id: 0,
                quality: 0x0,
            });

            let raw_dp_ptr = Box::into_raw(raw_dp) as raw::GCDatapointValue;

            let dp_wrapper: GCBorrowedDatapointValue = raw_dp_ptr.into();
            let copy = dp_wrapper.copy();

            assert_eq!(GCDatapointValueType::Int32(1234), copy.get_value().unwrap());
            assert_eq!(10, copy.get_timestamp());
            assert_eq!(0, copy.get_id());
        });

        assert_eq!(alloc_info.bytes_current, 0, "Memory leak detected");
        assert_eq!(
            alloc_info.count_total, 1,
            "Additional memory allocation detected, the conversion shouldn't allocate any memory"
        );
    }
}