jimtcl 0.5.0

Embed Jim Tcl in Rust.
Documentation
//! Support for custom Tcl objects.

use std::any::Any;
use std::os::raw::c_int;
use std::ptr::{self};
use std::rc::Rc;
use std::sync::OnceLock;

use jimtcl_sys::Jim_ObjType;

use crate::object::IntoJimObj;
use crate::{JimError, sys};
use crate::{JimObject, JimResult};

static JIM_EXTENSION_TYPE_NAME: &[u8] = b"jimtcl-rs-extension\0";
static JIM_EXTENSION_TYPE: OnceLock<JimTypeWrapper> = OnceLock::new();

/// Wrapper around a Jim type object to allow static management.
struct JimTypeWrapper {
    type_obj: *const Jim_ObjType,
}
// SAFETY: all we have is a pointer to static data
unsafe impl Sync for JimTypeWrapper {}
// SAFETY: all we have is a pointer to static data
unsafe impl Send for JimTypeWrapper {}

/// Get the pointer to the Jim extension type object.
pub fn jim_extension_type_ptr() -> *const sys::Jim_ObjType {
    let wrapper = JIM_EXTENSION_TYPE.get_or_init(|| {
        let obj = sys::Jim_ObjType {
            name: JIM_EXTENSION_TYPE_NAME.as_ptr().cast(),
            freeIntRepProc: Some(rust_jim_free_rep),
            dupIntRepProc: Some(rust_jim_dup_rep),
            updateStringProc: Some(rust_jim_update_string),
            flags: 0,
        };
        let ptr = Box::leak(Box::new(obj));
        JimTypeWrapper { type_obj: ptr }
    });
    wrapper.type_obj
}

/// Internal struct for storing data.
struct JimExtensionWrapper {
    object: Rc<dyn JimExtensionData>,
}

/// Dyn-compatible trait to be implemented by Jim Tcl extension types.
///
/// Most implementers will also want to implement [JimExtensionType].
pub trait JimExtensionData: Any {
    /// Convert this object to a Tcl string.
    ///
    /// The result of this should be valid input to
    /// [JimExtensionType::parse_string], unless the object is unrepresentable.
    fn to_tcl_string(&self) -> String;
}

/// Trait to be implemented by Jim Tcl extension types. It augments
/// [JimExtensionData] with functions to convert from strings and objects.
///
/// This is implemented as a separate trait so that [JimExtensionData] can be
/// [Dyn]-compatible, easing implementation of the FFI shims.
pub trait JimExtensionType: JimExtensionData + Sized {
    /// Parse the object from its Tcl string representation.
    fn parse_tcl_string(repr: &str) -> JimResult<Self>;

    /// Extract a ref-counted handle to this type from a Jim type.
    fn extract<'jim, 'o: 'jim>(obj: &'o JimObject<'jim>) -> JimResult<Rc<Self>> {
        let raw = obj.obj;
        unsafe {
            if (*raw).typePtr != jim_extension_type_ptr() {
                let data = Self::parse_tcl_string(obj.as_str()?)?;
                let _ = JimExtensionWrapper::inject(data, obj)?;
            }

            assert_eq!((*raw).typePtr, jim_extension_type_ptr());
            let pointer: *const JimExtensionWrapper = (*obj.obj).internalRep.ptr.cast();

            if let Some(data) = pointer.as_ref() {
                let ptr: Rc<dyn Any> = data.object.clone();
                ptr.downcast::<Self>()
                    .map_err(|_| JimError::from("object has unexpected Rust type"))
            } else {
                Err(JimError::from("object is null (already freed?)"))
            }
        }
    }

    /// Extract a reference to this type from a Jim type.
    fn extract_ref<'jim, 'o: 'jim>(obj: &'o JimObject<'jim>) -> JimResult<&'o Self> {
        let raw = obj.obj;
        unsafe {
            if (*raw).typePtr == jim_extension_type_ptr() {
                let pointer: *const JimExtensionWrapper = (*obj.obj).internalRep.ptr.cast();

                if let Some(data) = pointer.as_ref() {
                    <dyn Any>::downcast_ref::<Self>(data.object.as_ref())
                        .ok_or_else(|| JimError::from("object has unexpected Rust type"))
                } else {
                    Err(JimError::from("object is null (already freed?)"))
                }
            } else {
                let data = Self::parse_tcl_string(obj.as_str()?)?;
                let data = JimExtensionWrapper::inject(data, obj)?;
                Ok(data)
            }
        }
    }
}

