blob: 94f44e11bda324df82949ea26f0d05bb6c454cb8 [file]
// 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
use arc_anyhow::{anyhow, Result};
use database::code_snippet::BindingsTokens;
use database::rs_snippet::{Mutability, RsTypeKind};
use database::BindingsGenerator;
use googletest::{expect_eq, gtest};
use ir_testing::{retrieve_func, with_lifetime_macros};
use multiplatform_ir_testing::{ir_from_cc, ir_from_cc_dependency};
use quote::quote;
use static_assertions::{assert_impl_all, assert_not_impl_any};
use test_generators::{generate_bindings_tokens_for_test, TestDbFactory};
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;
#[gtest]
fn test_disable_thread_safety_warnings() -> Result<()> {
let ir = ir_from_cc("inline void foo() {}")?;
let rs_api_impl = generate_bindings_tokens_for_test(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(())
}
#[gtest]
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_for_test(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::*;
unsafe extern "C" {
#[link_name = "_Z15get_ptr_to_funcv"]
pub(crate) unsafe 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(())
}
#[gtest]
fn test_func_ref() -> Result<()> {
let ir = ir_from_cc(r#" int (&get_ref_to_func())(float, double); "#)?;
let rs_api = generate_bindings_tokens_for_test(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(())
}
#[gtest]
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_for_test(ir)?.rs_api;
assert_cc_matches!(rs_api, {
let txt = "Generated from: ir_from_cc_virtual_header.h;l=33\n\
Error while generating bindings for function 'get_ptr_to_func':\n\
Unable to get lifetime annotations: Type may not be annotated with lifetimes";
quote! { __COMMENT__ #txt }
});
Ok(())
}
#[gtest]
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_for_test(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::*;
unsafe extern "C" {
#[link_name = "_Z15get_ptr_to_funcv"]
pub(crate) unsafe 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;
#[gtest]
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 {
cc_name: "get_ptr_to_func", ...
return_type: CcType {
variant: FuncPointer {
non_null: false,
call_conv: X86VectorCall, ...
}, ...
}, ...
has_c_calling_convention: true, ...
}),
}
);
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(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::*;
unsafe extern "C" {
#[link_name = "_Z15get_ptr_to_funcv"]
pub(crate) unsafe 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(())
}
#[gtest]
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]] {
return nullptr;
}
"#,
)?;
// 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 {
cc_name: "inline_get_ptr_to_func", ...
return_type: CcType {
variant: FuncPointer {
non_null: false,
call_conv: X86VectorCall, ...
}, ...
}, ...
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_for_test(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(())
}
#[gtest]
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_for_test(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::*;
unsafe extern "C" {
pub(crate) unsafe fn __rust_thunk___Z31f_vectorcall_calling_conventionff(
p1: f32, p2: f32) -> f32;
#[link_name = "_Z22f_c_calling_conventiondd"]
pub(crate) unsafe 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(())
}
}
#[gtest]
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_for_test(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(())
}
/// At the least, a trivial type should have no drop impl if or until we add
/// empty drop impls.
#[gtest]
fn test_no_impl_drop() -> Result<()> {
let ir = ir_from_cc("struct Trivial {};")?;
let rs_api = generate_bindings_tokens_for_test(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
#[gtest]
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_for_test(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.
#[gtest]
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_for_test(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(())
}
#[gtest]
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_for_test(ir)?;
assert_rs_matches!(
rs_api,
quote! {
#[doc = " MyTypedefDecl doc comment\n \n Generated from: 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(())
}
#[gtest]
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: "* const :: core :: ffi :: c_int",
is_copy: true,
},
Test { cc: "int*", lifetimes: true, rs: "* mut :: core :: ffi :: c_int", is_copy: true },
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_factory = TestDbFactory::from_cc(&cc_input)?;
let db = db_factory.make_db();
let ir = db.ir();
let f = retrieve_func(&ir, "func");
let t = db.rs_type_kind(f.params[0].type_.clone())?;
let fmt = t.to_token_stream(&db).to_string();
expect_eq!(test.rs, fmt, "Testing: {}", test_name);
expect_eq!(test.is_copy, t.implements_copy(), "Testing: {}", test_name);
}
Ok(())
}
#[gtest]
fn test_rs_type_kind_is_shared_ref_to_with_lifetimes() -> Result<()> {
let cc_input = "#pragma clang lifetime_elision
struct SomeStruct {};
void foo(const SomeStruct& foo_param);
void bar(SomeStruct& bar_param);";
let db_factory = TestDbFactory::from_cc(cc_input)?;
let db = db_factory.make_db();
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_.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_.clone())?;
assert!(!bar_type.is_shared_ref_to(record));
assert!(matches!(bar_type, RsTypeKind::Reference { mutability: Mutability::Mut, .. }));
Ok(())
}
#[gtest]
fn test_rs_type_kind_is_shared_ref_to_without_lifetimes() -> Result<()> {
let cc_input = "struct SomeStruct {};
void foo(const SomeStruct& foo_param);";
let db_factory = TestDbFactory::from_cc(cc_input)?;
let db = db_factory.make_db();
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_.clone())?;
assert!(!foo_type.is_shared_ref_to(record));
assert!(matches!(foo_type, RsTypeKind::Pointer { mutability: Mutability::Const, .. }));
Ok(())
}
#[gtest]
fn test_rs_type_kind_lifetimes() -> Result<()> {
let cc_input = 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 db_factory = TestDbFactory::from_cc(cc_input)?;
let db = db_factory.make_db();
let ir = db.ir();
let func = retrieve_func(&ir, "foo");
let ret = db.rs_type_kind(func.return_type.clone())?;
let a = db.rs_type_kind(func.params[0].type_.clone())?;
let b = db.rs_type_kind(func.params[1].type_.clone())?;
let c = db.rs_type_kind(func.params[2].type_.clone())?;
let d = db.rs_type_kind(func.params[3].type_.clone())?;
let e = db.rs_type_kind(func.params[4].type_.clone())?;
let f = db.rs_type_kind(func.params[5].type_.clone())?;
let g = db.rs_type_kind(func.params[6].type_.clone())?;
expect_eq!(0, ret.lifetimes().count()); // No lifetimes on `void`.
expect_eq!(0, a.lifetimes().count()); // No lifetimes on `int`.
expect_eq!(1, b.lifetimes().count()); // `&'a i32` has a single lifetime.
expect_eq!(1, c.lifetimes().count()); // `RvalueReference<'a, i32>` has a single lifetime.
expect_eq!(0, d.lifetimes().count()); // `*mut i32` has no lifetimes.
expect_eq!(0, e.lifetimes().count()); // `*mut *mut i32` has no lifetimes.
expect_eq!(1, f.lifetimes().count()); // Lifetime of underlying type should show through.
expect_eq!(0, g.lifetimes().count()); // No lifetimes on structs (yet).
Ok(())
}
#[gtest]
fn test_rs_type_kind_lifetimes_raw_ptr() -> Result<()> {
let cc_input = "void foo(int* a);";
let db_factory = TestDbFactory::from_cc(cc_input)?;
let db = db_factory.make_db();
let ir = db.ir();
let f = retrieve_func(&ir, "foo");
let a = db.rs_type_kind(f.params[0].type_.clone())?;
assert_eq!(0, a.lifetimes().count()); // No lifetimes on `int*`.
Ok(())
}
#[gtest]
fn test_rs_type_kind_rejects_func_ptr_that_returns_struct_by_value() -> Result<()> {
let cc_input = r#"
struct SomeStruct {
int field;
};
SomeStruct (*get_ptr_to_func())();
"#;
let db_factory = TestDbFactory::from_cc(cc_input)?;
let db = db_factory.make_db();
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.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(())
}
#[gtest]
fn test_rs_type_kind_rejects_func_ptr_that_takes_struct_by_value() -> Result<()> {
let cc_input = r#"
struct SomeStruct {
int field;
};
void (*get_ptr_to_func())(SomeStruct);
"#;
let db_factory = TestDbFactory::from_cc(cc_input)?;
let db = db_factory.make_db();
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.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(())
}
#[gtest]
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_for_test(ir)?.rs_api;
assert_rs_matches!(rs_api, quote! { struct r#type { ... r#dyn: ::core::ffi::c_int ... } });
Ok(())
}
#[gtest]
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_for_test(ir)?.rs_api_impl;
assert_cc_matches!(
rs_api_impl,
quote! { static_assert(CRUBIT_OFFSET_OF(dyn, struct type) ... ) }
);
Ok(())
}
#[gtest]
fn test_namespace_module_items() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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(())
}
#[gtest]
fn test_detail_outside_of_namespace_module() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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::*;
unsafe extern "C" {
#[link_name = "_ZN23test_namespace_bindings1fEv"]
pub(crate) unsafe fn __rust_thunk___ZN23test_namespace_bindings1fEv() -> ::core::ffi::c_int;
}
}
...
}
);
Ok(())
}
#[gtest]
fn test_assertions_outside_of_namespace_module() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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(())
}
#[gtest]
fn test_reopened_namespaces() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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 {
pub mod inner {} ...
}
...
}
);
Ok(())
}
#[gtest]
fn test_qualified_identifiers_in_impl_file() -> Result<()> {
let rs_api_impl = generate_bindings_tokens_for_test(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(())
}
#[gtest]
fn test_inline_namespace() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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(())
}
#[gtest]
fn test_inline_namespace_not_marked_inline() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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 foo {}
pub mod my_inline {
...
pub struct MyStruct {...}
...
}
__HASH_TOKEN__[allow(unused_imports)]
pub use my_inline::*;
...
}
);
Ok(())
}
/// Enumerators with unknown attributes on otherwise-ok enums are omitted.
///
/// This is hard to test any other way than token comparison!
#[gtest]
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()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens_for_test(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.
#[gtest]
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()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens_for_test(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.
#[gtest]
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()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens_for_test(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.
#[gtest]
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()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, .. } = generate_bindings_tokens_for_test(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.
#[gtest]
fn test_default_crubit_features_disabled_supported() -> Result<()> {
for (item, kind) in [
("extern \"C\" void NotPresent() {}", "function"),
("struct NotPresent {};", "struct"),
("extern \"C\" int NotPresent() {}", "function"),
] {
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_for_test(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 = &format!("\
// Generated from: ir_from_cc_virtual_header.h;l=3\n\
// Error while generating bindings for {kind} '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 wrapper.
/// (Note that all experimental features are intended to also be included in
/// the wrapper feature set, so this subsumes any `disabled_experimental`
/// test.)
#[gtest]
fn test_default_crubit_features_disabled_wrapper() -> Result<()> {
let mut ir = ir_from_cc("struct NotPresent;")?;
ir.target_crubit_features_mut(&ir.current_target().clone()).clear();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_cc_not_matches!(rs_api_impl, quote! {NotPresent});
let expected = "\
Error while generating bindings for struct 'NotPresent':\n\
Can't generate bindings for NotPresent, because of missing required features (<internal link>):\n\
//test:testing_target needs [//features:wrapper] for NotPresent (incomplete type)";
assert_rs_matches!(rs_api, quote! { __COMMENT__ #expected});
Ok(())
}
#[gtest]
fn test_default_crubit_features_disabled_dependency_supported_function_parameter() -> Result<()> {
let mut ir = ir_from_cc_dependency(
"void Func(NotPresent);",
/*dependency=*/ "struct NotPresent {};",
)?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
Ok(())
}
#[gtest]
fn test_default_crubit_features_disabled_dependency_wrapper_function_parameter() -> Result<()> {
let mut ir = ir_from_cc_dependency(
"void Func(NotPresent);",
"template <typename T> struct NotPresentTemplate {T x;}; using NotPresent = NotPresentTemplate<int>;",
)?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
Ok(())
}
#[gtest]
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();
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
Ok(())
}
#[gtest]
fn test_default_crubit_features_disabled_dependency_wrapper_function_return_type() -> Result<()> {
let mut ir = ir_from_cc_dependency(
"NotPresent Func();",
"template <typename T> struct NotPresentTemplate {T x;}; using NotPresent = NotPresentTemplate<int>;")?;
ir.target_crubit_features_mut(&ir::BazelLabel("//test:dependency".into())).clear();
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {Func});
assert_cc_not_matches!(rs_api_impl, quote! {Func});
Ok(())
}
#[gtest]
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();
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl: _ } = generate_bindings_tokens_for_test(ir)?;
assert_rs_matches!(
rs_api,
quote! {
pub struct Present {
...
pub(crate) field: [::core::mem::MaybeUninit<u8>; 1],
}
}
);
}
Ok(())
}
#[gtest]
fn test_default_crubit_features_disabled_template_explicit_specialization() -> Result<()> {
let mut ir = ir_from_cc(
r#"
template <typename T>
struct X {
T t;
};
template <>
struct X<int> {
int val;
X<int>() : val(42) {}
};
inline X<int> NotPresent() { return X<int>(); }"#,
)?;
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {NotPresent});
assert_cc_not_matches!(rs_api_impl, quote! {NotPresent});
Ok(())
}
#[gtest]
fn test_default_crubit_features_disabled_variadic_function() -> Result<()> {
let mut ir = ir_from_cc(
r#"
int sprintf(char* str, const char* format, ...);
"#,
)?;
*ir.target_crubit_features_mut(&ir.current_target().clone()) =
crubit_feature::CrubitFeature::Supported.into();
let BindingsTokens { rs_api, rs_api_impl } = generate_bindings_tokens_for_test(ir)?;
assert_rs_not_matches!(rs_api, quote! {sprintf});
assert_cc_not_matches!(rs_api_impl, quote! {sprintf});
Ok(())
}
#[gtest]
fn test_existing_rust_type_assert() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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(())
}
#[gtest]
fn test_existing_rust_type_c_abi_incompatible() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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) unsafe fn __rust_thunk___Z4Makev(__return: *mut ::core::ffi::c_void);
}
);
Ok(())
}
#[gtest]
fn test_existing_rust_type_c_abi_compatible() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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) unsafe fn __rust_thunk___Z4Makev() -> i8;
}
);
Ok(())
}
/// We cannot generate size/align assertions for incomplete types.
#[gtest]
fn test_existing_rust_type_assert_incomplete() -> Result<()> {
let rs_api = generate_bindings_tokens_for_test(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(())
}