Squashed 'third_party/autocxx/' content from commit 629e8fa53
git-subtree-dir: third_party/autocxx
git-subtree-split: 629e8fa531a633164c0b52e2a3cab536d4cd0849
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
Change-Id: I62a03b0049f49adf029e0204639cdb5468dde1a1
diff --git a/parser/Cargo.toml b/parser/Cargo.toml
new file mode 100644
index 0000000..481f21d
--- /dev/null
+++ b/parser/Cargo.toml
@@ -0,0 +1,36 @@
+# 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.
+
+[package]
+name = "autocxx-parser"
+version = "0.22.0"
+authors = ["Adrian Taylor <adetaylor@chromium.org>"]
+license = "MIT OR Apache-2.0"
+description = "Safe autogenerated interop between Rust and C++"
+repository = "https://github.com/google/autocxx"
+edition = "2021"
+keywords = ["ffi"]
+categories = ["development-tools::ffi", "api-bindings"]
+
+[dependencies]
+log = "0.4"
+proc-macro2 = "1.0"
+quote = "1.0"
+serde = { version = "1.0", features = [ "derive" ]}
+thiserror = "1.0"
+once_cell = "1.10"
+itertools = "0.10.3"
+indexmap = { version="1.8", features = ["serde"]}
+serde_json = "1.0"
+
+[dependencies.syn]
+version = "1.0.39"
+features = [ "full", "extra-traits" ]
+
+[features]
+reproduction_case = []
diff --git a/parser/README.md b/parser/README.md
new file mode 100644
index 0000000..9b91d4a
--- /dev/null
+++ b/parser/README.md
@@ -0,0 +1 @@
+This crate is a [component of autocxx](https://google.github.io/autocxx/).
diff --git a/parser/src/config.rs b/parser/src/config.rs
new file mode 100644
index 0000000..4204acf
--- /dev/null
+++ b/parser/src/config.rs
@@ -0,0 +1,500 @@
+// 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)
+ }
+}
diff --git a/parser/src/directives.rs b/parser/src/directives.rs
new file mode 100644
index 0000000..160829e
--- /dev/null
+++ b/parser/src/directives.rs
@@ -0,0 +1,540 @@
+// Copyright 2022 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 once_cell::sync::OnceCell;
+use proc_macro2::Span;
+use proc_macro2::{Ident, TokenStream};
+use quote::{quote, ToTokens};
+use syn::parse::ParseStream;
+
+use crate::config::{Allowlist, AllowlistErr};
+use crate::directive_names::{EXTERN_RUST_FUN, EXTERN_RUST_TYPE, SUBCLASS};
+use crate::{AllowlistEntry, IncludeCppConfig};
+use crate::{ParseResult, RustFun, RustPath};
+
+pub(crate) struct DirectivesMap {
+ pub(crate) need_hexathorpe: HashMap<String, Box<dyn Directive>>,
+ pub(crate) need_exclamation: HashMap<String, Box<dyn Directive>>,
+}
+
+static DIRECTIVES: OnceCell<DirectivesMap> = OnceCell::new();
+
+pub(crate) fn get_directives() -> &'static DirectivesMap {
+ DIRECTIVES.get_or_init(|| {
+ let mut need_hexathorpe: HashMap<String, Box<dyn Directive>> = HashMap::new();
+ need_hexathorpe.insert("include".into(), Box::new(Inclusion));
+ let mut need_exclamation: HashMap<String, Box<dyn Directive>> = HashMap::new();
+ need_exclamation.insert("generate".into(), Box::new(Generate(false)));
+ need_exclamation.insert("generate_pod".into(), Box::new(Generate(true)));
+ need_exclamation.insert("generate_ns".into(), Box::new(GenerateNs));
+ need_exclamation.insert("generate_all".into(), Box::new(GenerateAll));
+ need_exclamation.insert("safety".into(), Box::new(Safety));
+ need_exclamation.insert(
+ "pod".into(),
+ Box::new(StringList(
+ |config| &mut config.pod_requests,
+ |config| &config.pod_requests,
+ )),
+ );
+ need_exclamation.insert(
+ "block".into(),
+ Box::new(StringList(
+ |config| &mut config.blocklist,
+ |config| &config.blocklist,
+ )),
+ );
+ need_exclamation.insert(
+ "block_constructors".into(),
+ Box::new(StringList(
+ |config| &mut config.constructor_blocklist,
+ |config| &config.constructor_blocklist,
+ )),
+ );
+ need_exclamation.insert(
+ "instantiable".into(),
+ Box::new(StringList(
+ |config| &mut config.instantiable,
+ |config| &config.instantiable,
+ )),
+ );
+ need_exclamation.insert(
+ "parse_only".into(),
+ Box::new(BoolFlag(
+ |config| &mut config.parse_only,
+ |config| &config.parse_only,
+ )),
+ );
+ need_exclamation.insert(
+ "exclude_impls".into(),
+ Box::new(BoolFlag(
+ |config| &mut config.exclude_impls,
+ |config| &config.exclude_impls,
+ )),
+ );
+ need_exclamation.insert(
+ "exclude_utilities".into(),
+ Box::new(BoolFlag(
+ |config| &mut config.exclude_utilities,
+ |config| &config.exclude_utilities,
+ )),
+ );
+ need_exclamation.insert("name".into(), Box::new(ModName));
+ need_exclamation.insert("concrete".into(), Box::new(Concrete));
+ need_exclamation.insert("rust_type".into(), Box::new(RustType { output: false }));
+ need_exclamation.insert(EXTERN_RUST_TYPE.into(), Box::new(RustType { output: true }));
+ need_exclamation.insert(SUBCLASS.into(), Box::new(Subclass));
+ need_exclamation.insert(EXTERN_RUST_FUN.into(), Box::new(ExternRustFun));
+ need_exclamation.insert(
+ "extern_cpp_type".into(),
+ Box::new(ExternCppType { opaque: false }),
+ );
+ need_exclamation.insert(
+ "extern_cpp_opaque_type".into(),
+ Box::new(ExternCppType { opaque: true }),
+ );
+
+ DirectivesMap {
+ need_hexathorpe,
+ need_exclamation,
+ }
+ })
+}
+
+/// Trait for handling an `include_cpp!` configuration directive.
+pub(crate) trait Directive: Send + Sync {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ span: &Span,
+ ) -> ParseResult<()>;
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a>;
+}
+
+struct Inclusion;
+
+impl Directive for Inclusion {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _span: &Span,
+ ) -> ParseResult<()> {
+ let hdr: syn::LitStr = args.parse()?;
+ config.inclusions.push(hdr.value());
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ Box::new(config.inclusions.iter().map(|val| quote! { #val }))
+ }
+}
+
+/// Directive for either `generate!` (false) or `generate_pod!` (true).
+struct Generate(bool);
+
+impl Directive for Generate {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ span: &Span,
+ ) -> ParseResult<()> {
+ let generate: syn::LitStr = args.parse()?;
+ config
+ .allowlist
+ .push(AllowlistEntry::Item(generate.value()))
+ .map_err(|e| allowlist_err_to_syn_err(e, span))?;
+ if self.0 {
+ config.pod_requests.push(generate.value());
+ }
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ match &config.allowlist {
+ Allowlist::Specific(items) if !self.0 => Box::new(
+ items
+ .iter()
+ .flat_map(|i| match i {
+ AllowlistEntry::Item(s) => Some(s),
+ _ => None,
+ })
+ .map(|s| quote! { #s }),
+ ),
+ Allowlist::Unspecified(_) => panic!("Allowlist mode not yet determined"),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+}
+
+struct GenerateNs;
+
+impl Directive for GenerateNs {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ span: &Span,
+ ) -> ParseResult<()> {
+ let generate: syn::LitStr = args.parse()?;
+ config
+ .allowlist
+ .push(AllowlistEntry::Namespace(generate.value()))
+ .map_err(|e| allowlist_err_to_syn_err(e, span))?;
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ match &config.allowlist {
+ Allowlist::Specific(items) => Box::new(
+ items
+ .iter()
+ .flat_map(|i| match i {
+ AllowlistEntry::Namespace(s) => Some(s),
+ _ => None,
+ })
+ .map(|s| quote! { #s }),
+ ),
+ Allowlist::Unspecified(_) => panic!("Allowlist mode not yet determined"),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+}
+
+struct GenerateAll;
+
+impl Directive for GenerateAll {
+ fn parse(
+ &self,
+ _args: ParseStream,
+ config: &mut IncludeCppConfig,
+ span: &Span,
+ ) -> ParseResult<()> {
+ config
+ .allowlist
+ .set_all()
+ .map_err(|e| allowlist_err_to_syn_err(e, span))?;
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ match &config.allowlist {
+ Allowlist::All => Box::new(std::iter::once(TokenStream::new())),
+ Allowlist::Unspecified(_) => panic!("Allowlist mode not yet determined"),
+ _ => Box::new(std::iter::empty()),
+ }
+ }
+}
+
+struct Safety;
+
+impl Directive for Safety {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ config.unsafe_policy = args.parse()?;
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ let policy = &config.unsafe_policy;
+ match config.unsafe_policy {
+ crate::UnsafePolicy::AllFunctionsSafe => {
+ Box::new(std::iter::once(policy.to_token_stream()))
+ }
+ crate::UnsafePolicy::AllFunctionsUnsafe => Box::new(std::iter::empty()),
+ }
+ }
+}
+
+fn allowlist_err_to_syn_err(err: AllowlistErr, span: &Span) -> syn::Error {
+ syn::Error::new(*span, format!("{}", err))
+}
+
+struct StringList<SET, GET>(SET, GET)
+where
+ SET: Fn(&mut IncludeCppConfig) -> &mut Vec<String>,
+ GET: Fn(&IncludeCppConfig) -> &Vec<String>;
+
+impl<SET, GET> Directive for StringList<SET, GET>
+where
+ SET: Fn(&mut IncludeCppConfig) -> &mut Vec<String> + Sync + Send,
+ GET: Fn(&IncludeCppConfig) -> &Vec<String> + Sync + Send,
+{
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let val: syn::LitStr = args.parse()?;
+ self.0(config).push(val.value());
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ Box::new(self.1(config).iter().map(|val| {
+ quote! {
+ #val
+ }
+ }))
+ }
+}
+
+struct BoolFlag<SET, GET>(SET, GET)
+where
+ SET: Fn(&mut IncludeCppConfig) -> &mut bool,
+ GET: Fn(&IncludeCppConfig) -> &bool;
+
+impl<SET, GET> Directive for BoolFlag<SET, GET>
+where
+ SET: Fn(&mut IncludeCppConfig) -> &mut bool + Sync + Send,
+ GET: Fn(&IncludeCppConfig) -> &bool + Sync + Send,
+{
+ fn parse(
+ &self,
+ _args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ *self.0(config) = true;
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ if *self.1(config) {
+ Box::new(std::iter::once(quote! {}))
+ } else {
+ Box::new(std::iter::empty())
+ }
+ }
+}
+
+struct ModName;
+
+impl Directive for ModName {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let id: Ident = args.parse()?;
+ config.mod_name = Some(id);
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ match &config.mod_name {
+ None => Box::new(std::iter::empty()),
+ Some(id) => Box::new(std::iter::once(quote! { #id })),
+ }
+ }
+}
+
+struct Concrete;
+
+impl Directive for Concrete {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let definition: syn::LitStr = args.parse()?;
+ args.parse::<syn::token::Comma>()?;
+ let rust_id: syn::Ident = args.parse()?;
+ config.concretes.0.insert(definition.value(), rust_id);
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ Box::new(config.concretes.0.iter().map(|(k, v)| {
+ quote! {
+ #k,#v
+ }
+ }))
+ }
+}
+
+struct RustType {
+ output: bool,
+}
+
+impl Directive for RustType {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let id: Ident = args.parse()?;
+ config.rust_types.push(RustPath::new_from_ident(id));
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ if self.output {
+ Box::new(config.rust_types.iter().map(|rp| rp.to_token_stream()))
+ } else {
+ Box::new(std::iter::empty())
+ }
+ }
+}
+
+struct Subclass;
+
+impl Directive for Subclass {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let superclass: syn::LitStr = args.parse()?;
+ args.parse::<syn::token::Comma>()?;
+ let subclass: syn::Ident = args.parse()?;
+ config.subclasses.push(crate::config::Subclass {
+ superclass: superclass.value(),
+ subclass,
+ });
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ Box::new(config.subclasses.iter().map(|sc| {
+ let superclass = &sc.superclass;
+ let subclass = &sc.subclass;
+ quote! {
+ #superclass,#subclass
+ }
+ }))
+ }
+}
+
+struct ExternRustFun;
+
+impl Directive for ExternRustFun {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let path: RustPath = args.parse()?;
+ args.parse::<syn::token::Comma>()?;
+ let sig: syn::Signature = args.parse()?;
+ config.extern_rust_funs.push(RustFun {
+ path,
+ sig,
+ receiver: None,
+ });
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ Box::new(config.extern_rust_funs.iter().map(|erf| {
+ let p = &erf.path;
+ let s = &erf.sig;
+ quote! { #p,#s }
+ }))
+ }
+}
+
+struct ExternCppType {
+ opaque: bool,
+}
+
+impl Directive for ExternCppType {
+ fn parse(
+ &self,
+ args: ParseStream,
+ config: &mut IncludeCppConfig,
+ _ident_span: &Span,
+ ) -> ParseResult<()> {
+ let definition: syn::LitStr = args.parse()?;
+ args.parse::<syn::token::Comma>()?;
+ let rust_path: syn::TypePath = args.parse()?;
+ config.externs.0.insert(
+ definition.value(),
+ crate::config::ExternCppType {
+ rust_path,
+ opaque: self.opaque,
+ },
+ );
+ Ok(())
+ }
+
+ fn output<'a>(
+ &self,
+ config: &'a IncludeCppConfig,
+ ) -> Box<dyn Iterator<Item = TokenStream> + 'a> {
+ let opaque_needed = self.opaque;
+ Box::new(
+ config
+ .externs
+ .0
+ .iter()
+ .filter_map(move |(definition, details)| {
+ if details.opaque == opaque_needed {
+ let rust_path = &details.rust_path;
+ Some(quote! {
+ #definition, #rust_path
+ })
+ } else {
+ None
+ }
+ }),
+ )
+ }
+}
diff --git a/parser/src/file_locations.rs b/parser/src/file_locations.rs
new file mode 100644
index 0000000..a113d0f
--- /dev/null
+++ b/parser/src/file_locations.rs
@@ -0,0 +1,159 @@
+// 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 proc_macro2::TokenStream;
+use quote::quote;
+use std::{fs::File, path::PathBuf};
+
+use crate::{multi_bindings::MultiBindings, IncludeCppConfig};
+
+/// The strategy used to generate, and to find generated, files.
+/// As standard, these are based off the OUT_DIR set by Cargo,
+/// but our code can't assume it can read OUT_DIR as it may
+/// be running in the rust-analyzer proc macro server where it's
+/// not available. We need to give provision for custom locations
+/// and we need our code generator build script to be able to pass
+/// locations through to the proc macro by env vars.
+///
+/// On the whole this class concerns itself with directory names
+/// and allows the actual file name to be determined elsewhere
+/// (based on a hash of the contents of `include_cpp!`.) But
+/// some types of build system need to know the precise file _name_
+/// produced by the codegen phase and passed into the macro phase,
+/// so we have some options for that. See `gen --help` for details.
+pub enum FileLocationStrategy {
+ Custom(PathBuf),
+ FromAutocxxRsFile(PathBuf),
+ FromAutocxxRs(PathBuf),
+ FromOutDir(PathBuf),
+ FromAutocxxRsJsonArchive(PathBuf),
+ UnknownMaybeFromOutdir,
+}
+
+static BUILD_DIR_NAME: &str = "autocxx-build-dir";
+static RS_DIR_NAME: &str = "rs";
+static AUTOCXX_RS: &str = "AUTOCXX_RS";
+static AUTOCXX_RS_FILE: &str = "AUTOCXX_RS_FILE";
+static AUTOCXX_RS_JSON_ARCHIVE: &str = "AUTOCXX_RS_JSON_ARCHIVE";
+
+impl FileLocationStrategy {
+ pub fn new() -> Self {
+ match std::env::var_os(AUTOCXX_RS_JSON_ARCHIVE) {
+ Some(of) => FileLocationStrategy::FromAutocxxRsJsonArchive(PathBuf::from(of)),
+ None => match std::env::var_os(AUTOCXX_RS_FILE) {
+ Some(of) => FileLocationStrategy::FromAutocxxRsFile(PathBuf::from(of)),
+ None => match std::env::var_os(AUTOCXX_RS) {
+ None => match std::env::var_os("OUT_DIR") {
+ None => FileLocationStrategy::UnknownMaybeFromOutdir,
+ Some(od) => FileLocationStrategy::FromOutDir(PathBuf::from(od)),
+ },
+ Some(acrs) => FileLocationStrategy::FromAutocxxRs(PathBuf::from(acrs)),
+ },
+ },
+ }
+ }
+
+ pub fn new_custom(gen_dir: PathBuf) -> Self {
+ FileLocationStrategy::Custom(gen_dir)
+ }
+
+ /// Make a macro to include a given generated Rust file name.
+ /// This can't simply be calculated from `get_rs_dir` because
+ /// of limitations in rust-analyzer.
+ pub fn make_include(&self, config: &IncludeCppConfig) -> TokenStream {
+ match self {
+ FileLocationStrategy::FromAutocxxRs(custom_dir) => {
+ let fname = config.get_rs_filename();
+ let fname = custom_dir.join(fname).to_str().unwrap().to_string();
+ quote! {
+ include!( #fname );
+ }
+ }
+ FileLocationStrategy::Custom(_) => panic!("Should never happen in the macro"),
+ FileLocationStrategy::UnknownMaybeFromOutdir | FileLocationStrategy::FromOutDir(_) => {
+ let fname = config.get_rs_filename();
+ let fname = format!("/{}/{}/{}", BUILD_DIR_NAME, RS_DIR_NAME, fname);
+ // rust-analyzer works better if we ask Rust to do the path
+ // concatenation rather than doing it in proc-macro code.
+ // proc-macro code does not itself have access to the value of
+ // OUT_DIR, but if we put it into a macro like the below,
+ // rust-analyzer can cope.
+ quote! {
+ include!(concat!(env!("OUT_DIR"), #fname));
+ }
+ }
+ FileLocationStrategy::FromAutocxxRsFile(fname) => {
+ let fname = fname
+ .to_str()
+ .expect("AUTOCXX_RS_FILE environment variable contained non-UTF8 characters");
+ quote! {
+ include!( #fname );
+ }
+ }
+ FileLocationStrategy::FromAutocxxRsJsonArchive(fname) => {
+ let archive = File::open(fname).unwrap_or_else(|_| panic!("Unable to open {}. This may mean you didn't run the codegen tool (autocxx_gen) before building the Rust code.", fname.to_string_lossy()));
+ let multi_bindings: MultiBindings = serde_json::from_reader(archive)
+ .unwrap_or_else(|_| {
+ panic!("Unable to interpret {} as JSON", fname.to_string_lossy())
+ });
+ multi_bindings.get(config).unwrap_or_else(|err| panic!("Unable to find a suitable set of bindings within the JSON archive {} ({}). This likely means that the codegen tool hasn't been rerun since some changes in your include_cpp! macro.", fname.to_string_lossy(), err))
+ }
+ }
+ }
+
+ fn get_gen_dir(&self, suffix: &str) -> PathBuf {
+ let root = match self {
+ FileLocationStrategy::Custom(gen_dir)
+ | FileLocationStrategy::FromAutocxxRs(gen_dir) => gen_dir.clone(),
+ FileLocationStrategy::FromOutDir(out_dir) => out_dir.join(BUILD_DIR_NAME),
+ FileLocationStrategy::UnknownMaybeFromOutdir => {
+ panic!("Could not determine OUT_DIR or AUTOCXX_RS dir")
+ }
+ FileLocationStrategy::FromAutocxxRsFile(_) => {
+ panic!("It's invalid to set AUTOCXX_RS_FILE during the codegen phase.")
+ }
+ FileLocationStrategy::FromAutocxxRsJsonArchive(_) => {
+ panic!("It's invalid to set AUTOCXX_RS_JSON_ARCHIVE during the codegen phase.")
+ }
+ };
+ root.join(suffix)
+ }
+
+ /// Location to generate Rust files.
+ pub fn get_rs_dir(&self) -> PathBuf {
+ self.get_gen_dir(RS_DIR_NAME)
+ }
+
+ /// Location to generate C++ header files.
+ pub fn get_include_dir(&self) -> PathBuf {
+ self.get_gen_dir("include")
+ }
+
+ /// Location to generate C++ code.
+ pub fn get_cxx_dir(&self) -> PathBuf {
+ self.get_gen_dir("cxx")
+ }
+
+ /// From a build script, inform cargo how to set environment variables
+ /// to make them available to the procedural macro.
+ pub fn set_cargo_env_vars_for_build(&self) {
+ if let FileLocationStrategy::Custom(_) = self {
+ println!(
+ "cargo:rustc-env={}={}",
+ AUTOCXX_RS,
+ self.get_rs_dir().to_str().unwrap()
+ );
+ }
+ }
+}
+
+impl Default for FileLocationStrategy {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/parser/src/lib.rs b/parser/src/lib.rs
new file mode 100644
index 0000000..9e3ba7e
--- /dev/null
+++ b/parser/src/lib.rs
@@ -0,0 +1,83 @@
+// 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.
+
+#![forbid(unsafe_code)]
+
+mod config;
+mod directives;
+pub mod file_locations;
+mod multi_bindings;
+mod path;
+mod subclass_attrs;
+
+pub use config::{
+ AllowlistEntry, ExternCppType, IncludeCppConfig, RustFun, Subclass, UnsafePolicy,
+};
+use file_locations::FileLocationStrategy;
+pub use multi_bindings::{MultiBindings, MultiBindingsErr};
+pub use path::RustPath;
+use proc_macro2::TokenStream as TokenStream2;
+pub use subclass_attrs::SubclassAttrs;
+use syn::Result as ParseResult;
+use syn::{
+ parse::{Parse, ParseStream},
+ Macro,
+};
+
+#[doc(hidden)]
+/// Ensure consistency between the `include_cpp!` parser
+/// and the standalone macro discoverer
+pub mod directive_names {
+ pub static EXTERN_RUST_TYPE: &str = "extern_rust_type";
+ pub static EXTERN_RUST_FUN: &str = "extern_rust_function";
+ pub static SUBCLASS: &str = "subclass";
+}
+
+/// Core of the autocxx engine. See `generate` for most details
+/// on how this works.
+pub struct IncludeCpp {
+ config: IncludeCppConfig,
+}
+
+impl Parse for IncludeCpp {
+ fn parse(input: ParseStream) -> ParseResult<Self> {
+ let config = input.parse::<IncludeCppConfig>()?;
+ Ok(Self { config })
+ }
+}
+
+impl IncludeCpp {
+ pub fn new_from_syn(mac: Macro) -> ParseResult<Self> {
+ mac.parse_body::<IncludeCpp>()
+ }
+
+ /// Generate the Rust bindings.
+ pub fn generate_rs(&self) -> TokenStream2 {
+ if self.config.parse_only {
+ return TokenStream2::new();
+ }
+ FileLocationStrategy::new().make_include(&self.config)
+ }
+
+ pub fn get_config(&self) -> &IncludeCppConfig {
+ &self.config
+ }
+}
+
+#[cfg(test)]
+mod parse_tests {
+ use crate::IncludeCpp;
+ use syn::parse_quote;
+
+ #[test]
+ fn test_basic() {
+ let _i: IncludeCpp = parse_quote! {
+ generate_all!()
+ };
+ }
+}
diff --git a/parser/src/multi_bindings.rs b/parser/src/multi_bindings.rs
new file mode 100644
index 0000000..4ca86d9
--- /dev/null
+++ b/parser/src/multi_bindings.rs
@@ -0,0 +1,93 @@
+// Copyright 2022 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::IndexMap;
+use proc_macro2::TokenStream;
+use serde::{Deserialize, Serialize};
+
+use crate::IncludeCppConfig;
+
+/// Struct which stores multiple sets of bindings and can be serialized
+/// to disk. This is used when our build system uses `autocxx_gen`; that
+/// can handle multiple `include_cpp!` macros and therefore generate multiple
+/// sets of Rust bindings. We can't simply `include!` those because there's
+/// no (easy) way to pass their details from the codegen phase across to
+/// the Rust macro phase. Instead, we use this data structure to store
+/// several sets of .rs bindings in a single file, and then the macro
+/// extracts the correct set of bindings at expansion time.
+#[derive(Serialize, Deserialize, Default)]
+pub struct MultiBindings(IndexMap<u64, String>);
+
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum MultiBindingsErr {
+ #[error("unable to find the desired bindings within the archive of Rust bindings produced by the autocxx code generation phase")]
+ MissingBindings,
+ #[error("the stored bindings within the JSON file could not be parsed as valid Rust tokens")]
+ BindingsNotParseable,
+}
+
+impl MultiBindings {
+ /// Insert some generated Rust bindings into this data structure.
+ pub fn insert(&mut self, config: &IncludeCppConfig, bindings: TokenStream) {
+ self.0.insert(config.get_hash(), bindings.to_string());
+ }
+
+ /// Retrieves the bindings corresponding to a given [`IncludeCppConfig`].
+ pub fn get(&self, config: &IncludeCppConfig) -> Result<TokenStream, MultiBindingsErr> {
+ match self.0.get(&(config.get_hash())) {
+ None => Err(MultiBindingsErr::MissingBindings),
+ Some(bindings) => Ok(bindings
+ .parse()
+ .map_err(|_| MultiBindingsErr::BindingsNotParseable)?),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use proc_macro2::Span;
+ use quote::quote;
+ use syn::parse_quote;
+
+ use crate::IncludeCppConfig;
+
+ use super::MultiBindings;
+
+ #[test]
+ fn test_multi_bindings() {
+ let hexathorpe = syn::token::Pound(Span::call_site());
+ let config1: IncludeCppConfig = parse_quote! {
+ #hexathorpe include "a.h"
+ generate!("Foo")
+ };
+ let config2: IncludeCppConfig = parse_quote! {
+ #hexathorpe include "b.h"
+ generate!("Bar")
+ };
+ let config3: IncludeCppConfig = parse_quote! {
+ #hexathorpe include "c.h"
+ generate!("Bar")
+ };
+ let mut multi_bindings = MultiBindings::default();
+ multi_bindings.insert(&config1, quote! { first; });
+ multi_bindings.insert(&config2, quote! { second; });
+ let json = serde_json::to_string(&multi_bindings).unwrap();
+ let multi_bindings2: MultiBindings = serde_json::from_str(&json).unwrap();
+ assert_eq!(
+ multi_bindings2.get(&config2).unwrap().to_string(),
+ "second ;"
+ );
+ assert_eq!(
+ multi_bindings2.get(&config1).unwrap().to_string(),
+ "first ;"
+ );
+ assert!(multi_bindings2.get(&config3).is_err());
+ }
+}
diff --git a/parser/src/path.rs b/parser/src/path.rs
new file mode 100644
index 0000000..415b2ef
--- /dev/null
+++ b/parser/src/path.rs
@@ -0,0 +1,67 @@
+// Copyright 2021 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 crate::ParseResult;
+use proc_macro2::Ident;
+use quote::{quote, ToTokens, TokenStreamExt};
+use syn::parse::{Parse, ParseStream};
+
+/// A little like [`syn::Path`] but simpler - contains only identifiers,
+/// no path arguments. Guaranteed to always have at least one identifier.
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct RustPath(Vec<Ident>);
+
+impl RustPath {
+ pub fn new_from_ident(id: Ident) -> Self {
+ Self(vec![id])
+ }
+
+ #[must_use]
+ pub fn append(&self, id: Ident) -> Self {
+ Self(self.0.iter().cloned().chain(std::iter::once(id)).collect())
+ }
+
+ pub fn get_final_ident(&self) -> &Ident {
+ self.0.last().unwrap()
+ }
+
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+impl ToTokens for RustPath {
+ fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+ let mut it = self.0.iter();
+ let mut id = it.next();
+ while id.is_some() {
+ id.unwrap().to_tokens(tokens);
+ let next = it.next();
+ if next.is_some() {
+ tokens.append_all(quote! { :: });
+ }
+ id = next;
+ }
+ }
+}
+
+impl Parse for RustPath {
+ fn parse(input: ParseStream) -> ParseResult<Self> {
+ let id: Ident = input.parse()?;
+ let mut p = RustPath::new_from_ident(id);
+ while input.parse::<Option<syn::token::Colon2>>()?.is_some() {
+ let id: Ident = input.parse()?;
+ p = p.append(id);
+ }
+ Ok(p)
+ }
+}
diff --git a/parser/src/subclass_attrs.rs b/parser/src/subclass_attrs.rs
new file mode 100644
index 0000000..9174bec
--- /dev/null
+++ b/parser/src/subclass_attrs.rs
@@ -0,0 +1,58 @@
+// Copyright 2021 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 proc_macro2::Ident;
+use quote::ToTokens;
+use syn::Result as ParseResult;
+use syn::{
+ parse::{Parse, ParseStream},
+ token::Comma,
+};
+
+#[derive(Default)]
+pub struct SubclassAttrs {
+ pub self_owned: bool,
+ pub superclass: Option<String>,
+}
+
+impl Parse for SubclassAttrs {
+ fn parse(input: ParseStream) -> ParseResult<Self> {
+ let mut me = Self::default();
+ let mut id = input.parse::<Option<Ident>>()?;
+ while id.is_some() {
+ match id {
+ Some(id) if id == "self_owned" => me.self_owned = true,
+ Some(id) if id == "superclass" => {
+ let args;
+ syn::parenthesized!(args in input);
+ let superclass: syn::LitStr = args.parse()?;
+ if me.superclass.is_some() {
+ return Err(syn::Error::new_spanned(
+ id.into_token_stream(),
+ "Expected single superclass specification",
+ ));
+ }
+ me.superclass = Some(superclass.value());
+ }
+ Some(id) => {
+ return Err(syn::Error::new_spanned(
+ id.into_token_stream(),
+ "Expected self_owned or superclass",
+ ))
+ }
+ None => {}
+ };
+ let comma = input.parse::<Option<Comma>>()?;
+ if comma.is_none() {
+ break;
+ }
+ id = input.parse::<Option<Ident>>()?;
+ }
+ Ok(me)
+ }
+}