rtea_proc/
lib.rs

1//! rtea-proc provides macros to ergonomically wrap the initialization and
2//! unload functions expected by TEA.
3//!
4//! The library provides the simple macros to conveniently wrap Rust
5//! initialization and unload functions without having to deal with
6//! `extern "C"` or raw pointers.  
7//!
8//! # Example
9//!
10//! ```rust
11//! use rtea::{Interpreter, TclStatus, TclUnloadFlag}; // Implicit dependency of macro when invoked.
12//!
13//! #[module_init(Example, "1.0.0")]
14//! fn init(interp: &Interpreter) -> Result<TclStatus, String> {
15//!     safe_init(interp, args)?;
16//!     // Add additional commands that may not be safe for untrusted code...
17//!     Ok(TclStatus::Ok)
18//! }
19//!
20//! #[module_safe_init(Example, "1.0.0")]
21//! fn safe_init(_interp: &Interpreter) -> Result<TclStatus, String> {
22//!     // Add commands that are safe even for untrusted code...
23//!     Ok(TclStatus::Ok)
24//! }
25//!
26//! #[module_unload(Example)]
27//! fn unload(interp: &Interpreter) -> Result<TclStatus, String> {
28//!     safe_unload(interp, args)?;
29//!     // Remove the additional commands that were not considered "safe"...
30//!     Ok(TclStatus::Ok)
31//! }
32//!
33//! #[module_safe_unload(Example)]
34//! fn safe_unload(_interp: &Interpreter) -> Result<TclStatus, String> {
35//!     // Remove the "safe" set of commands
36//!     Ok(TclStatus::Ok)
37//! }
38//! ```
39//!
40//! # Note
41//!
42//! This code assumes that it extends Tcl and treats any violations of Tcl's
43//! API (unexpected null-pointers, non-UTF8 strings, etc.) as irrecovable
44//! errors that should panic.
45
46use proc_macro::TokenStream;
47use proc_macro::TokenTree::Punct;
48use std::str::FromStr;
49
50fn module_init_common(prefix: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
51    let mut mod_name = None;
52    let mut version = None;
53    for a in attr {
54        if let Punct(_) = a {
55            continue;
56        }
57        if mod_name == None {
58            mod_name = Some(a.to_string());
59        } else if version == None {
60            version = Some(a.to_string());
61        } else {
62            panic!("Unexpected additional attributes to 'module_init': {}", a)
63        }
64    }
65    let mod_name = mod_name.expect("no module name found");
66    let version = version.unwrap_or("".to_string());
67
68    let mut out_stream = TokenStream::new();
69
70    let mut next_item = false;
71    let mut fn_name = None;
72    for i in item {
73        if next_item {
74            fn_name = Some(i.to_string());
75            next_item = false;
76        } else if fn_name.is_none() && i.to_string() == "fn" {
77            next_item = true;
78        }
79        out_stream.extend([i]);
80    }
81    let fn_name = fn_name.expect("'module_init' macro not used on a function");
82
83    out_stream.extend(
84        TokenStream::from_str(&format!(
85            r#"
86                #[no_mangle]
87                pub extern "C" fn {module_symbol}_{prefix}Init(interp: *const Interpreter) -> TclStatus {{
88                    Interpreter::from_raw(interp)
89                        .map(|interp| {{
90                            interp.init_global_functions();
91                            {init_fn}(interp)
92                                .and(interp.provide_package("{module_tcl}", {version}))
93                                .unwrap_or_else(|s| {{interp.set_result(&s); TclStatus::Error}})
94                        }})
95                        .unwrap_or(TclStatus::Error)
96                }}
97            "#,
98            prefix = prefix,
99            module_symbol = mod_name,
100            init_fn = fn_name,
101            module_tcl = mod_name.to_lowercase(),
102            version = version
103        ))
104        .unwrap(),
105    );
106
107    out_stream
108}
109
110/// Helper for creating the initialization function for Tcl extensions.
111///
112/// This macro will automatically create the appropriate wrapper to validate
113/// the interpreter and "provide" the package to the interpreter.  The
114/// prototype of the wrapped function should be
115///
116/// ```rust
117/// type init_fn = fn(interp: &rtea::Interpreter) -> Result<rtea::TclStatus, String>;
118/// ```
119///
120/// and one or two attributes should be passed to the macro.  The first must
121/// be the module's name with a capital first letter and all others lowercase
122/// (this is a Tcl requirement).  The second, optional attribute, is the
123/// version which by Tcl convention should be in accordance with semver.
124///
125/// # Example
126///
127/// ```rust
128/// #[module_init(Example, "1.0.0")]
129/// fn init(interp: &Interpreter) -> Result<TclStatus, String> {
130///     interp.eval("Initializing module...")
131/// }
132/// ```
133///
134/// The above example will create a function named `Example_Init` (with the
135/// `no_mangle` attribute) which Tcl will use as the initialization routine.
136/// This assumes that your files final library name matches the expectation
137/// of `-lexample` for the C linker (which is the case if used in a "cdylib"
138/// crate named "example").
139#[proc_macro_attribute]
140pub fn module_init(attr: TokenStream, item: TokenStream) -> TokenStream {
141    module_init_common("", attr, item)
142}
143
144/// Helper for creating the "safe" initialization function for Tcl extensions.
145///
146/// This macro will automatically create the appropriate wrapper to validate
147/// the interpreter and "provide" the package to the interpreter.  The
148/// prototype of the wrapped function should be
149///
150/// ```rust
151/// type init_fn = fn(interp: &rtea::Interpreter) -> Result<rtea::TclStatus, String>;
152/// ```
153///
154/// and one or two attributes should be passed to the macro.  The first must
155/// be the module's name with a capital first letter and all others lowercase
156/// (this is a Tcl requirement).  The second, optional attribute, is the
157/// version which by Tcl convention should be in accordance with semver.
158///
159/// # Example
160///
161/// ```rust
162/// #[module_safe_init(Example, "1.0.0")]
163/// fn init(interp: &Interpreter) -> Result<TclStatus, String> {
164///     interp.eval("Initializing module...")
165/// }
166/// ```
167///
168/// The above example will create a function named `Example_SafeInit` (with the
169/// `no_mangle` attribute) which Tcl will use as the initialization routine.
170/// This assumes that your files final library name matches the expectation
171/// of `-lexample` for the C linker (which is the case if used in a "cdylib"
172/// crate named "example").
173///
174/// # Warning
175///
176/// This initialization routine is intended to be safe to use
177/// from **untrusted** code.  Users must take care that the functionality
178/// they expose to Tcl scripts from here is truly "safe" (in the destroy a
179/// system sense, not Rust's crash a program sense).  It is highly
180/// recommended you read about [Safe Tcl](https://www.tcl.tk/man/tcl/TclCmd/safe.html)
181/// before using this macro.
182#[proc_macro_attribute]
183pub fn module_safe_init(attr: TokenStream, item: TokenStream) -> TokenStream {
184    module_init_common("Safe", attr, item)
185}
186
187fn module_unload_common(prefix: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
188    let mut mod_name = None;
189    for a in attr {
190        if mod_name == None {
191            mod_name = Some(a.to_string());
192        } else {
193            panic!("Unexpected additional attributes to 'module_init': {}", a)
194        }
195    }
196    let mod_name = mod_name.expect("no module name found");
197
198    let mut out_stream = TokenStream::new();
199
200    let mut next_item = false;
201    let mut fn_name = None;
202    for i in item {
203        if next_item {
204            fn_name = Some(i.to_string());
205            next_item = false;
206        } else if fn_name.is_none() && i.to_string() == "fn" {
207            next_item = true;
208        }
209        out_stream.extend([i]);
210    }
211    let fn_name = fn_name.expect("'module_unload' macro not used on a function");
212
213    out_stream.extend(
214        TokenStream::from_str(&format!(
215            r#"
216                #[no_mangle]
217                pub extern "C" fn {module_symbol}_{prefix}Unload(interp: *const Interpreter, flags: TclUnloadFlag) -> TclStatus {{
218                    Interpreter::from_raw(interp)
219                        .map(|interp| {unload_fn}(interp, flags)
220                            .unwrap_or_else(|s| {{interp.set_result(&s); TclStatus::Error}}))
221                        .unwrap_or(TclStatus::Error)
222                }}
223            "#,
224            prefix = prefix,
225            module_symbol = mod_name,
226            unload_fn = fn_name,
227        ))
228        .unwrap(),
229    );
230
231    out_stream
232}
233
234/// Helper for unloading a Tcl extension.
235///
236/// This macro will automatically create the appropriate wrapper to validate
237/// the interpreter and pass it to the given unload routine.  The prototype
238/// of the wrapped function should be
239///
240/// ```rust
241/// type unload_fn = fn(interp: &rtea::Interpreter, flags: TclUnloadFlag) -> Result<rtea::TclStatus, String>;
242/// ```
243///
244/// and the module's name (as given to [module_init]) should be given as the
245/// sole attribute to the macro.
246#[proc_macro_attribute]
247pub fn module_unload(attr: TokenStream, item: TokenStream) -> TokenStream {
248    module_unload_common("", attr, item)
249}
250
251/// Helper for unloading a "safe" Tcl extensions
252///
253/// This macro will automatically create the appropriate wrapper to validate
254/// the interpreter and pass it to the given unload routine.  The prototype
255/// of the wrapped function should be
256///
257/// ```rust
258/// type unload_fn = fn(interp: &rtea::Interpreter, flags: TclUnloadFlag) -> Result<rtea::TclStatus, String>;
259/// ```
260///
261/// and the module's name (as given to [module_init]) should be given as the
262/// sole attribute to the macro.
263#[proc_macro_attribute]
264pub fn module_safe_unload(attr: TokenStream, item: TokenStream) -> TokenStream {
265    module_unload_common("Safe", attr, item)
266}