blob: 3571d19351243fd9daa8d043b6ea0ff1e1bfdb4e [file] [log] [blame]
Brian Silverman4e662aa2022-05-11 23:10:19 -07001// 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 crate::ast_discoverer::{Discoveries, DiscoveryErr};
10use crate::output_generators::RsOutput;
11use crate::{
12 cxxbridge::CxxBridge, Error as EngineError, GeneratedCpp, IncludeCppEngine,
13 RebuildDependencyRecorder,
14};
15use crate::{CppCodegenOptions, LocatedSynError};
16use autocxx_parser::directive_names::SUBCLASS;
17use autocxx_parser::{AllowlistEntry, RustPath, Subclass, SubclassAttrs};
18use indexmap::set::IndexSet as HashSet;
19use miette::Diagnostic;
20use quote::ToTokens;
21use std::{io::Read, path::PathBuf};
22use std::{panic::UnwindSafe, path::Path, rc::Rc};
23use syn::{token::Brace, Item, ItemMod};
24use thiserror::Error;
25
26/// Errors which may occur when parsing a Rust source file to discover
27/// and interpret include_cxx macros.
28#[derive(Error, Diagnostic, Debug)]
29pub enum ParseError {
30 #[error("unable to open the source file: {0}")]
31 FileOpen(std::io::Error),
32 #[error("the .rs file couldn't be read: {0}")]
33 FileRead(std::io::Error),
34 #[error("syntax error interpreting Rust code: {0}")]
35 #[diagnostic(transparent)]
36 Syntax(LocatedSynError),
37 #[error("generate!/generate_ns! was used at the same time as generate_all!")]
38 ConflictingAllowlist,
39 #[error("the subclass attribute couldn't be parsed: {0}")]
40 #[diagnostic(transparent)]
41 SubclassSyntax(LocatedSynError),
42 /// The include CPP macro could not be expanded into
43 /// Rust bindings to C++, because of some problem during the conversion
44 /// process. This could be anything from a C++ parsing error to some
45 /// C++ feature that autocxx can't yet handle and isn't able to skip
46 /// over. It could also cover errors in your syntax of the `include_cpp`
47 /// macro or the directives inside.
48 #[error("the include_cpp! macro couldn't be expanded into Rust bindings to C++: {0}")]
49 #[diagnostic(transparent)]
50 AutocxxCodegenError(EngineError),
51 /// There are two or more `include_cpp` macros with the same
52 /// mod name.
53 #[error("there are two or more include_cpp! mods with the same mod name")]
54 ConflictingModNames,
55 #[error("dynamic discovery was enabled but no mod was found")]
56 ZeroModsForDynamicDiscovery,
57 #[error("dynamic discovery was enabled but multiple mods were found")]
58 MultipleModsForDynamicDiscovery,
59 #[error("a problem occurred while discovering C++ APIs used within the Rust: {0}")]
60 Discovery(DiscoveryErr),
61}
62
63/// Parse a Rust file, and spot any include_cpp macros within it.
64pub fn parse_file<P1: AsRef<Path>>(
65 rs_file: P1,
66 auto_allowlist: bool,
67) -> Result<ParsedFile, ParseError> {
68 let mut source_code = String::new();
69 let mut file = std::fs::File::open(rs_file).map_err(ParseError::FileOpen)?;
70 file.read_to_string(&mut source_code)
71 .map_err(ParseError::FileRead)?;
72 proc_macro2::fallback::force();
73 let source = syn::parse_file(&source_code)
74 .map_err(|e| ParseError::Syntax(LocatedSynError::new(e, &source_code)))?;
75 parse_file_contents(source, auto_allowlist, &source_code)
76}
77
78fn parse_file_contents(
79 source: syn::File,
80 auto_allowlist: bool,
81 file_contents: &str,
82) -> Result<ParsedFile, ParseError> {
83 #[derive(Default)]
84 struct State {
85 auto_allowlist: bool,
86 results: Vec<Segment>,
87 extra_superclasses: Vec<Subclass>,
88 discoveries: Discoveries,
89 }
90 impl State {
91 fn parse_item(
92 &mut self,
93 item: Item,
94 mod_path: Option<RustPath>,
95 file_contents: &str,
96 ) -> Result<(), ParseError> {
97 let result = match item {
98 Item::Macro(mac)
99 if mac
100 .mac
101 .path
102 .segments
103 .last()
104 .map(|s| s.ident == "include_cpp")
105 .unwrap_or(false) =>
106 {
107 Segment::Autocxx(
108 crate::IncludeCppEngine::new_from_syn(mac.mac, file_contents)
109 .map_err(ParseError::AutocxxCodegenError)?,
110 )
111 }
112 Item::Mod(itm)
113 if itm
114 .attrs
115 .iter()
116 .any(|attr| attr.path.to_token_stream().to_string() == "cxx :: bridge") =>
117 {
118 Segment::Cxx(CxxBridge::from(itm))
119 }
120 Item::Mod(itm) => {
121 if let Some((brace, items)) = itm.content {
122 let mut mod_state = State {
123 auto_allowlist: self.auto_allowlist,
124 ..Default::default()
125 };
126 let mod_path = match &mod_path {
127 None => RustPath::new_from_ident(itm.ident.clone()),
128 Some(mod_path) => mod_path.append(itm.ident.clone()),
129 };
130 for item in items {
131 mod_state.parse_item(item, Some(mod_path.clone()), file_contents)?
132 }
133 self.extra_superclasses.extend(mod_state.extra_superclasses);
134 self.discoveries.extend(mod_state.discoveries);
135 Segment::Mod(
136 mod_state.results,
137 (
138 brace,
139 ItemMod {
140 content: None,
141 ..itm
142 },
143 ),
144 )
145 } else {
146 Segment::Other(Item::Mod(itm))
147 }
148 }
149 Item::Struct(ref its) if self.auto_allowlist => {
150 let attrs = &its.attrs;
151 let is_superclass_attr = attrs.iter().find(|attr| {
152 attr.path
153 .segments
154 .last()
155 .map(|seg| seg.ident == "is_subclass" || seg.ident == SUBCLASS)
156 .unwrap_or(false)
157 });
158 if let Some(is_superclass_attr) = is_superclass_attr {
159 if !is_superclass_attr.tokens.is_empty() {
160 let subclass = its.ident.clone();
161 let args: SubclassAttrs =
162 is_superclass_attr.parse_args().map_err(|e| {
163 ParseError::SubclassSyntax(LocatedSynError::new(
164 e,
165 file_contents,
166 ))
167 })?;
168 if let Some(superclass) = args.superclass {
169 self.extra_superclasses.push(Subclass {
170 superclass,
171 subclass,
172 })
173 }
174 }
175 }
176 self.discoveries
177 .search_item(&item, mod_path)
178 .map_err(ParseError::Discovery)?;
179 Segment::Other(item)
180 }
181 _ => {
182 self.discoveries
183 .search_item(&item, mod_path)
184 .map_err(ParseError::Discovery)?;
185 Segment::Other(item)
186 }
187 };
188 self.results.push(result);
189 Ok(())
190 }
191 }
192 let mut state = State {
193 auto_allowlist,
194 ..Default::default()
195 };
196 for item in source.items {
197 state.parse_item(item, None, file_contents)?
198 }
199 let State {
200 auto_allowlist,
201 mut results,
202 mut extra_superclasses,
203 mut discoveries,
204 } = state;
205
206 let must_handle_discovered_things = discoveries.found_rust()
207 || !extra_superclasses.is_empty()
208 || (auto_allowlist && discoveries.found_allowlist());
209
210 // We do not want to enter this 'if' block unless the above conditions are true,
211 // since we may emit errors.
212 if must_handle_discovered_things {
213 let mut autocxx_seg_iterator = results.iter_mut().filter_map(|seg| match seg {
214 Segment::Autocxx(engine) => Some(engine),
215 _ => None,
216 });
217 let our_seg = autocxx_seg_iterator.next();
218 match our_seg {
219 None => return Err(ParseError::ZeroModsForDynamicDiscovery),
220 Some(engine) => {
221 engine
222 .config_mut()
223 .subclasses
224 .append(&mut extra_superclasses);
225 if auto_allowlist {
226 for cpp in discoveries.cpp_list {
227 engine
228 .config_mut()
229 .allowlist
230 .push(AllowlistEntry::Item(cpp))
231 .map_err(|_| ParseError::ConflictingAllowlist)?;
232 }
233 }
234 engine
235 .config_mut()
236 .extern_rust_funs
237 .append(&mut discoveries.extern_rust_funs);
238 engine
239 .config_mut()
240 .rust_types
241 .append(&mut discoveries.extern_rust_types);
242 }
243 }
244 if autocxx_seg_iterator.next().is_some() {
245 return Err(ParseError::MultipleModsForDynamicDiscovery);
246 }
247 }
248 let autocxx_seg_iterator = results.iter_mut().filter_map(|seg| match seg {
249 Segment::Autocxx(engine) => Some(engine),
250 _ => None,
251 });
252 for seg in autocxx_seg_iterator {
253 seg.config.confirm_complete();
254 }
255 Ok(ParsedFile(results))
256}
257
258/// A Rust file parsed by autocxx. May contain zero or more autocxx 'engines',
259/// i.e. the `IncludeCpp` class, corresponding to zero or more include_cpp
260/// macros within this file. Also contains `syn::Item` structures for all
261/// the rest of the Rust code, such that it can be reconstituted if necessary.
262pub struct ParsedFile(Vec<Segment>);
263
264#[allow(clippy::large_enum_variant)]
265enum Segment {
266 Autocxx(IncludeCppEngine),
267 Cxx(CxxBridge),
268 Mod(Vec<Segment>, (Brace, ItemMod)),
269 Other(Item),
270}
271
272pub trait CppBuildable {
273 fn generate_h_and_cxx(
274 &self,
275 cpp_codegen_options: &CppCodegenOptions,
276 ) -> Result<GeneratedCpp, cxx_gen::Error>;
277}
278
279impl ParsedFile {
280 /// Get all the autocxx `include_cpp` macros found in this file.
281 pub fn get_autocxxes(&self) -> impl Iterator<Item = &IncludeCppEngine> {
282 fn do_get_autocxxes(segments: &[Segment]) -> impl Iterator<Item = &IncludeCppEngine> {
283 segments
284 .iter()
285 .flat_map(|s| -> Box<dyn Iterator<Item = &IncludeCppEngine>> {
286 match s {
287 Segment::Autocxx(includecpp) => Box::new(std::iter::once(includecpp)),
288 Segment::Mod(segments, _) => Box::new(do_get_autocxxes(segments)),
289 _ => Box::new(std::iter::empty()),
290 }
291 })
292 }
293
294 do_get_autocxxes(&self.0)
295 }
296
297 /// Get all the areas of Rust code which need to be built for these bindings.
298 /// A shortcut for `get_autocxxes()` then calling `get_rs_output` on each.
299 pub fn get_rs_outputs(&self) -> impl Iterator<Item = RsOutput> {
300 self.get_autocxxes().map(|autocxx| autocxx.get_rs_output())
301 }
302
303 /// Get all items which can result in C++ code
304 pub fn get_cpp_buildables(&self) -> impl Iterator<Item = &dyn CppBuildable> {
305 fn do_get_cpp_buildables(segments: &[Segment]) -> impl Iterator<Item = &dyn CppBuildable> {
306 segments
307 .iter()
308 .flat_map(|s| -> Box<dyn Iterator<Item = &dyn CppBuildable>> {
309 match s {
310 Segment::Autocxx(includecpp) => {
311 Box::new(std::iter::once(includecpp as &dyn CppBuildable))
312 }
313 Segment::Cxx(cxxbridge) => {
314 Box::new(std::iter::once(cxxbridge as &dyn CppBuildable))
315 }
316 Segment::Mod(segments, _) => Box::new(do_get_cpp_buildables(segments)),
317 _ => Box::new(std::iter::empty()),
318 }
319 })
320 }
321
322 do_get_cpp_buildables(&self.0)
323 }
324
325 fn get_autocxxes_mut(&mut self) -> impl Iterator<Item = &mut IncludeCppEngine> {
326 fn do_get_autocxxes_mut(
327 segments: &mut [Segment],
328 ) -> impl Iterator<Item = &mut IncludeCppEngine> {
329 segments
330 .iter_mut()
331 .flat_map(|s| -> Box<dyn Iterator<Item = &mut IncludeCppEngine>> {
332 match s {
333 Segment::Autocxx(includecpp) => Box::new(std::iter::once(includecpp)),
334 Segment::Mod(segments, _) => Box::new(do_get_autocxxes_mut(segments)),
335 _ => Box::new(std::iter::empty()),
336 }
337 })
338 }
339
340 do_get_autocxxes_mut(&mut self.0)
341 }
342
343 /// Determines the include dirs that were set for each include_cpp, so they can be
344 /// used as input to a `cc::Build`.
345 #[cfg(any(test, feature = "build"))]
346 pub(crate) fn include_dirs(&self) -> impl Iterator<Item = &PathBuf> {
347 fn do_get_include_dirs(segments: &[Segment]) -> impl Iterator<Item = &PathBuf> {
348 segments
349 .iter()
350 .flat_map(|s| -> Box<dyn Iterator<Item = &PathBuf>> {
351 match s {
352 Segment::Autocxx(includecpp) => Box::new(includecpp.include_dirs()),
353 Segment::Mod(segments, _) => Box::new(do_get_include_dirs(segments)),
354 _ => Box::new(std::iter::empty()),
355 }
356 })
357 }
358
359 do_get_include_dirs(&self.0)
360 }
361
362 pub fn resolve_all(
363 &mut self,
364 autocxx_inc: Vec<PathBuf>,
365 extra_clang_args: &[&str],
366 dep_recorder: Option<Box<dyn RebuildDependencyRecorder>>,
367 cpp_codegen_options: &CppCodegenOptions,
368 ) -> Result<(), ParseError> {
369 let mut mods_found = HashSet::new();
370 let inner_dep_recorder: Option<Rc<dyn RebuildDependencyRecorder>> =
371 dep_recorder.map(Rc::from);
372 for include_cpp in self.get_autocxxes_mut() {
373 #[allow(clippy::manual_map)] // because of dyn shenanigans
374 let dep_recorder: Option<Box<dyn RebuildDependencyRecorder>> = match &inner_dep_recorder
375 {
376 None => None,
377 Some(inner_dep_recorder) => Some(Box::new(CompositeDepRecorder::new(
378 inner_dep_recorder.clone(),
379 ))),
380 };
381 if !mods_found.insert(include_cpp.get_mod_name()) {
382 return Err(ParseError::ConflictingModNames);
383 }
384 include_cpp
385 .generate(
386 autocxx_inc.clone(),
387 extra_clang_args,
388 dep_recorder,
389 cpp_codegen_options,
390 )
391 .map_err(ParseError::AutocxxCodegenError)?
392 }
393 Ok(())
394 }
395}
396
397/// Shenanigans required to share the same RebuildDependencyRecorder
398/// with all of the include_cpp instances in this one file.
399#[derive(Debug, Clone)]
400struct CompositeDepRecorder(Rc<dyn RebuildDependencyRecorder>);
401
402impl CompositeDepRecorder {
403 fn new(inner: Rc<dyn RebuildDependencyRecorder>) -> Self {
404 CompositeDepRecorder(inner)
405 }
406}
407
408impl UnwindSafe for CompositeDepRecorder {}
409
410impl RebuildDependencyRecorder for CompositeDepRecorder {
411 fn record_header_file_dependency(&self, filename: &str) {
412 self.0.record_header_file_dependency(filename);
413 }
414}