| // 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(()) | |
| } |