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}
267
268fn get_struct_name(item: TokenStream) -> String {
269 let mut next_item = false;
270 for i in item {
271 if next_item {
272 return i.to_string();
273 } else if i.to_string() == "struct" {
274 next_item = true;
275 }
276 }
277 panic!("Not a struct")
278}
279
280/// Creates a Tcl Object Type for this struct
281#[proc_macro_derive(TclObjectType)]
282pub fn generate_tcl_object(item: TokenStream) -> TokenStream {
283 let obj_name = get_struct_name(item);
284 let tcl_obj_name = format!("{}_TCL_OBJECT", obj_name.to_uppercase());
285 let mut out_stream = TokenStream::new();
286
287 out_stream.extend(
288 TokenStream::from_str(&format!(
289 r#"
290 extern "C" fn {obj_name}_tcl_free(obj: *mut RawObject) {{
291 unsafe {{
292 drop(Box::from_raw((*obj).ptr1 as *mut {obj_name}));
293 }}
294 }}
295
296 extern "C" fn {obj_name}_tcl_dup(obj: *const RawObject, new_obj: *mut RawObject) {{
297 unsafe {{
298 let new_rep = Box::into_raw(Box::new(((*obj).ptr1 as *mut {obj_name}).as_ref().unwrap().clone())) as *mut std::ffi::c_void;
299 (*new_obj).ptr1 = new_rep;
300 (*new_obj).obj_type = (&{tcl_obj_name}) as *const ObjectType;
301 }}
302 }}
303
304 extern "C" fn {obj_name}_tcl_update(obj: *mut RawObject) {{
305 unsafe {{
306 let inner = ((*obj).ptr1 as *mut {obj_name}).as_ref().unwrap();
307 let (tcl_str, tcl_str_len) = rtea::tcl_string(&inner.as_string());
308 (*obj).bytes = tcl_str;
309 (*obj).length = tcl_str_len as usize;
310 }}
311 }}
312
313 extern "C" fn {obj_name}_tcl_from(interp: *const Interpreter, obj: *mut RawObject) -> TclStatus {{
314 let interp = unsafe {{ interp.as_ref() }};
315 let obj = RawObject::wrap(obj);
316
317 let (res, _obj) = match {obj_name}::convert(obj) {{
318 Ok(obj) => {{
319 (TclStatus::Ok, obj)
320 }}
321 Err(obj) => {{
322 (TclStatus::Error, obj)
323 }}
324 }};
325
326 if res == TclStatus::Error && interp.is_some() {{
327 let interp = interp.unwrap();
328 interp.set_result("could not convert to '{obj_name}' type")
329 }}
330
331 res
332 }}
333
334 static {tcl_obj_name}: ObjectType = ObjectType {{
335 name: "{obj_name}\0".as_ptr(),
336 free_internal_rep_proc: Some({obj_name}_tcl_free),
337 dup_internal_rep_proc: {obj_name}_tcl_dup,
338 update_string_proc: Some({obj_name}_tcl_update),
339 set_from_any_proc: Some({obj_name}_tcl_from),
340 }};
341
342 impl rtea::TclObjectType for {obj_name} {{
343 fn from_object(obj: &rtea::Object) -> Option<&{obj_name}> {{
344 let obj_type_ptr = (&{tcl_obj_name}) as *const ObjectType;
345 unsafe {{
346 if (*obj.obj).obj_type != obj_type_ptr {{
347 {obj_name}_tcl_from(std::ptr::null(), obj.obj);
348 }}
349
350 if (*obj.obj).obj_type == obj_type_ptr {{
351 Some(((*obj.obj).ptr1 as *const {obj_name}).as_ref().unwrap())
352 }} else {{
353 None
354 }}
355 }}
356 }}
357
358 fn into_object(self) ->rtea::Object {{
359 let ptr = Box::into_raw(Box::new(self)) as *mut std::ffi::c_void;
360 let obj = rtea::Object::new();
361 unsafe {{
362 (*obj.obj).ptr1 = ptr;
363 (*obj.obj).obj_type = (&{tcl_obj_name}) as *const rtea::ObjectType;
364 (*obj.obj).bytes = std::ptr::null_mut();
365 }}
366 obj
367 }}
368
369 fn type_name() -> &'static str {{ "{obj_name}" }}
370
371 fn tcl_type() -> &'static ObjectType {{ &{tcl_obj_name} }}
372 }}
373
374 impl From<{obj_name}> for rtea::Object {{
375 fn from(pt: {obj_name}) -> rtea::Object {{
376 pt.into_object()
377 }}
378 }}
379 "#,
380 obj_name = obj_name,
381 tcl_obj_name = tcl_obj_name,
382 ))
383 .unwrap(),
384 );
385
386 out_stream
387}