eclisp 0.1.7

Bindings for Embeddable Common Lisp
use std::{
    cell::Cell,
    panic::{AssertUnwindSafe, catch_unwind},
    ptr,
    rc::Rc,
};

use crate::{Lisp, LispContext, LispError, Value, ValueRef, sys};

pub(crate) type Callback = Rc<
    dyn for<'rt, 'call> Fn(LispContext<'rt>, &[ValueRef<'call>]) -> Result<Value<'rt>, LispError>,
>;

#[derive(Default)]
pub(crate) struct CallbackRegistry {
    callbacks: Vec<Option<Callback>>,
}

impl CallbackRegistry {
    pub fn insert(&mut self, callback: Callback) -> usize {
        let id = self.callbacks.len();
        self.callbacks.push(Some(callback));
        id
    }

    pub fn get(&self, id: usize) -> Option<Callback> {
        self.callbacks.get(id)?.clone()
    }

    pub fn remove(&mut self, id: usize) {
        if let Some(slot) = self.callbacks.get_mut(id) {
            *slot = None;
        }
    }
}

thread_local! {
    static ACTIVE: Cell<*const Lisp> = const {
        Cell::new(ptr::null())
    };
}

pub(crate) fn enter<'rt, R>(runtime: &'rt Lisp, f: impl FnOnce(LispContext<'rt>) -> R) -> R {
    ACTIVE.with(|active| {
        let previous = active.replace(runtime as *const Lisp);

        struct Guard<'a> {
            active: &'a Cell<*const Lisp>,
            previous: *const Lisp,
        }

        impl Drop for Guard<'_> {
            fn drop(&mut self) {
                self.active.set(self.previous);
            }
        }

        let _guard = Guard { active, previous };

        f(LispContext { runtime })
    })
}

fn write_error(buffer: *mut u8, capacity: usize, message: &str) {
    if buffer.is_null() || capacity == 0 {
        return;
    }

    let bytes = message.as_bytes();
    let length = bytes.len().min(capacity - 1);

    unsafe {
        let output = std::slice::from_raw_parts_mut(buffer, capacity);

        for (dst, src) in output[..length].iter_mut().zip(&bytes[..length]) {
            *dst = if *src == 0 { b'?' } else { *src };
        }

        output[length] = 0;
    }
}

fn invoke(id: usize, arguments: sys::cl_object) -> Result<*mut sys::eclisp_root, LispError> {
    ACTIVE.with(|active| {
        let runtime = active.get();

        if runtime.is_null() {
            return Err(LispError::RuntimeError("No active Lisp context".into()));
        }

        // SAFETY: enter() keeps the runtime alive during dispatch.
        let runtime = unsafe { &*runtime };

        let callback = runtime
            .callbacks
            .borrow()
            .get(id)
            .ok_or_else(|| LispError::RuntimeError(format!("Unknown callback ID: {id}")))?;

        let mut length = 0usize;

        let success = unsafe { sys::eclisp_args_length(arguments, &mut length) };

        if success == 0 {
            return Err(LispError::RuntimeError(
                "Invalid callback argument list".into(),
            ));
        }

        let mut args = Vec::new();
        args.try_reserve_exact(length)
            .map_err(|e| LispError::RuntimeError(e.to_string()))?;

        for index in 0..length {
            let mut object = ptr::null_mut();

            let success = unsafe { sys::eclisp_args_get(arguments, index, &mut object) };

            if success == 0 {
                return Err(LispError::RuntimeError(
                    "Failed to read callback argument".into(),
                ));
            }

            args.push(unsafe { ValueRef::from_raw(object) });
        }

        let ctx = LispContext { runtime };
        let result = callback(ctx, &args)?;

        Ok(result.into_raw_root())
    })
}

pub(crate) unsafe extern "C" fn dispatch(
    id: usize,
    arguments: sys::cl_object,
    out: *mut *mut sys::eclisp_root,
    error: *mut std::ffi::c_char,
    error_capacity: usize,
) -> i32 {
    if out.is_null() {
        write_error(error.cast(), error_capacity, "Missing result pointer");
        return 1;
    }

    unsafe {
        *out = std::ptr::null_mut();
    }

    let result = catch_unwind(AssertUnwindSafe(|| invoke(id, arguments)));

    match result {
        Ok(Ok(root)) => {
            unsafe {
                *out = root;
            }
            0
        }

        Ok(Err(err)) => {
            write_error(error.cast(), error_capacity, &err.to_string());
            1
        }

        Err(_) => {
            write_error(error.cast(), error_capacity, "Rust callback panicked");
            1
        }
    }
}