blob: e7c651f75a7dfcffbd4ba38fae3567961daa7b8c [file] [log] [blame] [edit]
// 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
#![deny(missing_docs)]
//! Crubit annotations for Rust APIs.
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use std::collections::{hash_map::Entry, HashMap};
use syn::parse::{Parse, ParseStream};
use syn::token;
use syn::{parse_macro_input, Ident, LitStr};
/// A single `ident="string literal"` pair.
struct KeyValue {
key: Ident,
value: LitStr,
}
impl Parse for KeyValue {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let key = Ident::parse(input)?;
token::Eq::parse(input)?;
let value = <LitStr as Parse>::parse(input)?;
Ok(KeyValue { key, value })
}
}
/// A comma-separated list of `ident="string literal"` pairs.
struct KeyValueList(Vec<KeyValue>);
impl Parse for KeyValueList {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut entries = Vec::new();
while !input.is_empty() {
entries.push(KeyValue::parse(input)?);
if !input.is_empty() {
token::Comma::parse(input)?;
}
}
Ok(Self(entries))
}
}
fn combine(a: &mut syn::Result<()>, b: syn::Error) {
if let Err(a) = a {
a.combine(b);
} else {
*a = Err(b);
}
}
impl KeyValueList {
/// Returns an error if any key is duplicated, if an expected key is not present,
/// or if extra keys are present.
fn check_keys(&self, expected_keys: &[&str]) -> syn::Result<()> {
let mut key_seen: HashMap<String, bool> =
expected_keys.iter().map(|key| (key.to_string(), false)).collect();
let mut maybe_error = syn::Result::Ok(());
for entry in &self.0 {
let key_string = entry.key.to_string();
match key_seen.entry(key_string) {
Entry::Vacant(key_string_entry) => {
combine(
&mut maybe_error,
syn::Error::new(
entry.key.span(),
format!(
"Unexpected key `{}` provided to `crubit_annotate`",
key_string_entry.key()
),
),
);
}
Entry::Occupied(mut already_seen) => {
if *already_seen.get() {
combine(
&mut maybe_error,
syn::Error::new(
entry.key.span(),
format!(
"Duplicate key `{}` provided to `crubit_annotate`",
entry.key
),
),
);
}
*already_seen.get_mut() = true;
}
}
}
for (key, seen) in key_seen {
if !seen {
combine(
&mut maybe_error,
syn::Error::new(
Span::call_site(),
format!("Expected key `{}` not provided to `crubit_annotate`", key),
),
);
}
}
maybe_error
}
/// Transforms the this `KeyValueList` into a doc comment before the token stream so that
/// it can be consumed by Crubit via the Rust HIR.
///
/// Rust HIR only has exposes user-defined attributes that are either doc comments or tool
/// attributes, and tool attributes are unstable and require an additional crate level attribute
/// to declare.
///
/// The entries appear as `#[doc="CRUBIT_ANNOTATE: key=value"]`.
fn to_doc_comments(&self) -> TokenStream {
self.0
.iter()
.map(|entry| key_value_to_doc_comment(&entry.key.to_string(), &entry.value.value()))
.collect()
}
}
/// Creates a doc comment for a single `key=value` pair.
///
/// The values appear as `#[doc="CRUBIT_ANNOTATE: key=value"]`.
fn key_value_to_doc_comment(key: &str, value: &str) -> TokenStream {
let value = format!("CRUBIT_ANNOTATE: {}={}", key, value);
TokenStream::from(quote! { #[doc=#value] })
}
fn key_value_list_with_keys_to_doc_comment(
attribute: TokenStream,
expected_keys: &[&str],
) -> TokenStream {
let attribute_args = parse_macro_input!(attribute as KeyValueList);
if let Err(error) = attribute_args.check_keys(expected_keys) {
return TokenStream::from(error.into_compile_error());
}
attribute_args.to_doc_comments()
}
/// Creates a TokenStream that prepends the computed prefix onto the given body.
///
/// This function encourages users to write all compiler-error-driven early-returns in the body of
/// `make_prefix_fn`. This ensures that the body is always emitted regardless of whether
/// the attribute itself successfully parses.
fn make_prefix_for(body: TokenStream, make_prefix_fn: impl FnOnce() -> TokenStream) -> TokenStream {
let mut output = make_prefix_fn();
output.extend([body]);
output
}
/// Marks a Rust type as having equivalent layout to a particular pre-defined C++ type.
///
/// This annotation prevents Crubit from generating a C++ type for the Rust type,
/// instead binding it directly to the C++ type.
///
/// The annotation accepts two string arguments:
///
/// * `cpp_type`: The fully-qualified name of the C++ type to which the Rust type is equivalent.
/// * `include_path`: The path to the header file that defines the C++ type.
///
/// Example:
///
/// ```rs
/// #[crubit_annotate::cpp_layout_equivalent(
/// cpp_type="::std::string",
/// include_path="<string>",
/// )]
/// pub struct CppString {
/// ...
/// }
/// ```
#[proc_macro_attribute]
pub fn cpp_layout_equivalent(attribute: TokenStream, input: TokenStream) -> TokenStream {
make_prefix_for(input, || {
key_value_list_with_keys_to_doc_comment(attribute, &["cpp_type", "include_path"])
})
}
/// Marks a Rust type as being by-value convertible to a particular pre-defined C++ type.
///
/// This annotation prevents Crubit from generating a C++ type for the Rust type,
/// instead binding it directly to the C++ type.
///
/// This annotation accepts four string arguments:
///
/// * `cpp_type`: The fully-qualified name of the C++ type to which the Rust type is convertible.
/// * `include_path`: The path to the header file that defines the C++ type.
/// * `cpp_to_rust_converter`: The name of the `extern "C"` C++ to Rust converter function.
/// * `rust_to_cpp_converter`: The name of the `extern "C"` Rust to C++ converter function.
///
/// Both function arguments must be `extern "C"` functions that accept two void pointers: the first
/// for the input and the second for the output.
///
/// Example:
///
/// ```rs
/// #[crubit_annotate::cpp_convertible(
/// cpp_type="::std::string",
/// include_path="<string>",
/// cpp_to_rust_converter="cpp_string_to_rust_string",
/// rust_to_cpp_converter="rust_string_to_cpp_string",
/// )]
/// pub struct CppString {
/// ...
/// }
///
/// #[unsafe(no_mangle)]
/// pub unsafe extern "C" fn rust_string_to_cpp_string(input: *const c_void, output: *mut c_void) {
/// ...
/// }
///
/// #[unsafe(no_mangle)]
/// pub unsafe extern "C" fn cpp_string_to_rust_string(input: *const c_void, output: *mut c_void) {
/// ...
/// }
/// ```
///
#[proc_macro_attribute]
pub fn cpp_convertible(attribute: TokenStream, input: TokenStream) -> TokenStream {
make_prefix_for(input, || {
key_value_list_with_keys_to_doc_comment(
attribute,
&["cpp_type", "include_path", "cpp_to_rust_converter", "rust_to_cpp_converter"],
)
})
}
/// Marks a Rust item as having a different name when used from C++.
///
/// This allows for renaming Rust functions and types names that are not C++-compatible, such as
/// `new`.
///
/// For instance, the following annotation indicates that the Rust function
/// `new` should be renamed to `Create` in C++:
///
/// ```
/// #[crubit_annotate::cpp_name("Create")]
/// pub fn new() -> i32 {...}
/// ```
#[proc_macro_attribute]
pub fn cpp_name(attribute: TokenStream, input: TokenStream) -> TokenStream {
make_prefix_for(input, || {
let attribute_args = parse_macro_input!(attribute as LitStr);
key_value_to_doc_comment("cpp_name", &attribute_args.value())
})
}
/// Marks a Rust struct to be translated into a C++ enum or enum class.
///
/// This annotation accepts a single string `kind` argument, which must be either `enum` or `enum
/// class`.
///
/// Structs with this annotation must be repr-transparent structs with a single primitive field.
/// The structs also cannot have any methods, as the translated C++ enum cannot have methods.
///
/// Example:
///
/// ```rs
/// #[crubit_annotate::cpp_enum(kind="enum class")]
/// #[repr(transparent)]
/// pub struct MyEnum(i32);
///
/// impl MyEnum {
/// pub const VARIANT_0: MyEnum = MyEnum(0);
/// pub const VARIANT_1: MyEnum = MyEnum(1);
/// // ...
/// }
/// ```
///
/// This will generate (approximately) the following C++ code:
///
/// ```c++
/// enum class MyEnum : std::int32_t {
/// VARIANT_0 = 0,
/// VARIANT_1 = 1,
/// // ...
/// };
/// ```
#[proc_macro_attribute]
pub fn cpp_enum(attribute: TokenStream, input: TokenStream) -> TokenStream {
make_prefix_for(input, || {
let attribute_args = parse_macro_input!(attribute as KeyValueList);
if let Err(error) = attribute_args.check_keys(&["kind"]) {
return TokenStream::from(error.into_compile_error());
}
let [kind] = &attribute_args.0[..] else { unreachable!() };
let kind_str = kind.value.value();
if kind_str != "enum" && kind_str != "enum class" {
return TokenStream::from(
syn::Error::new(
kind.value.span(),
format!(
"Invalid `kind` value `{}` for `cpp_enum` annotation. \
Expected \"enum\" or \"enum class\".",
kind_str
),
)
.into_compile_error(),
);
}
key_value_to_doc_comment("cpp_enum", &kind_str)
})
}
/// Enforces that a Rust function or type receives C++ bindings.
///
/// Example:
///
/// ```rs
/// #[crubit_annotate::must_bind]
/// pub fn my_function() -> i32 {...}
/// ```
///
/// By default, Crubit will gracefully omit bindings for functions and types that cannot be bound to
/// C++. Applying `#[must_bind]` to the item ensures that Crubit will fail at bindings generation
/// time if the item cannot be bound to C++.
///
/// This can be useful for ensuring that particular functions or types are usable from C++.
#[proc_macro_attribute]
pub fn must_bind(attribute: TokenStream, input: TokenStream) -> TokenStream {
make_prefix_for(input, || {
if !attribute.is_empty() {
return TokenStream::from(
syn::Error::new(
attribute.into_iter().next().unwrap().span().into(),
"The `must_bind` annotation does not accept any arguments.",
)
.into_compile_error(),
);
}
key_value_to_doc_comment("must_bind", "")
})
}