| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! FOR_RELEASE: Docs |
| |
| use quote::quote; |
| use syn::{parse_macro_input, DeriveInput}; |
| |
| #[proc_macro_derive(MojomParse)] |
| pub fn derive_mojomparse(input: proc_macro::TokenStream) -> proc_macro::TokenStream { |
| let input = parse_macro_input!(input as DeriveInput); |
| let name = input.ident; |
| |
| let derived_tokens = match input.data { |
| syn::Data::Struct(syn::DataStruct { fields, .. }) => match fields { |
| syn::Fields::Named(syn::FieldsNamed { named, .. }) => { |
| derive_mojomparse_struct(name.clone(), named) |
| } |
| _ => panic!("Mojom structs do not support unnamed fields"), |
| }, |
| syn::Data::Enum(syn::DataEnum { variants, .. }) => { |
| derive_mojomparse_union(name.clone(), variants) |
| } |
| syn::Data::Union(_) => { |
| panic!("Mojom does not support untagged unions. Use a Rust enum instead.") |
| } |
| }; |
| |
| return proc_macro::TokenStream::from(derived_tokens); |
| } |
| |
| fn derive_mojomparse_struct( |
| name: syn::Ident, |
| struct_fields: syn::punctuated::Punctuated<syn::Field, syn::Token![,]>, |
| ) -> proc_macro2::TokenStream { |
| let num_fields = struct_fields.len(); |
| |
| // As far as I know, quote can only iterate over vectors of things that can |
| // be directly converted to tokens. Notably, this means they have to be |
| // single values. So if we want to write something like #name = #val, in a |
| // loop, we first have to combine each pair of names and values into a single |
| // token stream, and then can we iterate over that in the quote. |
| |
| // The names of the fields in the struct. |
| let field_idents: Vec<&syn::Ident> = |
| struct_fields.iter().map(|field| field.ident.as_ref().unwrap()).collect(); |
| |
| // A bunch of entries for a MojomType::Struct |
| let mojom_type_fields: Vec<proc_macro2::TokenStream> = struct_fields |
| .iter() |
| .map(|field| { |
| let ty = &field.ty; |
| let name = field.ident.as_ref().unwrap().to_string(); |
| quote! { (#name.to_string(), <#ty>::mojom_type()) } |
| }) |
| .collect(); |
| |
| // A bunch of entries for a MojomValue::Struct |
| let to_mojom_value_fields: Vec<proc_macro2::TokenStream> = struct_fields |
| .iter() |
| .map(|field| { |
| let name = &field.ident; |
| let name_str = field.ident.as_ref().unwrap().to_string(); |
| quote! { (#name_str.to_string(), value.#name.into()) } |
| }) |
| .collect(); |
| |
| // The body of a struct value, converting each field from a MojomValue with |
| // the same name as the field. |
| let from_mojom_value_fields: Vec<proc_macro2::TokenStream> = struct_fields |
| .iter() |
| .map(|field| { |
| let name = &field.ident; |
| quote! { #name: #name.try_into()? } |
| }) |
| .collect(); |
| |
| // We wrap the `impl` blocks in an anonymous scope so that we can |
| // import mojom_value_parser_core without polluting the caller's namespace. |
| return quote! { |
| const _: () = { |
| chromium::import! { |
| "//mojo/public/rust/mojom_value_parser:mojom_value_parser_core"; |
| } |
| |
| use mojom_value_parser_core::*; |
| |
| impl MojomParse for #name { |
| fn mojom_type() -> MojomType { |
| let (field_names, fields) : (Vec<String>, Vec<MojomType>) = vec![ |
| #(#mojom_type_fields),* |
| ] |
| .into_iter().unzip(); |
| MojomType::Struct { field_names, fields } |
| } |
| } |
| |
| impl From<#name> for MojomValue { |
| fn from(value: #name) -> MojomValue { |
| let (field_names, fields) : (Vec<String>, Vec<MojomValue>) = vec![ |
| #(#to_mojom_value_fields),* |
| ] |
| .into_iter().unzip(); |
| MojomValue::Struct ( field_names, fields ) |
| } |
| } |
| |
| impl TryFrom<MojomValue> for #name { |
| type Error = ::anyhow::Error; |
| |
| fn try_from(value: MojomValue) -> ::anyhow::Result<Self> { |
| let MojomValue::Struct(field_names, fields) = value else { |
| ::anyhow::bail!( |
| "Cannot construct a value of type {} from non-struct MojomValue {:?}", |
| std::any::type_name::<#name>(), |
| value |
| ); |
| }; |
| |
| if fields.len() != #num_fields { |
| ::anyhow::bail!( |
| "Wrong number of fields to construct a value of type {} from MojomValue {:?}", |
| std::any::type_name::<#name>(), |
| MojomValue::Struct(field_names, fields) |
| ) |
| }; |
| |
| // Try to extract all the field values at once |
| let fields: [MojomValue; #num_fields] = fields.try_into().unwrap(); |
| let [#(#field_idents),*] = fields; |
| return Ok(Self { |
| #(#from_mojom_value_fields),* |
| }) |
| } |
| }; |
| }; |
| }; |
| } |
| |
| fn derive_mojomparse_union( |
| name: syn::Ident, |
| variants: syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>, |
| ) -> proc_macro2::TokenStream { |
| // Extract/compute just the bits of the variants that we care about: |
| // The name, type, and discriminant value |
| let mut next_discriminant: i32 = 0; |
| let variant_info : Vec<(syn::Ident, syn::Type, i32)> |
| = variants.into_iter().map(|variant: syn::Variant| { |
| let variant_name = variant.ident; |
| let mut fields = match variant.fields { |
| syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed, .. }) => unnamed, |
| syn::Fields::Named(syn::FieldsNamed { named, .. }) => named, |
| syn::Fields::Unit => syn::punctuated::Punctuated::new(), // We'll panic shortly |
| }; |
| if fields.len() != 1 { |
| let mut panic_msg = format!("Variant {variant_name} of enum {name} must have exactly one field to be serialized as a Mojom union."); |
| if fields.len() == 0 { |
| panic_msg.push_str("\nTo serialize as a Mojom enum, derive PrimitiveEnum instead, which will automatically provide MojomParse.") |
| } |
| panic!("{}", panic_msg) |
| } |
| let field_ty = fields.pop().unwrap().into_value().ty; |
| let discriminant = compute_next_discriminant(&mut next_discriminant, variant.discriminant); |
| (variant_name, field_ty, discriminant) |
| }).collect(); |
| |
| let mojom_type_fields = variant_info |
| .iter() |
| .map(|(_, ty, discriminant)| quote! { (#discriminant, <#ty>::mojom_type()) }); |
| let to_mojom_value_branches = variant_info |
| .iter() |
| .map(|(variant_name, _, discriminant)| quote! { #name::#variant_name(v) => (#discriminant, v.into()) }); |
| let from_mojom_value_branches = variant_info.iter().map(|(name, _, discriminant)| { |
| // boxed_value is defined by the surrounding scope |
| quote! { #discriminant => Ok(Self::#name((*boxed_value).try_into()?)), } |
| }); |
| |
| return quote! { |
| const _: () = { |
| chromium::import! { |
| "//mojo/public/rust/mojom_value_parser:mojom_value_parser_core"; |
| } |
| |
| use mojom_value_parser_core::*; |
| use std::collections::BTreeMap; |
| |
| impl MojomParse for #name { |
| fn mojom_type() -> MojomType { |
| let variants : BTreeMap<i32, MojomType> = [ |
| #(#mojom_type_fields),* |
| ].into(); |
| MojomType::Union { variants } |
| } |
| } |
| |
| impl From<#name> for MojomValue { |
| fn from(value: #name) -> MojomValue { |
| let (discriminant, mojom_value) = match value { |
| #(#to_mojom_value_branches),* |
| }; |
| MojomValue::Union ( discriminant, Box::new(mojom_value) ) |
| } |
| } |
| |
| impl TryFrom<MojomValue> for #name { |
| type Error = ::anyhow::Error; |
| |
| fn try_from(value : MojomValue) -> ::anyhow::Result<Self> { |
| let MojomValue::Union(discriminant, boxed_value) = value else { |
| ::anyhow::bail!( |
| "Cannot construct a value of type {} from non-union MojomValue {:?}", |
| std::any::type_name::<#name>(), |
| value |
| ); |
| }; |
| |
| match discriminant { |
| #(#from_mojom_value_branches)* |
| discriminant => ::anyhow::bail!( |
| "Invalid discriminant to construct a value of type {} from MojomValue {:?}", |
| std::any::type_name::<#name>(), |
| MojomValue::Union(discriminant, boxed_value)) |
| } |
| } |
| } |
| }; |
| }; |
| } |
| |
| // Compute the discriminant for this field, as either the provided value or the |
| // value of `next_discriminant`. In either case, set `next_discriminant` to the |
| // computed value + 1 |
| fn compute_next_discriminant( |
| next_discriminant: &mut i32, |
| discriminant_opt: Option<(syn::Token![=], syn::Expr)>, |
| ) -> i32 { |
| let discriminant = match discriminant_opt { |
| Some((_, syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(n), .. }))) => { |
| let discriminant = n |
| .base10_parse::<i32>() |
| .expect("Enum/Union discriminants must be a 32-bit integer literal."); |
| discriminant |
| } |
| Some((_, syn::Expr::Unary(syn::ExprUnary { op: syn::UnOp::Neg(_), expr, .. }))) => { |
| if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(n), .. }) = *expr { |
| let discriminant = n |
| .base10_parse::<i32>() |
| .expect("Enum/Union discriminants must be a 32-bit integer literal."); |
| -discriminant |
| } else { |
| panic!("Enum/Union discriminants must be a 32-bit integer literal."); |
| } |
| } |
| None => *next_discriminant, |
| _ => panic!("Enum/Union discriminants must be a 32-bit integer literal."), |
| }; |
| *next_discriminant = discriminant + 1; |
| discriminant |
| } |
| |
| #[proc_macro_derive(PrimitiveEnum)] |
| pub fn derive_primitiveenum(input: proc_macro::TokenStream) -> proc_macro::TokenStream { |
| let input = parse_macro_input!(input as DeriveInput); |
| let name = input.ident; |
| |
| let variants = match input.data { |
| syn::Data::Enum(syn::DataEnum { variants, .. }) => variants, |
| _ => panic!("No structs or unions allowed!"), |
| }; |
| |
| let mut next_discriminant: i32 = 0; |
| let mut default_variant: Option<syn::Ident> = None; |
| let generate_branch = |variant: syn::Variant| { |
| if !variant.fields.is_empty() { |
| panic!("Mojom enums must not have any variants with fields!") |
| } |
| |
| // FOR_RELEASE: See if any variants have a "default" attribute |
| default_variant = None; // Silence compiler until we do that |
| |
| let discriminant = compute_next_discriminant(&mut next_discriminant, variant.discriminant); |
| let variant_name = variant.ident; |
| |
| quote! { |
| #discriminant => Ok(#name::#variant_name) |
| } |
| }; |
| |
| let mut branches = variants.into_iter().map(generate_branch).collect::<Vec<_>>(); |
| if let Some(default) = default_variant { |
| branches.push(quote! { _ => Ok(#default)}) |
| } else { |
| branches.push(quote! { _ => Err(anyhow::anyhow!( |
| "Invalid discriminant {value} for type {}", |
| std::any::type_name::<#name>() |
| ))}) |
| } |
| |
| let quoted = quote! { |
| const _ : () = { |
| chromium::import! { |
| "//mojo/public/rust/mojom_value_parser:mojom_value_parser_core"; |
| } |
| |
| use mojom_value_parser_core::*; |
| |
| impl From<#name> for i32 { |
| fn from(value: #name) -> i32 { value as i32 } |
| } |
| |
| impl TryFrom<i32> for #name { |
| type Error = ::anyhow::Error; |
| |
| fn try_from(value : i32) -> ::anyhow::Result<Self> { |
| match value { |
| #(#branches),* |
| } |
| } |
| } |
| |
| impl TryFrom<MojomValue> for #name { |
| type Error = ::anyhow::Error; |
| |
| fn try_from(value: MojomValue) -> ::anyhow::Result<Self> { |
| if let MojomValue::Enum(v) = value { |
| Ok(Self::try_from(v)?) |
| } else { |
| ::anyhow::bail!( |
| "Cannot construct a value of type {} from non-enum MojomValue {:?}", |
| std::any::type_name::<#name>(), |
| value |
| ) |
| } |
| } |
| } |
| |
| impl PrimitiveEnum for #name {} |
| }; |
| }; |
| |
| return proc_macro::TokenStream::from(quoted); |
| } |