blob: 3571d19351243fd9daa8d043b6ea0ff1e1bfdb4e [file] [log] [blame]
// 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 crate::ast_discoverer::{Discoveries, DiscoveryErr};
use crate::output_generators::RsOutput;
use crate::{
cxxbridge::CxxBridge, Error as EngineError, GeneratedCpp, IncludeCppEngine,
RebuildDependencyRecorder,
};
use crate::{CppCodegenOptions, LocatedSynError};
use autocxx_parser::directive_names::SUBCLASS;
use autocxx_parser::{AllowlistEntry, RustPath, Subclass, SubclassAttrs};
use indexmap::set::IndexSet as HashSet;
use miette::Diagnostic;
use quote::ToTokens;
use std::{io::Read, path::PathBuf};
use std::{panic::UnwindSafe, path::Path, rc::Rc};
use syn::{token::Brace, Item, ItemMod};
use thiserror::Error;
/// Errors which may occur when parsing a Rust source file to discover
/// and interpret include_cxx macros.
#[derive(Error, Diagnostic, Debug)]
pub enum ParseError {
#[error("unable to open the source file: {0}")]
FileOpen(std::io::Error),
#[error("the .rs file couldn't be read: {0}")]
FileRead(std::io::Error),
#[error("syntax error interpreting Rust code: {0}")]
#[diagnostic(transparent)]
Syntax(LocatedSynError),
#[error("generate!/generate_ns! was used at the same time as generate_all!")]
ConflictingAllowlist,
#[error("the subclass attribute couldn't be parsed: {0}")]
#[diagnostic(transparent)]
SubclassSyntax(LocatedSynError),
/// The include CPP macro could not be expanded into
/// Rust bindings to C++, because of some problem during the conversion
/// process. This could be anything from a C++ parsing error to some
/// C++ feature that autocxx can't yet handle and isn't able to skip
/// over. It could also cover errors in your syntax of the `include_cpp`
/// macro or the directives inside.
#[error("the include_cpp! macro couldn't be expanded into Rust bindings to C++: {0}")]
#[diagnostic(transparent)]
AutocxxCodegenError(EngineError),
/// There are two or more `include_cpp` macros with the same
/// mod name.
#[error("there are two or more include_cpp! mods with the same mod name")]
ConflictingModNames,
#[error("dynamic discovery was enabled but no mod was found")]
ZeroModsForDynamicDiscovery,
#[error("dynamic discovery was enabled but multiple mods were found")]
MultipleModsForDynamicDiscovery,
#[error("a problem occurred while discovering C++ APIs used within the Rust: {0}")]
Discovery(DiscoveryErr),
}
/// Parse a Rust file, and spot any include_cpp macros within it.
pub fn parse_file<P1: AsRef<Path>>(
rs_file: P1,
auto_allowlist: bool,
) -> Result<ParsedFile, ParseError> {
let mut source_code = String::new();
let mut file = std::fs::File::open(rs_file).map_err(ParseError::FileOpen)?;
file.read_to_string(&mut source_code)
.map_err(ParseError::FileRead)?;
proc_macro2::fallback::force();
let source = syn::parse_file(&source_code)
.map_err(|e| ParseError::Syntax(LocatedSynError::new(e, &source_code)))?;
parse_file_contents(source, auto_allowlist, &source_code)
}
fn parse_file_contents(
source: syn::File,
auto_allowlist: bool,
file_contents: &str,
) -> Result<ParsedFile, ParseError> {
#[derive(Default)]
struct State {
auto_allowlist: bool,
results: Vec<Segment>,
extra_superclasses: Vec<Subclass>,
discoveries: Discoveries,
}
impl State {
fn parse_item(
&mut self,
item: Item,
mod_path: Option<RustPath>,
file_contents: &str,
) -> Result<(), ParseError> {
let result = match item {
Item::Macro(mac)
if mac
.mac
.path
.segments
.last()
.map(|s| s.ident == "include_cpp")
.unwrap_or(false) =>
{
Segment::Autocxx(
crate::IncludeCppEngine::new_from_syn(mac.mac, file_contents)
.map_err(ParseError::AutocxxCodegenError)?,
)
}
Item::Mod(itm)
if itm
.attrs
.iter()
.any(|attr| attr.path.to_token_stream().to_string() == "cxx :: bridge") =>
{
Segment::Cxx(CxxBridge::from(itm))
}
Item::Mod(itm) => {
if let Some((brace, items)) = itm.content {
let mut mod_state = State {
auto_allowlist: self.auto_allowlist,
..Default::default()
};
let mod_path = match &mod_path {
None => RustPath::new_from_ident(itm.ident.clone()),
Some(mod_path) => mod_path.append(itm.ident.clone()),
};
for item in items {
mod_state.parse_item(item, Some(mod_path.clone()), file_contents)?
}
self.extra_superclasses.extend(mod_state.extra_superclasses);
self.discoveries.extend(mod_state.discoveries);
Segment::Mod(
mod_state.results,
(
brace,
ItemMod {
content: None,
..itm
},
),
)
} else {
Segment::Other(Item::Mod(itm))
}
}
Item::Struct(ref its) if self.auto_allowlist => {
let attrs = &its.attrs;
let is_superclass_attr = attrs.iter().find(|attr| {
attr.path
.segments
.last()
.map(|seg| seg.ident == "is_subclass" || seg.ident == SUBCLASS)
.unwrap_or(false)
});
if let Some(is_superclass_attr) = is_superclass_attr {
if !is_superclass_attr.tokens.is_empty() {
let subclass = its.ident.clone();
let args: SubclassAttrs =
is_superclass_attr.parse_args().map_err(|e| {
ParseError::SubclassSyntax(LocatedSynError::new(
e,
file_contents,
))
})?;
if let Some(superclass) = args.superclass {
self.extra_superclasses.push(Subclass {
superclass,
subclass,
})
}
}
}
self.discoveries
.search_item(&item, mod_path)
.map_err(ParseError::Discovery)?;
Segment::Other(item)
}
_ => {
self.discoveries
.search_item(&item, mod_path)
.map_err(ParseError::Discovery)?;
Segment::Other(item)
}
};
self.results.push(result);
Ok(())
}
}
let mut state = State {
auto_allowlist,
..Default::default()
};
for item in source.items {
state.parse_item(item, None, file_contents)?
}
let State {
auto_allowlist,
mut results,
mut extra_superclasses,
mut discoveries,
} = state;
let must_handle_discovered_things = discoveries.found_rust()
|| !extra_superclasses.is_empty()
|| (auto_allowlist && discoveries.found_allowlist());
// We do not want to enter this 'if' block unless the above conditions are true,
// since we may emit errors.
if must_handle_discovered_things {
let mut autocxx_seg_iterator = results.iter_mut().filter_map(|seg| match seg {
Segment::Autocxx(engine) => Some(engine),
_ => None,
});
let our_seg = autocxx_seg_iterator.next();
match our_seg {
None => return Err(ParseError::ZeroModsForDynamicDiscovery),
Some(engine) => {
engine
.config_mut()
.subclasses
.append(&mut extra_superclasses);
if auto_allowlist {
for cpp in discoveries.cpp_list {
engine
.config_mut()
.allowlist
.push(AllowlistEntry::Item(cpp))
.map_err(|_| ParseError::ConflictingAllowlist)?;
}
}
engine
.config_mut()
.extern_rust_funs
.append(&mut discoveries.extern_rust_funs);
engine
.config_mut()
.rust_types
.append(&mut discoveries.extern_rust_types);
}
}
if autocxx_seg_iterator.next().is_some() {
return Err(ParseError::MultipleModsForDynamicDiscovery);
}
}
let autocxx_seg_iterator = results.iter_mut().filter_map(|seg| match seg {
Segment::Autocxx(engine) => Some(engine),
_ => None,
});
for seg in autocxx_seg_iterator {
seg.config.confirm_complete();
}
Ok(ParsedFile(results))
}
/// A Rust file parsed by autocxx. May contain zero or more autocxx 'engines',
/// i.e. the `IncludeCpp` class, corresponding to zero or more include_cpp
/// macros within this file. Also contains `syn::Item` structures for all
/// the rest of the Rust code, such that it can be reconstituted if necessary.
pub struct ParsedFile(Vec<Segment>);
#[allow(clippy::large_enum_variant)]
enum Segment {
Autocxx(IncludeCppEngine),
Cxx(CxxBridge),
Mod(Vec<Segment>, (Brace, ItemMod)),
Other(Item),
}
pub trait CppBuildable {
fn generate_h_and_cxx(
&self,
cpp_codegen_options: &CppCodegenOptions,
) -> Result<GeneratedCpp, cxx_gen::Error>;
}
impl ParsedFile {
/// Get all the autocxx `include_cpp` macros found in this file.
pub fn get_autocxxes(&self) -> impl Iterator<Item = &IncludeCppEngine> {
fn do_get_autocxxes(segments: &[Segment]) -> impl Iterator<Item = &IncludeCppEngine> {
segments
.iter()
.flat_map(|s| -> Box<dyn Iterator<Item = &IncludeCppEngine>> {
match s {
Segment::Autocxx(includecpp) => Box::new(std::iter::once(includecpp)),
Segment::Mod(segments, _) => Box::new(do_get_autocxxes(segments)),
_ => Box::new(std::iter::empty()),
}
})
}
do_get_autocxxes(&self.0)
}
/// Get all the areas of Rust code which need to be built for these bindings.
/// A shortcut for `get_autocxxes()` then calling `get_rs_output` on each.
pub fn get_rs_outputs(&self) -> impl Iterator<Item = RsOutput> {
self.get_autocxxes().map(|autocxx| autocxx.get_rs_output())
}
/// Get all items which can result in C++ code
pub fn get_cpp_buildables(&self) -> impl Iterator<Item = &dyn CppBuildable> {
fn do_get_cpp_buildables(segments: &[Segment]) -> impl Iterator<Item = &dyn CppBuildable> {
segments
.iter()
.flat_map(|s| -> Box<dyn Iterator<Item = &dyn CppBuildable>> {
match s {
Segment::Autocxx(includecpp) => {
Box::new(std::iter::once(includecpp as &dyn CppBuildable))
}
Segment::Cxx(cxxbridge) => {
Box::new(std::iter::once(cxxbridge as &dyn CppBuildable))
}
Segment::Mod(segments, _) => Box::new(do_get_cpp_buildables(segments)),
_ => Box::new(std::iter::empty()),
}
})
}
do_get_cpp_buildables(&self.0)
}
fn get_autocxxes_mut(&mut self) -> impl Iterator<Item = &mut IncludeCppEngine> {
fn do_get_autocxxes_mut(
segments: &mut [Segment],
) -> impl Iterator<Item = &mut IncludeCppEngine> {
segments
.iter_mut()
.flat_map(|s| -> Box<dyn Iterator<Item = &mut IncludeCppEngine>> {
match s {
Segment::Autocxx(includecpp) => Box::new(std::iter::once(includecpp)),
Segment::Mod(segments, _) => Box::new(do_get_autocxxes_mut(segments)),
_ => Box::new(std::iter::empty()),
}
})
}
do_get_autocxxes_mut(&mut self.0)
}
/// Determines the include dirs that were set for each include_cpp, so they can be
/// used as input to a `cc::Build`.
#[cfg(any(test, feature = "build"))]
pub(crate) fn include_dirs(&self) -> impl Iterator<Item = &PathBuf> {
fn do_get_include_dirs(segments: &[Segment]) -> impl Iterator<Item = &PathBuf> {
segments
.iter()
.flat_map(|s| -> Box<dyn Iterator<Item = &PathBuf>> {
match s {
Segment::Autocxx(includecpp) => Box::new(includecpp.include_dirs()),
Segment::Mod(segments, _) => Box::new(do_get_include_dirs(segments)),
_ => Box::new(std::iter::empty()),
}
})
}
do_get_include_dirs(&self.0)
}
pub fn resolve_all(
&mut self,
autocxx_inc: Vec<PathBuf>,
extra_clang_args: &[&str],
dep_recorder: Option<Box<dyn RebuildDependencyRecorder>>,
cpp_codegen_options: &CppCodegenOptions,
) -> Result<(), ParseError> {
let mut mods_found = HashSet::new();
let inner_dep_recorder: Option<Rc<dyn RebuildDependencyRecorder>> =
dep_recorder.map(Rc::from);
for include_cpp in self.get_autocxxes_mut() {
#[allow(clippy::manual_map)] // because of dyn shenanigans
let dep_recorder: Option<Box<dyn RebuildDependencyRecorder>> = match &inner_dep_recorder
{
None => None,
Some(inner_dep_recorder) => Some(Box::new(CompositeDepRecorder::new(
inner_dep_recorder.clone(),
))),
};
if !mods_found.insert(include_cpp.get_mod_name()) {
return Err(ParseError::ConflictingModNames);
}
include_cpp
.generate(
autocxx_inc.clone(),
extra_clang_args,
dep_recorder,
cpp_codegen_options,
)
.map_err(ParseError::AutocxxCodegenError)?
}
Ok(())
}
}
/// Shenanigans required to share the same RebuildDependencyRecorder
/// with all of the include_cpp instances in this one file.
#[derive(Debug, Clone)]
struct CompositeDepRecorder(Rc<dyn RebuildDependencyRecorder>);
impl CompositeDepRecorder {
fn new(inner: Rc<dyn RebuildDependencyRecorder>) -> Self {
CompositeDepRecorder(inner)
}
}
impl UnwindSafe for CompositeDepRecorder {}
impl RebuildDependencyRecorder for CompositeDepRecorder {
fn record_header_file_dependency(&self, filename: &str) {
self.0.record_header_file_dependency(filename);
}
}