ewwii_plugin_api 1.2.0

A shared library for building plugins for ewwii
Documentation
//! This module provides plugin requests and host proxy
//! that are used to redirect API calls to host after serialization

use crate::{
    ConfigCallbackFn, ConfigInfo, EwwiiAPI, IpcRequest, NativeFn, NbclType, ParseFn, PluginError,
    PluginValue,
};
use ewwii_shared_utils::ast::WidgetNode;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};

/// Represents the different types of callbacks that can be registered by a plugin.
pub enum CallbackHandler {
    NativeFn(NativeFn),
    ParseFn(ParseFn),
    ConfigCallbackFn(ConfigCallbackFn),
}

/// Represents the possible response types returned by a plugin callback.
#[derive(Serialize, Deserialize)]
pub enum CallbackResponse {
    PluginValue(PluginValue),
    WidgetNode(WidgetNode),
    Error(PluginError),
}

static CALLBACKS: OnceLock<Mutex<HashMap<u64, CallbackHandler>>> = OnceLock::new();

/// Helper to get or initialize the map
fn get_callbacks() -> &'static Mutex<HashMap<u64, CallbackHandler>> {
    CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()))
}

/// Plugin Requests that needs to be send to host
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginRequest {
    // (id, msg)
    Log((String, String)),
    Warn((String, String)),
    Error((String, String)),

    // IPC API (Data transfer)
    Ipc((String, IpcRequest)),

    // Registration API (Complex)
    RegisterFn {
        id: String,
        name: String,
        types: Vec<NbclType>,
        return_type: NbclType,
        callback_id: u64,
    },
    RegisterConfigEngine {
        id: String,
        extension: String,
        main_file: String,
        callback_id: u64,
    },

    // Dynamic Runtime
    InjectCss(String),

    // Handlers
    ConfigCallbackHandle(u64),
}

// This is provided on the host side
extern "C" {
    fn ffi_gateway(ptr: *const u8, len: usize);
}

#[no_mangle]
pub extern "C" fn plugin_callback_handler(
    id: u64,
    arg_ptr: *const u8,
    arg_len: usize,
    output_len: *mut usize,
) -> *mut u8 {
    let bytes = unsafe { std::slice::from_raw_parts(arg_ptr, arg_len) };
    let callbacks = get_callbacks().lock().unwrap();

    let res_bytes = match callbacks.get(&id) {
        Some(CallbackHandler::NativeFn(f)) => {
            let args: Vec<PluginValue> = bincode::deserialize(bytes).unwrap_or_default();
            let result = f(args).unwrap_or(PluginValue::Null);
            bincode::serialize(&CallbackResponse::PluginValue(result)).unwrap_or_default()
        }
        Some(CallbackHandler::ParseFn(f)) => {
            let (source, path): (String, String) = bincode::deserialize(bytes).unwrap_or_default();
            match f(&source, &path) {
                Ok(node) => {
                    bincode::serialize(&CallbackResponse::WidgetNode(node)).unwrap_or_default()
                }
                Err(e) => bincode::serialize(&CallbackResponse::Error(PluginError::ParseError(e)))
                    .unwrap_or_default(),
            }
        }
        Some(CallbackHandler::ConfigCallbackFn(f)) => {
            let (name, id): (String, String) = bincode::deserialize(bytes).unwrap_or_default();
            f(&name, &id);
            return std::ptr::null_mut();
        }
        None => return std::ptr::null_mut(),
    };

    unsafe {
        let len = res_bytes.len();
        *output_len = len;
        let boxed_slice = res_bytes.into_boxed_slice();
        Box::into_raw(boxed_slice) as *mut u8
    }
}

#[no_mangle]
pub extern "C" fn plugin_free_buffer(ptr: *mut u8, len: usize) {
    if !ptr.is_null() {
        unsafe {
            let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len));
        }
    }
}

/// Proxy that redirects requests to host
pub struct HostProxy {
    id: String,
}

impl HostProxy {
    /// The constructor the macro calls.
    pub fn new(id: &str) -> Self {
        Self { id: id.to_string() }
    }

    /// Internal helper to get the ID for every request.
    /// This now uses the instance ID instead of a global static.
    fn get_id(&self) -> &str {
        &self.id
    }

    fn call_host(&self, req: PluginRequest) {
        let bytes = match bincode::serialize(&req) {
            Ok(o) => o,
            Err(e) => {
                println!("Failed to serialize request: {}", e);
                return;
            }
        };

        unsafe {
            ffi_gateway(bytes.as_ptr(), bytes.len());
        }
    }
}

impl EwwiiAPI for HostProxy {
    fn log(&self, msg: &str) {
        let plugid = &self.get_id();
        let req = PluginRequest::Log((plugid.to_string(), msg.to_string()));

        self.call_host(req);
    }

    fn warn(&self, msg: &str) {
        let plugid = &self.get_id();
        let req = PluginRequest::Warn((plugid.to_string(), msg.to_string()));

        self.call_host(req);
    }

    fn error(&self, msg: &str) {
        let plugid = &self.get_id();
        let req = PluginRequest::Error((plugid.to_string(), msg.to_string()));

        self.call_host(req);
    }

    fn ipc_request(&self, req: IpcRequest) {
        let plugid = &self.get_id();
        let req = PluginRequest::Ipc((plugid.to_string(), req));

        self.call_host(req);
    }

    // === Registration === //

    fn register_function(
        &self,
        name: &str,
        types: Vec<NbclType>,
        return_type: NbclType,
        handler: NativeFn,
    ) {
        // Register id
        let id = rand::random::<u64>();
        get_callbacks().lock().unwrap().insert(id, CallbackHandler::NativeFn(handler));

        // Send request
        let req = PluginRequest::RegisterFn {
            id: self.get_id().to_string(),
            name: name.to_string(),
            types,
            return_type,
            callback_id: id,
        };

        self.call_host(req)
    }

    fn register_config_engine(&self, info: ConfigInfo, parser: ParseFn) {
        // Register id
        let id = rand::random::<u64>();
        get_callbacks().lock().unwrap().insert(id, CallbackHandler::ParseFn(parser));

        // Send request
        let req = PluginRequest::RegisterConfigEngine {
            id: self.get_id().to_string(),
            extension: info.extension.to_string(),
            main_file: info.main_file.to_string(),
            callback_id: id,
        };

        self.call_host(req)
    }

    // === Dynamic Runtime === //

    fn inject_css(&self, css: String) {
        let req = PluginRequest::InjectCss(css);
        self.call_host(req);
    }

    // === Handlers === //

    fn handle_config_callbacks(&self, handle: ConfigCallbackFn) {
        let id = rand::random::<u64>();
        get_callbacks().lock().unwrap().insert(id, CallbackHandler::ConfigCallbackFn(handle));

        let req = PluginRequest::ConfigCallbackHandle(id);
        self.call_host(req);
    }
}