blob: feaeafc0fd1b015833e2b4a8b5a28af033006b12 [file] [log] [blame]
// Part of the Crubit project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#![allow(clippy::collapsible_else_if)]
mod generate_func;
mod generate_record;
mod rs_snippet;
use generate_func::{
generate_func, get_binding, is_record_clonable, overloaded_funcs, FunctionId, ImplKind,
};
use generate_record::{
collect_unqualified_member_functions, generate_incomplete_record, generate_record,
};
use crate::rs_snippet::{CratePath, Lifetime, Mutability, PrimitiveType, RsTypeKind};
use arc_anyhow::{Context, Error, Result};
use code_gen_utils::{format_cc_includes, make_rs_ident, CcInclude};
use error_report::{anyhow, bail, ensure, ErrorReport, ErrorReporting, IgnoreErrors};
use ffi_types::*;
use ir::*;
use itertools::Itertools;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::{quote, ToTokens};
use std::collections::{BTreeSet, HashSet};
use std::ffi::{OsStr, OsString};
use std::fmt::{Display, Formatter};
use std::panic::catch_unwind;
use std::path::Path;
use std::process;
use std::rc::Rc;
use token_stream_printer::{
cc_tokens_to_formatted_string, rs_tokens_to_formatted_string, RustfmtConfig,
};
/// FFI equivalent of `Bindings`.
#[repr(C)]
pub struct FfiBindings {
rs_api: FfiU8SliceBox,
rs_api_impl: FfiU8SliceBox,
error_report: FfiU8SliceBox,
}
/// Deserializes IR from `json` and generates bindings source code.
///
/// This function panics on error.
///
/// # Safety
///
/// Expectations:
/// * `json` should be a FfiU8Slice for a valid array of bytes with the given
/// size.
/// * `crubit_support_path_format` should be a FfiU8Slice for a valid array
/// of bytes representing an UTF8-encoded string
/// * `rustfmt_exe_path` and `rustfmt_config_path` should both be a
/// FfiU8Slice for a valid array of bytes representing an UTF8-encoded
/// string (without the UTF-8 requirement, it seems that Rust doesn't offer
/// a way to convert to OsString on Windows)
/// * `json`, `crubit_support_path_format`, `rustfmt_exe_path`, and
/// `rustfmt_config_path` shouldn't change during the call.
///
/// Ownership:
/// * function doesn't take ownership of (in other words it borrows) the
/// input params: `json`, `crubit_support_path_format`, `rustfmt_exe_path`,
/// and `rustfmt_config_path`
/// * function passes ownership of the returned value to the caller
#[no_mangle]
pub unsafe extern "C" fn GenerateBindingsImpl(
json: FfiU8Slice,
crubit_support_path_format: FfiU8Slice,
clang_format_exe_path: FfiU8Slice,
rustfmt_exe_path: FfiU8Slice,
rustfmt_config_path: FfiU8Slice,
generate_error_report: bool,
generate_source_loc_doc_comment: SourceLocationDocComment,
) -> FfiBindings {
let json: &[u8] = json.as_slice();
let crubit_support_path_format: &str =
std::str::from_utf8(crubit_support_path_format.as_slice()).unwrap();
let clang_format_exe_path: OsString =
std::str::from_utf8(clang_format_exe_path.as_slice()).unwrap().into();
let rustfmt_exe_path: OsString =
std::str::from_utf8(rustfmt_exe_path.as_slice()).unwrap().into();
let rustfmt_config_path: OsString =
std::str::from_utf8(rustfmt_config_path.as_slice()).unwrap().into();
catch_unwind(|| {
// It is ok to abort here.
let errors: Rc<dyn ErrorReporting> =
if generate_error_report { Rc::new(ErrorReport::new()) } else { Rc::new(IgnoreErrors) };
let Bindings { rs_api, rs_api_impl } = generate_bindings(
json,
crubit_support_path_format,
&clang_format_exe_path,
&rustfmt_exe_path,
&rustfmt_config_path,
errors.clone(),
generate_source_loc_doc_comment,
)
.unwrap();
FfiBindings {
rs_api: FfiU8SliceBox::from_boxed_slice(rs_api.into_bytes().into_boxed_slice()),
rs_api_impl: FfiU8SliceBox::from_boxed_slice(
rs_api_impl.into_bytes().into_boxed_slice(),
),
error_report: FfiU8SliceBox::from_boxed_slice(
errors.serialize_to_vec().unwrap().into_boxed_slice(),
),
}
})
.unwrap_or_else(|_| process::abort())
}
memoized::query_group! {
trait BindingsGenerator {
#[input]
fn ir(&self) -> Rc<IR>;
#[input]
fn errors(&self) -> Rc<dyn ErrorReporting>;
#[input]
fn generate_source_loc_doc_comment(&self) -> SourceLocationDocComment;
fn rs_type_kind(&self, rs_type: RsType) -> Result<RsTypeKind>;
fn generate_func(&self, func: Rc<Func>, record_overwrite: Option<Rc<Record>>) -> Result<Option<(Rc<GeneratedItem>, Rc<FunctionId>)>>;
fn overloaded_funcs(&self) -> Rc<HashSet<Rc<FunctionId>>>;
fn is_record_clonable(&self, record: Rc<Record>) -> bool;
fn get_binding(
&self,
expected_function_name: UnqualifiedIdentifier,
expected_param_types: Vec<RsTypeKind>,
) -> Option<(Ident, ImplKind)>;
fn collect_unqualified_member_functions(
&self,
record: Rc<Record>,
) -> Rc<[Rc<Func>]>;
}
struct Database;
}
/// Source code for generated bindings.
struct Bindings {
// Rust source code.
rs_api: String,
// C++ source code.
rs_api_impl: String,
}
/// Source code for generated bindings, as tokens.
///
/// This is public within the crate for testing purposes.
pub(crate) struct BindingsTokens {
// Rust source code.
rs_api: TokenStream,
// C++ source code.
rs_api_impl: TokenStream,
}
fn generate_bindings(
json: &[u8],
crubit_support_path_format: &str,
clang_format_exe_path: &OsStr,
rustfmt_exe_path: &OsStr,
rustfmt_config_path: &OsStr,
errors: Rc<dyn ErrorReporting>,
generate_source_loc_doc_comment: SourceLocationDocComment,
) -> Result<Bindings> {
let ir = Rc::new(deserialize_ir(json)?);
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(
ir.clone(),
crubit_support_path_format,
errors,
generate_source_loc_doc_comment,
)?;
let rs_api = {
let rustfmt_exe_path = Path::new(rustfmt_exe_path);
let rustfmt_config_path = if rustfmt_config_path.is_empty() {
None
} else {
Some(Path::new(rustfmt_config_path))
};
let rustfmt_config = RustfmtConfig::new(rustfmt_exe_path, rustfmt_config_path);
rs_tokens_to_formatted_string(rs_api, &rustfmt_config)?
};
let rs_api_impl = cc_tokens_to_formatted_string(rs_api_impl, Path::new(clang_format_exe_path))?;
// Add top-level comments that help identify where the generated bindings came
// from.
let top_level_comment = {
// The "@generated" marker is an informal convention for identifying
// automatically generated code. This marker is recognized by `rustfmt`
// (see the `format_generated_files` option [1]) and some other tools.
// For more info see https://generated.at/.
//
// [1]
// https://rust-lang.github.io/rustfmt/?version=v1.4.38&search=#format_generated_files
//
// TODO(b/255784681): It would be nice to include "by $argv[0]"" in the
// @generated comment below. OTOH, `std::env::current_exe()` in our
// current build environment returns a guid-like path... :-/
//
// TODO(b/255784681): Consider including cmdline arguments.
let target = &ir.current_target().0;
let crubit_features = {
let mut crubit_features: Vec<&str> = ir
.target_crubit_features(ir.current_target())
.into_iter()
.map(|feature| feature.short_name())
.collect();
crubit_features.sort();
if crubit_features.is_empty() {
"<none>".to_string()
} else {
crubit_features.join(", ")
}
};
format!(
"// Automatically @generated Rust bindings for the following C++ target:\n\
// {target}\n\
// Features: {crubit_features}\n"
)
};
// TODO(lukasza): Try to remove `#![rustfmt:skip]` - in theory it shouldn't
// be needed when `@generated` comment/keyword is present...
let rs_api = format!(
"{top_level_comment}\n\
#![rustfmt::skip]\n\
{rs_api}"
);
let rs_api_impl = format!(
"{top_level_comment}\n\
{rs_api_impl}"
);
Ok(Bindings { rs_api, rs_api_impl })
}
fn generate_doc_comment(
comment: Option<&str>,
source_loc: Option<&str>,
generate_source_loc_doc_comment: SourceLocationDocComment,
) -> TokenStream {
let source_loc = match generate_source_loc_doc_comment {
SourceLocationDocComment::Enabled => source_loc,
SourceLocationDocComment::Disabled => None,
};
let (comment, sep, source_loc) = match (comment, source_loc) {
(None, None) => return quote! {},
(None, Some(source_loc)) => ("", "", source_loc),
(Some(comment), Some(source_loc)) => (comment, "\n\n", source_loc),
(Some(comment), None) => (comment, "", ""),
};
// token_stream_printer (and rustfmt) don't put a space between /// and the doc
// comment, let's add it here so our comments are pretty.
let doc_comment = format!(" {comment}{sep}{source_loc}").replace('\n', "\n ");
quote! {#[doc = #doc_comment]}
}
fn generate_enum(db: &Database, enum_: &Enum) -> Result<GeneratedItem> {
let ident = crate::format_cc_ident(&enum_.identifier.identifier);
let namespace_qualifier = db.ir().namespace_qualifier(enum_)?.format_for_cc()?;
let fully_qualified_cc_name = quote! { #namespace_qualifier #ident }.to_string();
let name = make_rs_ident(&enum_.identifier.identifier);
let underlying_type = db.rs_type_kind(enum_.underlying_type.rs_type.clone())?;
let Some(enumerators) = &enum_.enumerators else {
return generate_unsupported(
db,
&UnsupportedItem::new_with_message(
&db.ir(),
enum_,
"b/322391132: Forward-declared (opaque) enums are not supported yet",
),
);
};
let enumerators = enumerators.iter().map(|enumerator| {
if let Some(unknown_attr) = &enumerator.unknown_attr {
let comment = format!(
"Omitting bindings for {ident}\nreason: unknown attribute(s): {unknown_attr}",
ident = &enumerator.identifier.identifier
);
return quote! {
__COMMENT__ #comment
};
}
let ident = make_rs_ident(&enumerator.identifier.identifier);
let value = if underlying_type.is_bool() {
if enumerator.value.wrapped_value == 0 {
quote! {false}
} else {
quote! {true}
}
} else {
if enumerator.value.is_negative {
Literal::i64_unsuffixed(enumerator.value.wrapped_value as i64).into_token_stream()
} else {
Literal::u64_unsuffixed(enumerator.value.wrapped_value).into_token_stream()
}
};
quote! {pub const #ident: #name = #name(#value);}
});
let item = quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type=#fully_qualified_cc_name)]
pub struct #name(#underlying_type);
impl #name {
#(#enumerators)*
}
impl From<#underlying_type> for #name {
fn from(value: #underlying_type) -> #name {
#name(value)
}
}
impl From<#name> for #underlying_type {
fn from(value: #name) -> #underlying_type {
value.0
}
}
};
Ok(GeneratedItem {
item,
features: BTreeSet::from([make_rs_ident("register_tool")]),
..Default::default()
})
}
fn generate_type_alias(db: &Database, type_alias: &TypeAlias) -> Result<GeneratedItem> {
let ident = make_rs_ident(&type_alias.identifier.identifier);
let doc_comment = generate_doc_comment(
type_alias.doc_comment.as_deref(),
Some(&type_alias.source_loc),
db.generate_source_loc_doc_comment(),
);
let underlying_type = db
.rs_type_kind(type_alias.underlying_type.rs_type.clone())
.with_context(|| format!("Failed to format underlying type for {}", type_alias))?;
Ok(quote! {
#doc_comment
pub type #ident = #underlying_type;
}
.into())
}
/// Generates Rust source code for a given `UnsupportedItem`.
fn generate_unsupported(db: &Database, item: &UnsupportedItem) -> Result<GeneratedItem> {
for error in &item.errors {
db.errors().insert(&error.to_error());
}
let source_loc = item.source_loc();
let source_loc = match &source_loc {
Some(loc) if db.generate_source_loc_doc_comment() == SourceLocationDocComment::Enabled => {
loc.as_ref()
}
_ => "",
};
let mut message = format!(
"{source_loc}{}Error while generating bindings for item '{}':\n",
if source_loc.is_empty() { "" } else { "\n" },
item.name.as_ref(),
);
for (index, error) in item.errors.iter().enumerate() {
message = format!("{message}{}{}", if index == 0 { "" } else { "\n\n" }, error.message,);
}
Ok(GeneratedItem { item: quote! { __COMMENT__ #message }, ..Default::default() })
}
/// Generates Rust source code for a given `Comment`.
fn generate_comment(comment: &Comment) -> Result<GeneratedItem> {
let text = comment.text.as_ref();
Ok(quote! { __COMMENT__ #text }.into())
}
fn generate_namespace(db: &Database, namespace: &Namespace) -> Result<GeneratedItem> {
let ir = db.ir();
let mut items = vec![];
let mut thunks = vec![];
let mut thunk_impls = vec![];
let mut assertions = vec![];
let mut features = BTreeSet::new();
for item_id in namespace.child_item_ids.iter() {
let item = ir.find_decl(*item_id).with_context(|| {
format!("Failed to look up namespace.child_item_ids for {:?}", namespace)
})?;
let generated = generate_item(db, item)?;
items.push(generated.item);
if !generated.thunks.is_empty() {
thunks.push(generated.thunks);
}
if !generated.thunk_impls.is_empty() {
thunk_impls.push(generated.thunk_impls);
}
if !generated.assertions.is_empty() {
assertions.push(generated.assertions);
}
features.extend(generated.features);
}
let reopened_namespace_idx = ir.get_reopened_namespace_idx(namespace.id)?;
// True if this is actually the module with the name `#name`, rather than e.g.
// `#name_0`, `#name_1`, etc.
let is_canonical_namespace_module =
ir.is_last_reopened_namespace(namespace.id, namespace.canonical_namespace_id)?;
let name = if is_canonical_namespace_module {
make_rs_ident(&namespace.name.identifier)
} else {
make_rs_ident(&format!("{}_{}", &namespace.name.identifier, reopened_namespace_idx))
};
let use_stmt_for_previous_namespace = if reopened_namespace_idx == 0 {
quote! {}
} else {
let previous_namespace_ident = make_rs_ident(&format!(
"{}_{}",
&namespace.name.identifier,
reopened_namespace_idx - 1
));
// unused_imports warns a re-export of an empty module. Currently, there is no
// infra in Crubit to tell if the (generated) module is empty, so we
// emit `allow(unused_imports)`. TODO(b/308949532): Skip re-export if
// previous module is empty (transitively).
quote! {
__HASH_TOKEN__ [allow(unused_imports)]
pub use super::#previous_namespace_ident::*; __NEWLINE__ __NEWLINE__
}
};
let use_stmt_for_inline_namespace = if namespace.is_inline && is_canonical_namespace_module {
// TODO(b/308949532): Skip re-export if the canonical module is empty
// (transitively).
quote! {
__HASH_TOKEN__ [allow(unused_imports)]
pub use #name::*; __NEWLINE__
}
} else {
quote! {}
};
let namespace_tokens = quote! {
pub mod #name {
#use_stmt_for_previous_namespace
#( #items __NEWLINE__ __NEWLINE__ )*
}
__NEWLINE__
#use_stmt_for_inline_namespace
};
Ok(GeneratedItem {
item: namespace_tokens,
features,
thunks: quote! { #( #thunks )* },
thunk_impls: quote! { #( #thunk_impls )* },
assertions: quote! { #( #assertions )* },
..Default::default()
})
}
#[derive(Clone, Debug, Default)]
struct GeneratedItem {
item: TokenStream,
thunks: TokenStream,
// C++ source code for helper functions.
thunk_impls: TokenStream,
assertions: TokenStream,
features: BTreeSet<Ident>,
}
impl From<TokenStream> for GeneratedItem {
fn from(item: TokenStream) -> Self {
GeneratedItem { item, ..Default::default() }
}
}
/// Returns generated bindings for an item, or `Err` if bindings generation
/// failed in such a way as to make the generated bindings as a whole invalid.
fn generate_item(db: &Database, item: &Item) -> Result<GeneratedItem> {
match generate_item_impl(db, item) {
Ok(generated) => Ok(generated),
Err(err) => {
let ir = db.ir();
if has_bindings(db, item) != HasBindings::Yes {
// We didn't guarantee that bindings would exist, so it is not invalid to
// write down the error but continue.
return generate_unsupported(db, &UnsupportedItem::new_with_cause(&ir, item, err));
}
Err(err)
}
}
}
/// The implementation of generate_item, without the error recovery logic.
///
/// Returns Err if bindings could not be generated for this item.
fn generate_item_impl(db: &Database, item: &Item) -> Result<GeneratedItem> {
let ir = db.ir();
if let Some(owning_target) = item.owning_target() {
if !ir.is_current_target(owning_target) {
return Ok(GeneratedItem::default());
}
}
let overloaded_funcs = db.overloaded_funcs();
let generated_item = match item {
Item::Func(func) => match db.generate_func(func.clone(), None)? {
None => GeneratedItem::default(),
Some((item, function_id)) => {
if overloaded_funcs.contains(&function_id) {
bail!("Cannot generate bindings for overloaded function")
} else {
(*item).clone()
}
}
},
Item::IncompleteRecord(incomplete_record) => {
generate_incomplete_record(db, incomplete_record)?
}
Item::Record(record) => generate_record(db, record)?,
Item::Enum(enum_) => generate_enum(db, enum_)?,
Item::TypeAlias(type_alias) => generate_type_alias(db, type_alias)?,
Item::UnsupportedItem(unsupported) => generate_unsupported(db, unsupported)?,
Item::Comment(comment) => generate_comment(comment)?,
Item::Namespace(namespace) => generate_namespace(db, namespace)?,
Item::UseMod(use_mod) => {
let UseMod { path, mod_name, .. } = &**use_mod;
let mod_name = make_rs_ident(&mod_name.identifier);
// TODO(b/308949532): Skip re-export if the module being used is empty
// (transitively).
quote! {
#[path = #path]
mod #mod_name;
__HASH_TOKEN__ [allow(unused_imports)]
pub use #mod_name::*;
}
.into()
}
Item::TypeMapOverride(type_override) => {
// (This shouldn't fail, since we replace with known Rust types via a string.)
let rs_type = RsTypeKind::new_type_map_override(type_override);
let disable_comment = format!(
"Type bindings for {cpp_type} suppressed due to being mapped to \
an existing Rust type ({rs_type})",
cpp_type = type_override.debug_name(&ir),
);
let assertions = if let Some(size_align) = &type_override.size_align {
generate_record::rs_size_align_assertions(rs_type, size_align)
} else {
quote! {}
};
GeneratedItem {
item: quote! {
__COMMENT__ #disable_comment
},
assertions,
..Default::default()
}
}
};
// Suppress bindings at the last minute, to collect other errors first.
if let HasBindings::No(reason) = has_bindings(db, item) {
return Err(reason.into());
}
Ok(generated_item)
}
#[derive(Clone, PartialEq, Eq)]
enum HasBindings {
/// This item is guaranteed to have bindings. If the translation unit
/// defining the item fails to generate bindings for it, it will not
/// compile.
Yes,
/// This item is not guaranteed to have bindings. There is no way to tell if
/// bindings were generated unless the item is defined in the current
/// translation unit.
Maybe,
/// These bindings are guaranteed not to exist.
No(NoBindingsReason),
}
#[derive(Clone, PartialEq, Eq)]
enum NoBindingsReason {
MissingRequiredFeatures {
context: Rc<str>,
missing_features: Vec<RequiredCrubitFeature>,
},
DependencyFailed {
context: Rc<str>,
error: Error,
},
/// This is directly unsupported.
Unsupported {
context: Rc<str>,
error: Error,
},
}
/// A missing set of crubit features caused by a capability that requires that
/// feature.
///
/// For example, if addition is not implemented due to missing the Experimental
/// feature on //foo, then we might have something like:
///
/// ```
/// RequiredCrubitFeature {
/// target: "//foo".into(),
/// item: "kFoo".into(),
/// missing_features: ir::CrubitFeature::Experimental.into(),
/// capability_description: "int addition".into(),
/// }
/// ```
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RequiredCrubitFeature {
pub target: BazelLabel,
pub item: Rc<str>,
pub missing_features: flagset::FlagSet<ir::CrubitFeature>,
pub capability_description: Rc<str>,
}
impl Display for RequiredCrubitFeature {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let Self { target, item, missing_features, capability_description } = self;
let feature_strings: Vec<&str> =
missing_features.into_iter().map(|feature| feature.aspect_hint()).collect();
write!(f, "{target} needs [{features}] for {item}", features = feature_strings.join(", "),)?;
if !capability_description.is_empty() {
write!(f, " ({capability_description})")?;
}
Ok(())
}
}
#[must_use]
fn has_bindings(db: &dyn BindingsGenerator, item: &Item) -> HasBindings {
let ir = db.ir();
match required_crubit_features(db, item) {
Ok(missing_features) if missing_features.is_empty() => {}
Ok(missing_features) => {
return HasBindings::No(NoBindingsReason::MissingRequiredFeatures {
context: item.debug_name(&db.ir()),
missing_features,
});
}
Err(error) => {
return HasBindings::No(NoBindingsReason::DependencyFailed {
context: item.debug_name(&db.ir()),
error,
});
}
}
if let Some(parent) = item.enclosing_item_id() {
let parent = ir.find_untyped_decl(parent);
match has_bindings(db, parent) {
HasBindings::No(no_parent_bindings) => {
return HasBindings::No(NoBindingsReason::DependencyFailed {
context: item.debug_name(&ir),
error: no_parent_bindings.into(),
});
}
HasBindings::Maybe => {
// This shouldn't happen, Maybe is meant for Func items.
return HasBindings::No(NoBindingsReason::DependencyFailed {
context: item.debug_name(&ir),
error: anyhow!("parent item might not be defined"),
});
}
HasBindings::Yes => {}
}
// TODO(b/200067824): Allow nested type items inside records.
if item.is_type_definition() {
if let ir::Item::Record(_) = parent {
return HasBindings::No(NoBindingsReason::Unsupported {
context: item.debug_name(&ir),
error: anyhow!(
"b/200067824: type definitions nested inside records are not yet supported"
),
});
}
}
}
match item {
// Function bindings aren't guaranteed, because they don't _need_ to be guaranteed. We
// choose not to generate code which relies on functions existing in other TUs.
Item::Func(..) => HasBindings::Maybe,
Item::TypeAlias(alias) => match db.rs_type_kind(alias.underlying_type.rs_type.clone()) {
Ok(_) => HasBindings::Yes,
Err(error) => HasBindings::No(NoBindingsReason::DependencyFailed {
context: alias.debug_name(&ir),
error,
}),
},
_ => HasBindings::Yes,
}
}
impl From<NoBindingsReason> for Error {
fn from(reason: NoBindingsReason) -> Error {
match reason {
NoBindingsReason::MissingRequiredFeatures { context, missing_features } => {
// This maybe could use .context(), but the ordering is backward.
let mut all_missing = vec![];
for missing in missing_features {
all_missing.push(missing.to_string());
}
anyhow!(
"Can't generate bindings for {context}, because of missing required features (<internal link>):\n{}",
all_missing.join("\n")
)
}
NoBindingsReason::DependencyFailed { context, error } => error.context(format!(
"Can't generate bindings for {context} due to missing bindings for its dependency"
)),
NoBindingsReason::Unsupported { context, error } => error.context(format!(
"Can't generate bindings for {context}, because it is unsupported"
)),
}
}
}
/// Returns the list of features required to use the item which are not yet
/// enabled.
///
/// If the item doesn't have a defining target, the return value is meaningless,
/// and bindings will always be generated.
///
/// If the item does have a defining target, and it doesn't enable the specified
/// features, then bindings are suppressed for this item.
fn required_crubit_features(
db: &dyn BindingsGenerator,
item: &Item,
) -> Result<Vec<RequiredCrubitFeature>> {
let mut missing_features = vec![];
let ir = &db.ir();
let require_any_feature =
|missing_features: &mut Vec<RequiredCrubitFeature>,
alternative_required_features: flagset::FlagSet<ir::CrubitFeature>,
capability_description: &dyn Fn() -> Rc<str>| {
// We refuse to generate bindings if either the definition of an item, or
// instantiation (if it is a template) of an item are in a translation unit
// which doesn't have the required Crubit features.
for target in item.defining_target().into_iter().chain(item.owning_target()) {
let enabled_features = ir.target_crubit_features(target);
if (alternative_required_features & enabled_features).is_empty() {
missing_features.push(RequiredCrubitFeature {
target: target.clone(),
item: item.debug_name(ir),
missing_features: alternative_required_features,
capability_description: capability_description(),
});
}
}
};
let require_rs_type_kind = |missing_features: &mut Vec<RequiredCrubitFeature>,
rs_type_kind: &RsTypeKind,
context: &dyn Fn() -> Rc<str>| {
for target in item.defining_target().into_iter().chain(item.owning_target()) {
let (missing, desc) =
rs_type_kind.required_crubit_features(ir.target_crubit_features(target));
if !missing.is_empty() {
let context = context();
let capability_description = if desc.is_empty() {
context
} else if context.is_empty() {
desc.into()
} else {
format!("{context}: {desc}").into()
};
missing_features.push(RequiredCrubitFeature {
target: target.clone(),
item: item.debug_name(ir),
missing_features: missing,
capability_description,
});
}
}
};
if let Some(unknown_attr) = item.unknown_attr() {
require_any_feature(&mut missing_features, ir::CrubitFeature::Experimental.into(), &|| {
format!("unknown attribute(s): {unknown_attr}").into()
});
}
match item {
Item::UnsupportedItem(..) => {}
Item::Func(func) => {
if func.name == UnqualifiedIdentifier::Destructor {
// We support destructors in supported even though they use some features we
// don't generally support with that feature set, because in this
// particular case, it's safe.
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Supported.into(),
&|| "destructors".into(),
);
} else {
let return_type = db.rs_type_kind(func.return_type.rs_type.clone())?;
require_rs_type_kind(&mut missing_features, &return_type, &|| "return type".into());
for (i, param) in func.params.iter().enumerate() {
let param_type = db.rs_type_kind(param.type_.rs_type.clone())?;
require_rs_type_kind(&mut missing_features, &param_type, &|| {
format!("the type of {} (parameter #{i})", &param.identifier).into()
});
}
if func.is_extern_c {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Supported.into(),
&|| "extern \"C\" function".into(),
);
} else {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::NonExternCFunctions | ir::CrubitFeature::Experimental,
&|| "non-extern \"C\" function".into(),
);
}
if !func.has_c_calling_convention {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| "non-C calling convention".into(),
);
}
if func.is_noreturn {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| "[[noreturn]] attribute".into(),
);
}
if func.nodiscard.is_some() {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| "[[nodiscard]] attribute".into(),
);
}
if func.deprecated.is_some() {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| "[[deprecated]] attribute".into(),
);
}
for param in &func.params {
if let Some(unknown_attr) = &param.unknown_attr {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| {
format!(
"param {param} has unknown attribute(s): {unknown_attr}",
param = &param.identifier.identifier
)
.into()
},
);
}
}
}
}
Item::Record(record) => {
require_rs_type_kind(
&mut missing_features,
&RsTypeKind::new_record(record.clone(), &db.ir())?,
&|| "".into(),
);
}
Item::TypeAlias(alias) => {
require_rs_type_kind(
&mut missing_features,
&new_type_alias(db, alias.clone())?,
&|| "".into(),
);
}
Item::Enum(e) => {
require_rs_type_kind(
&mut missing_features,
&RsTypeKind::new_enum(e.clone(), &db.ir())?,
&|| "".into(),
);
}
Item::Namespace(_) => {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Supported.into(),
&|| "namespace".into(),
);
}
Item::IncompleteRecord(_) => {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| "incomplete type".into(),
);
}
Item::Comment { .. } | Item::UseMod { .. } => {}
Item::TypeMapOverride { .. } => {
require_any_feature(
&mut missing_features,
ir::CrubitFeature::Experimental.into(),
&|| "type map override".into(),
);
}
}
Ok(missing_features)
}
// Returns the Rust code implementing bindings, plus any auxiliary C++ code
// needed to support it.
fn generate_bindings_tokens(
ir: Rc<IR>,
crubit_support_path_format: &str,
errors: Rc<dyn ErrorReporting>,
generate_source_loc_doc_comment: SourceLocationDocComment,
) -> Result<BindingsTokens> {
let db = Database::new(ir.clone(), errors, generate_source_loc_doc_comment);
let mut items = vec![];
let mut thunks = vec![];
let mut thunk_impls = vec![
generate_rs_api_impl_includes(&db, crubit_support_path_format)?,
quote! {
__HASH_TOKEN__ pragma clang diagnostic push __NEWLINE__
// Disable Clang thread-safety-analysis warnings that would otherwise
// complain about thunks that call mutex locking functions in an unpaired way.
__HASH_TOKEN__ pragma clang diagnostic ignored "-Wthread-safety-analysis" __NEWLINE__
},
];
let mut assertions = vec![];
let mut features = BTreeSet::new();
// For #![rustfmt::skip].
features.insert(make_rs_ident("custom_inner_attributes"));
for top_level_item_id in ir.top_level_item_ids() {
let item =
ir.find_decl(*top_level_item_id).context("Failed to look up ir.top_level_item_ids")?;
let generated = generate_item(&db, item)?;
items.push(generated.item);
if !generated.thunks.is_empty() {
thunks.push(generated.thunks);
}
if !generated.assertions.is_empty() {
assertions.push(generated.assertions);
}
if !generated.thunk_impls.is_empty() {
thunk_impls.push(generated.thunk_impls);
}
features.extend(generated.features);
}
thunk_impls.push(quote! {
__NEWLINE__
__HASH_TOKEN__ pragma clang diagnostic pop __NEWLINE__
// To satisfy http://cs/symbol:devtools.metadata.Presubmit.CheckTerminatingNewline check.
__NEWLINE__
});
let mod_detail = if thunks.is_empty() {
quote! {}
} else {
quote! {
mod detail {
#[allow(unused_imports)]
use super::*;
extern "C" {
#( #thunks )*
}
}
}
};
// Allows the use of #[__crubit::foo] attributes to control the behavior of
// cc_bindings_from_rs on the generated code.
//
// Note that we use `__crubit`, not `crubit`. This way, namespaces and types can
// be named `crubit` without causing obscure internal failures during
// bindings generation. In particular, well, crubit itself does use
// `namespace crubit`...
//
// Note also that there is only one tool namespace we use, __crubit. So we can
// use the existence of a register_tool feature requirement to signal
// whether or not we need to bother registering __crubit, and make the
// bindings more compact for headers that don't define any types.
let register_crubit_tool = if features.contains(&make_rs_ident("register_tool")) {
quote! {#![register_tool(__crubit)] __NEWLINE__}
} else {
quote! {}
};
let features = if features.is_empty() {
quote! {}
} else {
quote! {
#![feature( #(#features),* )] __NEWLINE__
#![allow(stable_features)]
}
};
let assertions = if assertions.is_empty() {
quote! {}
} else {
quote! {
const _: () = { __NEWLINE__
#( #assertions __NEWLINE__ __NEWLINE__ )*
}; __NEWLINE__
}
};
Ok(BindingsTokens {
rs_api: quote! {
#features __NEWLINE__
#![no_std] __NEWLINE__
#register_crubit_tool
// `rust_builtin_type_abi_assumptions.md` documents why the generated
// bindings need to relax the `improper_ctypes_definitions` warning
// for `char` (and possibly for other built-in types in the future).
#![allow(improper_ctypes)] __NEWLINE__
// C++ names don't follow Rust guidelines:
#![allow(nonstandard_style)] __NEWLINE__
#![deny(warnings)] __NEWLINE__ __NEWLINE__
#( #items __NEWLINE__ __NEWLINE__ )*
#mod_detail __NEWLINE__ __NEWLINE__
#assertions
},
rs_api_impl: quote! {#(#thunk_impls __NEWLINE__ __NEWLINE__ )*},
})
}
/// Formats a C++ identifier. Panics if `ident` is a C++ reserved keyword.
fn format_cc_ident(ident: &str) -> TokenStream {
code_gen_utils::format_cc_ident(ident).expect("IR should only contain valid C++ identifiers")
}
fn rs_type_kind(db: &dyn BindingsGenerator, ty: ir::RsType) -> Result<RsTypeKind> {
if let Some(unknown_attr) = &ty.unknown_attr {
// In most places, we only bail for unknown attributes in supported. However,
// it's difficult and expensive to generate an RsTypeKind differently
// depending on the translation unit for the item that contains it.
// Rather than trying to keep going in experimental, we bail
// unconditionally.
//
// The correct fix for this error is to add support for the attributes which are
// not yet understood, but need to be used in practice.
bail!("unknown attribute(s): {unknown_attr}")
}
let ir = db.ir();
// The lambdas deduplicate code needed by multiple `match` branches.
let get_type_args = || -> Result<Vec<RsTypeKind>> {
ty.type_args.iter().map(|type_arg| db.rs_type_kind(type_arg.clone())).collect()
};
let get_pointee = || -> Result<Rc<RsTypeKind>> {
if ty.type_args.len() != 1 {
bail!("Missing pointee/referent type (need exactly 1 type argument): {:?}", ty);
}
Ok(Rc::new(get_type_args()?.remove(0)))
};
let get_lifetime = || -> Result<Lifetime> {
if ty.lifetime_args.len() != 1 {
bail!("Missing reference lifetime (need exactly 1 lifetime argument): {:?}", ty);
}
let lifetime_id = ty.lifetime_args[0];
ir.get_lifetime(lifetime_id)
.ok_or_else(|| anyhow!("no known lifetime with id {lifetime_id:?}"))
.map(Lifetime::from)
};
let result = match ty.name.as_deref() {
None => {
ensure!(
ty.type_args.is_empty(),
"Type arguments on records nor type aliases are not yet supported: {:?}",
ty
);
let item = ir.item_for_type(&ty)?;
let fallback_type = match item {
// Type aliases are unique among items, in that if the item defining the alias fails
// to receive bindings, we can still use the aliased type.
ir::Item::TypeAlias(alias) => Some(&alias.underlying_type.rs_type),
_ => None,
};
match (has_bindings(db, item), fallback_type) {
(HasBindings::Yes, _) => {}
// Additionally, we should not "see through" type aliases that are specifically not
// on targets that intend to support Rust users of those type aliases.
// (If we did, then a C++ library owner could break Rust callers, which is a
// maintenance responsibility that they did not sign up for!)
(has_bindings, Some(fallback_type))
if !matches!(
has_bindings,
HasBindings::No(NoBindingsReason::MissingRequiredFeatures { .. })
) =>
{
return db.rs_type_kind(fallback_type.clone());
}
(HasBindings::Maybe, _) => {
bail!(
"Type {} may or may not exist, and cannot be used.",
item.debug_name(&ir)
);
}
(HasBindings::No(reason), _) => {
return Err(reason.into());
}
}
match item {
Item::IncompleteRecord(incomplete_record) => RsTypeKind::IncompleteRecord {
incomplete_record: incomplete_record.clone(),
crate_path: Rc::new(CratePath::new(
&ir,
ir.namespace_qualifier(incomplete_record)?,
rs_imported_crate_name(&incomplete_record.owning_target, &ir),
)),
},
Item::Record(record) => RsTypeKind::new_record(record.clone(), &ir)?,
Item::Enum(enum_) => RsTypeKind::new_enum(enum_.clone(), &ir)?,
Item::TypeAlias(type_alias) => new_type_alias(db, type_alias.clone())?,
Item::TypeMapOverride(type_map_override) => {
RsTypeKind::new_type_map_override(type_map_override)
}
other_item => bail!("Item does not define a type: {other_item:?}"),
}
}
Some(name) => match name {
"*mut" => RsTypeKind::Pointer { pointee: get_pointee()?, mutability: Mutability::Mut },
"*const" => {
RsTypeKind::Pointer { pointee: get_pointee()?, mutability: Mutability::Const }
}
"&mut" => RsTypeKind::Reference {
referent: get_pointee()?,
mutability: Mutability::Mut,
lifetime: get_lifetime()?,
},
"&" => RsTypeKind::Reference {
referent: get_pointee()?,
mutability: Mutability::Const,
lifetime: get_lifetime()?,
},
"#RvalueReference mut" => RsTypeKind::RvalueReference {
referent: get_pointee()?,
mutability: Mutability::Mut,
lifetime: get_lifetime()?,
},
"#RvalueReference const" => RsTypeKind::RvalueReference {
referent: get_pointee()?,
mutability: Mutability::Const,
lifetime: get_lifetime()?,
},
"Option" => {
let mut type_args = get_type_args()?;
ensure!(
type_args.len() == 1,
"Option should have exactly 1 type argument (got {})",
type_args.len()
);
RsTypeKind::Option(Rc::new(type_args.remove(0)))
}
name => {
let mut type_args = get_type_args()?;
if let Some(primitive) = PrimitiveType::from_str(name) {
if !type_args.is_empty() {
bail!("{name} type must not have type arguments: {:?}", ty);
}
RsTypeKind::Primitive(primitive)
} else if let Some(abi) = name.strip_prefix("#funcPtr ") {
// Assert that function pointers in the IR either have static lifetime or
// no lifetime.
match get_lifetime() {
Err(_) => (), // No lifetime
Ok(lifetime) => assert_eq!(lifetime.0.as_ref(), "static"),
}
assert!(
!type_args.is_empty(),
"In well-formed IR function pointers include at least the return type",
);
ensure!(
type_args.iter().all(|t| t.is_c_abi_compatible_by_value()),
"Either the return type or some of the parameter types require \
an FFI thunk (and function pointers don't have a thunk)",
);
RsTypeKind::FuncPtr {
abi: abi.into(),
return_type: Rc::new(type_args.remove(type_args.len() - 1)),
param_types: Rc::from(type_args),
}
} else {
RsTypeKind::Other {
name: name.into(),
type_args: Rc::from(type_args),
is_same_abi: true,
}
}
}
},
};
Ok(result)
}
fn new_type_alias(db: &dyn BindingsGenerator, type_alias: Rc<TypeAlias>) -> Result<RsTypeKind> {
let ir = db.ir();
let underlying_type = Rc::new(db.rs_type_kind(type_alias.underlying_type.rs_type.clone())?);
let crate_path = Rc::new(CratePath::new(
&ir,
ir.namespace_qualifier(&type_alias)?,
rs_imported_crate_name(&type_alias.owning_target, &ir),
));
Ok(RsTypeKind::TypeAlias { type_alias: type_alias, crate_path, underlying_type })
}
fn cpp_type_name_for_record(record: &Record, ir: &IR) -> Result<TokenStream> {
let tagless = cc_tagless_type_name_for_record(record, ir)?;
let tag_kind = cc_tag_kind(record);
Ok(quote! { #tag_kind #tagless })
}
fn cc_tagless_type_name_for_record(record: &Record, ir: &IR) -> Result<TokenStream> {
let ident = crate::format_cc_ident(record.cc_name.as_ref());
let namespace_qualifier = ir.namespace_qualifier(record)?.format_for_cc()?;
Ok(quote! { #namespace_qualifier #ident })
}
fn cpp_type_name_for_item(item: &ir::Item, ir: &IR) -> Result<TokenStream> {
match item {
Item::IncompleteRecord(incomplete_record) => {
let ident = crate::format_cc_ident(incomplete_record.cc_name.as_ref());
let namespace_qualifier = ir.namespace_qualifier(incomplete_record)?.format_for_cc()?;
let tag_kind = incomplete_record.record_type;
Ok(quote! { #tag_kind #namespace_qualifier #ident })
}
Item::Record(record) => cpp_type_name_for_record(record, ir),
Item::Enum(enum_) => {
let ident = crate::format_cc_ident(&enum_.identifier.identifier);
let qualifier = cc_qualified_path_prefix(item, ir)?;
Ok(quote! { #qualifier #ident })
}
Item::TypeAlias(type_alias) => {
let ident = crate::format_cc_ident(&type_alias.identifier.identifier);
let qualifier = cc_qualified_path_prefix(item, ir)?;
Ok(quote! { #qualifier #ident })
}
Item::TypeMapOverride(type_map_override) => type_map_override
.cc_name
.parse::<TokenStream>()
.map_err(|_| anyhow!("malformed type name: {:?}", type_map_override.cc_name)),
_ => bail!("Item does not define a type: {:?}", item),
}
}
/// Returns the namespace / class qualifiers necessary to access the item.
///
/// For example, for `namespace x { struct Y { using X = int; }; }`, the prefix
/// for `X` is `x::Y::`.
fn cc_qualified_path_prefix(item: &ir::Item, ir: &ir::IR) -> Result<TokenStream> {
let Some(parent) = item.enclosing_item_id() else {
return Ok(quote! {});
};
let parent: &ir::Item = ir.find_decl(parent)?;
match parent {
ir::Item::Namespace(_) => Ok(ir.namespace_qualifier(item)?.format_for_cc()?),
ir::Item::Record(r) => {
let name = cc_tagless_type_name_for_record(r, ir)?;
Ok(quote! {#name ::})
}
_ => bail!("Unexpected enclosing item: {item:?}"),
}
}
fn cc_tag_kind(record: &ir::Record) -> TokenStream {
if record.is_anon_record_with_typedef {
quote! {}
} else {
record.record_type.into_token_stream()
}
}
// Maps a Rust ABI [1] into a Clang attribute. See also
// `ConvertCcCallConvIntoRsApi` in importer.cc.
// [1]
// https://doc.rust-lang.org/reference/items/functions.html#extern-function-qualifier
fn format_cc_call_conv_as_clang_attribute(rs_abi: &str) -> Result<TokenStream> {
match rs_abi {
"cdecl" => Ok(quote! {}),
"fastcall" => Ok(quote! { __attribute__((fastcall)) }),
"stdcall" => Ok(quote! { __attribute__((stdcall)) }),
"thiscall" => Ok(quote! { __attribute__((thiscall)) }),
"vectorcall" => Ok(quote! { __attribute__((vectorcall)) }),
_ => bail!("Unsupported ABI: {}", rs_abi),
}
}
pub(crate) fn format_cpp_type(ty: &ir::CcType, ir: &IR) -> Result<TokenStream> {
// Formatting *both* pointers *and* references as pointers, because:
// - Pointers and references have the same representation in the ABI.
// - Clang's `-Wreturn-type-c-linkage` warns when using references in C++
// function thunks declared as `extern "C"` (see b/238681766).
format_cpp_type_inner(ty, ir, /* references_ok= */ false)
}
fn format_cpp_type_inner(ty: &ir::CcType, ir: &IR, references_ok: bool) -> Result<TokenStream> {
let const_fragment = if ty.is_const {
quote! {const}
} else {
quote! {}
};
if let Some(ref name) = ty.name {
match name.as_ref() {
mut name @ ("*" | "&" | "&&") => {
if ty.type_args.len() != 1 {
bail!("Invalid pointer type (need exactly 1 type argument): {:?}", ty);
}
let nested_type = format_cpp_type_inner(&ty.type_args[0], ir, references_ok)?;
if !references_ok {
name = "*";
}
let ptr = match name {
"*" => quote! {*},
"&" => quote! {&},
"&&" => quote! {&&},
_ => unreachable!(),
};
Ok(quote! {#nested_type #ptr #const_fragment})
}
cpp_type_name => match cpp_type_name.strip_prefix("#funcValue ") {
None => {
if !ty.type_args.is_empty() {
bail!("Type not yet supported: {:?}", ty);
}
// Not using `code_gen_utils::format_cc_ident`, because
// `cpp_type_name` may be a C++ reserved keyword (e.g.
// `int`).
let cc_ident: TokenStream = cpp_type_name.parse().unwrap();
Ok(quote! { #cc_ident #const_fragment })
}
Some(abi) => match ty.type_args.split_last() {
None => bail!("funcValue type without a return type: {:?}", ty),
Some((ret_type, param_types)) => {
// Function pointer types don't ignore references, but luckily,
// `-Wreturn-type-c-linkage` does. So we can just re-enable references now
// so that the function type is exactly correct.
let ret_type =
format_cpp_type_inner(ret_type, ir, /* references_ok= */ true)?;
let param_types = param_types
.iter()
.map(|t| format_cpp_type_inner(t, ir, /* references_ok= */ true))
.collect::<Result<Vec<_>>>()?;
let attr = format_cc_call_conv_as_clang_attribute(abi)?;
// `type_identity_t` is used below to avoid having to
// emit spiral-like syntax where some syntax elements of
// an inner type (e.g. function type as below) can
// surround syntax elements of an outer type (e.g. a
// pointer type). Compare: `int (*foo)(int, int)` VS
// `type_identity_t<int(int, int)>* foo`.
Ok(quote! { crubit::type_identity_t<
#ret_type ( #( #param_types ),* ) #attr
> })
}
},
},
}
} else {
let item = ir.item_for_type(ty)?;
let type_name = cpp_type_name_for_item(item, ir)?;
Ok(quote! {#const_fragment #type_name})
}
}
pub(crate) fn crate_root_path_tokens(ir: &IR) -> TokenStream {
match ir.crate_root_path().as_deref().map(make_rs_ident) {
None => quote! { crate },
Some(crate_root_path) => quote! { crate :: #crate_root_path },
}
}
fn generate_rs_api_impl_includes(
db: &Database,
crubit_support_path_format: &str,
) -> Result<TokenStream> {
let ir = db.ir();
let mut internal_includes = BTreeSet::new();
internal_includes.insert(CcInclude::memory()); // ubiquitous.
if ir.records().next().is_some() {
internal_includes.insert(CcInclude::cstddef());
internal_includes.insert(CcInclude::SupportLibHeader(
crubit_support_path_format.into(),
"internal/sizeof.h".into(),
));
};
for crubit_header in ["internal/cxx20_backports.h", "internal/offsetof.h"] {
internal_includes.insert(CcInclude::SupportLibHeader(
crubit_support_path_format.into(),
crubit_header.into(),
));
}
let internal_includes = format_cc_includes(&internal_includes);
// In order to generate C++ thunk in all the cases Clang needs to be able to
// access declarations from public headers of the C++ library. We don't
// process these includes via `format_cc_includes` to preserve their
// original order (some libraries require certain headers to be included
// first - e.g. `config.h`).
let ir_includes =
ir.public_headers().map(|hdr| CcInclude::user_header(hdr.name.clone())).collect_vec();
Ok(quote! {
#internal_includes
__NEWLINE__
__COMMENT__ "Public headers of the C++ library being wrapped."
#( #ir_includes )* __NEWLINE__
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use ir_testing::{make_ir_from_items, retrieve_func, with_lifetime_macros};
use static_assertions::{assert_impl_all, assert_not_impl_any};
use token_stream_matchers::{
assert_cc_matches, assert_cc_not_matches, assert_rs_matches, assert_rs_not_matches,
};
use token_stream_printer::rs_tokens_to_formatted_string_for_tests;
pub fn ir_from_cc(header: &str) -> Result<IR> {
ir_testing::ir_from_cc(multiplatform_testing::test_platform(), header)
}
pub fn ir_from_cc_dependency(header: &str, dep_header: &str) -> Result<IR> {
ir_testing::ir_from_cc_dependency(
multiplatform_testing::test_platform(),
header,
dep_header,
)
}
pub fn ir_record(name: &str) -> Record {
ir_testing::ir_record(multiplatform_testing::test_platform(), name)
}
pub fn generate_bindings_tokens(ir: IR) -> Result<BindingsTokens> {
super::generate_bindings_tokens(
Rc::new(ir),
"crubit/rs_bindings_support",
Rc::new(IgnoreErrors),
SourceLocationDocComment::Enabled,
)
}
pub fn db_from_cc(cc_src: &str) -> Result<Database> {
Ok(Database::new(
Rc::new(ir_from_cc(cc_src)?),
Rc::new(ErrorReport::new()),
SourceLocationDocComment::Enabled,
))
}
#[test]
fn test_disable_thread_safety_warnings() -> Result<()> {
let ir = ir_from_cc("inline void foo() {}")?;
let rs_api_impl = generate_bindings_tokens(ir)?.rs_api_impl;
assert_cc_matches!(
rs_api_impl,
quote! {
...
__HASH_TOKEN__ pragma clang diagnostic push
__HASH_TOKEN__ pragma clang diagnostic ignored "-Wthread-safety-analysis"
...
__HASH_TOKEN__ pragma clang diagnostic pop
...
}
);
Ok(())
}
// TODO(b/200067824): These should generate nested types.
#[test]
fn test_nested_type_definitions() -> Result<()> {
for nested_type in ["enum NotPresent {};", "struct NotPresent {};", "struct NotPresent;"] {
let ir = ir_from_cc(&format!(
r#"
struct SomeStruct final {{
{nested_type}
}};
SomeStruct::NotPresent* AlsoNotPresent();
"#
))?;
let BindingsTokens { rs_api, .. } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! { NotPresent });
assert_rs_not_matches!(rs_api, quote! { AlsoNotPresent });
}
Ok(())
}
/// Unlike other nested type definitions, typedefs can use the aliased type
/// instead.
#[test]
fn test_typedef_member() -> Result<()> {
let ir = ir_from_cc(
r#"
struct SomeStruct final {
typedef int Type;
};
inline SomeStruct::Type Function() {return 0;}
"#,
)?;
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
// TODO(b/200067824): This should use the alias's real name in Rust, as well.
assert_rs_matches!(rs_api, quote! { pub fn Function() -> ::core::ffi::c_int { ... } },);
assert_cc_matches!(
rs_api_impl,
quote! {
extern "C" SomeStruct::Type __rust_thunk___Z8Functionv(){ return Function(); }
},
);
Ok(())
}
#[test]
fn test_struct_from_other_target() -> Result<()> {
let ir = ir_from_cc_dependency("// intentionally empty", "struct SomeStruct {};")?;
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! { SomeStruct });
assert_cc_not_matches!(rs_api_impl, quote! { SomeStruct });
Ok(())
}
#[test]
fn test_func_ptr_where_params_are_primitive_types() -> Result<()> {
let ir = ir_from_cc(r#" int (*get_ptr_to_func())(float, double); "#)?;
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_matches!(
rs_api,
quote! {
#[inline(always)]
pub fn get_ptr_to_func() -> Option<extern "C" fn (f32, f64) -> ::core::ffi::c_int> {
unsafe { crate::detail::__rust_thunk___Z15get_ptr_to_funcv() }
}
}
);
assert_rs_matches!(
rs_api,
quote! {
mod detail {
#[allow(unused_imports)]
use super::*;
extern "C" {
#[link_name = "_Z15get_ptr_to_funcv"]
pub(crate) fn __rust_thunk___Z15get_ptr_to_funcv()
-> Option<extern "C" fn(f32, f64) -> ::core::ffi::c_int>;
}
}
}
);
// Verify that no C++ thunk got generated.
assert_cc_not_matches!(rs_api_impl, quote! { __rust_thunk___Z15get_ptr_to_funcv });
// TODO(b/217419782): Add another test for more exotic calling conventions /
// abis.
// TODO(b/276461979): Add another test for pointer to a function that requires
// thunks - e.g. because it takes/returns structs value. See also
// b/276461979 and <internal link>
Ok(())
}
#[test]
fn test_func_ref() -> Result<()> {
let ir = ir_from_cc(r#" int (&get_ref_to_func())(float, double); "#)?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[inline(always)]
pub fn get_ref_to_func() -> extern "C" fn (f32, f64) -> ::core::ffi::c_int {
unsafe { crate::detail::__rust_thunk___Z15get_ref_to_funcv() }
}
}
);
Ok(())
}
#[test]
fn test_func_ptr_with_non_static_lifetime() -> Result<()> {
let ir = ir_from_cc(&with_lifetime_macros(
r#"
int (* $a get_ptr_to_func())(float, double); "#,
))?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_cc_matches!(rs_api, {
let txt = "Generated from: google3/ir_from_cc_virtual_header.h;l=33\n\
Error while generating bindings for item 'get_ptr_to_func':\n\
Type may not be annotated with lifetimes";
quote! { __COMMENT__ #txt }
});
Ok(())
}
#[test]
fn test_func_ptr_where_params_are_raw_ptrs() -> Result<()> {
let ir = ir_from_cc(r#" const int* (*get_ptr_to_func())(const int*); "#)?;
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_matches!(
rs_api,
quote! {
#[inline(always)]
pub fn get_ptr_to_func() -> Option<unsafe extern "C" fn (*const ::core::ffi::c_int) -> *const ::core::ffi::c_int> {
unsafe { crate::detail::__rust_thunk___Z15get_ptr_to_funcv() }
}
}
);
assert_rs_matches!(
rs_api,
quote! {
mod detail {
#[allow(unused_imports)]
use super::*;
extern "C" {
#[link_name = "_Z15get_ptr_to_funcv"]
pub(crate) fn __rust_thunk___Z15get_ptr_to_funcv()
-> Option<unsafe extern "C" fn(*const ::core::ffi::c_int) -> *const ::core::ffi::c_int>;
}
}
}
);
// Verify that no C++ thunk got generated.
assert_cc_not_matches!(rs_api_impl, quote! { __rust_thunk___Z15get_ptr_to_funcv });
// TODO(b/217419782): Add another test where params (and the return
// type) are references with lifetimes. Something like this:
// #pragma clang lifetime_elision
// const int& (*get_ptr_to_func())(const int&, const int&); "#)?;
// 1) Need to investigate why this fails - seeing raw pointers in Rust seems to
// indicate that no lifetimes are present at the `importer.cc` level. Maybe
// lifetime elision doesn't support this scenario? Unclear how to explicitly
// apply [[clang::annotate("lifetimes", "a, b -> a")]] to the _inner_
// function.
// 2) It is important to have 2 reference parameters, so see if the problem of
// passing `lifetimes` by value would have been caught - see:
// cl/428079010/depot/rs_bindings_from_cc/
// importer.cc?version=s6#823
// TODO(b/217419782): Decide what to do if the C++ pointer is *not*
// annotated with a lifetime - emit `unsafe fn(...) -> ...` in that
// case?
Ok(())
}
mod custom_abi_tests {
use super::*;
use ir_matchers::assert_ir_matches;
#[test]
fn test_func_ptr_with_custom_abi() -> Result<()> {
if multiplatform_testing::test_platform() != multiplatform_testing::Platform::X86Linux {
return Ok(());
}
let ir =
ir_from_cc(r#" int (*get_ptr_to_func())(float, double) [[clang::vectorcall]]; "#)?;
// Verify that the test input correctly represents what we intend to
// test - we want [[clang::vectorcall]] to apply to the returned
// function pointer, but *not* apply to the `get_ptr_to_func` function.
assert_ir_matches!(
ir,
quote! {
Func(Func {
name: "get_ptr_to_func", ...
return_type: MappedType {
rs_type: RsType {
name: Some("Option"), ...
type_args: [RsType { name: Some("#funcPtr vectorcall"), ... }], ...
},
cpp_type: CcType {
name: Some("*"), ...
type_args: [CcType { name: Some("#funcValue vectorcall"), ... }], ...
},
}, ...
has_c_calling_convention: true, ...
}),
}
);
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
// Check that the custom "vectorcall" ABI gets propagated into the
// return type (i.e. into `extern "vectorcall" fn`).
assert_rs_matches!(
rs_api,
quote! {
#[inline(always)]
pub fn get_ptr_to_func() -> Option<extern "vectorcall" fn (f32, f64) -> ::core::ffi::c_int> {
unsafe { crate::detail::__rust_thunk___Z15get_ptr_to_funcv() }
}
}
);
// The usual `extern "C"` ABI should be used for "get_ptr_to_func".
assert_rs_matches!(
rs_api,
quote! {
mod detail {
#[allow(unused_imports)]
use super::*;
extern "C" {
#[link_name = "_Z15get_ptr_to_funcv"]
pub(crate) fn __rust_thunk___Z15get_ptr_to_funcv()
-> Option<extern "vectorcall" fn(f32, f64) -> ::core::ffi::c_int>;
}
}
}
);
// Verify that no C++ thunk got generated.
assert_cc_not_matches!(rs_api_impl, quote! { __rust_thunk___Z15get_ptr_to_funcv });
Ok(())
}
#[test]
fn test_func_ptr_with_custom_abi_thunk() -> Result<()> {
if multiplatform_testing::test_platform() != multiplatform_testing::Platform::X86Linux {
return Ok(());
}
// Using an `inline` keyword forces generation of a C++ thunk in
// `rs_api_impl` (i.e. exercises `format_cpp_type`,
// `format_cc_call_conv_as_clang_attribute` and similar code).
let ir = ir_from_cc(
r#"
inline int (*inline_get_ptr_to_func())(float, double) [[clang::vectorcall]];
"#,
)?;
// Verify that the test input correctly represents what we intend to
// test - we want [[clang::vectorcall]] to apply to the returned
// function pointer, but *not* apply to the `get_ptr_to_func` function.
assert_ir_matches!(
ir,
quote! {
Func(Func {
name: "inline_get_ptr_to_func", ...
return_type: MappedType {
rs_type: RsType {
name: Some("Option"), ...
type_args: [RsType { name: Some("#funcPtr vectorcall"), ... }], ...
},
cpp_type: CcType {
name: Some("*"), ...
type_args: [CcType { name: Some("#funcValue vectorcall"), ... }], ...
},
}, ...
has_c_calling_convention: true, ...
}),
}
);
// This test is quite similar to `test_func_ptr_thunk` - the main
// difference is verification of the `__attribute__((vectorcall))` in
// the expected signature of the generated thunk below.
let rs_api_impl = generate_bindings_tokens(ir)?.rs_api_impl;
assert_cc_matches!(
rs_api_impl,
quote! {
extern "C" crubit::type_identity_t<
int(float , double) __attribute__((vectorcall))
>* __rust_thunk___Z22inline_get_ptr_to_funcv() {
return inline_get_ptr_to_func();
}
}
);
Ok(())
}
#[test]
fn test_custom_abi_thunk() -> Result<()> {
if multiplatform_testing::test_platform() != multiplatform_testing::Platform::X86Linux {
return Ok(());
}
let ir = ir_from_cc(
r#"
float f_vectorcall_calling_convention(float p1, float p2) [[clang::vectorcall]];
double f_c_calling_convention(double p1, double p2);
"#,
)?;
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_matches!(
rs_api,
quote! {
#[inline(always)]
pub fn f_vectorcall_calling_convention(p1: f32, p2: f32) -> f32 {
unsafe {
crate::detail::__rust_thunk___Z31f_vectorcall_calling_conventionff(p1, p2)
}
}
}
);
assert_rs_matches!(
rs_api,
quote! {
#[inline(always)]
pub fn f_c_calling_convention(p1: f64, p2: f64) -> f64 {
unsafe { crate::detail::__rust_thunk___Z22f_c_calling_conventiondd(p1, p2) }
}
}
);
// `link_name` (i.e. no thunk) for `f_c_calling_convention`. No
// `link_name` (i.e. indicates presence of a thunk) for
// `f_vectorcall_calling_convention`.
assert_rs_matches!(
rs_api,
quote! {
mod detail {
#[allow(unused_imports)]
use super::*;
extern "C" {
pub(crate) fn __rust_thunk___Z31f_vectorcall_calling_conventionff(
p1: f32, p2: f32) -> f32;
#[link_name = "_Z22f_c_calling_conventiondd"]
pub(crate) fn __rust_thunk___Z22f_c_calling_conventiondd(
p1: f64, p2: f64) -> f64;
}
}
}
);
// C++ thunk needed for `f_vectorcall_calling_convention`.
assert_cc_matches!(
rs_api_impl,
quote! {
extern "C" float __rust_thunk___Z31f_vectorcall_calling_conventionff(
float p1, float p2) {
return f_vectorcall_calling_convention(p1, p2);
}
}
);
// No C++ thunk expected for `f_c_calling_convention`.
assert_cc_not_matches!(rs_api_impl, quote! { f_c_calling_convention });
Ok(())
}
}
#[test]
fn test_item_order() -> Result<()> {
let ir = ir_from_cc(
"int first_func();
struct FirstStruct {};
int second_func();
struct SecondStruct {};",
)?;
let rs_api = rs_tokens_to_formatted_string_for_tests(generate_bindings_tokens(ir)?.rs_api)?;
let idx = |s: &str| rs_api.find(s).ok_or_else(|| anyhow!("'{}' missing", s));
let f1 = idx("fn first_func")?;
let f2 = idx("fn second_func")?;
let s1 = idx("struct FirstStruct")?;
let s2 = idx("struct SecondStruct")?;
let t1 = idx("fn __rust_thunk___Z10first_funcv")?;
let t2 = idx("fn __rust_thunk___Z11second_funcv")?;
assert!(f1 < s1);
assert!(s1 < f2);
assert!(f2 < s2);
assert!(s2 < t1);
assert!(t1 < t2);
Ok(())
}
#[test]
fn test_generate_enum_basic() -> Result<()> {
let ir = ir_from_cc("enum Color { kRed = 5, kBlue };")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Color")]
pub struct Color(::core::ffi::c_uint);
impl Color {
pub const kRed: Color = Color(5);
pub const kBlue: Color = Color(6);
}
impl From<::core::ffi::c_uint> for Color {
fn from(value: ::core::ffi::c_uint) -> Color {
Color(value)
}
}
impl From<Color> for ::core::ffi::c_uint {
fn from(value: Color) -> ::core::ffi::c_uint {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_opaque_enum() -> Result<()> {
let ir = ir_from_cc("enum Color : int;")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_not_matches!(rs_api, quote! {Color});
Ok(())
}
#[test]
fn test_generate_scoped_enum_basic() -> Result<()> {
let ir = ir_from_cc("enum class Color { kRed = -5, kBlue };")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Color")]
pub struct Color(::core::ffi::c_int);
impl Color {
pub const kRed: Color = Color(-5);
pub const kBlue: Color = Color(-4);
}
impl From<::core::ffi::c_int> for Color {
fn from(value: ::core::ffi::c_int) -> Color {
Color(value)
}
}
impl From<Color> for ::core::ffi::c_int {
fn from(value: Color) -> ::core::ffi::c_int {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_enum_with_64_bit_signed_vals() -> Result<()> {
let ir = ir_from_cc(
r#"enum Color : long {
kViolet = -9223372036854775807 - 1LL,
kRed = -5,
kBlue,
kGreen = 3,
kMagenta = 9223372036854775807
};"#,
)?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Color")]
pub struct Color(::core::ffi::c_long);
impl Color {
pub const kViolet: Color = Color(-9223372036854775808);
pub const kRed: Color = Color(-5);
pub const kBlue: Color = Color(-4);
pub const kGreen: Color = Color(3);
pub const kMagenta: Color = Color(9223372036854775807);
}
impl From<::core::ffi::c_long> for Color {
fn from(value: ::core::ffi::c_long) -> Color {
Color(value)
}
}
impl From<Color> for ::core::ffi::c_long {
fn from(value: Color) -> ::core::ffi::c_long {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_enum_with_64_bit_unsigned_vals() -> Result<()> {
let ir = ir_from_cc(
r#" enum Color: unsigned long {
kRed,
kBlue,
kLimeGreen = 18446744073709551615
}; "#,
)?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Color")]
pub struct Color(::core::ffi::c_ulong);
impl Color {
pub const kRed: Color = Color(0);
pub const kBlue: Color = Color(1);
pub const kLimeGreen: Color = Color(18446744073709551615);
}
impl From<::core::ffi::c_ulong> for Color {
fn from(value: ::core::ffi::c_ulong) -> Color {
Color(value)
}
}
impl From<Color> for ::core::ffi::c_ulong {
fn from(value: Color) -> ::core::ffi::c_ulong {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_enum_with_32_bit_signed_vals() -> Result<()> {
let ir = ir_from_cc(
"enum Color { kViolet = -2147483647 - 1, kRed = -5, kBlue, kGreen = 3, kMagenta = 2147483647 };",
)?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Color")]
pub struct Color(::core::ffi::c_int);
impl Color {
pub const kViolet: Color = Color(-2147483648);
pub const kRed: Color = Color(-5);
pub const kBlue: Color = Color(-4);
pub const kGreen: Color = Color(3);
pub const kMagenta: Color = Color(2147483647);
}
impl From<::core::ffi::c_int> for Color {
fn from(value: ::core::ffi::c_int) -> Color {
Color(value)
}
}
impl From<Color> for ::core::ffi::c_int {
fn from(value: Color) -> ::core::ffi::c_int {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_enum_with_32_bit_unsigned_vals() -> Result<()> {
let ir = ir_from_cc("enum Color: unsigned int { kRed, kBlue, kLimeGreen = 4294967295 };")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Color")]
pub struct Color(::core::ffi::c_uint);
impl Color {
pub const kRed: Color = Color(0);
pub const kBlue: Color = Color(1);
pub const kLimeGreen: Color = Color(4294967295);
}
impl From<::core::ffi::c_uint> for Color {
fn from(value: ::core::ffi::c_uint) -> Color {
Color(value)
}
}
impl From<Color> for ::core::ffi::c_uint {
fn from(value: Color) -> ::core::ffi::c_uint {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_enum_bool() -> Result<()> {
let ir = ir_from_cc("enum Bool : bool { kFalse, kTrue };")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Bool")]
pub struct Bool(bool);
impl Bool {
pub const kFalse: Bool = Bool(false);
pub const kTrue: Bool = Bool(true);
}
impl From<bool> for Bool {
fn from(value: bool) -> Bool {
Bool(value)
}
}
impl From<Bool> for bool {
fn from(value: Bool) -> bool {
value.0
}
}
}
);
Ok(())
}
#[test]
fn test_generate_enum_bool_alias() -> Result<()> {
let ir = ir_from_cc("using MyBool = bool; enum Bool : MyBool { kFalse, kTrue };")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash, PartialOrd, Ord)]
#[__crubit::annotate(cpp_type = "Bool")]
pub struct Bool(crate::MyBool);
impl Bool {
pub const kFalse: Bool = Bool(false);
pub const kTrue: Bool = Bool(true);
}
impl From<crate::MyBool> for Bool {
fn from(value: crate::MyBool) -> Bool {
Bool(value)
}
}
impl From<Bool> for crate::MyBool {
fn from(value: Bool) -> crate::MyBool {
value.0
}
}
}
);
Ok(())
}
/// At the least, a trivial type should have no drop impl if or until we add
/// empty drop impls.
#[test]
fn test_no_impl_drop() -> Result<()> {
let ir = ir_from_cc("struct Trivial {};")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_not_matches!(rs_api, quote! {impl Drop});
assert_rs_not_matches!(rs_api, quote! {impl ::ctor::PinnedDrop});
Ok(())
}
/// User-defined destructors *must* become Drop impls with ManuallyDrop
/// fields
#[test]
fn test_impl_drop_user_defined_destructor() -> Result<()> {
let ir = ir_from_cc(
r#" struct NontrivialStruct { ~NontrivialStruct(); };
struct UserDefinedDestructor {
~UserDefinedDestructor();
int x;
NontrivialStruct nts;
};"#,
)?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
impl ::ctor::PinnedDrop for UserDefinedDestructor {
#[inline(always)]
unsafe fn pinned_drop<'a>(self: ::core::pin::Pin<&'a mut Self>) {
crate::detail::__rust_thunk___ZN21UserDefinedDestructorD1Ev(self)
}
}
}
);
assert_rs_matches!(rs_api, quote! {pub x: ::core::ffi::c_int,});
assert_rs_matches!(
rs_api,
quote! {pub nts: ::core::mem::ManuallyDrop<crate::NontrivialStruct>,}
);
Ok(())
}
/// nontrivial types without user-defined destructors should invoke
/// the C++ destructor to preserve the order of field destructions.
#[test]
fn test_impl_drop_nontrivial_member_destructor() -> Result<()> {
// TODO(jeanpierreda): This would be cleaner if the UserDefinedDestructor code were
// omitted. For example, we simulate it so that UserDefinedDestructor
// comes from another library.
let ir = ir_from_cc(
r#"struct UserDefinedDestructor final {
~UserDefinedDestructor();
};
struct TrivialStruct final { int i; };
struct NontrivialMembers final {
UserDefinedDestructor udd;
TrivialStruct ts;
int x;
};"#,
)?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(
rs_api,
quote! {
impl ::ctor::PinnedDrop for NontrivialMembers {
#[inline(always)]
unsafe fn pinned_drop<'a>(self: ::core::pin::Pin<&'a mut Self>) {
crate::detail::__rust_thunk___ZN17NontrivialMembersD1Ev(self)
}
}
}
);
assert_rs_matches!(rs_api, quote! {pub x: ::core::ffi::c_int,});
assert_rs_matches!(rs_api, quote! {pub ts: crate::TrivialStruct,});
assert_rs_matches!(
rs_api,
quote! {pub udd: ::core::mem::ManuallyDrop<crate::UserDefinedDestructor>,}
);
Ok(())
}
#[test]
fn test_type_alias() -> Result<()> {
let ir = ir_from_cc(
r#"
// MyTypedefDecl doc comment
typedef int MyTypedefDecl;
using MyTypeAliasDecl = int;
using MyTypeAliasDecl_Alias = MyTypeAliasDecl;
struct S final {};
using S_Alias = S;
using S_Alias_Alias = S_Alias;
inline void f(MyTypedefDecl t) {}
"#,
)?;
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_matches!(
rs_api,
quote! {
#[doc = " MyTypedefDecl doc comment\n \n Generated from: google3/ir_from_cc_virtual_header.h;l=5"]
pub type MyTypedefDecl = ::core::ffi::c_int;
}
);
assert_rs_matches!(rs_api, quote! { pub type MyTypeAliasDecl = ::core::ffi::c_int; });
assert_rs_matches!(
rs_api,
quote! { pub type MyTypeAliasDecl_Alias = crate::MyTypeAliasDecl; }
);
assert_rs_matches!(rs_api, quote! { pub type S_Alias = crate::S; });
assert_rs_matches!(rs_api, quote! { pub type S_Alias_Alias = crate::S_Alias; });
assert_rs_matches!(rs_api, quote! { pub fn f(t: crate::MyTypedefDecl) });
assert_cc_matches!(
rs_api_impl,
quote! {
extern "C" void __rust_thunk___Z1fi(MyTypedefDecl t) { f(t); }
}
);
Ok(())
}
#[test]
fn test_rs_type_kind_implements_copy() -> Result<()> {
let template = r#" LIFETIMES
struct [[clang::trivial_abi]] TrivialStruct final { int i; };
struct [[clang::trivial_abi]] UserDefinedCopyConstructor final {
UserDefinedCopyConstructor(const UserDefinedCopyConstructor&);
};
using IntAlias = int;
using TrivialAlias = TrivialStruct;
using NonTrivialAlias = UserDefinedCopyConstructor;
void func(PARAM_TYPE some_param);
"#;
assert_impl_all!(i32: Copy);
assert_impl_all!(&i32: Copy);
assert_not_impl_any!(&mut i32: Copy);
assert_impl_all!(Option<&i32>: Copy);
assert_not_impl_any!(Option<&mut i32>: Copy);
assert_impl_all!(*const i32: Copy);
assert_impl_all!(*mut i32: Copy);
struct Test {
// Test inputs:
cc: &'static str,
lifetimes: bool,
// Expected test outputs:
rs: &'static str,
is_copy: bool,
}
let tests = vec![
// Validity of the next few tests is verified via
// `assert_[not_]impl_all!` static assertions above.
Test { cc: "int", lifetimes: true, rs: ":: core :: ffi :: c_int", is_copy: true },
Test {
cc: "const int&",
lifetimes: true,
rs: "& 'a :: core :: ffi :: c_int",
is_copy: true,
},
Test {
cc: "int&",
lifetimes: true,
rs: "& 'a mut :: core :: ffi :: c_int",
is_copy: false,
},
Test {
cc: "const int*",
lifetimes: true,
rs: "Option < & 'a :: core :: ffi :: c_int >",
is_copy: true,
},
Test {
cc: "int*",
lifetimes: true,
rs: "Option < & 'a mut :: core :: ffi :: c_int >",
is_copy: false,
},
Test {
cc: "const int*",
lifetimes: false,
rs: "* const :: core :: ffi :: c_int",
is_copy: true,
},
Test {
cc: "int*",
lifetimes: false,
rs: "* mut :: core :: ffi :: c_int",
is_copy: true,
},
Test {
cc: "void*",
lifetimes: false,
rs: "* mut :: core :: ffi :: c_void",
is_copy: true,
},
Test {
cc: "const void*",
lifetimes: false,
rs: "* const :: core :: ffi :: c_void",
is_copy: true,
},
Test {
cc: "void* const*",
lifetimes: false,
rs: "* const * mut :: core :: ffi :: c_void",
is_copy: true,
},
// Tests below have been thought-through and verified "manually".
// TrivialStruct is expected to derive Copy.
Test {
cc: "TrivialStruct",
lifetimes: true,
rs: "crate :: TrivialStruct",
is_copy: true,
},
Test {
cc: "UserDefinedCopyConstructor",
lifetimes: true,
rs: "crate :: UserDefinedCopyConstructor",
is_copy: false,
},
Test { cc: "IntAlias", lifetimes: true, rs: "crate :: IntAlias", is_copy: true },
Test {
cc: "TrivialAlias",
lifetimes: true,
rs: "crate :: TrivialAlias",
is_copy: true,
},
Test {
cc: "NonTrivialAlias",
lifetimes: true,
rs: "crate :: NonTrivialAlias",
is_copy: false,
},
];
for test in tests.iter() {
let test_name = format!("cc='{}', lifetimes={}", test.cc, test.lifetimes);
let cc_input = template.replace("PARAM_TYPE", test.cc).replace(
"LIFETIMES",
if test.lifetimes { "#pragma clang lifetime_elision" } else { "" },
);
let db = db_from_cc(&cc_input)?;
let ir = db.ir();
let f = retrieve_func(&ir, "func");
let t = db.rs_type_kind(f.params[0].type_.rs_type.clone())?;
let fmt = t.to_token_stream().to_string();
assert_eq!(test.rs, fmt, "Testing: {}", test_name);
assert_eq!(test.is_copy, t.implements_copy(), "Testing: {}", test_name);
}
Ok(())
}
#[test]
fn test_rs_type_kind_is_shared_ref_to_with_lifetimes() -> Result<()> {
let db = db_from_cc(
"#pragma clang lifetime_elision
struct SomeStruct {};
void foo(const SomeStruct& foo_param);
void bar(SomeStruct& bar_param);",
)?;
let ir = db.ir();
let record = ir.records().next().unwrap();
let foo_func = retrieve_func(&ir, "foo");
let bar_func = retrieve_func(&ir, "bar");
// const-ref + lifetimes in C++ ===> shared-ref in Rust
assert_eq!(foo_func.params.len(), 1);
let foo_param = &foo_func.params[0];
assert_eq!(foo_param.identifier.identifier.as_ref(), "foo_param");
let foo_type = db.rs_type_kind(foo_param.type_.rs_type.clone())?;
assert!(foo_type.is_shared_ref_to(record));
assert!(matches!(foo_type, RsTypeKind::Reference { mutability: Mutability::Const, .. }));
// non-const-ref + lifetimes in C++ ===> mutable-ref in Rust
assert_eq!(bar_func.params.len(), 1);
let bar_param = &bar_func.params[0];
assert_eq!(bar_param.identifier.identifier.as_ref(), "bar_param");
let bar_type = db.rs_type_kind(bar_param.type_.rs_type.clone())?;
assert!(!bar_type.is_shared_ref_to(record));
assert!(matches!(bar_type, RsTypeKind::Reference { mutability: Mutability::Mut, .. }));
Ok(())
}
#[test]
fn test_rs_type_kind_is_shared_ref_to_without_lifetimes() -> Result<()> {
let db = db_from_cc(
"struct SomeStruct {};
void foo(const SomeStruct& foo_param);",
)?;
let ir = db.ir();
let record = ir.records().next().unwrap();
let foo_func = retrieve_func(&ir, "foo");
// const-ref + *no* lifetimes in C++ ===> const-pointer in Rust
assert_eq!(foo_func.params.len(), 1);
let foo_param = &foo_func.params[0];
assert_eq!(foo_param.identifier.identifier.as_ref(), "foo_param");
let foo_type = db.rs_type_kind(foo_param.type_.rs_type.clone())?;
assert!(!foo_type.is_shared_ref_to(record));
assert!(matches!(foo_type, RsTypeKind::Pointer { mutability: Mutability::Const, .. }));
Ok(())
}
#[test]
fn test_rs_type_kind_lifetimes() -> Result<()> {
let db = db_from_cc(
r#"
#pragma clang lifetime_elision
using TypeAlias = int&;
struct SomeStruct {};
void foo(int a, int& b, int&& c, int* d, int** e, TypeAlias f, SomeStruct g); "#,
)?;
let ir = db.ir();
let func = retrieve_func(&ir, "foo");
let ret = db.rs_type_kind(func.return_type.rs_type.clone())?;
let a = db.rs_type_kind(func.params[0].type_.rs_type.clone())?;
let b = db.rs_type_kind(func.params[1].type_.rs_type.clone())?;
let c = db.rs_type_kind(func.params[2].type_.rs_type.clone())?;
let d = db.rs_type_kind(func.params[3].type_.rs_type.clone())?;
let e = db.rs_type_kind(func.params[4].type_.rs_type.clone())?;
let f = db.rs_type_kind(func.params[5].type_.rs_type.clone())?;
let g = db.rs_type_kind(func.params[6].type_.rs_type.clone())?;
assert_eq!(0, ret.lifetimes().count()); // No lifetimes on `void`.
assert_eq!(0, a.lifetimes().count()); // No lifetimes on `int`.
assert_eq!(1, b.lifetimes().count()); // `&'a i32` has a single lifetime.
assert_eq!(1, c.lifetimes().count()); // `RvalueReference<'a, i32>` has a single lifetime.
assert_eq!(1, d.lifetimes().count()); // `Option<&'b i32>` has a single lifetime.
assert_eq!(2, e.lifetimes().count()); // `&'c Option<&'d i32>` has two lifetimes.
assert_eq!(1, f.lifetimes().count()); // Lifetime of underlying type should show through.
assert_eq!(0, g.lifetimes().count()); // No lifetimes on structs (yet).
Ok(())
}
#[test]
fn test_rs_type_kind_lifetimes_raw_ptr() -> Result<()> {
let db = db_from_cc("void foo(int* a);")?;
let ir = db.ir();
let f = retrieve_func(&ir, "foo");
let a = db.rs_type_kind(f.params[0].type_.rs_type.clone())?;
assert_eq!(0, a.lifetimes().count()); // No lifetimes on `int*`.
Ok(())
}
#[test]
fn test_rs_type_kind_rejects_func_ptr_that_returns_struct_by_value() -> Result<()> {
let db = db_from_cc(
r#"
struct SomeStruct {
int field;
};
SomeStruct (*get_ptr_to_func())();
"#,
)?;
let ir = db.ir();
let f = retrieve_func(&ir, "get_ptr_to_func");
// Expecting an error, because passing a struct by value requires a thunk and
// function pointers don't have a thunk.
let err = db.rs_type_kind(f.return_type.rs_type.clone()).unwrap_err();
let msg = err.to_string();
assert_eq!(
msg,
"Either the return type or some of the parameter types require \
an FFI thunk (and function pointers don't have a thunk)",
);
Ok(())
}
#[test]
fn test_rs_type_kind_rejects_func_ptr_that_takes_struct_by_value() -> Result<()> {
let db = db_from_cc(
r#"
struct SomeStruct {
int field;
};
void (*get_ptr_to_func())(SomeStruct);
"#,
)?;
let ir = db.ir();
let f = retrieve_func(&ir, "get_ptr_to_func");
// Expecting an error, because passing a struct by value requires a thunk and
// function pointers don't have a thunk.
let err = db.rs_type_kind(f.return_type.rs_type.clone()).unwrap_err();
let msg = err.to_string();
assert_eq!(
msg,
"Either the return type or some of the parameter types require \
an FFI thunk (and function pointers don't have a thunk)",
);
Ok(())
}
#[test]
fn test_rust_keywords_are_escaped_in_rs_api_file() -> Result<()> {
let ir = ir_from_cc("struct type { int dyn; };")?;
let rs_api = generate_bindings_tokens(ir)?.rs_api;
assert_rs_matches!(rs_api, quote! { struct r#type { ... r#dyn: ::core::ffi::c_int ... } });
Ok(())
}
#[test]
fn test_rust_keywords_are_not_escaped_in_rs_api_impl_file() -> Result<()> {
let ir = ir_from_cc("struct type { int dyn; };")?;
let rs_api_impl = generate_bindings_tokens(ir)?.rs_api_impl;
assert_cc_matches!(
rs_api_impl,
quote! { static_assert(CRUBIT_OFFSET_OF(dyn, struct type) ... ) }
);
Ok(())
}
#[test]
fn test_namespace_module_items() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#"
namespace test_namespace_bindings {
int func();
struct S {};
namespace inner {
int inner_func();
struct InnerS {};
}
}
"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
pub mod test_namespace_bindings {
...
pub fn func() -> ::core::ffi::c_int { ... }
...
pub struct S { ... }
...
pub mod inner {
...
pub fn inner_func() -> ::core::ffi::c_int { ... }
...
pub struct InnerS { ... }
...
}
...
}
}
);
Ok(())
}
#[test]
fn test_detail_outside_of_namespace_module() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#"
namespace test_namespace_bindings {
int f();
}
"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
pub mod test_namespace_bindings {
...
}
...
mod detail {
#[allow(unused_imports)]
use super::*;
extern "C" {
#[link_name = "_ZN23test_namespace_bindings1fEv"]
pub(crate) fn __rust_thunk___ZN23test_namespace_bindings1fEv() -> ::core::ffi::c_int;
}
}
...
}
);
Ok(())
}
#[test]
fn test_assertions_outside_of_namespace_module() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#"
namespace test_namespace_bindings {
struct S {
int i;
};
}
"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
pub mod test_namespace_bindings {
...
}
...
const _: () = {
...
assert!(::core::mem::size_of::<crate::test_namespace_bindings::S>() == 4);
assert!(::core::mem::align_of::<crate::test_namespace_bindings::S>() == 4);
...
assert!(::core::mem::offset_of!(crate::test_namespace_bindings::S, i) == 0);
...
};
}
);
Ok(())
}
#[test]
fn test_reopened_namespaces() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#"
namespace test_namespace_bindings {
namespace inner {}
} // namespace test_namespace_bindings
namespace test_namespace_bindings {
namespace inner {}
} // namespace test_namespace_bindings"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
...
pub mod test_namespace_bindings_0 {
pub mod inner_0 {} ...
}
...
pub mod test_namespace_bindings {
__HASH_TOKEN__[allow(unused_imports)]
pub use super::test_namespace_bindings_0::*;
...
pub mod inner {
__HASH_TOKEN__[allow(unused_imports)]
pub use super::inner_0::*;
...
}
}
...
}
);
Ok(())
}
#[test]
fn test_qualified_identifiers_in_impl_file() -> Result<()> {
let rs_api_impl = generate_bindings_tokens(ir_from_cc(
r#"
namespace test_namespace_bindings {
inline void f() {};
struct S final {};
}
inline void useS(test_namespace_bindings::S s) {};"#,
)?)?
.rs_api_impl;
assert_cc_matches!(
rs_api_impl,
quote! {
extern "C" void __rust_thunk___ZN23test_namespace_bindings1fEv() {
test_namespace_bindings::f();
}
...
extern "C" void __rust_thunk___Z4useSN23test_namespace_bindings1SE(
struct test_namespace_bindings::S* s) {
useS(std::move(*s));
}
...
}
);
Ok(())
}
#[test]
fn test_inline_namespace() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#"
namespace test_namespace_bindings {
inline namespace inner {
struct MyStruct final {};
}
void processMyStruct(MyStruct s);
}
void processMyStructOutsideNamespace(test_namespace_bindings::inner::MyStruct s);
void processMyStructSkipInlineNamespaceQualifier(test_namespace_bindings::MyStruct s);
"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
...
pub mod test_namespace_bindings {
...
pub mod inner {
...
pub struct MyStruct {...} ...
}
__HASH_TOKEN__[allow(unused_imports)]
pub use inner::*;
...
pub fn processMyStruct(
mut s: crate::test_namespace_bindings::inner::MyStruct)
...
}
...
pub fn processMyStructOutsideNamespace(
mut s: crate::test_namespace_bindings::inner::MyStruct)
...
pub fn processMyStructSkipInlineNamespaceQualifier(
mut s: crate::test_namespace_bindings::inner::MyStruct)
...
}
);
Ok(())
}
#[test]
fn test_inline_namespace_not_marked_inline() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#"
inline namespace my_inline {}
namespace foo {}
namespace my_inline { // still an inline namespace!
struct MyStruct final {};
}
"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
...
pub mod my_inline_0 {}
pub mod foo {}
pub mod my_inline {
__HASH_TOKEN__[allow(unused_imports)]
pub use super::my_inline_0::*;
...
pub struct MyStruct {...}
...
}
__HASH_TOKEN__[allow(unused_imports)]
pub use my_inline::*;
...
}
);
Ok(())
}
#[test]
fn test_generate_doc_comment_with_no_comment_with_no_source_loc_with_source_loc_enabled() {
let actual = generate_doc_comment(None, None, SourceLocationDocComment::Enabled);
assert!(actual.is_empty());
}
#[test]
fn test_generate_doc_comment_with_no_comment_with_source_loc_with_source_loc_enabled() {
let actual = generate_doc_comment(
None,
Some("google3/some/header;l=11"),
SourceLocationDocComment::Enabled,
);
assert_rs_matches!(actual, quote! {#[doc = " google3/some/header;l=11"]});
}
#[test]
fn test_generate_doc_comment_with_comment_with_source_loc_with_source_loc_enabled() {
let actual = generate_doc_comment(
Some("Some doc comment"),
Some("google3/some/header;l=12"),
SourceLocationDocComment::Enabled,
);
assert_rs_matches!(
actual,
quote! {#[doc = " Some doc comment\n \n google3/some/header;l=12"]}
);
}
#[test]
fn test_generate_doc_comment_with_comment_with_no_source_loc_with_source_loc_enabled() {
let actual =
generate_doc_comment(Some("Some doc comment"), None, SourceLocationDocComment::Enabled);
assert_rs_matches!(actual, quote! {#[doc = " Some doc comment"]});
}
#[test]
fn test_no_generate_doc_comment_with_no_comment_with_no_source_loc_with_source_loc_disabled() {
let actual = generate_doc_comment(None, None, SourceLocationDocComment::Disabled);
assert!(actual.is_empty());
}
#[test]
fn test_no_generate_doc_comment_with_no_comment_with_source_loc_with_source_loc_disabled() {
let actual = generate_doc_comment(
None,
Some("google3/some/header;l=13"),
SourceLocationDocComment::Disabled,
);
assert!(actual.is_empty());
}
#[test]
fn test_no_generate_doc_comment_with_comment_with_source_loc_with_source_loc_disabled() {
let actual = generate_doc_comment(
Some("Some doc comment"),
Some("google3/some/header;l=14"),
SourceLocationDocComment::Disabled,
);
assert_rs_matches!(actual, quote! {#[doc = " Some doc comment"]});
}
#[test]
fn test_no_generate_doc_comment_with_comment_with_no_source_loc_with_source_loc_disabled() {
let actual = generate_doc_comment(
Some("Some doc comment"),
None,
SourceLocationDocComment::Disabled,
);
assert_rs_matches!(actual, quote! {#[doc = " Some doc comment"]});
}
struct TestItem {
source_loc: Option<Rc<str>>,
}
impl ir::GenericItem for TestItem {
fn id(&self) -> ItemId {
ItemId::new_for_testing(123)
}
fn debug_name(&self, _: &IR) -> Rc<str> {
"test_item".into()
}
fn source_loc(&self) -> Option<Rc<str>> {
self.source_loc.clone()
}
fn unknown_attr(&self) -> Option<Rc<str>> {
None
}
}
#[test]
fn test_generate_unsupported_item_with_source_loc_enabled() -> Result<()> {
let db = Database::new(
Rc::new(make_ir_from_items([])),
Rc::new(ErrorReport::new()),
SourceLocationDocComment::Enabled,
);
let actual = generate_unsupported(
&db,
&UnsupportedItem::new_with_message(
&db.ir(),
&TestItem { source_loc: Some("Generated from: google3/some/header;l=1".into()) },
"unsupported_message",
),
)?;
let expected = "Generated from: google3/some/header;l=1\nError while generating bindings for item 'test_item':\nunsupported_message";
assert_rs_matches!(actual.item, quote! { __COMMENT__ #expected});
Ok(())
}
/// Not all items currently have source_loc(), e.g. comments.
///
/// For these, we omit the mention of the location.
#[test]
fn test_generate_unsupported_item_with_missing_source_loc() -> Result<()> {
let db = Database::new(
Rc::new(make_ir_from_items([])),
Rc::new(ErrorReport::new()),
SourceLocationDocComment::Enabled,
);
let actual = generate_unsupported(
&db,
&UnsupportedItem::new_with_message(
&db.ir(),
&TestItem { source_loc: None },
"unsupported_message",
),
)?;
let expected = "Error while generating bindings for item 'test_item':\nunsupported_message";
assert_rs_matches!(actual.item, quote! { __COMMENT__ #expected});
Ok(())
}
#[test]
fn test_generate_unsupported_item_with_source_loc_disabled() -> Result<()> {
let db = Database::new(
Rc::new(make_ir_from_items([])),
Rc::new(ErrorReport::new()),
SourceLocationDocComment::Disabled,
);
let actual = generate_unsupported(
&db,
&UnsupportedItem::new_with_message(
&db.ir(),
&TestItem { source_loc: Some("Generated from: google3/some/header;l=1".into()) },
"unsupported_message",
),
)?;
let expected = "Error while generating bindings for item 'test_item':\nunsupported_message";
assert_rs_matches!(actual.item, quote! { __COMMENT__ #expected});
Ok(())
}
/// Enumerators with unknown attributes on otherwise-ok enums are omitted.
///
/// This is hard to test any other way than token comparison!
#[test]
fn test_supported_unknown_attr_enumerator() -> Result<()> {
let mut ir = ir_from_cc(
r#"
enum Enum {
kHidden [[deprecated]],
};
"#,
)?;
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
ir::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens(ir)?;
assert_rs_matches!(rs_api, quote! {pub struct Enum});
assert_rs_not_matches!(rs_api, quote! {kHidden});
Ok(())
}
/// Namespaces with an unknown attribute are not present in supported.
///
/// This is hard to test any other way than token comparison, because it's
/// hard to test for the nonexistence of a module.
#[test]
fn test_supported_unknown_attr_namespace() -> Result<()> {
for nested_notpresent in
["struct NotPresent {};", "struct NotPresent;", "enum NotPresent {};"]
{
let mut ir = ir_from_cc(&format!(
r#"
namespace [[deprecated]] unknown_attr_namespace {{
{nested_notpresent}
}}
extern "C" {{
void NotPresent(unknown_attr_namespace::NotPresent);
unknown_attr_namespace::NotPresent AlsoNotPresent();
}}
"#
))?;
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
ir::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens(ir)?;
// The namespace, and everything in it or using it, will be missing from the
// output.
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_rs_not_matches!(rs_api, quote! {AlsoNotPresent});
assert_rs_not_matches!(rs_api, quote! {unknown_attr_namespace});
}
Ok(())
}
/// Namespaces with an unknown attribute are still merged with the same
/// namespace with no unknown attribute.
#[test]
fn test_supported_unknown_attr_namespace_merge() -> Result<()> {
let mut ir = ir_from_cc(
r#"
namespace unknown_attr_namespace {
enum Present {};
}
namespace [[deprecated]] unknown_attr_namespace {
enum NotPresent {};
}
namespace unknown_attr_namespace {
enum AlsoPresent {};
}
"#,
)?;
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
ir::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens(ir)?;
// The namespace, and everything in it or using it, will be missing from the
// output.
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_rs_matches!(rs_api, quote! {Present});
assert_rs_matches!(rs_api, quote! {AlsoPresent});
assert_rs_matches!(rs_api, quote! {unknown_attr_namespace});
Ok(())
}
/// Namespaces with an unknown attribute are not present in supported, but
/// their typedefs are.
#[test]
fn test_supported_unknown_attr_namespace_typedef() -> Result<()> {
let mut ir = ir_from_cc(
r#"
namespace [[deprecated]] unknown_attr_namespace {
using NotPresent = int;
}
extern "C" {
void Func(unknown_attr_namespace::NotPresent x);
unknown_attr_namespace::NotPresent Func2();
}
"#,
)?;
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
ir::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens(ir)?;
// The namespace, and everything in it or using it, will be missing from the
// output.
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_rs_matches!(rs_api, quote! {pub fn Func(x: ::core::ffi::c_int)});
assert_rs_matches!(rs_api, quote! {pub fn Func2() -> ::core::ffi::c_int});
Ok(())
}
/// The default crubit feature set currently doesn't include supported.
#[test]
fn test_default_crubit_features_disabled_supported() -> Result<()> {
for item in [
"extern \"C\" void NotPresent() {}",
"struct NotPresent {};",
"extern \"C\" int NotPresent() {}",
] {
let mut ir = ir_from_cc(item)?;
ir.target_crubit_features_mut(&ir.current_target().clone()).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_cc_not_matches!(rs_api_impl, quote! {NotPresent});
let contents = rs_tokens_to_formatted_string_for_tests(rs_api)?;
// using a string comparison and leaving off the end, because the exact reason
// why differs per item.
let expected = "\
// Generated from: google3/ir_from_cc_virtual_header.h;l=3\n\
// Error while generating bindings for item 'NotPresent':\n\
// Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
// //test:testing_target needs [//features:supported] for NotPresent";
assert!(contents.contains(expected), "Missing expected string: {contents}\n")
}
Ok(())
}
/// The default crubit feature set currently doesn't include experimetnal.
#[test]
fn test_default_crubit_features_disabled_experimental() -> Result<()> {
let mut ir = ir_from_cc("struct NotPresent {~NotPresent();};")?;
ir.target_crubit_features_mut(&ir.current_target().clone()).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_cc_not_matches!(rs_api_impl, quote! {NotPresent});
let expected = "\
Generated from: google3/ir_from_cc_virtual_header.h;l=3\n\
Error while generating bindings for item 'NotPresent':\n\
Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
//test:testing_target needs [//features:experimental] for NotPresent (<internal link>_relocatable_error: crate::NotPresent is not rust-movable)";
assert_rs_matches!(rs_api, quote! { __COMMENT__ #expected});
Ok(())
}
#[test]
fn test_default_crubit_features_disabled_dependency_supported_function_parameter() -> Result<()>
{
for dependency in ["struct NotPresent {};"] {
let mut ir = ir_from_cc_dependency("void Func(NotPresent);", dependency)?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
let expected = "\
Generated from: google3/ir_from_cc_virtual_header.h;l=3\n\
Error while generating bindings for item 'Func':\n\
Failed to format type of parameter 0: Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
//test:dependency needs [//features:supported] for NotPresent";
assert_rs_matches!(rs_api, quote! { __COMMENT__ #expected});
}
Ok(())
}
#[test]
fn test_default_crubit_features_disabled_dependency_experimental_function_parameter()
-> Result<()> {
let mut ir =
ir_from_cc_dependency("void Func(NotPresent);", "struct NotPresent {~NotPresent();};")?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
let expected = "\
Generated from: google3/ir_from_cc_virtual_header.h;l=3\n\
Error while generating bindings for item 'Func':\n\
Failed to format type of parameter 0: Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
//test:dependency needs [//features:experimental] for NotPresent (<internal link>_relocatable_error: dependency::NotPresent is not rust-movable)";
assert_rs_matches!(rs_api, quote! { __COMMENT__ #expected});
Ok(())
}
#[test]
fn test_default_crubit_features_disabled_dependency_supported_function_return_type()
-> Result<()> {
let mut ir = ir_from_cc_dependency("NotPresent Func();", "struct NotPresent {};")?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
let expected = "\
Generated from: google3/ir_from_cc_virtual_header.h;l=3\n\
Error while generating bindings for item 'Func':\n\
Failed to format return type: Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
//test:dependency needs [//features:supported] for NotPresent";
assert_rs_matches!(rs_api, quote! { __COMMENT__ #expected});
Ok(())
}
#[test]
fn test_default_crubit_features_disabled_dependency_experimental_function_return_type()
-> Result<()> {
let mut ir =
ir_from_cc_dependency("NotPresent Func();", "struct NotPresent {~NotPresent();};")?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
let expected = "\
Generated from: google3/ir_from_cc_virtual_header.h;l=3\n\
Error while generating bindings for item 'Func':\n\
Failed to format return type: Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
//test:dependency needs [//features:experimental] for NotPresent (<internal link>_relocatable_error: dependency::NotPresent is not rust-movable)";
assert_rs_matches!(rs_api, quote! { __COMMENT__ #expected});
Ok(())
}
#[test]
fn test_default_crubit_features_disabled_dependency_struct() -> Result<()> {
for dependency in ["struct NotPresent {signed char x;};", "using NotPresent = signed char;"]
{
let mut ir = ir_from_cc_dependency("struct Present {NotPresent field;};", dependency)?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
let BindingsTokens { rs_api, rs_api_impl: _ } = generate_bindings_tokens(ir)?;
assert_rs_matches!(
rs_api,
quote! {
pub struct Present {
...
pub(crate) field: [::core::mem::MaybeUninit<u8>; 1],
}
}
);
}
Ok(())
}
#[test]
fn test_type_map_override_assert() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#" #pragma clang lifetime_elision
// Broken class: uses i32 but has size 1.
// (These asserts would fail if this were compiled.)
class [[clang::annotate("crubit_internal_rust_type", "i32")]] Class final {};"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
assert!(::core::mem::size_of::<i32>() == 1);
}
);
assert_rs_matches!(
rs_api,
quote! {
assert!(::core::mem::align_of::<i32>() == 1);
}
);
Ok(())
}
#[test]
fn test_type_map_override_c_abi_incompatible() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#" #pragma clang lifetime_elision
// Broken class: uses i32 but has size 1.
// (These asserts would fail if this were compiled.)
class [[clang::annotate("crubit_internal_rust_type", "i8")]] MyI8 {unsigned char field;};
MyI8 Make();"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
pub fn Make() -> i8 {...}
}
);
assert_rs_matches!(
rs_api,
quote! {
pub(crate) fn __rust_thunk___Z4Makev(__return: &mut ::core::mem::MaybeUninit<i8>);
}
);
Ok(())
}
#[test]
fn test_type_map_override_c_abi_compatible() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#" #pragma clang lifetime_elision
class
[[clang::annotate("crubit_internal_rust_type", "i8")]]
[[clang::annotate("crubit_internal_same_abi")]]
MyI8 {unsigned char field;};
MyI8 Make();"#,
)?)?
.rs_api;
assert_rs_matches!(
rs_api,
quote! {
pub fn Make() -> i8 {...}
}
);
assert_rs_matches!(
rs_api,
quote! {
pub(crate) fn __rust_thunk___Z4Makev() -> i8;
}
);
Ok(())
}
/// We cannot generate size/align assertions for incomplete types.
#[test]
fn test_type_map_override_assert_incomplete() -> Result<()> {
let rs_api = generate_bindings_tokens(ir_from_cc(
r#" #pragma clang lifetime_elision
// Broken class: uses i32 but has size 1.
// (These asserts would fail if this were compiled.)
class [[clang::annotate("crubit_internal_rust_type", "i32")]] Incomplete;
"#,
)?)?
.rs_api;
assert_rs_not_matches!(
rs_api,
quote! {
const _: () = { ... ::core::mem::size_of::<i32>() ... } }
);
assert_rs_not_matches!(
rs_api,
quote! {
const _: () = { ... ::core::mem::align_of::<i32>() ... }}
);
Ok(())
}
}