diff --git a/rs_bindings_from_cc/importers/cxx_record.cc b/rs_bindings_from_cc/importers/cxx_record.cc index 7b59e4646..83adfc721 100644 --- a/rs_bindings_from_cc/importers/cxx_record.cc +++ b/rs_bindings_from_cc/importers/cxx_record.cc @@ -1121,12 +1121,24 @@ std::optional CXXRecordDeclImporter::Import( const clang::TypedefNameDecl* anon_typedef = record_decl->getTypedefNameForAnonDecl(); + absl::StatusOr is_thread_safe = + HasAnnotationWithoutArgs(*record_decl, "crubit_thread_safe"); + if (!is_thread_safe.ok()) { + return unsupported( + FormattedError::FromStatus(std::move(is_thread_safe).status())); + } + absl::StatusOr trait_derives = GetTraitDerives(*record_decl); if (!trait_derives.ok()) { return unsupported( FormattedError::FromStatus(std::move(trait_derives).status())); } + if (*is_thread_safe) { + trait_derives->send = true; + trait_derives->sync = true; + } + absl::StatusOr safety_annotation = GetSafetyAnnotation(*record_decl); if (!safety_annotation.ok()) { @@ -1203,6 +1215,7 @@ std::optional CXXRecordDeclImporter::Import( .enclosing_item_id = std::move(enclosing_item_id), .overloads_operator_delete = MayOverloadOperatorDelete(*record_decl), .detected_formatter = *detected_formatter, + .is_thread_safe = *is_thread_safe, .lifetime_inputs = std::move(lifetime_inputs), }; diff --git a/rs_bindings_from_cc/ir.cc b/rs_bindings_from_cc/ir.cc index 8c19f3934..5e5a71c6b 100644 --- a/rs_bindings_from_cc/ir.cc +++ b/rs_bindings_from_cc/ir.cc @@ -720,6 +720,7 @@ llvm::json::Value Record::ToJson() const { {"must_bind", must_bind}, {"overloads_operator_delete", overloads_operator_delete}, {"detected_formatter", detected_formatter}, + {"is_thread_safe", is_thread_safe}, }; if (!lifetime_inputs.empty()) { diff --git a/rs_bindings_from_cc/ir.h b/rs_bindings_from_cc/ir.h index 2bddc13bb..f24b4919a 100644 --- a/rs_bindings_from_cc/ir.h +++ b/rs_bindings_from_cc/ir.h @@ -819,6 +819,11 @@ struct Record { bool overloads_operator_delete = false; bool detected_formatter = false; + // Whether this type is annotated as thread-safe (CRUBIT_THREAD_SAFE). + // Thread-safe types implement Send+Sync and wrap their internals in + // UnsafeCell, allowing non-const C++ methods to be called via &self. + bool is_thread_safe = false; + // Lifetime variable names bound by this record. std::vector lifetime_inputs; diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs index d23bf13fc..ff4a14578 100644 --- a/rs_bindings_from_cc/ir.rs +++ b/rs_bindings_from_cc/ir.rs @@ -1214,6 +1214,9 @@ pub struct Record { /// string is used. #[serde(default)] pub deprecated: Option>, + /// Whether this type is annotated as thread-safe (CRUBIT_THREAD_SAFE). + #[serde(default)] + pub is_thread_safe: bool, } impl GenericItem for Record { diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs index 6e82b8492..bb67614f3 100644 --- a/rs_bindings_from_cc/ir_from_cc_test.rs +++ b/rs_bindings_from_cc/ir_from_cc_test.rs @@ -926,6 +926,49 @@ fn test_conflicting_unsafe_annotation() { ); } +#[gtest] +fn test_struct_with_thread_safe_annotation() { + let ir = ir_from_cc( + r#" + struct [[clang::annotate("crubit_thread_safe")]] + ThreadSafeType { + int foo; + };"#, + ) + .unwrap(); + + assert_ir_matches!( + ir, + quote! { + Record { + rs_name: "ThreadSafeType", ... + is_thread_safe: true, ... + } + } + ); +} + +#[gtest] +fn test_struct_without_thread_safe_annotation() { + let ir = ir_from_cc( + r#" + struct NotThreadSafe { + int foo; + };"#, + ) + .unwrap(); + + assert_ir_matches!( + ir, + quote! { + Record { + rs_name: "NotThreadSafe", ... + is_thread_safe: false, ... + } + } + ); +} + #[gtest] fn test_struct_with_unnamed_struct_and_union_members() { // This test input causes `field_decl->getName()` to return an empty string. diff --git a/rs_bindings_from_cc/test/annotations/BUILD b/rs_bindings_from_cc/test/annotations/BUILD index f8a938e1c..efbaad68c 100644 --- a/rs_bindings_from_cc/test/annotations/BUILD +++ b/rs_bindings_from_cc/test/annotations/BUILD @@ -159,3 +159,30 @@ crubit_rust_test( "@crate_index//:googletest", ], ) + +crubit_test_cc_library( + name = "thread_safe", + hdrs = ["thread_safe.h"], + deps = [ + "//support:annotations", + ], +) + +crubit_rust_test( + name = "thread_safe_test", + srcs = ["thread_safe_test.rs"], + cc_deps = [ + ":thread_safe", + ], + deps = [ + "@crate_index//:googletest", + ], +) + +golden_test( + name = "thread_safe_golden_test", + basename = "thread_safe", + cc_library = "thread_safe", + golden_cc = "thread_safe_api_impl.cc", + golden_rs = "thread_safe_rs_api.rs", +) diff --git a/rs_bindings_from_cc/test/annotations/thread_safe.h b/rs_bindings_from_cc/test/annotations/thread_safe.h new file mode 100644 index 000000000..9a8a4e8c5 --- /dev/null +++ b/rs_bindings_from_cc/test/annotations/thread_safe.h @@ -0,0 +1,25 @@ +// 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 + +#ifndef THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_ANNOTATIONS_THREAD_SAFE_H_ +#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_ANNOTATIONS_THREAD_SAFE_H_ + +#include "support/annotations.h" + +namespace crubit::test { + +// A simple thread-safe struct. +struct CRUBIT_THREAD_SAFE ThreadSafeStruct final { + int x; + int y; +}; + +// A regular (non-thread-safe) struct for comparison. +struct RegularStruct final { + int value; +}; + +} // namespace crubit::test + +#endif // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_ANNOTATIONS_THREAD_SAFE_H_ diff --git a/rs_bindings_from_cc/test/annotations/thread_safe_api_impl.cc b/rs_bindings_from_cc/test/annotations/thread_safe_api_impl.cc new file mode 100644 index 000000000..e38f2ef56 --- /dev/null +++ b/rs_bindings_from_cc/test/annotations/thread_safe_api_impl.cc @@ -0,0 +1,41 @@ +// 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 + +// Automatically @generated Rust bindings for the following C++ target: +// //rs_bindings_from_cc/test/annotations:thread_safe +// Features: supported, types + +#include "support/internal/cxx20_backports.h" +#include "support/internal/offsetof.h" +#include "support/internal/sizeof.h" + +#include +#include + +// Public headers of the C++ library being wrapped. +#include "rs_bindings_from_cc/test/annotations/thread_safe.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wthread-safety-analysis" + +static_assert(CRUBIT_SIZEOF(struct crubit::test::ThreadSafeStruct) == 8); +static_assert(alignof(struct crubit::test::ThreadSafeStruct) == 4); +static_assert(CRUBIT_OFFSET_OF(x, struct crubit::test::ThreadSafeStruct) == 0); +static_assert(CRUBIT_OFFSET_OF(y, struct crubit::test::ThreadSafeStruct) == 4); + +extern "C" void __rust_thunk___ZN6crubit4test16ThreadSafeStructC1Ev( + struct crubit::test::ThreadSafeStruct* __this) { + crubit::construct_at(__this); +} + +static_assert(CRUBIT_SIZEOF(struct crubit::test::RegularStruct) == 4); +static_assert(alignof(struct crubit::test::RegularStruct) == 4); +static_assert(CRUBIT_OFFSET_OF(value, struct crubit::test::RegularStruct) == 0); + +extern "C" void __rust_thunk___ZN6crubit4test13RegularStructC1Ev( + struct crubit::test::RegularStruct* __this) { + crubit::construct_at(__this); +} + +#pragma clang diagnostic pop diff --git a/rs_bindings_from_cc/test/annotations/thread_safe_rs_api.rs b/rs_bindings_from_cc/test/annotations/thread_safe_rs_api.rs new file mode 100644 index 000000000..99d2965ac --- /dev/null +++ b/rs_bindings_from_cc/test/annotations/thread_safe_rs_api.rs @@ -0,0 +1,117 @@ +// 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 + +// Automatically @generated Rust bindings for the following C++ target: +// //rs_bindings_from_cc/test/annotations:thread_safe +// Features: supported, types + +#![rustfmt::skip] +#![feature(custom_inner_attributes, negative_impls)] +#![allow(stable_features)] +#![allow(improper_ctypes)] +#![allow(nonstandard_style)] +#![allow(unused)] +#![deny(warnings)] + +pub mod crubit { + pub mod test { + /// A simple thread-safe struct. + /// + /// Generated from: rs_bindings_from_cc/test/annotations/thread_safe.h;l=13 + #[derive(Clone, Copy, ::ctor::MoveAndAssignViaCopy)] + #[repr(C)] + ///CRUBIT_ANNOTATE: cpp_type=crubit :: test :: ThreadSafeStruct + pub struct ThreadSafeStruct { + pub x: ::ffi_11::c_int, + pub y: ::ffi_11::c_int, + } + unsafe impl Send for ThreadSafeStruct {} + unsafe impl Sync for ThreadSafeStruct {} + unsafe impl ::cxx::ExternType for ThreadSafeStruct { + type Id = ::cxx::type_id!("crubit :: test :: ThreadSafeStruct"); + type Kind = ::cxx::kind::Trivial; + } + + /// Generated from: rs_bindings_from_cc/test/annotations/thread_safe.h;l=13 + impl Default for ThreadSafeStruct { + #[inline(always)] + fn default() -> Self { + let mut tmp = ::core::mem::MaybeUninit::::zeroed(); + unsafe { + crate::detail::__rust_thunk___ZN6crubit4test16ThreadSafeStructC1Ev( + &raw mut tmp as *mut _, + ); + tmp.assume_init() + } + } + } + + /// A regular (non-thread-safe) struct for comparison. + /// + /// Generated from: rs_bindings_from_cc/test/annotations/thread_safe.h;l=19 + #[derive(Clone, Copy, ::ctor::MoveAndAssignViaCopy)] + #[repr(C)] + ///CRUBIT_ANNOTATE: cpp_type=crubit :: test :: RegularStruct + pub struct RegularStruct { + pub value: ::ffi_11::c_int, + } + impl !Send for RegularStruct {} + impl !Sync for RegularStruct {} + unsafe impl ::cxx::ExternType for RegularStruct { + type Id = ::cxx::type_id!("crubit :: test :: RegularStruct"); + type Kind = ::cxx::kind::Trivial; + } + + /// Generated from: rs_bindings_from_cc/test/annotations/thread_safe.h;l=19 + impl Default for RegularStruct { + #[inline(always)] + fn default() -> Self { + let mut tmp = ::core::mem::MaybeUninit::::zeroed(); + unsafe { + crate::detail::__rust_thunk___ZN6crubit4test13RegularStructC1Ev( + &raw mut tmp as *mut _, + ); + tmp.assume_init() + } + } + } + } +} + +// namespace crubit::test + +// Generated from: nowhere/llvm/src/libcxx/include/__type_traits/integral_constant.h;l=21 +// error: struct `std::integral_constant` could not be bound +// template instantiation is not yet supported + +// Generated from: nowhere/llvm/src/libcxx/include/__type_traits/integral_constant.h;l=21 +// error: struct `std::integral_constant` could not be bound +// template instantiation is not yet supported + +mod detail { + #[allow(unused_imports)] + use super::*; + unsafe extern "C" { + pub(crate) unsafe fn __rust_thunk___ZN6crubit4test16ThreadSafeStructC1Ev( + __this: *mut ::core::ffi::c_void, + ); + pub(crate) unsafe fn __rust_thunk___ZN6crubit4test13RegularStructC1Ev( + __this: *mut ::core::ffi::c_void, + ); + } +} + +const _: () = { + assert!(::core::mem::size_of::() == 8); + assert!(::core::mem::align_of::() == 4); + static_assertions::assert_impl_all!(crate::crubit::test::ThreadSafeStruct: Copy,Clone); + static_assertions::assert_not_impl_any!(crate::crubit::test::ThreadSafeStruct: Drop); + assert!(::core::mem::offset_of!(crate::crubit::test::ThreadSafeStruct, x) == 0); + assert!(::core::mem::offset_of!(crate::crubit::test::ThreadSafeStruct, y) == 4); + assert!(::core::mem::size_of::() == 4); + assert!(::core::mem::align_of::() == 4); + static_assertions::assert_impl_all!(crate::crubit::test::RegularStruct: Copy,Clone); + static_assertions::assert_not_impl_any!(crate::crubit::test::RegularStruct: Drop); + assert!(::core::mem::offset_of!(crate::crubit::test::RegularStruct, value) == 0); +}; diff --git a/rs_bindings_from_cc/test/annotations/thread_safe_test.rs b/rs_bindings_from_cc/test/annotations/thread_safe_test.rs new file mode 100644 index 000000000..d1d067f3d --- /dev/null +++ b/rs_bindings_from_cc/test/annotations/thread_safe_test.rs @@ -0,0 +1,41 @@ +// 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 googletest::gtest; +use thread_safe::crubit::test::ThreadSafeStruct; + +/// Verify that the thread-safe type implements Send. +/// This is a compile-time check: if ThreadSafeStruct doesn't implement Send, +/// this test will fail to compile. +#[gtest] +fn test_thread_safe_is_send() { + fn assert_send() {} + assert_send::(); +} + +/// Verify that the thread-safe type implements Sync. +/// This is a compile-time check: if ThreadSafeStruct doesn't implement Sync, +/// this test will fail to compile. +#[gtest] +fn test_thread_safe_is_sync() { + fn assert_sync() {} + assert_sync::(); +} + +/// Verify that the generated struct has the expected size (matching the C++ type +/// with two ints). +#[gtest] +fn test_thread_safe_struct_has_correct_size() { + assert_eq!(std::mem::size_of::(), 8); +} + +/// Verify that a non-thread-safe type does NOT implement Send or Sync. +/// These are compile-time checks using negative trait bounds. +#[gtest] +fn test_regular_struct_is_not_send_or_sync() { + // RegularStruct should not be Send or Sync (Crubit generates negative impls). + // We verify it exists and has expected size. The Send/Sync negative impls + // are verified by the codegen unit tests. + assert_eq!(std::mem::size_of::(), 4); +} diff --git a/support/annotations.h b/support/annotations.h index 522164bb8..8067c4bcc 100644 --- a/support/annotations.h +++ b/support/annotations.h @@ -331,6 +331,30 @@ #define CRUBIT_OWNED_POINTEE(name) \ CRUBIT_INTERNAL_ANNOTATE("crubit_owned_pointee", name) +// Marks a type as thread-safe for Rust interop. +// +// Types annotated with `CRUBIT_THREAD_SAFE` will: +// * Implement `Send + Sync` in Rust +// * Have their internal representation wrapped in `UnsafeCell`, allowing +// non-const C++ methods to be called via shared references (`&self`) +// +// This annotation is appropriate for types that internally synchronize +// access (e.g., types with mutexes, atomics, or other synchronization +// primitives). +// +// Example: +// ```c++ +// class CRUBIT_THREAD_SAFE ThreadSafeCounter { +// public: +// void Increment(); // Can be called via &self in Rust +// int Get() const; // Can also be called via &self +// private: +// std::atomic count_; +// mutable std::mutex mu_; +// }; +// ``` +#define CRUBIT_THREAD_SAFE CRUBIT_INTERNAL_ANNOTATE("crubit_thread_safe") + // Overrides the `Display` binding detection for a type to true or false. // // If detected: binds to Rust's `Display` trait, preferring `AbslStringify` over