dynamic_plugin_macros/
lib.rs

1#![deny(missing_docs)]
2#![warn(clippy::pedantic)]
3
4//! # Macros for the [`dynamic-plugin`](https://docs.rs/dynamic-plugin/latest/dynamic_plugin/) crate.
5
6use std::hash::{Hash, Hasher};
7
8use def::PluginDefinition;
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use proc_macro_error2::{abort, proc_macro_error};
12use quote::quote;
13use syn::{parse_macro_input, FnArg, Lit, ReturnType, Type};
14
15use crate::hasher::PluginSignatureHasher;
16
17mod def;
18mod hasher;
19mod implementation;
20
21/// Define an interface for a plugin. See the `dynamic_plugin` crate documentation for more.
22///
23/// ## Example
24/// ```ignore
25/// plugin_interface! {
26///     extern trait ExamplePlugin {
27///         /// Ask the plugin to do a thing
28///         fn do_a_thing();
29///         /// Say hello to a person
30///         fn say_hello(to: *const c_char) -> bool;
31///     }
32/// }
33/// ```
34#[proc_macro]
35#[proc_macro_error]
36pub fn plugin_interface(tokens: TokenStream) -> TokenStream {
37    let plugin_def = parse_macro_input!(tokens as PluginDefinition);
38    let plugin_ident = &plugin_def.name;
39
40    let mut hasher = PluginSignatureHasher::default();
41    plugin_def.hash(&mut hasher);
42    let hash = hasher.finish();
43
44    let hash_debug: Option<TokenStream2> = {
45        #[cfg(feature = "debug-hashes")]
46        {
47            let hash_debug = format!("{hasher:?}");
48            Some(quote! {
49                #[no_mangle]
50                pub fn _dynamic_plugin_signature_unhashed() -> &'static str {
51                    #hash_debug
52                }
53            })
54        }
55        #[cfg(not(feature = "debug-hashes"))]
56        {
57            None
58        }
59    };
60
61    let host_impl = if cfg!(feature = "host") {
62        let funcs = plugin_def.functions.iter().map(|pf| {
63            let attributes = &pf.attributes;
64            let name = &pf.name;
65            let name_as_str = format!(r#"b"{name}""#).parse::<TokenStream2>().unwrap();
66            let args = &pf.arguments;
67            let mut arg_types = vec![];
68            let mut arg_names = vec![];
69            for arg in args {
70                if let FnArg::Typed(typed) = arg {
71                    arg_types.push(typed.ty.clone());
72                    arg_names.push(typed.pat.clone());
73                }
74            }
75            let ret = if let Some(typ) = &pf.return_type { quote! { #typ } } else { quote! { () } };
76            let sig = quote! { unsafe extern fn(#(#arg_types),*) -> #ret };
77            quote! {
78                #(#attributes)*
79                pub extern "C" fn #name(&self, #(#args),*) -> ::dynamic_plugin::Result<#ret> {
80                    unsafe {
81                        let func: ::dynamic_plugin::PluginLibrarySymbol<#sig> = self.library.get(#name_as_str)?;
82                        Ok(func(#(#arg_names),*))
83                    }
84                }
85            }
86        });
87
88        let fn_checks = plugin_def.functions.iter().map(|f| {
89            let name_bytes = f.name.to_string();
90            quote! {
91                let _: ::dynamic_plugin::PluginLibrarySymbol<unsafe extern fn()> =
92                    library.get(#name_bytes.as_bytes()).map_err(|_| ::dynamic_plugin::Error::NotAPlugin)?;
93            }
94        });
95
96        Some(quote! {
97            impl #plugin_ident {
98                #hash_debug
99
100                /// Search `path` to find compatible plugins.
101                pub fn find_plugins<P>(path: P) -> ::std::vec::Vec<Self>
102                where
103                    P: ::std::convert::AsRef<::std::path::Path>,
104                {
105                    let mut plugins = vec![];
106
107                    // Iterate through directory entries
108                    if let Ok(paths) = ::std::fs::read_dir(path) {
109                        for path in paths {
110                            if let Ok(path) = path {
111                                // Try to load each potential plugin. Catch errors and ignore.
112                                if let Ok(plugin) = Self::load_plugin_and_check(path.path()) {
113                                    plugins.push(plugin);
114                                }
115                            }
116                        }
117                    }
118
119                    plugins
120                }
121
122                /// Load the plugin at `path`
123                ///
124                /// # Errors
125                ///
126                /// - [`::dynamic_plugin::Error::NotAPlugin`] if the file provided is determined not to be a compatible (dynamic_plugin style) plugin.
127                /// - [`::dynamic_plugin::Error::InvalidPluginSignature`] if the signature does not match this loader.
128                pub fn load_plugin_and_check<P>(path: P) -> ::dynamic_plugin::Result<Self>
129                where
130                    P: ::std::convert::AsRef<::std::ffi::OsStr>,
131                {
132                    Self::load_plugin(path, true)
133                }
134
135                /// Load the plugin at `path`
136                ///
137                /// # Errors
138                ///
139                /// - [`::dynamic_plugin::Error::NotAPlugin`] if the file provided is determined not to be a compatible (dynamic_plugin style) plugin.
140                /// - [`::dynamic_plugin::Error::InvalidPluginSignature`] if `check_signature` is true and the signature does not match this loader.
141                pub fn load_plugin<P>(path: P, check_signature: bool) -> ::dynamic_plugin::Result<Self>
142                where
143                    P: ::std::convert::AsRef<::std::ffi::OsStr>,
144                {
145                    unsafe {
146                        // Attempt to load library
147                        let library = ::dynamic_plugin::PluginDynamicLibrary::new(path)?;
148
149                        // Check that signature function exists
150                        let func: ::dynamic_plugin::PluginLibrarySymbol<unsafe extern fn() -> u64> =
151                            library.get(b"_dynamic_plugin_signature").map_err(|_| ::dynamic_plugin::Error::NotAPlugin)?;
152                        if check_signature {
153                            // Check plugin library signature
154                            let hash = func();
155
156                            if hash != #hash {
157                                return ::dynamic_plugin::Result::Err(::dynamic_plugin::Error::InvalidPluginSignature);
158                            }
159                        }
160
161                        Ok(Self {
162                            library,
163                        })
164                    }
165                }
166
167                /// Load the plugin at `path`, checking if it is valid
168                /// using a more compatible method, checking for the
169                /// presence of each function rather than just the
170                /// signature function.
171                ///
172                /// This makes it slightly easier to implement plugins
173                /// in languages other than Rust, however slightly
174                /// increases the chances of errors being returned
175                /// later, for example if function parameters do not
176                /// match, as in compatability mode this is not checked
177                /// when the plugin is loaded.
178                ///
179                /// # Errors
180                ///
181                /// - [`::dynamic_plugin::Error::NotAPlugin`] if the file provided is determined not to be a compatible plugin, i.e. not having the required functions present and exposed.
182                pub fn load_plugin_and_check_compat<P>(path: P) -> ::dynamic_plugin::Result<Self>
183                where
184                    P: ::std::convert::AsRef<::std::ffi::OsStr>,
185                {
186                    unsafe {
187                        // Attempt to load library
188                        let library = ::dynamic_plugin::PluginDynamicLibrary::new(path)?;
189
190                        // Check that each function exists
191                        #(#fn_checks)*
192
193                        Ok(Self {
194                            library,
195                        })
196                    }
197                }
198
199                #(#funcs)*
200            }
201        })
202    } else {
203        None
204    };
205
206    let definition =
207        {
208            let mut s = String::new();
209            for def::PluginFunction {
210                attributes,
211                name,
212                arguments,
213                return_type,
214                ..
215            } in &plugin_def.functions
216            {
217                for attr in attributes {
218                    if attr.path().is_ident("doc") {
219                        match &attr.meta {
220                            syn::Meta::NameValue(inner) => {
221                                if inner.path.is_ident("doc") {
222                                    if let syn::Expr::Lit(expr) = &inner.value {
223                                        if let Lit::Str(doc) = &expr.lit {
224                                            s.push_str(&format!("/// {}\n", doc.value().trim()));
225                                        }
226                                    }
227                                }
228                            }
229                            _ => (),
230                        }
231                    }
232                }
233                s.push_str("fn ");
234                s.push_str(&name.to_string());
235                s.push('(');
236                for (idx, arg) in arguments.iter().enumerate() {
237                    match arg {
238                        FnArg::Receiver(..) => s.push_str("self"),
239                        FnArg::Typed(ty) => {
240                            s.push_str("_: ");
241                            s.push_str(&crate::type_to_string(*ty.ty.clone()).expect(
242                                "this should have failed earlier! please open a bug report!",
243                            ));
244                        }
245                    };
246                    if idx < arguments.len() - 1 {
247                        s.push_str(", ");
248                    }
249                }
250                s.push(')');
251                if let ::std::option::Option::Some(ret) = return_type {
252                    s.push_str(" -> ");
253                    s.push_str(
254                        &crate::type_to_string(ret.clone())
255                            .expect("this should have failed earlier! please open a bug report!"),
256                    );
257                }
258                s.push_str(r#" { todo!("not yet implemented") }"#);
259                s.push('\n');
260            }
261            s
262        };
263    let func_sigs = plugin_def.functions.iter().map(|f| {
264        let func_name = f.name.to_string();
265        let args = f.arguments.iter().map(|a| match a {
266            FnArg::Receiver(..) => "self".to_string(),
267            FnArg::Typed(ty) => crate::type_to_string(*ty.ty.clone())
268                .expect("this should have failed earlier! please open a bug report!"),
269        });
270        let return_typ = if let Some(ty) = f
271            .return_type
272            .as_ref()
273            .map(|ty| crate::type_to_string(ty.clone()))
274        {
275            quote!(::std::option::Option::Some(#ty))
276        } else {
277            quote!(::std::option::Option::None)
278        };
279        quote! {
280            (#func_name, &[#(#args),*], #return_typ)
281        }
282    });
283
284    quote! {
285        pub struct #plugin_ident {
286            library: ::dynamic_plugin::PluginDynamicLibrary,
287        }
288
289        impl #plugin_ident {
290            /// The signature of this plugin. This number is dependent
291            /// on the functions, their arguments and their return
292            /// types. Two plugins with the same signature are *likely*
293            /// to be compatible.
294            pub const PLUGIN_SIGNATURE: u64 = #hash;
295            /// The plugin definition is a string which defines an empty
296            /// Rust definition of the plugin. It is used to generate
297            /// useful error messages.
298            pub const PLUGIN_DEFINITION: &str = #definition;
299            /// The functions and their signatures. Each tuple holds
300            /// (function name, [arguments], maybe return type)
301            pub const PLUGIN_FUNCTIONS: &[(&'static str, &[&'static str], ::std::option::Option<&'static str>)] = &[
302                #(#func_sigs),*
303            ];
304        }
305
306        #host_impl
307    }
308    .into()
309}
310
311/// Write an implementation for a plugin. See the `dynamic_plugin` crate documentation for more.
312///
313/// ## `attempt to compute '0_usize - 1_usize', which would overflow`
314///
315/// If you come across this compile-time error, this indicates that the implementation you are writing does not match the expected implementation for the plugin definition. Please check that you:
316///
317/// - Are using the correct definition.
318/// - Have all the functions you need to meet the definition.
319/// - That all the functions are named correctly (identically to the definition).
320/// - That all the function arguments are the same order and types as the definition.
321/// - That all the function return types are the same as the definition.
322///
323/// ## Example
324///
325/// ```ignore
326/// plugin_impl! {
327///     ExamplePlugin,
328///
329///     fn do_a_thing() {
330///         println!("A thing has been done!");
331///     }
332///
333///     fn say_hello(name: *const c_char) -> bool {
334///         unsafe {
335///             let name = CStr::from_ptr(name);
336///             println!("Hello, {}!", name.to_string_lossy());
337///         }
338///         true
339///     }
340/// }
341/// ```
342#[proc_macro]
343#[cfg(feature = "client")]
344pub fn plugin_impl(tokens: TokenStream) -> TokenStream {
345    use implementation::PluginImplementation;
346
347    let plugin = parse_macro_input!(tokens as PluginImplementation);
348    let target_plugin = &plugin.target_plugin;
349    let functions = plugin.functions.iter().map(|maybe_unsafe_func| {
350        let unsafe_ = maybe_unsafe_func._unsafe;
351        let func = &maybe_unsafe_func.func;
352        quote! {
353            #[no_mangle]
354            pub #unsafe_ extern "C" #func
355        }
356    });
357    let mut hasher = PluginSignatureHasher::default();
358    plugin.hash(&mut hasher);
359    let hash = hasher.finish();
360
361    let hash_debug: Option<TokenStream2> = {
362        #[cfg(feature = "debug-hashes")]
363        {
364            let hash_debug = format!("{hasher:?}");
365            Some(quote! {
366                #[no_mangle]
367                pub fn _dynamic_plugin_signature_unhashed() -> &'static str {
368                    #hash_debug
369                }
370            })
371        }
372        #[cfg(not(feature = "debug-hashes"))]
373        {
374            None
375        }
376    };
377
378    quote! {
379        ::dynamic_plugin::static_assert!(
380            #target_plugin::PLUGIN_SIGNATURE == #hash,
381            ::dynamic_plugin::const_concat!(
382                "\nThe implementation does not match the definition:\n\n",
383                #target_plugin::PLUGIN_DEFINITION
384            )
385        );
386
387        #[no_mangle]
388        pub extern "C" fn _dynamic_plugin_signature() -> u64 {
389            #hash
390        }
391
392        #hash_debug
393
394        #(#functions)*
395    }
396    .into()
397}
398
399/// Convert a type to string, returning None if the macro would be
400/// failing elsewhere
401fn type_to_string(ty: Type) -> Option<String> {
402    match ty {
403        Type::Array(inner) => Some(format!("[{}]", type_to_string(*inner.elem)?)),
404        Type::BareFn(inner) => {
405            let mut s = String::new();
406            s.push_str(r#"unsafe extern "C" fn("#);
407            let has_inputs = !inner.inputs.is_empty();
408            for inp in inner.inputs {
409                s.push_str(&type_to_string(inp.ty)?);
410                s.push_str(", ");
411            }
412            if has_inputs {
413                // remove last ", "
414                s.pop();
415                s.pop();
416            }
417            s.push(')');
418            if inner.variadic.is_some() {
419                return None;
420            }
421            match inner.output {
422                ReturnType::Default => (),
423                ReturnType::Type(_, ty) => s.push_str(&format!("-> {}", type_to_string(*ty)?)),
424            }
425            Some(s)
426        }
427        Type::Group(inner) => type_to_string(*inner.elem),
428        Type::Paren(inner) => type_to_string(*inner.elem),
429        Type::Ptr(inner) => type_to_string(*inner.elem),
430        Type::Never(_) => Some("!".to_string()),
431        Type::Path(inner) => {
432            if inner.qself.is_some() {
433                return None;
434            }
435            // Hash only last segment
436            let last_segment = inner.path.segments.last().unwrap();
437            if !last_segment.arguments.is_none() {
438                return None;
439            }
440            Some(last_segment.ident.to_string())
441        }
442        Type::ImplTrait(_)
443        | Type::Infer(_)
444        | Type::Macro(_)
445        | Type::Reference(_)
446        | Type::Slice(_)
447        | Type::TraitObject(_)
448        | Type::Tuple(_)
449        | Type::Verbatim(_) => None,
450        _ => todo!("This type is not yet supported by dynamic-plugin"),
451    }
452}
453
454fn hash_type<H: Hasher>(hasher: &mut H, ty: Type) {
455    match ty {
456        Type::Array(inner) => {
457            "arr".hash(hasher);
458            hash_type(hasher, *inner.elem);
459        }
460        Type::BareFn(inner) => {
461            "fn".hash(hasher);
462            for inp in inner.inputs {
463                hash_type(hasher, inp.ty);
464            }
465            if inner.variadic.is_some() {
466                abort!(
467                    inner.variadic,
468                    "Bare functions with variadics are not supported in plugin interfaces"
469                );
470            }
471            "->".hash(hasher);
472            match inner.output {
473                ReturnType::Default => "()".hash(hasher),
474                ReturnType::Type(_, ty) => hash_type(hasher, *ty),
475            }
476            ";".hash(hasher);
477        }
478        Type::Group(inner) => hash_type(hasher, *inner.elem),
479        Type::ImplTrait(inner) => abort!(inner, "Traits are supported in plugin interfaces"),
480        Type::Infer(inner) => abort!(
481            inner,
482            "Compiler inference is supported in plugin interfaces"
483        ),
484        Type::Macro(inner) => abort!(inner, "Macros are not supported in plugin interfaces"),
485        Type::Never(_) => "never".hash(hasher),
486        Type::Paren(inner) => hash_type(hasher, *inner.elem),
487        Type::Path(inner) => {
488            if inner.qself.is_some() {
489                abort!(
490                    inner,
491                    "Qualified types are not supported in plugin interfaces"
492                );
493            }
494            // Hash only last segment
495            let last_segment = inner.path.segments.last().unwrap();
496            if !last_segment.arguments.is_none() {
497                abort!(
498                    last_segment.arguments,
499                    "Types cannot be generic or require lifetimes in plugin interfaces"
500                );
501            }
502            last_segment.ident.hash(hasher);
503        }
504        Type::Ptr(inner) => hash_type(hasher, *inner.elem),
505        Type::Reference(inner) => abort!(
506            inner,
507            "References are not supported in plugin interfaces (use raw pointers instead)"
508        ),
509        Type::Slice(inner) => abort!(
510            inner,
511            "Slices are not supported in plugin interfaces (use raw pointers instead)"
512        ),
513        Type::TraitObject(inner) => {
514            abort!(inner, "Trait objects not supported in plugin interfaces")
515        }
516        Type::Tuple(inner) => abort!(inner, "Tuples not supported in plugin interfaces"),
517        Type::Verbatim(inner) => abort!(inner, "This type is not supported in plugin interfaces"),
518        _ => todo!("This type is not yet supported by dynamic-plugin"),
519    }
520}