impl JimExtensionWrapper {
    fn inject<'jim, 'o: 'jim, T: JimExtensionType>(
        data: T,
        obj: &'o JimObject<'jim>,
    ) -> JimResult<&'o T> {
        let wrapper = JimExtensionWrapper {
            object: Rc::new(data),
        };
        let wrapper = Box::leak(Box::new(wrapper));
        obj.free_internal();
        unsafe {
            (*obj.obj).typePtr = jim_extension_type_ptr();
            (*obj.obj).internalRep.ptr = ptr::from_mut(wrapper).cast();
        }
        obj.invalidate_string_rep();
        Ok(<dyn Any>::downcast_ref::<T>(wrapper.object.as_ref()).unwrap())
    }
}

impl Clone for JimExtensionWrapper {
    fn clone(&self) -> Self {
        JimExtensionWrapper {
            object: self.object.clone(),
        }
    }
}

impl<T: JimExtensionType + 'static> IntoJimObj for T {
    fn to_jim<'jim>(self, interp: &'jim crate::Interp) -> JimObject<'jim> {
        Rc::new(self).to_jim(interp)
    }
}

impl<T: JimExtensionType + 'static> IntoJimObj for Rc<T> {
    fn to_jim<'jim>(self, interp: &'jim crate::Interp) -> JimObject<'jim> {
        let wrapper = JimExtensionWrapper { object: self };
        let obj = unsafe {
            let obj = sys::Jim_NewObj(interp.interp);
            let o = obj.as_mut_unchecked();
            o.bytes = ptr::null_mut();
            o.length = 0;
            o.typePtr = jim_extension_type_ptr();
            o.internalRep.ptr = ptr::from_mut(Box::leak(Box::new(wrapper))).cast();
            obj
        };
        JimObject::wrap(interp, obj)
    }
}

unsafe extern "C" fn rust_jim_free_rep(_interp: *mut sys::Jim_Interp, data: *mut sys::Jim_Obj) {
    let pointer: *mut JimExtensionWrapper = unsafe { (*data).internalRep.ptr.cast() };
    if !pointer.is_null() {
        let _ = unsafe { Box::from_raw(pointer) };
        unsafe {
            (*data).internalRep.ptr = ptr::null_mut();
        }
    }
}

unsafe extern "C" fn rust_jim_dup_rep(
    _interp: *mut sys::Jim_Interp,
    src: *mut sys::Jim_Obj,
    dst: *mut sys::Jim_Obj,
) {
    unsafe {
        let pointer: *const JimExtensionWrapper = (*src).internalRep.ptr.cast();
        if let Some(obj) = pointer.as_ref() {
            let dup = Box::new(obj.clone());
            (*dst).internalRep.ptr = ptr::from_mut(Box::leak(dup)).cast();
        } else {
            (*dst).internalRep.ptr = ptr::null_mut();
        }
        (*dst).typePtr = jim_extension_type_ptr();
    }
}

unsafe extern "C" fn rust_jim_update_string(obj: *mut sys::Jim_Obj) {
    unsafe {
        let pointer: *const JimExtensionWrapper = (*obj).internalRep.ptr.cast();
        if let Some(data) = pointer.as_ref() {
            let str = data.object.to_tcl_string();
            let len = str.len() as c_int;
            (*obj).bytes = sys::Jim_StrDupLen(str.as_ptr().cast(), len);
            (*obj).length = len;
        } else {
            (*obj).bytes = sys::Jim_StrDupLen("".as_ptr().cast(), 0);
            (*obj).length = 0;
        }
    };
}