#![forbid(unsafe_code)]
#![cfg_attr(feature = "nightly", feature(doc_cfg))]
mod ast_discoverer;
mod conversion;
mod cxxbridge;
mod known_types;
mod output_generators;
mod parse_callbacks;
mod parse_file;
mod rust_pretty_printer;
mod types;
#[cfg(any(test, feature = "build"))]
mod builder;
use autocxx_bindgen::BindgenError;
use autocxx_parser::{IncludeCppConfig, UnsafePolicy};
use conversion::BridgeConverter;
use miette::{SourceOffset, SourceSpan};
use parse_callbacks::AutocxxParseCallbacks;
use parse_file::CppBuildable;
use proc_macro2::TokenStream as TokenStream2;
use regex::Regex;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::{
fs::File,
io::prelude::*,
path::Path,
process::{Command, Stdio},
};
use tempfile::NamedTempFile;
use quote::ToTokens;
use syn::Result as ParseResult;
use syn::{
parse::{Parse, ParseStream},
parse_quote, ItemMod, Macro,
};
use thiserror::Error;
use itertools::{join, Itertools};
use known_types::known_types;
use log::info;
use miette::Diagnostic;
use autocxx_bindgen as bindgen;
#[cfg(any(test, feature = "build"))]
pub use builder::{
Builder, BuilderBuild, BuilderContext, BuilderError, BuilderResult, BuilderSuccess,
};
pub use output_generators::{generate_rs_archive, generate_rs_single, RsOutput};
pub use parse_file::{parse_file, ParseError, ParsedFile};
pub use cxx_gen::HEADER;
#[derive(Clone)]
pub struct CppFilePair {
pub header: Vec<u8>,
pub implementation: Option<Vec<u8>>,
pub header_name: String,
}
pub struct GeneratedCpp(pub Vec<CppFilePair>);
#[derive(Error, Debug, Diagnostic)]
#[error("{err}")]
pub struct LocatedSynError {
err: syn::Error,
#[source_code]
file: String,
#[label("error here")]
span: SourceSpan,
}
impl LocatedSynError {
fn new(err: syn::Error, file: &str) -> Self {
let span = proc_macro_span_to_miette_span(&err.span());
Self {
err,
file: file.to_string(),
span,
}
}
}
#[derive(Debug, Error, Diagnostic)]
pub enum Error {
#[error("Bindgen was unable to generate the initial .rs bindings for this file. This may indicate a parsing problem with the C++ headers.")]
Bindgen(BindgenError),
#[error(transparent)]
#[diagnostic(transparent)]
MacroParsing(LocatedSynError),
#[error(transparent)]
#[diagnostic(transparent)]
BindingsParsing(LocatedSynError),
#[error("no C++ include directory was provided.")]
NoAutoCxxInc,
#[error(transparent)]
#[diagnostic(transparent)]
Conversion(conversion::ConvertError),
#[error("Using `unsafe_references_wrapped` requires the Rust nightly `arbitrary_self_types` feature")]
WrappedReferencesButNoArbitrarySelfTypes,
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
struct GenerationResults {
item_mod: ItemMod,
cpp: Option<CppFilePair>,
#[allow(dead_code)]
inc_dirs: Vec<PathBuf>,
cxxgen_header_name: String,
}
enum State {
NotGenerated,
ParseOnly,
Generated(Box<GenerationResults>),
}
const AUTOCXX_CLANG_ARGS: &[&str; 4] = &["-x", "c++", "-std=c++14", "-DBINDGEN"];
pub trait RebuildDependencyRecorder: std::fmt::Debug {
fn record_header_file_dependency(&self, filename: &str);
}
#[cfg_attr(doc, aquamarine::aquamarine)]
pub struct IncludeCppEngine {
config: IncludeCppConfig,
state: State,
source_code: Option<Rc<String>>, }
impl Parse for IncludeCppEngine {
fn parse(input: ParseStream) -> ParseResult<Self> {
let config = input.parse::<IncludeCppConfig>()?;
let state = if config.parse_only {
State::ParseOnly
} else {
State::NotGenerated
};
Ok(Self {
config,
state,
source_code: None,
})
}
}
impl IncludeCppEngine {
pub fn new_from_syn(mac: Macro, file_contents: Rc<String>) -> Result<Self> {
let mut this = mac
.parse_body::<IncludeCppEngine>()
.map_err(|e| Error::MacroParsing(LocatedSynError::new(e, &file_contents)))?;
this.source_code = Some(file_contents);
Ok(this)
}
pub fn config_mut(&mut self) -> &mut IncludeCppConfig {
assert!(
matches!(self.state, State::NotGenerated),
"Can't alter config after generation commenced"
);
&mut self.config
}
fn build_header(&self) -> String {
join(
self.config
.inclusions
.iter()
.map(|path| format!("#include \"{path}\"\n")),
"",
)
}
fn make_bindgen_builder(
&self,
inc_dirs: &[PathBuf],
extra_clang_args: &[&str],
) -> bindgen::Builder {
let mut builder = bindgen::builder()
.clang_args(make_clang_args(inc_dirs, extra_clang_args))
.derive_copy(false)
.derive_debug(false)
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
.rustfmt_bindings(log::log_enabled!(log::Level::Info))
.size_t_is_usize(true)
.enable_cxx_namespaces()
.generate_inline_functions(true)
.respect_cxx_access_specs(true)
.use_specific_virtual_function_receiver(true)
.cpp_semantic_attributes(true)
.represent_cxx_operators(true)
.use_distinct_char16_t(true)
.layout_tests(false); for item in known_types().get_initial_blocklist() {
builder = builder.blocklist_item(item);
}
if let Some(allowlist) = self.config.bindgen_allowlist() {
for a in allowlist {
builder = builder
.allowlist_type(&a)
.allowlist_function(&a)
.allowlist_var(&a);
}
}
log::info!(
"Bindgen flags would be: {}",
builder
.command_line_flags()
.into_iter()
.map(|f| format!("\"{f}\""))
.join(" ")
);
builder
}
pub fn get_rs_filename(&self) -> String {
self.config.get_rs_filename()
}
pub fn get_rs_output(&self) -> RsOutput {
RsOutput {
config: &self.config,
rs: match &self.state {
State::NotGenerated => panic!("Generate first"),
State::Generated(gen_results) => gen_results.item_mod.to_token_stream(),
State::ParseOnly => TokenStream2::new(),
},
}
}
pub fn get_mod_name(&self) -> String {
self.config.get_mod_name().to_string()
}
fn parse_bindings(&self, bindings: bindgen::Bindings) -> Result<ItemMod> {
let bindings = bindings.to_string();
let bindings = format!("mod bindgen {{ {bindings} }}");
info!("Bindings: {}", bindings);
syn::parse_str::<ItemMod>(&bindings)
.map_err(|e| Error::BindingsParsing(LocatedSynError::new(e, &bindings)))
}
pub fn generate(
&mut self,
inc_dirs: Vec<PathBuf>,
extra_clang_args: &[&str],
dep_recorder: Option<Box<dyn RebuildDependencyRecorder>>,
cpp_codegen_options: &CppCodegenOptions,
) -> Result<()> {
match self.state {
State::ParseOnly => return Ok(()),
State::NotGenerated => {}
State::Generated(_) => panic!("Only call generate once"),
}
if matches!(
self.config.unsafe_policy,
UnsafePolicy::ReferencesWrappedAllFunctionsSafe
) && !rustversion::cfg!(nightly)
{
return Err(Error::WrappedReferencesButNoArbitrarySelfTypes);
}
let mod_name = self.config.get_mod_name();
let mut builder = self.make_bindgen_builder(&inc_dirs, extra_clang_args);
if let Some(dep_recorder) = dep_recorder {
builder = builder.parse_callbacks(Box::new(AutocxxParseCallbacks(dep_recorder)));
}
let header_contents = self.build_header();
self.dump_header_if_so_configured(&header_contents, &inc_dirs, extra_clang_args);
let header_and_prelude = format!("{}\n\n{}", known_types().get_prelude(), header_contents);
log::info!("Header and prelude for bindgen:\n{}", header_and_prelude);
builder = builder.header_contents("example.hpp", &header_and_prelude);
let bindings = builder.generate().map_err(Error::Bindgen)?;
let bindings = self.parse_bindings(bindings)?;
let source_file_contents = self
.source_code
.as_ref()
.cloned()
.unwrap_or_else(|| Rc::new("".to_string()));
let converter = BridgeConverter::new(&self.config.inclusions, &self.config);
let conversion = converter
.convert(
bindings,
self.config.unsafe_policy.clone(),
header_contents,
cpp_codegen_options,
&source_file_contents,
)
.map_err(Error::Conversion)?;
let mut items = conversion.rs;
let mut new_bindings: ItemMod = parse_quote! {
#[allow(non_snake_case)]
#[allow(dead_code)]
#[allow(non_upper_case_globals)]
#[allow(non_camel_case_types)]
mod #mod_name {
}
};
new_bindings.content.as_mut().unwrap().1.append(&mut items);
info!(
"New bindings:\n{}",
rust_pretty_printer::pretty_print(&new_bindings)
);
self.state = State::Generated(Box::new(GenerationResults {
item_mod: new_bindings,
cpp: conversion.cpp,
inc_dirs,
cxxgen_header_name: conversion.cxxgen_header_name,
}));
Ok(())
}
#[cfg(any(test, feature = "build"))]
fn include_dirs(&self) -> impl Iterator<Item = &PathBuf> {
match &self.state {
State::Generated(gen_results) => gen_results.inc_dirs.iter(),
_ => panic!("Must call generate() before include_dirs()"),
}
}
fn dump_header_if_so_configured(
&self,
header: &str,
inc_dirs: &[PathBuf],
extra_clang_args: &[&str],
) {
if let Ok(output_path) = std::env::var("AUTOCXX_PREPROCESS") {
self.make_preprocessed_file(
&PathBuf::from(output_path),
header,
inc_dirs,
extra_clang_args,
);
}
#[cfg(feature = "reproduction_case")]
if let Ok(output_path) = std::env::var("AUTOCXX_REPRO_CASE") {
let tf = NamedTempFile::new().unwrap();
self.make_preprocessed_file(
&PathBuf::from(tf.path()),
header,
inc_dirs,
extra_clang_args,
);
let header = std::fs::read(tf.path()).unwrap();
let header = String::from_utf8_lossy(&header);
let output_path = PathBuf::from(output_path);
let config = self.config.to_token_stream().to_string();
let json = serde_json::json!({
"header": header,
"config": config
});
let f = File::create(output_path).unwrap();
serde_json::to_writer(f, &json).unwrap();
}
}
fn make_preprocessed_file(
&self,
output_path: &Path,
header: &str,
inc_dirs: &[PathBuf],
extra_clang_args: &[&str],
) {
let suffix = ALL_KNOWN_SYSTEM_HEADERS
.iter()
.map(|hdr| format!("#include <{hdr}>\n"))
.join("\n");
let input = format!("/*\nautocxx config:\n\n{:?}\n\nend autocxx config.\nautocxx preprocessed input:\n*/\n\n{}\n\n/* autocxx: extra headers added below for completeness. */\n\n{}\n{}\n",
self.config, header, suffix, cxx_gen::HEADER);
let mut tf = NamedTempFile::new().unwrap();
write!(tf, "{input}").unwrap();
let tp = tf.into_temp_path();
preprocess(&tp, &PathBuf::from(output_path), inc_dirs, extra_clang_args).unwrap();
}
}
static ALL_KNOWN_SYSTEM_HEADERS: &[&str] = &[
"memory",
"string",
"algorithm",
"array",
"cassert",
"cstddef",
"cstdint",
"cstring",
"exception",
"functional",
"initializer_list",
"iterator",
"memory",
"new",
"stdexcept",
"type_traits",
"utility",
"vector",
"sys/types.h",
];
pub fn do_cxx_cpp_generation(
rs: TokenStream2,
cpp_codegen_options: &CppCodegenOptions,
cxxgen_header_name: String,
) -> Result<CppFilePair, cxx_gen::Error> {
let mut opt = cxx_gen::Opt::default();
opt.cxx_impl_annotations = cpp_codegen_options.cxx_impl_annotations.clone();
let cxx_generated = cxx_gen::generate_header_and_cc(rs, &opt)?;
Ok(CppFilePair {
header: strip_system_headers(
cxx_generated.header,
cpp_codegen_options.suppress_system_headers,
),
header_name: cxxgen_header_name,
implementation: Some(strip_system_headers(
cxx_generated.implementation,
cpp_codegen_options.suppress_system_headers,
)),
})
}
pub(crate) fn strip_system_headers(input: Vec<u8>, suppress_system_headers: bool) -> Vec<u8> {
if suppress_system_headers {
std::str::from_utf8(&input)
.unwrap()
.lines()
.filter(|l| !l.starts_with("#include <"))
.join("\n")
.as_bytes()
.to_vec()
} else {
input
}
}
impl CppBuildable for IncludeCppEngine {
fn generate_h_and_cxx(
&self,
cpp_codegen_options: &CppCodegenOptions,
) -> Result<GeneratedCpp, cxx_gen::Error> {
let mut files = Vec::new();
match &self.state {
State::ParseOnly => panic!("Cannot generate C++ in parse-only mode"),
State::NotGenerated => panic!("Call generate() first"),
State::Generated(gen_results) => {
let rs = gen_results.item_mod.to_token_stream();
files.push(do_cxx_cpp_generation(
rs,
cpp_codegen_options,
gen_results.cxxgen_header_name.clone(),
)?);
if let Some(cpp_file_pair) = &gen_results.cpp {
files.push(cpp_file_pair.clone());
}
}
};
Ok(GeneratedCpp(files))
}
}
pub fn make_clang_args<'a>(
incs: &'a [PathBuf],
extra_args: &'a [&str],
) -> impl Iterator<Item = String> + 'a {
AUTOCXX_CLANG_ARGS
.iter()
.map(|s| s.to_string())
.chain(incs.iter().map(|i| format!("-I{}", i.to_str().unwrap())))
.chain(extra_args.iter().map(|s| s.to_string()))
}
pub fn preprocess(
listing_path: &Path,
preprocess_path: &Path,
incs: &[PathBuf],
extra_clang_args: &[&str],
) -> Result<(), std::io::Error> {
let mut cmd = Command::new(get_clang_path());
cmd.arg("-E");
cmd.arg("-C");
cmd.args(make_clang_args(incs, extra_clang_args));
cmd.arg(listing_path.to_str().unwrap());
cmd.stderr(Stdio::inherit());
let result = cmd.output().expect("failed to execute clang++");
assert!(result.status.success(), "failed to preprocess");
let mut file = File::create(preprocess_path)?;
file.write_all(&result.stdout)?;
Ok(())
}
pub fn get_clang_path() -> String {
std::env::var("CLANG_PATH")
.or_else(|_| std::env::var("CXX"))
.unwrap_or_else(|_| "clang++".to_string())
}
pub struct AutocxxgenHeaderNamer<'a>(pub Box<dyn 'a + Fn(String) -> String>);
impl Default for AutocxxgenHeaderNamer<'static> {
fn default() -> Self {
Self(Box::new(|mod_name| format!("autocxxgen_{mod_name}.h")))
}
}
impl AutocxxgenHeaderNamer<'_> {
fn name_header(&self, mod_name: String) -> String {
self.0(mod_name)
}
}
pub struct CxxgenHeaderNamer<'a>(pub Box<dyn 'a + Fn() -> String>);
impl Default for CxxgenHeaderNamer<'static> {
fn default() -> Self {
let header_counter = Rc::new(RefCell::new(0));
Self(Box::new(move || {
let header_counter = header_counter.clone();
let header_counter_cell = header_counter.as_ref();
let mut header_counter = header_counter_cell.borrow_mut();
if *header_counter == 0 {
*header_counter += 1;
"cxxgen.h".into()
} else {
let count = *header_counter;
*header_counter += 1;
format!("cxxgen{count}.h")
}
}))
}
}
impl CxxgenHeaderNamer<'_> {
fn name_header(&self) -> String {
self.0()
}
}
#[derive(Default)]
pub struct CppCodegenOptions<'a> {
pub suppress_system_headers: bool,
pub path_to_cxx_h: Option<String>,
pub path_to_cxxgen_h: Option<String>,
pub autocxxgen_header_namer: AutocxxgenHeaderNamer<'a>,
pub cxxgen_header_namer: CxxgenHeaderNamer<'a>,
pub cxx_impl_annotations: Option<String>,
}
fn proc_macro_span_to_miette_span(span: &proc_macro2::Span) -> SourceSpan {
struct Err;
let r: Result<(usize, usize), Err> = (|| {
let span_desc = format!("{span:?}");
let re = Regex::new(r"(\d+)..(\d+)").unwrap();
let captures = re.captures(&span_desc).ok_or(Err)?;
let start = captures.get(1).ok_or(Err)?;
let start: usize = start.as_str().parse().map_err(|_| Err)?;
let start = start.saturating_sub(1); let end = captures.get(2).ok_or(Err)?;
let end: usize = end.as_str().parse().map_err(|_| Err)?;
let end = end.saturating_sub(1); Ok((start, end.saturating_sub(start)))
})();
let (start, end) = r.unwrap_or((0, 0));
SourceSpan::new(SourceOffset::from(start), SourceOffset::from(end))
}