rtea 0.4.0

Makes writing Tcl extensions in Rust ergonomic.
Documentation

rtea

rtea (Rust Tcl Extension Architecture) is a library for writing Tcl extensions in Rust. It wraps the Tcl stubs interface so that extensions can be written without unsafe code or direct knowledge of Tcl's C API.

Setup

Your crate must be a cdylib:

[lib]
crate-type = ["cdylib"]

[dependencies]
rtea = "0.3"

Writing an Extension

Initialization

Use the #[module_init] macro to define the entry point Tcl calls when loading your extension. The macro generates the correctly named and typed extern "C" symbol, validates the interpreter, and registers the package version.

use rtea::*;

#[module_init(Example, "1.0.0")]
fn init(interp: &Interpreter) -> Result<TclStatus, String> {
    interp.create_command("hello", hello_cmd)
}

fn hello_cmd(interp: &Interpreter, _args: Vec<&str>) -> Result<TclStatus, String> {
    interp.set_result("Hello from Rust!");
    Ok(TclStatus::Ok)
}

The module name passed to the macro must match Tcl's naming conventions: first letter capitalised, all others lowercase (e.g. Example for a library named libexample). The generated symbol is Example_Init.

If the extension should also be loadable into a safe interpreter, provide a second entry point with #[module_safe_init], which generates Example_SafeInit.

Unloading

#[module_unload(Example)]
fn unload(_interp: &Interpreter, _flags: TclUnloadFlag) -> Result<TclStatus, String> {
    Ok(TclStatus::Ok)
}

#[module_safe_unload] generates the corresponding Example_SafeUnload symbol.

Commands

Stateless commands

Commands that do not need to carry state take the form:

fn my_cmd(interp: &Interpreter, args: Vec<&str>) -> Result<TclStatus, String> {
    // args[0] is the command name, args[1..] are the arguments.
    interp.set_result("ok");
    Ok(TclStatus::Ok)
}

interp.create_command("mycmd", my_cmd)?;

Return Err(message) to signal a Tcl error; the message becomes the interpreter's error result automatically.

Stateful commands

StatefulCommand associates owned data with a command. The data is dropped automatically when the command is deleted from the interpreter.

use std::cell::RefCell;

fn counter_cmd(
    interp: &Interpreter,
    count: &RefCell<usize>,
    _args: Vec<&str>,
) -> Result<TclStatus, String> {
    let mut n = count.borrow_mut();
    interp.set_result(&n.to_string());
    *n += 1;
    Ok(TclStatus::Ok)
}

StatefulCommand::new(counter_cmd, RefCell::new(0usize))
    .attach_command(interp, "counter")?;

Object commands

Commands that work with typed Tcl objects use create_obj_command:

fn add_cmd(interp: &Interpreter, args: Vec<Object>) -> Result<TclStatus, Object> {
    let a: i64 = args[1].get_string().parse().map_err(|_| Object::new_string("bad int"))?;
    let b: i64 = args[2].get_string().parse().map_err(|_| Object::new_string("bad int"))?;
    interp.set_result(&(a + b).to_string());
    Ok(TclStatus::Ok)
}

interp.create_obj_command("add", add_cmd)?;

Custom Object Types

The TclObjectType derive macro registers a Rust struct as a native Tcl object type, allowing values to be passed between Tcl and Rust without repeated string parsing.

use rtea::*;

#[derive(Clone, Debug, TclObjectType)]
struct Point { x: f64, y: f64 }

impl std::fmt::Display for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{} {}", self.x, self.y)
    }
}

After calling interp.register_obj_type::<Point>(), the type is available to Tcl. Use Point::from_object(&obj) to borrow the inner value and point.into_object() (or the From impl) to wrap one in an Object.

Evaluating Tcl

let result = interp.eval("expr {1 + 1}")?;
println!("{}", result.get_string()); // "2"

eval returns Ok(Object) on success and Err(Object) on error, where the object holds the interpreter's result or error message respectively.

Maintenance

stubParser.tcl regenerates the Tcl stubs vtable in src/interpreter.rs from a Tcl source tree. Run it whenever updating the Tcl version:

tclsh stubParser.tcl /path/to/tcl/generic/tcl.decls