rs_bindings_from_cc: Use an attribute to dynamically specify type mapping.
---
This Cl allows you to override Crubit's type-mapping, so that instead of generating bindings for a type `X`, it instead looks up an already-existing type `Y`. This is useful, for example:
* For specialty types like `rs_char`, where the other language's equivalent is a builtin type.
* For types with native language support, like protocol buffers, where we want to use the native languages' proto rather than generated bindings
* For types which are themselves bindings: the bindings for the bindings for `X` should be `X` again, not an infinite-turtle bindings-of-bindings situation.
This is the first step to all of those: for calling C++ from Rust, we allow C++ types to override which Rust type they are.
A rough TODO list of next steps:
1. suppress bindings generation for the record itself, when annotated with `crubit_rust_type`.
2. apply `crubit_rust_type` annotations to generated bindings, so that bindings-of-bindings can DTRT.
3. do ~something about crubit features. Currently they would block round-tripped bindings.
4. rinse and repeat for cc_bindings_from_rs.
PiperOrigin-RevId: 529212737
diff --git a/rs_bindings_from_cc/BUILD b/rs_bindings_from_cc/BUILD
index 3aafd29..61f456e 100644
--- a/rs_bindings_from_cc/BUILD
+++ b/rs_bindings_from_cc/BUILD
@@ -160,13 +160,18 @@
)
cc_library(
- name = "known_types_map",
- srcs = ["known_types_map.cc"],
- hdrs = ["known_types_map.h"],
+ name = "type_map",
+ srcs = ["type_map.cc"],
+ hdrs = ["type_map.h"],
visibility = ["//:__subpackages__"],
deps = [
+ ":cc_ir",
+ "//common:status_macros",
"@absl//absl/container:flat_hash_map",
+ "@absl//absl/status",
+ "@absl//absl/status:statusor",
"@absl//absl/strings",
+ "@llvm-project//clang:ast",
],
)
@@ -250,7 +255,7 @@
":bazel_types",
":cc_ir",
":decl_importer",
- ":known_types_map",
+ ":type_map",
"//common:status_macros",
"//lifetime_annotations:type_lifetimes",
"//rs_bindings_from_cc/importers:class_template",
@@ -418,7 +423,6 @@
hdrs = ["src_code_gen.h"],
deps = [
":cc_ir",
- ":cmdline",
":src_code_gen_impl", # buildcleaner: keep
"//common:cc_ffi_types",
"//common:status_macros",
diff --git a/rs_bindings_from_cc/importer.cc b/rs_bindings_from_cc/importer.cc
index 3afb187..94fb472 100644
--- a/rs_bindings_from_cc/importer.cc
+++ b/rs_bindings_from_cc/importer.cc
@@ -33,7 +33,7 @@
#include "rs_bindings_from_cc/ast_util.h"
#include "rs_bindings_from_cc/bazel_types.h"
#include "rs_bindings_from_cc/ir.h"
-#include "rs_bindings_from_cc/known_types_map.h"
+#include "rs_bindings_from_cc/type_map.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
@@ -759,9 +759,9 @@
// Qualifiers are handled separately in ConvertQualType().
std::string type_string = clang::QualType(type, 0).getAsString();
- if (auto maybe_mapped_type = MapKnownCcTypeToRsType(type_string);
- maybe_mapped_type.has_value()) {
- return MappedType::Simple(std::string(*maybe_mapped_type), type_string);
+ CRUBIT_ASSIGN_OR_RETURN(auto override_type, TypeMapOverride(*type));
+ if (override_type.has_value()) {
+ return *std::move(override_type);
} else if (type->isPointerType() || type->isLValueReferenceType() ||
type->isRValueReferenceType()) {
clang::QualType pointee_type = type->getPointeeType();
diff --git a/rs_bindings_from_cc/importers/BUILD b/rs_bindings_from_cc/importers/BUILD
index 66da70c..35f1126 100644
--- a/rs_bindings_from_cc/importers/BUILD
+++ b/rs_bindings_from_cc/importers/BUILD
@@ -98,7 +98,7 @@
deps = [
"@absl//absl/log:check",
"//rs_bindings_from_cc:decl_importer",
- "//rs_bindings_from_cc:known_types_map",
+ "//rs_bindings_from_cc:type_map",
"@llvm-project//clang:ast",
],
)
diff --git a/rs_bindings_from_cc/importers/cxx_record.cc b/rs_bindings_from_cc/importers/cxx_record.cc
index 181d1bc..be93427 100644
--- a/rs_bindings_from_cc/importers/cxx_record.cc
+++ b/rs_bindings_from_cc/importers/cxx_record.cc
@@ -181,14 +181,18 @@
}
}
- // The less expensive `getName` comparison is done first, because the
- // documentation of `NamedDecl::getQualifiedNameAsString` says that "it should
- // be called only when performance doesn't matter".
+ // TODO(b/274834739): Automatically suppress this based on crubit_rust_type,
+ // similar to the logic for type aliases.
+ // TODO(b/274834739): emit size/align assertions for these mapped types.
+ //
+ // The less expensive `getName` comparison is done
+ // first, because the documentation of `NamedDecl::getQualifiedNameAsString`
+ // says that "it should be called only when performance doesn't matter".
if (record_decl->getName() == "rs_char" &&
record_decl->getQualifiedNameAsString() == "rs_std::rs_char") {
return ictx_.ImportUnsupportedItem(
record_decl,
- "Round-tripping of `rs_char` is not supported yet (b/270160530)");
+ "Round-tripping of `rs_char` is not supported yet (b/274834739)");
}
absl::StatusOr<RecordType> record_type = TranslateRecordType(*record_decl);
diff --git a/rs_bindings_from_cc/importers/typedef_name.cc b/rs_bindings_from_cc/importers/typedef_name.cc
index 8a08e18..9e4eea8 100644
--- a/rs_bindings_from_cc/importers/typedef_name.cc
+++ b/rs_bindings_from_cc/importers/typedef_name.cc
@@ -7,7 +7,7 @@
#include <optional>
#include "absl/log/check.h"
-#include "rs_bindings_from_cc/known_types_map.h"
+#include "rs_bindings_from_cc/type_map.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
@@ -37,7 +37,8 @@
// into their item, instead of having a separate TypeAlias item in addition.
return std::nullopt;
}
- if (MapKnownCcTypeToRsType(type.getAsString()).has_value()) {
+ auto override_type = TypeMapOverride(*type.getTypePtr());
+ if (override_type.ok() && override_type->has_value()) {
return std::nullopt;
}
diff --git a/rs_bindings_from_cc/known_types_map.cc b/rs_bindings_from_cc/known_types_map.cc
deleted file mode 100644
index 40f35ef..0000000
--- a/rs_bindings_from_cc/known_types_map.cc
+++ /dev/null
@@ -1,71 +0,0 @@
-// 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
-
-#include "rs_bindings_from_cc/known_types_map.h"
-
-#include "absl/container/flat_hash_map.h"
-
-namespace crubit {
-
-// A mapping of C++ standard types to their equivalent Rust types.
-// To produce more idiomatic results, these types receive special handling
-// instead of using the generic type mapping mechanism.
-std::optional<absl::string_view> MapKnownCcTypeToRsType(
- absl::string_view cc_type) {
- static const auto* const kWellKnownTypes =
- new absl::flat_hash_map<absl::string_view, absl::string_view>({
- // TODO(lukasza): Try to deduplicate the entries below - for example:
- // - Try to unify `std::int32_t` and `int32_t`
- // - Try to unify `class rs_std::rs_char` and `rs_std::rs_char`
- // One approach would be to desugar the types before calling
- // `MapKnownCcTypeToRsType`, but note that desugaring of type aliases
- // may be undesirable (i.e. we may want the bindings to refer to
- // `TypeAlias` rather than directly to the type that it desugars to).
- // Note that b/254096006 tracks desire to preserve type aliases in
- // `cc_bindings_from_rs`.
- {"ptrdiff_t", "isize"},
- {"intptr_t", "isize"},
- {"size_t", "usize"},
- {"uintptr_t", "usize"},
- {"std::ptrdiff_t", "isize"},
- {"std::intptr_t", "isize"},
- {"std::size_t", "usize"},
- {"std::uintptr_t", "usize"},
-
- {"int8_t", "i8"},
- {"int16_t", "i16"},
- {"int32_t", "i32"},
- {"int64_t", "i64"},
- {"std::int8_t", "i8"},
- {"std::int16_t", "i16"},
- {"std::int32_t", "i32"},
- {"std::int64_t", "i64"},
-
- {"uint8_t", "u8"},
- {"uint16_t", "u16"},
- {"uint32_t", "u32"},
-
- {"uint64_t", "u64"},
- {"std::uint8_t", "u8"},
- {"std::uint16_t", "u16"},
- {"std::uint32_t", "u32"},
- {"std::uint64_t", "u64"},
-
- {"char16_t", "u16"},
- {"char32_t", "u32"},
- {"wchar_t", "i32"},
-
- // `class rs_std::rs_char` key covers direct usage of
- // `rs_std::rs_char`. `rs_std::rs_char` key covers scenarios when
- // `using` has imported `rs_char` into another namespace. See also
- // the deduplication TODO comment above.
- {"class rs_std::rs_char", "char"},
- {"rs_std::rs_char", "char"},
- });
- auto it = kWellKnownTypes->find(cc_type);
- if (it == kWellKnownTypes->end()) return std::nullopt;
- return it->second;
-}
-
-} // namespace crubit
diff --git a/rs_bindings_from_cc/known_types_map.h b/rs_bindings_from_cc/known_types_map.h
deleted file mode 100644
index 925d676..0000000
--- a/rs_bindings_from_cc/known_types_map.h
+++ /dev/null
@@ -1,21 +0,0 @@
-// 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_KNOWN_TYPES_MAP_H_
-#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_KNOWN_TYPES_MAP_H_
-
-#include <optional>
-
-#include "absl/strings/string_view.h"
-
-namespace crubit {
-
-// Converts primitive types like `std::usize` or `int64_t` into their Rust
-// equivalents.
-std::optional<absl::string_view> MapKnownCcTypeToRsType(
- absl::string_view cc_type);
-
-} // namespace crubit
-
-#endif // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_KNOWN_TYPES_MAP_H_
diff --git a/rs_bindings_from_cc/test/types/BUILD b/rs_bindings_from_cc/test/types/BUILD
index 08021c4..d16babe 100644
--- a/rs_bindings_from_cc/test/types/BUILD
+++ b/rs_bindings_from_cc/test/types/BUILD
@@ -6,6 +6,7 @@
crubit_test_cc_library(
name = "types_nonptr",
hdrs = ["types_nonptr.h"],
+ deps = ["//support/internal:bindings_support"],
)
crubit_test_cc_library(
diff --git a/rs_bindings_from_cc/test/types/types_nonptr.h b/rs_bindings_from_cc/test/types/types_nonptr.h
index 12561cd..78d5e32 100644
--- a/rs_bindings_from_cc/test/types/types_nonptr.h
+++ b/rs_bindings_from_cc/test/types/types_nonptr.h
@@ -11,6 +11,8 @@
#include <cstddef>
#include <cstdint>
+#include "support/internal/attribute_macros.h"
+
// Not a template, so that it isn't visible to the bindings generator.
// We're just here to save typing.
#define TEST(Name, T) \
@@ -77,4 +79,18 @@
struct ExampleStruct final {};
TEST(Struct, ExampleStruct);
+struct CRUBIT_INTERNAL_RUST_TYPE("i8") MyI8Struct final {
+ signed char x;
+};
+
+struct CRUBIT_INTERNAL_RUST_TYPE("i8") MyI8Class final {
+ signed char x;
+};
+
+enum CRUBIT_INTERNAL_RUST_TYPE("i8") MyI8Enum : unsigned char { kX };
+
+TEST(TypeMapOverrideStruct, MyI8Struct);
+TEST(TypeMapOverrideClass, MyI8Class);
+TEST(TypeMapOverrideEnum, MyI8Enum);
+
#endif // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_TEST_TYPES_TYPES_NONPTR_H_
diff --git a/rs_bindings_from_cc/test/types/types_test.rs b/rs_bindings_from_cc/test/types/types_test.rs
index b8de88b..ea29907 100644
--- a/rs_bindings_from_cc/test/types/types_test.rs
+++ b/rs_bindings_from_cc/test/types/types_test.rs
@@ -113,6 +113,9 @@
Double => f64,
Struct => types_nonptr::ExampleStruct,
+ TypeMapOverrideStruct => i8,
+ TypeMapOverrideClass => i8,
+ TypeMapOverrideEnum => i8,
);
// TODO(b/228569417): These should all generate bindings and be & (mut) 'static.
diff --git a/rs_bindings_from_cc/type_map.cc b/rs_bindings_from_cc/type_map.cc
new file mode 100644
index 0000000..5f452b0
--- /dev/null
+++ b/rs_bindings_from_cc/type_map.cc
@@ -0,0 +1,144 @@
+// 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
+
+#include "rs_bindings_from_cc/type_map.h"
+
+#include <optional>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "common/status_macros.h"
+#include "rs_bindings_from_cc/ir.h"
+#include "clang/AST/Attr.h"
+#include "clang/AST/Attrs.inc"
+#include "clang/AST/Decl.h"
+
+namespace crubit {
+
+namespace {
+// A mapping of C++ standard types to their equivalent Rust types.
+std::optional<absl::string_view> MapKnownCcTypeToRsType(
+ absl::string_view cc_type) {
+ static const auto* const kWellKnownTypes =
+ new absl::flat_hash_map<absl::string_view, absl::string_view>({
+ // TODO(lukasza): Try to deduplicate the entries below - for example:
+ // - Try to unify `std::int32_t` and `int32_t`
+ // One approach would be to desugar the types before calling
+ // `MapKnownCcTypeToRsType`, but note that desugaring of type aliases
+ // may be undesirable (i.e. we may want the bindings to refer to
+ // `TypeAlias` rather than directly to the type that it desugars to).
+ // Note that b/254096006 tracks desire to preserve type aliases in
+ // `cc_bindings_from_rs`.
+ {"ptrdiff_t", "isize"},
+ {"intptr_t", "isize"},
+ {"size_t", "usize"},
+ {"uintptr_t", "usize"},
+ {"std::ptrdiff_t", "isize"},
+ {"std::intptr_t", "isize"},
+ {"std::size_t", "usize"},
+ {"std::uintptr_t", "usize"},
+
+ {"int8_t", "i8"},
+ {"int16_t", "i16"},
+ {"int32_t", "i32"},
+ {"int64_t", "i64"},
+ {"std::int8_t", "i8"},
+ {"std::int16_t", "i16"},
+ {"std::int32_t", "i32"},
+ {"std::int64_t", "i64"},
+
+ {"uint8_t", "u8"},
+ {"uint16_t", "u16"},
+ {"uint32_t", "u32"},
+
+ {"uint64_t", "u64"},
+ {"std::uint8_t", "u8"},
+ {"std::uint16_t", "u16"},
+ {"std::uint32_t", "u32"},
+ {"std::uint64_t", "u64"},
+
+ {"char16_t", "u16"},
+ {"char32_t", "u32"},
+ {"wchar_t", "i32"},
+ });
+ auto it = kWellKnownTypes->find(cc_type);
+ if (it == kWellKnownTypes->end()) return std::nullopt;
+ return it->second;
+}
+
+// Copied from lifetime_annotations/type_lifetimes.cc, which is expected to move
+// into ClangTidy. See:
+// https://discourse.llvm.org/t/rfc-lifetime-annotations-for-c/61377
+absl::StatusOr<absl::string_view> EvaluateAsStringLiteral(
+ const clang::Expr& expr, const clang::ASTContext& ast_context) {
+ auto error = []() {
+ return absl::InvalidArgumentError(
+ "cannot evaluate argument as a string literal");
+ };
+
+ clang::Expr::EvalResult eval_result;
+ if (!expr.EvaluateAsConstantExpr(eval_result, ast_context) ||
+ !eval_result.Val.isLValue()) {
+ return error();
+ }
+
+ const auto* eval_result_expr =
+ eval_result.Val.getLValueBase().dyn_cast<const clang::Expr*>();
+ if (!eval_result_expr) {
+ return error();
+ }
+
+ const auto* string_literal =
+ clang::dyn_cast<clang::StringLiteral>(eval_result_expr);
+ if (!string_literal) {
+ return error();
+ }
+
+ return {string_literal->getString()};
+}
+
+absl::StatusOr<std::optional<absl::string_view>> GetRustTypeAttribute(
+ const clang::Type& cc_type) {
+ std::optional<absl::string_view> rust_type;
+ if (const clang::TagDecl* tag_decl = cc_type.getAsTagDecl();
+ tag_decl != nullptr) {
+ for (clang::AnnotateAttr* attr :
+ tag_decl->specific_attrs<clang::AnnotateAttr>()) {
+ if (attr->getAnnotation() != "crubit_internal_rust_type") continue;
+
+ if (rust_type.has_value())
+ return absl::InvalidArgumentError(
+ "Only one `crubit_internal_rust_type` attribute may be placed on a "
+ "type.");
+ if (attr->args_size() != 1)
+ return absl::InvalidArgumentError(
+ "The `crubit_internal_rust_type` attribute requires a single "
+ "string literal "
+ "argument, the Rust type.");
+ const clang::Expr& arg = **attr->args_begin();
+ CRUBIT_ASSIGN_OR_RETURN(
+ rust_type, EvaluateAsStringLiteral(arg, tag_decl->getASTContext()));
+ }
+ }
+ return rust_type;
+}
+
+} // namespace
+
+absl::StatusOr<std::optional<MappedType>> TypeMapOverride(
+ const clang::Type& cc_type) {
+ std::string type_string = clang::QualType(&cc_type, 0).getAsString();
+ std::optional<absl::string_view> rust_type;
+ CRUBIT_ASSIGN_OR_RETURN(rust_type, GetRustTypeAttribute(cc_type));
+ if (!rust_type.has_value()) {
+ rust_type = MapKnownCcTypeToRsType(type_string);
+ }
+ if (rust_type.has_value()) {
+ return MappedType::Simple(std::string(*rust_type), type_string);
+ }
+ return std::nullopt;
+}
+
+} // namespace crubit
diff --git a/rs_bindings_from_cc/type_map.h b/rs_bindings_from_cc/type_map.h
new file mode 100644
index 0000000..acb8f38
--- /dev/null
+++ b/rs_bindings_from_cc/type_map.h
@@ -0,0 +1,31 @@
+// 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_KNOWN_TYPES_MAP_H_
+#define THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_KNOWN_TYPES_MAP_H_
+
+#include <optional>
+
+#include "absl/strings/string_view.h"
+#include "rs_bindings_from_cc/ir.h"
+#include "clang/AST/Type.h"
+
+namespace crubit {
+
+// Converts C++ types to an already-existing Rust type, instead of generating
+// bindings for the C++ type.
+//
+// The return value is a fully-qualified Rust name, including builtin type
+// names.
+//
+// For example, C++ `int64_t` becomes Rust `i64`.
+//
+// To create a new type mapping, either add the type to the hardcoded list
+// of types, or else add the `crubit_rust_type` attribute.
+absl::StatusOr<std::optional<MappedType>> TypeMapOverride(
+ const clang::Type& cc_type);
+
+} // namespace crubit
+
+#endif // THIRD_PARTY_CRUBIT_RS_BINDINGS_FROM_CC_KNOWN_TYPES_MAP_H_
diff --git a/support/internal/BUILD b/support/internal/BUILD
index 7750a9d..0e7fb77 100644
--- a/support/internal/BUILD
+++ b/support/internal/BUILD
@@ -3,6 +3,7 @@
cc_library(
name = "bindings_support",
hdrs = [
+ "attribute_macros.h",
"cxx20_backports.h",
"offsetof.h",
"return_value_slot.h",
diff --git a/support/internal/attribute_macros.h b/support/internal/attribute_macros.h
new file mode 100644
index 0000000..6018a8d
--- /dev/null
+++ b/support/internal/attribute_macros.h
@@ -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
+
+#ifndef THIRD_PARTY_CRUBIT_SUPPORT_INTERNAL_ATTRIBUTES_H_
+#define THIRD_PARTY_CRUBIT_SUPPORT_INTERNAL_ATTRIBUTES_H_
+
+// TODO(jeanpierreda): Get a waiver for these, or else a waiver for each caller.
+#define CRUBIT_INTERNAL_ANNOTATE(...) [[clang::annotate(__VA_ARGS__)]]
+#define CRUBIT_INTERNAL_ANNOTATE_TYPE(...) [[clang::annotate_type(__VA_ARGS__)]]
+
+// Unsafe: disables bindings, and reinterprets all uses of this type as `t`.
+//
+// This attribute completely disables automated bindings for the type which it
+// appertains to. All uses of that type are replaced with uses of `t`, which
+// must be a rust type which exists and is guaranteed to be available by that
+// name.
+//
+// This can be applied to a struct, class, or enum.
+//
+// TODO(b/274834739): also support type aliases.
+//
+// For example, this C++ header:
+//
+// ```c++
+// struct CRUBIT_INTERNAL_RUST_TYPE("char") CharT {std::uint32_t c; };
+// CharT foo() { return {0};}
+// ```
+//
+// Becomes this Rust interface:
+//
+// ```rust
+// pub fn foo() -> char; // returns '\0'
+// ```
+//
+// SAFETY:
+// If the type is not ABI-compatible with `t`, the behavior is undefined.
+#define CRUBIT_INTERNAL_RUST_TYPE(t) \
+ CRUBIT_INTERNAL_ANNOTATE("crubit_internal_rust_type", t)
+
+#endif // THIRD_PARTY_CRUBIT_SUPPORT_INTERNAL_ATTRIBUTES_H_
diff --git a/support/rs_std/BUILD b/support/rs_std/BUILD
index 1785f29..a059e28 100644
--- a/support/rs_std/BUILD
+++ b/support/rs_std/BUILD
@@ -14,7 +14,10 @@
# seems okay - we should be able to assume that Crubit users have a version
# of Abseil that is relatively recent (although we can't rely on an
# exact version and/or exact absl/base/options.h).
- deps = ["@absl//absl/base:core_headers"],
+ deps = [
+ "@absl//absl/base:core_headers",
+ "//support/internal:bindings_support",
+ ],
)
cc_test(
diff --git a/support/rs_std/rs_char.h b/support/rs_std/rs_char.h
index 8bec775..a13101e 100644
--- a/support/rs_std/rs_char.h
+++ b/support/rs_std/rs_char.h
@@ -9,13 +9,14 @@
#include <optional>
#include "absl/base/optimization.h"
+#include "support/internal/attribute_macros.h"
namespace rs_std {
// `rs_std::rs_char` is a C++ representation of the `char` type from Rust.
-// `rust_builtin_type_abi_assumptions.md` documents the ABI compatiblity of
+// `rust_builtin_type_abi_assumptions.md` documents the ABI compatibility of
// these types.
-class rs_char final {
+class CRUBIT_INTERNAL_RUST_TYPE("char") rs_char final {
public:
// Creates a default `rs_char` - one that represents ASCII NUL character.
//