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}