autocxx_parser/
config.rs

1// Copyright 2020 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use indexmap::map::IndexMap as HashMap;
10use indexmap::set::IndexSet as HashSet;
11use std::borrow::Cow;
12use std::collections::hash_map::DefaultHasher;
13use std::hash::{Hash, Hasher};
14
15use itertools::Itertools;
16use proc_macro2::Span;
17use quote::ToTokens;
18
19#[cfg(feature = "reproduction_case")]
20use quote::format_ident;
21use syn::{
22    parse::{Parse, ParseStream},
23    Signature, Token, TypePath,
24};
25use syn::{Ident, Result as ParseResult};
26use thiserror::Error;
27
28use crate::{directives::get_directives, RustPath};
29
30use quote::quote;
31
32#[derive(PartialEq, Eq, Clone, Debug, Hash)]
33pub enum UnsafePolicy {
34    AllFunctionsSafe,
35    AllFunctionsUnsafe,
36    ReferencesWrappedAllFunctionsSafe,
37}
38
39impl Default for UnsafePolicy {
40    fn default() -> Self {
41        Self::AllFunctionsUnsafe
42    }
43}
44
45impl Parse for UnsafePolicy {
46    fn parse(input: ParseStream) -> ParseResult<Self> {
47        if input.parse::<Option<Token![unsafe]>>()?.is_some() {
48            return Ok(UnsafePolicy::AllFunctionsSafe);
49        }
50        let r = match input.parse::<Option<syn::Ident>>()? {
51            Some(id) => {
52                if id == "unsafe_ffi" {
53                    Ok(UnsafePolicy::AllFunctionsSafe)
54                } else if id == "unsafe_references_wrapped" {
55                    Ok(UnsafePolicy::ReferencesWrappedAllFunctionsSafe)
56                } else {
57                    Err(syn::Error::new(
58                        id.span(),
59                        "expected unsafe_ffi or unsafe_references_wrapped",
60                    ))
61                }
62            }
63            None => Ok(UnsafePolicy::AllFunctionsUnsafe),
64        };
65        if !input.is_empty() {
66            return Err(syn::Error::new(
67                Span::call_site(),
68                "unexpected tokens within safety directive",
69            ));
70        }
71        r
72    }
73}
74
75impl ToTokens for UnsafePolicy {
76    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
77        if *self == UnsafePolicy::AllFunctionsSafe {
78            tokens.extend(quote! { unsafe })
79        } else if *self == UnsafePolicy::ReferencesWrappedAllFunctionsSafe {
80            tokens.extend(quote! { unsafe_references_wrapped })
81        }
82    }
83}
84
85impl UnsafePolicy {
86    /// Whether we are treating C++ references as a different thing from Rust
87    /// references and therefore have to generate lots of code for a CppRef type
88    pub fn requires_cpprefs(&self) -> bool {
89        matches!(self, Self::ReferencesWrappedAllFunctionsSafe)
90    }
91}
92
93/// An entry in the allowlist.
94#[derive(Hash, Debug)]
95pub enum AllowlistEntry {
96    Item(String),
97    Namespace(String),
98}
99
100impl AllowlistEntry {
101    fn to_bindgen_item(&self) -> String {
102        match self {
103            AllowlistEntry::Item(i) => i.clone(),
104            AllowlistEntry::Namespace(ns) => format!("{ns}::.*"),
105        }
106    }
107}
108
109/// Allowlist configuration.
110#[derive(Hash, Debug)]
111pub enum Allowlist {
112    Unspecified(Vec<AllowlistEntry>),
113    All,
114    Specific(Vec<AllowlistEntry>),
115}
116
117/// Errors that may be encountered while adding allowlist entries.
118#[derive(Error, Debug)]
119pub enum AllowlistErr {
120    #[error("Conflict between generate/generate_ns! and generate_all! - use one not both")]
121    ConflictingGenerateAndGenerateAll,
122}
123
124impl Allowlist {
125    pub fn push(&mut self, item: AllowlistEntry) -> Result<(), AllowlistErr> {
126        match self {
127            Allowlist::Unspecified(ref mut uncommitted_list) => {
128                let new_list = uncommitted_list
129                    .drain(..)
130                    .chain(std::iter::once(item))
131                    .collect();
132                *self = Allowlist::Specific(new_list);
133            }
134            Allowlist::All => {
135                return Err(AllowlistErr::ConflictingGenerateAndGenerateAll);
136            }
137            Allowlist::Specific(list) => list.push(item),
138        };
139        Ok(())
140    }
141
142    pub(crate) fn set_all(&mut self) -> Result<(), AllowlistErr> {
143        if matches!(self, Allowlist::Specific(..)) {
144            return Err(AllowlistErr::ConflictingGenerateAndGenerateAll);
145        }
146        *self = Allowlist::All;
147        Ok(())
148    }
149}
150
151#[allow(clippy::derivable_impls)] // nightly-only
152impl Default for Allowlist {
153    fn default() -> Self {
154        Allowlist::Unspecified(Vec::new())
155    }
156}
157
158#[derive(Debug, Hash)]
159pub struct Subclass {
160    pub superclass: String,
161    pub subclass: Ident,
162}
163
164#[derive(Clone, Hash)]
165pub struct RustFun {
166    pub path: RustPath,
167    pub sig: Signature,
168    pub has_receiver: bool,
169}
170
171impl std::fmt::Debug for RustFun {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct("RustFun")
174            .field("path", &self.path)
175            .field("sig", &self.sig.to_token_stream().to_string())
176            .finish()
177    }
178}
179
180#[derive(Debug, Clone, Hash)]
181pub struct ExternCppType {
182    pub rust_path: TypePath,
183    pub opaque: bool,
184}
185
186/// Newtype wrapper so we can implement Hash.
187#[derive(Debug, Default)]
188pub struct ExternCppTypeMap(pub HashMap<String, ExternCppType>);
189
190impl std::hash::Hash for ExternCppTypeMap {
191    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
192        for (k, v) in &self.0 {
193            k.hash(state);
194            v.hash(state);
195        }
196    }
197}
198
199/// Newtype wrapper so we can implement Hash.
200#[derive(Debug, Default)]
201pub struct ConcretesMap(pub HashMap<String, Ident>);
202
203impl std::hash::Hash for ConcretesMap {
204    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
205        for (k, v) in &self.0 {
206            k.hash(state);
207            v.hash(state);
208        }
209    }
210}
211
212#[derive(Debug, Default, Hash)]
213pub struct IncludeCppConfig {
214    pub inclusions: Vec<String>,
215    pub unsafe_policy: UnsafePolicy,
216    pub parse_only: bool,
217    pub exclude_impls: bool,
218    pub(crate) pod_requests: Vec<String>,
219    pub allowlist: Allowlist,
220    pub(crate) blocklist: Vec<String>,
221    pub(crate) constructor_blocklist: Vec<String>,
222    pub instantiable: Vec<String>,
223    pub(crate) exclude_utilities: bool,
224    pub(crate) mod_name: Option<Ident>,
225    pub rust_types: Vec<RustPath>,
226    pub subclasses: Vec<Subclass>,
227    pub extern_rust_funs: Vec<RustFun>,
228    pub concretes: ConcretesMap,
229    pub externs: ExternCppTypeMap,
230    pub opaquelist: Vec<String>,
231}
232
233impl Parse for IncludeCppConfig {
234    fn parse(input: ParseStream) -> ParseResult<Self> {
235        let mut config = IncludeCppConfig::default();
236
237        while !input.is_empty() {
238            let has_hexathorpe = input.parse::<Option<syn::token::Pound>>()?.is_some();
239            let ident: syn::Ident = input.parse()?;
240            let args;
241            let (possible_directives, to_parse, parse_completely) = if has_hexathorpe {
242                (&get_directives().need_hexathorpe, input, false)
243            } else {
244                input.parse::<Option<syn::token::Not>>()?;
245                syn::parenthesized!(args in input);
246                (&get_directives().need_exclamation, &args, true)
247            };
248            let all_possible = possible_directives.keys().join(", ");
249            let ident_str = ident.to_string();
250            match possible_directives.get(&ident_str) {
251                None => {
252                    return Err(syn::Error::new(
253                        ident.span(),
254                        format!("expected {all_possible}"),
255                    ));
256                }
257                Some(directive) => directive.parse(to_parse, &mut config, &ident.span())?,
258            }
259            if parse_completely && !to_parse.is_empty() {
260                return Err(syn::Error::new(
261                    ident.span(),
262                    format!("found unexpected input within the directive {ident_str}"),
263                ));
264            }
265            if input.is_empty() {
266                break;
267            }
268        }
269        Ok(config)
270    }
271}
272
273impl IncludeCppConfig {
274    pub fn get_pod_requests(&self) -> &[String] {
275        &self.pod_requests
276    }
277
278    pub fn get_mod_name(&self) -> Ident {
279        self.mod_name
280            .as_ref()
281            .cloned()
282            .unwrap_or_else(|| Ident::new("ffi", Span::call_site()))
283    }
284
285    /// Whether to avoid generating the standard helpful utility
286    /// functions which we normally include in every mod.
287    pub fn exclude_utilities(&self) -> bool {
288        self.exclude_utilities
289    }
290
291    /// Items which the user has explicitly asked us to generate;
292    /// we should raise an error if we weren't able to do so.
293    pub fn must_generate_list(&self) -> Box<dyn Iterator<Item = String> + '_> {
294        if let Allowlist::Specific(items) = &self.allowlist {
295            Box::new(
296                items
297                    .iter()
298                    .filter_map(|i| match i {
299                        AllowlistEntry::Item(i) => Some(i),
300                        AllowlistEntry::Namespace(_) => None,
301                    })
302                    .chain(self.pod_requests.iter())
303                    .cloned(),
304            )
305        } else {
306            Box::new(self.pod_requests.iter().cloned())
307        }
308    }
309
310    /// The allowlist of items to be passed into bindgen, if any.
311    pub fn bindgen_allowlist(&self) -> Option<Box<dyn Iterator<Item = String> + '_>> {
312        match &self.allowlist {
313            Allowlist::All => None,
314            Allowlist::Specific(items) => Some(Box::new(
315                items
316                    .iter()
317                    .map(AllowlistEntry::to_bindgen_item)
318                    .chain(self.pod_requests.iter().cloned())
319                    .chain(self.active_utilities())
320                    .chain(self.subclasses.iter().flat_map(|sc| {
321                        [
322                            format!("{}Cpp", sc.subclass),
323                            sc.subclass.to_string(), // TODO may not be necessary
324                            sc.superclass.clone(),
325                        ]
326                    })),
327            )),
328            Allowlist::Unspecified(_) => unreachable!(),
329        }
330    }
331
332    fn active_utilities(&self) -> Vec<String> {
333        if self.exclude_utilities {
334            Vec::new()
335        } else {
336            vec![self.get_makestring_name()]
337        }
338    }
339
340    fn is_subclass_or_superclass(&self, cpp_name: &str) -> bool {
341        self.subclasses
342            .iter()
343            .flat_map(|sc| {
344                [
345                    Cow::Owned(sc.subclass.to_string()),
346                    Cow::Borrowed(&sc.superclass),
347                ]
348            })
349            .any(|item| cpp_name == item.as_str())
350    }
351
352    /// Whether this type is on the allowlist specified by the user.
353    ///
354    /// A note on the allowlist handling in general. It's used in two places:
355    /// 1) As directives to bindgen
356    /// 2) After bindgen has generated code, to filter the APIs which
357    ///    we pass to cxx.
358    ///
359    /// This second pass may seem redundant. But sometimes bindgen generates
360    /// unnecessary stuff.
361    pub fn is_on_allowlist(&self, cpp_name: &str) -> bool {
362        self.active_utilities().iter().any(|item| *item == cpp_name)
363            || self.is_subclass_or_superclass(cpp_name)
364            || self.is_subclass_holder(cpp_name)
365            || self.is_subclass_cpp(cpp_name)
366            || self.is_rust_fun(cpp_name)
367            || self.is_rust_type_name(cpp_name)
368            || self.is_concrete_type(cpp_name)
369            || match &self.allowlist {
370                Allowlist::Unspecified(_) => panic!("Eek no allowlist yet"),
371                Allowlist::All => true,
372                Allowlist::Specific(items) => items.iter().any(|entry| match entry {
373                    AllowlistEntry::Item(i) => i == cpp_name,
374                    AllowlistEntry::Namespace(ns) => cpp_name.starts_with(ns),
375                }),
376            }
377    }
378
379    pub fn is_on_blocklist(&self, cpp_name: &str) -> bool {
380        self.blocklist.contains(&cpp_name.to_string())
381    }
382
383    pub fn is_on_constructor_blocklist(&self, cpp_name: &str) -> bool {
384        self.constructor_blocklist.contains(&cpp_name.to_string())
385    }
386
387    pub fn get_blocklist(&self) -> impl Iterator<Item = &String> {
388        self.blocklist.iter()
389    }
390
391    pub fn get_opaquelist(&self) -> impl Iterator<Item = &String> {
392        self.opaquelist.iter()
393    }
394
395    fn is_concrete_type(&self, cpp_name: &str) -> bool {
396        self.concretes.0.values().any(|val| *val == cpp_name)
397    }
398
399    /// Get a hash of the contents of this `include_cpp!` block.
400    pub fn get_hash(&self) -> u64 {
401        let mut s = DefaultHasher::new();
402        self.hash(&mut s);
403        s.finish()
404    }
405
406    /// In case there are multiple sets of ffi mods in a single binary,
407    /// endeavor to return a name which can be used to make symbols
408    /// unique.
409    pub fn uniquify_name_per_mod(&self, name: &str) -> String {
410        format!("{}_{:#x}", name, self.get_hash())
411    }
412
413    pub fn get_makestring_name(&self) -> String {
414        self.uniquify_name_per_mod("autocxx_make_string")
415    }
416
417    pub fn is_rust_type(&self, id: &Ident) -> bool {
418        let id_string = id.to_string();
419        self.is_rust_type_name(&id_string) || self.is_subclass_holder(&id_string)
420    }
421
422    fn is_rust_type_name(&self, possible_ty: &str) -> bool {
423        self.rust_types
424            .iter()
425            .any(|rt| rt.get_final_ident() == possible_ty)
426    }
427
428    fn is_rust_fun(&self, possible_fun: &str) -> bool {
429        self.extern_rust_funs
430            .iter()
431            .map(|fun| &fun.sig.ident)
432            .any(|id| id == possible_fun)
433    }
434
435    pub fn superclasses(&self) -> impl Iterator<Item = &String> {
436        let mut uniquified = HashSet::new();
437        uniquified.extend(self.subclasses.iter().map(|sc| &sc.superclass));
438        uniquified.into_iter()
439    }
440
441    pub fn is_subclass_holder(&self, id: &str) -> bool {
442        self.subclasses
443            .iter()
444            .any(|sc| format!("{}Holder", sc.subclass) == id)
445    }
446
447    fn is_subclass_cpp(&self, id: &str) -> bool {
448        self.subclasses
449            .iter()
450            .any(|sc| format!("{}Cpp", sc.subclass) == id)
451    }
452
453    /// Return the filename to which generated .rs should be written.
454    pub fn get_rs_filename(&self) -> String {
455        format!(
456            "autocxx-{}-gen.rs",
457            self.mod_name
458                .as_ref()
459                .map(|id| id.to_string())
460                .unwrap_or_else(|| "ffi-default".into())
461        )
462    }
463
464    pub fn confirm_complete(&mut self) {
465        if matches!(self.allowlist, Allowlist::Unspecified(_)) {
466            self.allowlist = Allowlist::Specific(Vec::new());
467        }
468    }
469
470    /// Used in reduction to substitute all included headers with a single
471    /// preprocessed replacement.
472    pub fn replace_included_headers(&mut self, replacement: &str) {
473        self.inclusions.clear();
474        self.inclusions.push(replacement.to_string());
475    }
476}
477
478#[cfg(feature = "reproduction_case")]
479impl ToTokens for IncludeCppConfig {
480    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
481        let directives = get_directives();
482        let hexathorpe = syn::token::Pound(Span::call_site());
483        for (id, directive) in &directives.need_hexathorpe {
484            let id = format_ident!("{}", id);
485            for output in directive.output(self) {
486                tokens.extend(quote! {
487                    #hexathorpe #id #output
488                })
489            }
490        }
491        for (id, directive) in &directives.need_exclamation {
492            let id = format_ident!("{}", id);
493            for output in directive.output(self) {
494                tokens.extend(quote! {
495                    #id ! (#output)
496                })
497            }
498        }
499    }
500}
501
502#[cfg(test)]
503mod parse_tests {
504    use crate::config::UnsafePolicy;
505    use syn::parse_quote;
506    #[test]
507    fn test_safety_unsafe() {
508        let us: UnsafePolicy = parse_quote! {
509            unsafe
510        };
511        assert_eq!(us, UnsafePolicy::AllFunctionsSafe)
512    }
513
514    #[test]
515    fn test_safety_unsafe_ffi() {
516        let us: UnsafePolicy = parse_quote! {
517            unsafe_ffi
518        };
519        assert_eq!(us, UnsafePolicy::AllFunctionsSafe)
520    }
521
522    #[test]
523    fn test_safety_safe() {
524        let us: UnsafePolicy = parse_quote! {};
525        assert_eq!(us, UnsafePolicy::AllFunctionsUnsafe)
526    }
527}