-
Notifications
You must be signed in to change notification settings - Fork 63
Expand file tree
/
Copy pathcrubit_annotate.rs
More file actions
348 lines (329 loc) · 11.6 KB
/
crubit_annotate.rs
File metadata and controls
348 lines (329 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// 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"],
)
})
}
/// Composable bridging.
#[proc_macro_attribute]
pub fn cpp_bridge(attribute: TokenStream, input: TokenStream) -> TokenStream {
make_prefix_for(input, || {
key_value_list_with_keys_to_doc_comment(
attribute,
&["cpp_type", "bridge_abi_cpp", "bridge_abi_rust"],
)
})
}
/// 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", "")
})
}