Skip to main content

protobuf_macros/
proto_proc_macro_impl.rs

1// Protocol Buffers - Google's data interchange format
2// Copyright 2025 Google LLC.  All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file or at
6// https://developers.google.com/open-source/licenses/bsd
7
8extern crate proc_macro;
9
10use quote::{format_ident, quote, ToTokens};
11use syn::{
12    parse::ParseStream, parse_macro_input, parse_quote, punctuated::Punctuated, Error, Expr,
13    ExprArray, ExprPath, ExprStruct, ExprTuple, FieldValue, Ident, Member, Path, QSelf, Result,
14    Stmt, Token, Type, TypePath,
15};
16
17/// proto! enables the use of Rust struct initialization syntax to create
18/// protobuf messages. The macro does its best to try and detect the
19/// initialization of submessages, but it is only able to do so while the
20/// submessage is being defined as part of the original struct literal.
21/// Introducing an expression using () or {} as the value of a field will
22/// require another call to this macro in order to return a submessage
23/// literal.
24///
25/// ```rust,ignore
26/// /*
27/// Given the following proto definition:
28/// message Data {
29///     int32 number = 1;
30///     string letters = 2;
31///     Data nested = 3;
32/// }
33/// */
34/// use protobuf_proc_macro::proto_proc;
35/// let message = proto_proc!(Data {
36///     number: 42,
37///     letters: "Hello World",
38///     nested: Data {
39///         number: {
40///             let x = 100;
41///             x + 1
42///         }
43///     }
44/// });
45/// ```
46#[proc_macro]
47pub fn proto_proc(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
48    let result = parse_macro_input!(input with parse_and_expand_top_level_struct);
49    result.to_token_stream().into()
50}
51
52fn parse_and_expand_top_level_struct(input: ParseStream) -> Result<Expr> {
53    expand_struct(input.parse()?, EnclosingContext::TopLevel)
54}
55
56/// The context in which an expression (struct or array literal) is being
57/// expanded.
58///
59/// This is needed because it subtly affects the generated code. For example,
60/// when expanding a nested message, the field is mutated in-place, but when
61/// expanding a message inside an array, a new message is created.
62#[derive(Clone)]
63enum EnclosingContext {
64    /// The current expression is the top-level message, directly inside the
65    /// `proto!` invocation.
66    TopLevel,
67
68    /// The current expression will be assigned to a field of a parent message.
69    Struct {
70        /// The name of the enclosing field.
71        field: Ident,
72    },
73
74    /// The current expression is an element of a repeated field (i.e. array).
75    Array {
76        /// The name of the repeated field.
77        repeated_field: Ident,
78    },
79
80    /// The current expression is (key, value) tuple literal for a map field.
81    Map { map_field: Ident },
82}
83
84impl EnclosingContext {
85    pub fn repeated_field(&self) -> Ident {
86        match self {
87            EnclosingContext::Array { repeated_field } => repeated_field.clone(),
88            _ => panic!("Can only get the repeated field of an array context"),
89        }
90    }
91}
92
93fn expand_struct(
94    ExprStruct { attrs, qself, path, fields, rest, .. }: ExprStruct,
95    enclosing_context: EnclosingContext,
96) -> Result<Expr> {
97    if let Some(attr) = attrs.first() {
98        return Err(Error::new_spanned(attr, "unsupported syntax"));
99    }
100
101    let merge = rest.map(|rest| {
102        quote! {
103            ::protobuf::MergeFrom::merge_from(&mut this, #rest);
104        }
105    });
106
107    let fields = expand_struct_fields(fields)?;
108    let this_type = expand_struct_type(qself.clone(), path.clone(), enclosing_context.clone());
109    let (head, tail) =
110        expand_struct_head_tail(qself.clone(), path.clone(), enclosing_context.clone())?;
111
112    Ok(parse_quote! {{
113        let mut this: #this_type = #head;
114        #merge
115        #(#fields)*
116        #tail
117    }})
118}
119
120fn expand_struct_fields(fields: Punctuated<FieldValue, Token![,]>) -> Result<Vec<Stmt>> {
121    fields
122        .into_iter()
123        .map(|field| {
124            let Member::Named(ident) = field.member else {
125                return Err(Error::new_spanned(field, "field must be an identifier, not an index"));
126            };
127            let set_method = format_ident!("set_{}", ident);
128            let enclosing_context = EnclosingContext::Struct { field: ident };
129            let expr = match field.expr {
130                Expr::Struct(struct_) => {
131                    // In a nested message, the value will be mutated in-place, so there is no need
132                    // to call the top-level setter.
133                    let expanded = expand_struct(struct_, enclosing_context)?;
134                    return Ok(parse_quote!(#expanded));
135                }
136                Expr::Array(array) => expand_array(array, enclosing_context)?,
137                expr => expr,
138            };
139            Ok(parse_quote! {
140                this.#set_method(#expr);
141            })
142        })
143        .collect()
144}
145
146fn expand_struct_type(
147    qself: Option<QSelf>,
148    path: Path,
149    enclosing_context: EnclosingContext,
150) -> Type {
151    if should_infer_message_type(&qself, &path) {
152        return parse_quote!(_);
153    }
154
155    let type_path = TypePath { qself, path };
156    if let EnclosingContext::Struct { .. } = enclosing_context {
157        // Nested messages use a mutable view type.
158        parse_quote!(::protobuf::Mut<'_, #type_path>)
159    } else {
160        parse_quote!(#type_path)
161    }
162}
163
164/// Builds two expressions: one that initializes the message, and another that
165/// returns it (if any).
166///
167/// If the enclosing context is another message, the child message is mutated
168/// in-place, so the returning expression is `None`.
169fn expand_struct_head_tail(
170    qself: Option<QSelf>,
171    path: Path,
172    enclosing_context: EnclosingContext,
173) -> Result<(Expr, Option<Expr>)> {
174    match enclosing_context {
175        EnclosingContext::TopLevel => {
176            if should_infer_message_type(&qself, &path) {
177                Err(Error::new_spanned(path, "message type must be specified explicitly"))
178            } else {
179                let path = ExprPath { attrs: Vec::new(), qself, path };
180                Ok((parse_quote!(#path::new()), Some(parse_quote!(this))))
181            }
182        }
183        EnclosingContext::Struct { field } => {
184            // In a nested message, mutate the field in-place.
185            let field_mut = format_ident!("{}_mut", field);
186            Ok((parse_quote!(this.#field_mut()), None))
187        }
188        EnclosingContext::Array { repeated_field } => {
189            // In a message inside an array, create a new message and return it.
190            // The type trickery is used to infer the message type from the repeated wrapper.
191            Ok((
192                parse_quote!(::protobuf::__internal::get_repeated_default_value(
193                    ::protobuf::__internal::Private,
194                    this.#repeated_field()
195                )),
196                Some(parse_quote!(this)),
197            ))
198        }
199        EnclosingContext::Map { map_field } => {
200            // In a message inside a key value tuple, create a new message and return it.
201            // The type trickery is used to infer the message type from the map wrapper.
202            Ok((
203                parse_quote!(::protobuf::__internal::get_map_default_value(
204                    ::protobuf::__internal::Private,
205                    this.#map_field()
206                )),
207                Some(parse_quote!(this)),
208            ))
209        }
210    }
211}
212
213/// Returns `true` if the given path is `__` (two underscores), which indicates
214/// an inferred type.
215fn should_infer_message_type(qself: &Option<QSelf>, path: &Path) -> bool {
216    qself.is_none() && path.get_ident().is_some_and(|ident| *ident == "__")
217}
218
219fn expand_array(array: ExprArray, enclosing_context: EnclosingContext) -> Result<Expr> {
220    if let Some(attr) = array.attrs.first() {
221        return Err(Error::new_spanned(attr, "unsupported syntax"));
222    }
223
224    let enclosing_context = EnclosingContext::Array {
225        repeated_field: match enclosing_context {
226            EnclosingContext::Struct { field } => field,
227            EnclosingContext::TopLevel
228            | EnclosingContext::Array { .. }
229            | EnclosingContext::Map { .. } => {
230                return Err(Error::new_spanned(array, "arrays must be nested inside a message"))
231            }
232        },
233    };
234
235    let array = array
236        .elems
237        .into_iter()
238        .map(|elem| match elem {
239            Expr::Struct(struct_) => expand_struct(struct_, enclosing_context.clone()),
240            Expr::Array(array) => expand_array(array, enclosing_context.clone()),
241            Expr::Tuple(tuple) => expand_tuple(
242                tuple,
243                EnclosingContext::Map { map_field: enclosing_context.repeated_field() },
244            ),
245            expr => Ok(expr),
246        })
247        .collect::<Result<Vec<Expr>>>()?;
248
249    Ok(parse_quote!([#(#array),*].into_iter()))
250}
251
252fn expand_tuple(tuple: ExprTuple, enclosing_context: EnclosingContext) -> Result<Expr> {
253    if let Some(attr) = tuple.attrs.first() {
254        return Err(Error::new_spanned(attr, "unsupported syntax"));
255    }
256
257    if tuple.elems.len() != 2 {
258        return Err(Error::new_spanned(tuple, "Map tuple literals must have exactly two elements"));
259    }
260
261    let key = tuple.elems[0].clone();
262    let value = match &tuple.elems[1] {
263        Expr::Struct(struct_) => expand_struct(struct_.clone(), enclosing_context.clone()),
264        expr => Ok(expr.clone()),
265    }?;
266    Ok(parse_quote!((#key, #value)))
267}