| // Copyright 2020 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
| // option. This file may not be copied, modified, or distributed |
| // except according to those terms. |
| |
| use indexmap::map::IndexMap as HashMap; |
| use indexmap::set::IndexSet as HashSet; |
| use std::borrow::Cow; |
| use std::collections::hash_map::DefaultHasher; |
| use std::hash::{Hash, Hasher}; |
| |
| use itertools::Itertools; |
| use proc_macro2::Span; |
| use quote::ToTokens; |
| |
| #[cfg(feature = "reproduction_case")] |
| use quote::format_ident; |
| use syn::{ |
| parse::{Parse, ParseStream}, |
| Signature, Token, TypePath, |
| }; |
| use syn::{Ident, Result as ParseResult}; |
| use thiserror::Error; |
| |
| use crate::{directives::get_directives, RustPath}; |
| |
| use quote::quote; |
| |
| #[derive(PartialEq, Clone, Debug, Hash)] |
| pub enum UnsafePolicy { |
| AllFunctionsSafe, |
| AllFunctionsUnsafe, |
| } |
| |
| impl Default for UnsafePolicy { |
| fn default() -> Self { |
| Self::AllFunctionsUnsafe |
| } |
| } |
| |
| impl Parse for UnsafePolicy { |
| fn parse(input: ParseStream) -> ParseResult<Self> { |
| if input.parse::<Option<Token![unsafe]>>()?.is_some() { |
| return Ok(UnsafePolicy::AllFunctionsSafe); |
| } |
| let r = match input.parse::<Option<syn::Ident>>()? { |
| Some(id) => { |
| if id == "unsafe_ffi" { |
| Ok(UnsafePolicy::AllFunctionsSafe) |
| } else { |
| Err(syn::Error::new(id.span(), "expected unsafe_ffi")) |
| } |
| } |
| None => Ok(UnsafePolicy::AllFunctionsUnsafe), |
| }; |
| if !input.is_empty() { |
| return Err(syn::Error::new( |
| Span::call_site(), |
| "unexpected tokens within safety directive", |
| )); |
| } |
| r |
| } |
| } |
| |
| impl ToTokens for UnsafePolicy { |
| fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { |
| if *self == UnsafePolicy::AllFunctionsSafe { |
| tokens.extend(quote! { unsafe }) |
| } |
| } |
| } |
| |
| /// An entry in the allowlist. |
| #[derive(Hash, Debug)] |
| pub enum AllowlistEntry { |
| Item(String), |
| Namespace(String), |
| } |
| |
| impl AllowlistEntry { |
| fn to_bindgen_item(&self) -> String { |
| match self { |
| AllowlistEntry::Item(i) => i.clone(), |
| AllowlistEntry::Namespace(ns) => format!("{}::.*", ns), |
| } |
| } |
| } |
| |
| /// Allowlist configuration. |
| #[derive(Hash, Debug)] |
| pub enum Allowlist { |
| Unspecified(Vec<AllowlistEntry>), |
| All, |
| Specific(Vec<AllowlistEntry>), |
| } |
| |
| /// Errors that may be encountered while adding allowlist entries. |
| #[derive(Error, Debug)] |
| pub enum AllowlistErr { |
| #[error("Conflict between generate/generate_ns! and generate_all! - use one not both")] |
| ConflictingGenerateAndGenerateAll, |
| } |
| |
| impl Allowlist { |
| pub fn push(&mut self, item: AllowlistEntry) -> Result<(), AllowlistErr> { |
| match self { |
| Allowlist::Unspecified(ref mut uncommitted_list) => { |
| let new_list = uncommitted_list |
| .drain(..) |
| .chain(std::iter::once(item)) |
| .collect(); |
| *self = Allowlist::Specific(new_list); |
| } |
| Allowlist::All => { |
| return Err(AllowlistErr::ConflictingGenerateAndGenerateAll); |
| } |
| Allowlist::Specific(list) => list.push(item), |
| }; |
| Ok(()) |
| } |
| |
| pub(crate) fn set_all(&mut self) -> Result<(), AllowlistErr> { |
| if matches!(self, Allowlist::Specific(..)) { |
| return Err(AllowlistErr::ConflictingGenerateAndGenerateAll); |
| } |
| *self = Allowlist::All; |
| Ok(()) |
| } |
| } |
| |
| #[allow(clippy::derivable_impls)] // nightly-only |
| impl Default for Allowlist { |
| fn default() -> Self { |
| Allowlist::Unspecified(Vec::new()) |
| } |
| } |
| |
| #[derive(Debug, Hash)] |
| pub struct Subclass { |
| pub superclass: String, |
| pub subclass: Ident, |
| } |
| |
| #[derive(Clone, Hash)] |
| pub struct RustFun { |
| pub path: RustPath, |
| pub sig: Signature, |
| pub receiver: Option<Ident>, |
| } |
| |
| impl std::fmt::Debug for RustFun { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("RustFun") |
| .field("path", &self.path) |
| .field("sig", &self.sig.to_token_stream().to_string()) |
| .finish() |
| } |
| } |
| |
| #[derive(Debug, Clone, Hash)] |
| pub struct ExternCppType { |
| pub rust_path: TypePath, |
| pub opaque: bool, |
| } |
| |
| /// Newtype wrapper so we can implement Hash. |
| #[derive(Debug, Default)] |
| pub struct ExternCppTypeMap(pub HashMap<String, ExternCppType>); |
| |
| impl std::hash::Hash for ExternCppTypeMap { |
| fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
| for (k, v) in &self.0 { |
| k.hash(state); |
| v.hash(state); |
| } |
| } |
| } |
| |
| /// Newtype wrapper so we can implement Hash. |
| #[derive(Debug, Default)] |
| pub struct ConcretesMap(pub HashMap<String, Ident>); |
| |
| impl std::hash::Hash for ConcretesMap { |
| fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
| for (k, v) in &self.0 { |
| k.hash(state); |
| v.hash(state); |
| } |
| } |
| } |
| |
| #[derive(Debug, Default, Hash)] |
| pub struct IncludeCppConfig { |
| pub inclusions: Vec<String>, |
| pub unsafe_policy: UnsafePolicy, |
| pub parse_only: bool, |
| pub exclude_impls: bool, |
| pub(crate) pod_requests: Vec<String>, |
| pub allowlist: Allowlist, |
| pub(crate) blocklist: Vec<String>, |
| pub(crate) constructor_blocklist: Vec<String>, |
| pub instantiable: Vec<String>, |
| pub(crate) exclude_utilities: bool, |
| pub(crate) mod_name: Option<Ident>, |
| pub rust_types: Vec<RustPath>, |
| pub subclasses: Vec<Subclass>, |
| pub extern_rust_funs: Vec<RustFun>, |
| pub concretes: ConcretesMap, |
| pub externs: ExternCppTypeMap, |
| } |
| |
| impl Parse for IncludeCppConfig { |
| fn parse(input: ParseStream) -> ParseResult<Self> { |
| let mut config = IncludeCppConfig::default(); |
| |
| while !input.is_empty() { |
| let has_hexathorpe = input.parse::<Option<syn::token::Pound>>()?.is_some(); |
| let ident: syn::Ident = input.parse()?; |
| let args; |
| let (possible_directives, to_parse, parse_completely) = if has_hexathorpe { |
| (&get_directives().need_hexathorpe, input, false) |
| } else { |
| input.parse::<Option<syn::token::Bang>>()?; |
| syn::parenthesized!(args in input); |
| (&get_directives().need_exclamation, &args, true) |
| }; |
| let all_possible = possible_directives.keys().join(", "); |
| let ident_str = ident.to_string(); |
| match possible_directives.get(&ident_str) { |
| None => { |
| return Err(syn::Error::new( |
| ident.span(), |
| format!("expected {}", all_possible), |
| )); |
| } |
| Some(directive) => directive.parse(to_parse, &mut config, &ident.span())?, |
| } |
| if parse_completely && !to_parse.is_empty() { |
| return Err(syn::Error::new( |
| ident.span(), |
| format!("found unexpected input within the directive {}", ident_str), |
| )); |
| } |
| if input.is_empty() { |
| break; |
| } |
| } |
| Ok(config) |
| } |
| } |
| |
| impl IncludeCppConfig { |
| pub fn get_pod_requests(&self) -> &[String] { |
| &self.pod_requests |
| } |
| |
| pub fn get_mod_name(&self) -> Ident { |
| self.mod_name |
| .as_ref() |
| .cloned() |
| .unwrap_or_else(|| Ident::new("ffi", Span::call_site())) |
| } |
| |
| /// Whether to avoid generating the standard helpful utility |
| /// functions which we normally include in every mod. |
| pub fn exclude_utilities(&self) -> bool { |
| self.exclude_utilities |
| } |
| |
| /// Items which the user has explicitly asked us to generate; |
| /// we should raise an error if we weren't able to do so. |
| pub fn must_generate_list(&self) -> Box<dyn Iterator<Item = String> + '_> { |
| if let Allowlist::Specific(items) = &self.allowlist { |
| Box::new( |
| items |
| .iter() |
| .filter_map(|i| match i { |
| AllowlistEntry::Item(i) => Some(i), |
| AllowlistEntry::Namespace(_) => None, |
| }) |
| .chain(self.pod_requests.iter()) |
| .cloned(), |
| ) |
| } else { |
| Box::new(self.pod_requests.iter().cloned()) |
| } |
| } |
| |
| /// The allowlist of items to be passed into bindgen, if any. |
| pub fn bindgen_allowlist(&self) -> Option<Box<dyn Iterator<Item = String> + '_>> { |
| match &self.allowlist { |
| Allowlist::All => None, |
| Allowlist::Specific(items) => Some(Box::new( |
| items |
| .iter() |
| .map(AllowlistEntry::to_bindgen_item) |
| .chain(self.pod_requests.iter().cloned()) |
| .chain(self.active_utilities()) |
| .chain(self.subclasses.iter().flat_map(|sc| { |
| [ |
| format!("{}Cpp", sc.subclass), |
| sc.subclass.to_string(), // TODO may not be necessary |
| sc.superclass.clone(), |
| ] |
| })), |
| )), |
| Allowlist::Unspecified(_) => unreachable!(), |
| } |
| } |
| |
| fn active_utilities(&self) -> Vec<String> { |
| if self.exclude_utilities { |
| Vec::new() |
| } else { |
| vec![self.get_makestring_name()] |
| } |
| } |
| |
| fn is_subclass_or_superclass(&self, cpp_name: &str) -> bool { |
| self.subclasses |
| .iter() |
| .flat_map(|sc| { |
| [ |
| Cow::Owned(sc.subclass.to_string()), |
| Cow::Borrowed(&sc.superclass), |
| ] |
| }) |
| .any(|item| cpp_name == item.as_str()) |
| } |
| |
| /// Whether this type is on the allowlist specified by the user. |
| /// |
| /// A note on the allowlist handling in general. It's used in two places: |
| /// 1) As directives to bindgen |
| /// 2) After bindgen has generated code, to filter the APIs which |
| /// we pass to cxx. |
| /// This second pass may seem redundant. But sometimes bindgen generates |
| /// unnecessary stuff. |
| pub fn is_on_allowlist(&self, cpp_name: &str) -> bool { |
| self.active_utilities().iter().any(|item| *item == cpp_name) |
| || self.is_subclass_or_superclass(cpp_name) |
| || self.is_subclass_holder(cpp_name) |
| || self.is_subclass_cpp(cpp_name) |
| || self.is_rust_fun(cpp_name) |
| || self.is_concrete_type(cpp_name) |
| || match &self.allowlist { |
| Allowlist::Unspecified(_) => panic!("Eek no allowlist yet"), |
| Allowlist::All => true, |
| Allowlist::Specific(items) => items.iter().any(|entry| match entry { |
| AllowlistEntry::Item(i) => i == cpp_name, |
| AllowlistEntry::Namespace(ns) => cpp_name.starts_with(ns), |
| }), |
| } |
| } |
| |
| pub fn is_on_blocklist(&self, cpp_name: &str) -> bool { |
| self.blocklist.contains(&cpp_name.to_string()) |
| } |
| |
| pub fn is_on_constructor_blocklist(&self, cpp_name: &str) -> bool { |
| self.constructor_blocklist.contains(&cpp_name.to_string()) |
| } |
| |
| pub fn get_blocklist(&self) -> impl Iterator<Item = &String> { |
| self.blocklist.iter() |
| } |
| |
| fn is_concrete_type(&self, cpp_name: &str) -> bool { |
| self.concretes.0.values().any(|val| *val == cpp_name) |
| } |
| |
| /// Get a hash of the contents of this `include_cpp!` block. |
| pub fn get_hash(&self) -> u64 { |
| let mut s = DefaultHasher::new(); |
| self.hash(&mut s); |
| s.finish() |
| } |
| |
| /// In case there are multiple sets of ffi mods in a single binary, |
| /// endeavor to return a name which can be used to make symbols |
| /// unique. |
| pub fn uniquify_name_per_mod(&self, name: &str) -> String { |
| format!("{}_{:#x}", name, self.get_hash()) |
| } |
| |
| pub fn get_makestring_name(&self) -> String { |
| self.uniquify_name_per_mod("autocxx_make_string") |
| } |
| |
| pub fn is_rust_type(&self, id: &Ident) -> bool { |
| self.rust_types |
| .iter() |
| .any(|rt| rt.get_final_ident() == &id.to_string()) |
| || self.is_subclass_holder(&id.to_string()) |
| } |
| |
| fn is_rust_fun(&self, possible_fun: &str) -> bool { |
| self.extern_rust_funs |
| .iter() |
| .map(|fun| &fun.sig.ident) |
| .any(|id| id == possible_fun) |
| } |
| |
| pub fn superclasses(&self) -> impl Iterator<Item = &String> { |
| let mut uniquified = HashSet::new(); |
| uniquified.extend(self.subclasses.iter().map(|sc| &sc.superclass)); |
| uniquified.into_iter() |
| } |
| |
| pub fn is_subclass_holder(&self, id: &str) -> bool { |
| self.subclasses |
| .iter() |
| .any(|sc| format!("{}Holder", sc.subclass) == id) |
| } |
| |
| fn is_subclass_cpp(&self, id: &str) -> bool { |
| self.subclasses |
| .iter() |
| .any(|sc| format!("{}Cpp", sc.subclass) == id) |
| } |
| |
| /// Return the filename to which generated .rs should be written. |
| pub fn get_rs_filename(&self) -> String { |
| format!( |
| "autocxx-{}-gen.rs", |
| self.mod_name |
| .as_ref() |
| .map(|id| id.to_string()) |
| .unwrap_or_else(|| "ffi-default".into()) |
| ) |
| } |
| |
| pub fn confirm_complete(&mut self) { |
| if matches!(self.allowlist, Allowlist::Unspecified(_)) { |
| self.allowlist = Allowlist::Specific(Vec::new()); |
| } |
| } |
| |
| /// Used in reduction to substitute all included headers with a single |
| /// preprocessed replacement. |
| pub fn replace_included_headers(&mut self, replacement: &str) { |
| self.inclusions.clear(); |
| self.inclusions.push(replacement.to_string()); |
| } |
| } |
| |
| #[cfg(feature = "reproduction_case")] |
| impl ToTokens for IncludeCppConfig { |
| fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { |
| let directives = get_directives(); |
| let hexathorpe = syn::token::Pound(Span::call_site()); |
| for (id, directive) in &directives.need_hexathorpe { |
| let id = format_ident!("{}", id); |
| for output in directive.output(self) { |
| tokens.extend(quote! { |
| #hexathorpe #id #output |
| }) |
| } |
| } |
| for (id, directive) in &directives.need_exclamation { |
| let id = format_ident!("{}", id); |
| for output in directive.output(self) { |
| tokens.extend(quote! { |
| #id ! (#output) |
| }) |
| } |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod parse_tests { |
| use crate::config::UnsafePolicy; |
| use syn::parse_quote; |
| #[test] |
| fn test_safety_unsafe() { |
| let us: UnsafePolicy = parse_quote! { |
| unsafe |
| }; |
| assert_eq!(us, UnsafePolicy::AllFunctionsSafe) |
| } |
| |
| #[test] |
| fn test_safety_unsafe_ffi() { |
| let us: UnsafePolicy = parse_quote! { |
| unsafe_ffi |
| }; |
| assert_eq!(us, UnsafePolicy::AllFunctionsSafe) |
| } |
| |
| #[test] |
| fn test_safety_safe() { |
| let us: UnsafePolicy = parse_quote! {}; |
| assert_eq!(us, UnsafePolicy::AllFunctionsUnsafe) |
| } |
| } |