jimtcl 0.3.0

Embed Jim Tcl in Rust.
Documentation
//! Wrapper for the Jim Tcl interpreter.

use std::ffi::CString;
use std::ffi::c_int;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::str::FromStr;

use jimtcl_sys::Jim_GetExitCode;

use crate::JimError;
use crate::JimResult;
use crate::command::JimCommand;
use crate::command::register_command;
use crate::error::ExitCode;
use crate::object::IntoJimObj;
use crate::object::JimObject;
use crate::object::jim_decref;
use crate::sys;

/// Safe wrapper for a Jim Tcl interpreter.
///
/// When the `serde` feature is enabled, interpreters implement
/// [serde::de::DeserializeSeed], allowing Serde formats to be directly
/// deserialized to Tcl data structures. The base crate does not use this for
/// anything, but programs embedding Jim can use this functionality to implement
/// data deserialization support with little additional code.
///
/// **Note on lifetimes:** The `Interp` structure is primarily built around
/// owning the underlying Jim interpreter, and a reference to `Interp` sufficies
/// to access the interpreter. Objects therefore have the lifetime of the
/// interpreter to which they are attached. In some cases, such as in Rust
/// procedure calls, an `Interp` may have a different lifetime, but this
/// lifetime will be shorter than the root `Interp` lifetime. This difference
/// should not be apparent to client code, or introduce unsafety.
pub struct Interp {
    pub(crate) interp: *mut sys::Jim_Interp,
    owned: bool,
}

impl Interp {
    /// Create a new Jim Tcl interpreter with core commands and built-in
    /// extensions.
    pub fn new() -> JimResult<Interp> {
        let jim = Interp::new_uninit()?;
        jim.std_init()?;
        Ok(jim)
    }

    /// Create a new, **uninitialized** Jim interpreter. This interpreter does
    /// not have the Tcl core commands or built-in extensions initialized.
    pub fn new_uninit() -> JimResult<Interp> {
        let interp = unsafe { sys::Jim_CreateInterp() };
        if interp.is_null() {
            panic!("could not allocate Jim interpreter");
        }
        Ok(Interp {
            interp,
            owned: true,
        })
    }

    /// Wrap a Jim interpreter pointer.
    pub(crate) fn wrap(interp: *mut sys::Jim_Interp) -> Interp {
        if interp.is_null() {
            panic!("cannot wrap null interpreter");
        }
        Interp {
            interp,
            owned: false,
        }
    }

    /// Invoke the standard initialization operations (core commands and built-in extensions).
    pub fn std_init(&self) -> JimResult<()> {
        self.register_core_commands();
        self.init_static_extensions()
    }

    /// Register the Jim Tcl core commands.
    pub fn register_core_commands(&self) {
        unsafe {
            sys::Jim_RegisterCoreCommands(self.interp);
        }
    }

    /// Evaluate Tcl code in global context.
    pub fn eval<'jim>(&'jim self, script: &str) -> JimResult<JimObject<'jim>> {
        let script = CString::from_str(script)?;
        let rc = unsafe { sys::Jim_Eval(self.interp, script.as_ptr()) };
        self.require_ok(rc as u32)?;
        let ptr = unsafe { (*self.interp).result };
        Ok(JimObject::wrap(self, ptr))
    }

    /// Evaluate a Tcl expression.
    pub fn eval_expr<'jim>(&'jim self, expr: &str) -> JimResult<JimObject<'jim>> {
        let expr = expr.to_jim(self);
        let rc = unsafe { sys::Jim_EvalExpression(self.interp, expr.obj) };
        self.require_ok(rc as u32)?;
        let ptr = unsafe { (*self.interp).result };
        Ok(JimObject::wrap(self, ptr))
    }

    /// Evaluate a file.
    pub fn eval_file<P: AsRef<Path>>(&self, path: P) -> JimResult<()> {
        let pstr = CString::new(path.as_ref().as_os_str().as_bytes())?;
        let rc = unsafe { sys::Jim_EvalFile(self.interp, pstr.as_ptr()) };
        self.require_ok(rc as u32)
    }

    /// Initialize the static (built-in) extensions.
    pub fn init_static_extensions(&self) -> JimResult<()> {
        let rc = unsafe { sys::Jim_InitStaticExtensions(self.interp) };
        self.require_ok(rc as u32)
    }

    /// Get the Jim exit code.
    pub fn exit_code(&self) -> i32 {
        unsafe { Jim_GetExitCode(self.interp) }
    }

    /// Resolve a result into an exit code.  Primarily used to resolve
    /// the results of evaluating scripts.
    pub fn resolve_exit(&self, result: JimResult<()>) -> JimResult<i32> {
        match result {
            Err(JimError::OtherCode(ExitCode::Exit)) => Ok(self.exit_code()),
            Err(e) => Err(e),
            Ok(()) => Ok(0),
        }
    }

    /// Check a return code and succeed if it is OK, returning an error otherwise.
    pub fn require_ok(&self, code: u32) -> JimResult<()> {
        if code == sys::JIM_OK {
            Ok(())
        } else if code == sys::JIM_ERR {
            let obj = unsafe { JimObject::wrap(self, (*self.interp).result) };
            Err(JimError::Error(obj.to_string()))
        } else {
            Err(JimError::OtherCode(code.into()))
        }
    }

    /// Run a Jim interactive prompt.
    pub fn interactive_prompt(&self) -> JimResult<()> {
        let rc = unsafe { sys::Jim_InteractivePrompt(self.interp) as u32 };
        self.require_ok(rc)
    }

    /// Set the current Jim result. Most code won't need to use this directly —
    /// it is primarily used by the command-wrapping logic used by
    /// [Interp::add_command] to pass Rust command results back to their Tcl
    /// callers.
    pub fn set_result(&self, result: &mut JimObject<'_>) {
        assert_eq!(self.interp, result.interp.interp);
        let rp = result.as_ref_ptr();
        unsafe {
            let mut result = (*self.interp).result;
            jim_decref(&self, &mut result);
            (*self.interp).result = rp;
        }
    }

    /// Get the value of a Jim variable in the current context.
    pub fn get_variable(&self, name: &str) -> Option<JimObject<'_>> {
        let name = name.to_jim(self);
        let obj = unsafe { sys::Jim_GetVariable(self.interp, name.obj, sys::JIM_NONE as c_int) };
        JimObject::wrap(self, obj).non_null()
    }

    /// Set a Jim variable in the current context.
    pub fn set_variable<'jim, T: IntoJimObj<'jim>>(
        &'jim self,
        name: &str,
        value: T,
    ) -> JimResult<()> {
        let name = name.to_jim(self);
        let value = value.to_jim(self);
        let rc = unsafe { sys::Jim_SetVariable(self.interp, name.obj, value.obj) };
        self.require_ok(rc as u32)
    }

    /// Get the value of a global Jim variable.
    pub fn get_global_variable(&self, name: &str) -> Option<JimObject<'_>> {
        let name = name.to_jim(self);
        let obj =
            unsafe { sys::Jim_GetGlobalVariable(self.interp, name.obj, sys::JIM_NONE as c_int) };
        JimObject::wrap(self, obj).non_null()
    }

    /// Add a command to the interpreter.
    pub fn add_command<'jim, CF>(&'jim self, name: &str, cmd: CF) -> JimResult<()>
    where
        CF: JimCommand + 'static,
    {
        register_command(self, name, cmd)
    }

    /// Free a Jim object.
    pub(crate) fn free_obj(&self, obj_ptr: *mut sys::Jim_Obj) {
        unsafe {
            sys::Jim_FreeObj(self.interp, obj_ptr);
        }
    }
}

impl Drop for Interp {
    fn drop(&mut self) {
        if self.owned {
            unsafe {
                sys::Jim_FreeInterp(self.interp);
            }
        }
    }
}