Rustでstruct - mapの相互変換をするための derive macroを実装した
structとmapの相互変換
任意のstructをHashMap
に変換する必要があったのでderive macroで実装しました。
できたものは以下になります。
Rustにはreflectionがない
Rustは構造体からmap(HashMap
)へ変換するための機能がないらしいです。
例えばGolangでは以下のように reflect
パッケージを使うと実装できます。
Rustにはreflectionの機能がないみたいです。
注釈: リフレクションとは、実行時に型名や関数の中身などを取得する機能のことです。 言語によって提供されていたりいなかったりしますが、実行時にメタデータがないと取得できないので、 RustやC++のようなアセンブリコードに翻訳され、パフォーマンスを要求される高級言語では、提供されないのが一般的と思われます。
調べてみると、構造体・mapからjsonへの変換や、jsonから構造体・mapへの変換ができるライブラリはありました。
このライブラリを使うと以下のように構造体からmapへ変換できます。
extern crate rust_map_macro; | |
use serde::{Deserialize, Serialize}; | |
use serde_json::{Map, Value}; | |
use std::collections::HashMap; | |
fn main() { | |
let test = Test { | |
null: None, | |
boolean: true, | |
int: 1234, | |
float: 56.78, | |
string: "str".to_string(), | |
array: vec!["array1".to_string(), "array2".to_string()], | |
map: vec![("seconds".to_string(), 1234), ("nanos".to_string(), 5678)] | |
.into_iter() | |
.collect(), | |
time: Time { | |
seconds: 1234, | |
nanos: 5678, | |
}, | |
}; | |
let r = serde_json::to_string(&test).unwrap(); | |
let test_map: Map<String, Value> = serde_json::from_str(&r).unwrap(); | |
println!("map"); | |
println!("{:?}", test_map); | |
let r = serde_json::to_string(&test_map).unwrap(); | |
let test: Test = serde_json::from_str(&r).unwrap(); | |
println!("struct"); | |
println!("{:?}", test); | |
} | |
#[derive(Debug, Deserialize, Serialize)] | |
struct Test { | |
null: Option<String>, | |
boolean: bool, | |
int: i64, | |
float: f64, | |
string: String, | |
array: Vec<String>, | |
map: HashMap<String, i64>, | |
time: Time, | |
} | |
#[derive(Debug, Deserialize, Serialize)] | |
pub struct Time { | |
pub seconds: i64, | |
pub nanos: i32, | |
} |
map
{"array": Array([String("array1"), String("array2")]), "boolean": Bool(true), "float": Number(56.78), "int": Number(1234), "map": Object({"nanos": Number(5678), "seconds": Number(1234)}), "null": Null, "string": String("str"), "time": Object({"nanos": Number(5678), "seconds": Number(1234)})}
struct
Test { null: None, boolean: true, int: 1234, float: 56.78, string: "str", array: ["array1", "array2"], map: {"nanos": 5678, "seconds": 1234}, time: Time { seconds: 1234, nanos: 5678 } }
ただし、この方法を使った場合はjsonを経由するので余分な処理が走ります。また、fieldに構造体が入っていた場合、変換後はObjectになるので、mapと区別ができなくなります。
...
"map": Object({"nanos": Number(5678), "seconds": Number(1234)}),
...
"time": Object({"nanos": Number(5678), "seconds": Number(1234)})
...
今回はmapの変換後に一部の型で変換前の型情報を持っておきたかったので、この方法は使えませんでした。
そこで、独自にderive macroを使って実装することにしました。
derive macro
Rustではmacroを使うことができます。簡単に言うと、RustでRustを書く、みたいな感じです。
上で触れたserde
でもmacroを使用しています。このserde
を参考にmacroを実装しました。
下準備
macroを書く前に、macro外でいくつか必要な処理を書いておきます。
pub trait Mapper {
fn to_map(&self) -> HashMap<String, FieldValue>;
fn from_map(map: HashMap<String, FieldValue>) -> Result<Self>
where
Self: std::marker::Sized;
}
#[derive(Clone, Debug)]
pub enum FieldValue {
Null,
Boolean(bool),
Integer(i64),
Double(f64),
String(String),
Time(Time),
Array(Vec<FieldValue>),
Map(HashMap<String, FieldValue>),
}
#[derive(Clone, Debug)]
pub struct Time {
pub seconds: i64,
pub nanos: i32,
}
Mapper
は変換用のtraitです。このtraitが実装されている型は
to_map
:構造体 =>HashMap<String, FieldValue>
from_map
:HashMap<String, FieldValue>
=> 構造体
の変換ができるようになります。
FieldValue
はmap変換後のfieldの型になります。Time
をTime
として保持できるようにしています。
このMapper
をderive macroで任意の型に実装できるようにします。以下のように使うイメージです。
#[derive(Mapper)]
struct Test {
null: Option<String>,
boolean: bool,
int: i64,
float: f64,
string: String,
array: Vec<String>,
map: HashMap<String, i64>,
time: Time,
}
型変換を全てmacroで実装するのは大変なので、型変換のためのtraitをmacro外で定義・実装しておきます。
pub trait Converter: Sized {
fn to_field_value(&self) -> FieldValue;
fn to_primitive(fv: FieldValue) -> Result<Self>;
}
例えばString
には以下のようにConverter
を実装しておきます。
impl Converter for String {
fn to_field_value(&self) -> FieldValue {
FieldValue::String(self.to_string())
}
fn to_primitive(fv: FieldValue) -> Result<Self> {
match fv {
FieldValue::String(value) => Ok(value),
_ => Err(anyhow!("invalid type: String")),
}
}
}
Converter
のメソッドをmacroで呼ぶようにします。変換対象の全ての型について、Converter
を実装しました。serde
ライブラリでこんな感じに実装されていて、なるほどなと思いました。
具体的なConverter
の実装は以下になります。
use anyhow::{anyhow, Result}; | |
use num_traits::cast::FromPrimitive; | |
use std::collections::HashMap; | |
use std::convert::TryFrom; | |
#[derive(Clone, Debug)] | |
pub enum FieldValue { | |
Null, | |
Boolean(bool), | |
Integer(i64), | |
Double(f64), | |
String(String), | |
Time(Time), | |
Array(Vec<FieldValue>), | |
Map(HashMap<String, FieldValue>), | |
} | |
#[derive(Clone, Debug)] | |
pub struct Time { | |
pub seconds: i64, | |
pub nanos: i32, | |
} | |
pub trait Mapper { | |
fn to_map(&self) -> HashMap<String, FieldValue>; | |
fn from_map(map: HashMap<String, FieldValue>) -> Result<Self> | |
where | |
Self: std::marker::Sized; | |
} | |
pub trait Converter: Sized { | |
fn to_field_value(&self) -> FieldValue; | |
fn to_primitive(fv: FieldValue) -> Result<Self>; | |
} | |
impl Converter for String { | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::String(self.to_string()) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::String(value) => Ok(value), | |
_ => Err(anyhow!("invalid type: String")), | |
} | |
} | |
} | |
impl Converter for char { | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::String(self.to_string()) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::String(value) => { | |
let chars: Vec<char> = value.chars().collect(); | |
if chars.len() != 1 { | |
return Err(anyhow!("invalid type: char")); | |
} | |
return Ok(chars[0]); | |
} | |
_ => Err(anyhow!("invalid type: char")), | |
} | |
} | |
} | |
impl Converter for bool { | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Boolean(*self) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Boolean(value) => Ok(value), | |
_ => Err(anyhow!("invalid type: bool")), | |
} | |
} | |
} | |
impl Converter for Time { | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Time(self.clone()) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Time(value) => Ok(value), | |
_ => Err(anyhow!("invalid type: Time")), | |
} | |
} | |
} | |
macro_rules! integer_impls { // int系の型は多くて大変なので一括で実装するためにmacroを使用している | |
($($type:ty)+) => { | |
$( | |
impl Converter for $type { | |
#[inline] | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Integer(*self as i64) | |
} | |
#[inline] | |
fn to_primitive(fv: FieldValue) ->Result<Self>{ | |
match fv { | |
FieldValue::Integer(value)=>{ | |
if let Ok(value) = <$type>::try_from(value) { | |
return Ok(value); | |
} | |
return Err(anyhow!("invalid type: {}",stringify!($type))) | |
}, | |
_=> Err(anyhow!("invalid type: {}",stringify!($type))), | |
} | |
} | |
} | |
)+ | |
} | |
} | |
integer_impls! { | |
i8 i16 i32 i64 isize u8 u16 u32 | |
} | |
impl Converter for f32 { | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Double(*self as f64) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Double(value) => { | |
if let Some(value) = f32::from_f64(value) { | |
return Ok(value); | |
} | |
return Err(anyhow!("invalid type: f32")); | |
} | |
_ => Err(anyhow!("invalid type: f32")), | |
} | |
} | |
} | |
impl Converter for f64 { | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Double(*self) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Double(value) => Ok(value), | |
_ => Err(anyhow!("invalid type: f64")), | |
} | |
} | |
} | |
impl<T> Converter for Option<T> | |
where | |
T: Converter, | |
{ | |
fn to_field_value(&self) -> FieldValue { | |
match self { | |
Some(some) => some.to_field_value(), | |
None => FieldValue::Null, | |
} | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Null => Ok(None), | |
_ => Ok(Some(T::to_primitive(fv)?)), | |
} | |
} | |
} | |
impl<T> Converter for Vec<T> | |
where | |
T: Converter, | |
{ | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Array(self.iter().map(|v| v.to_field_value()).collect()) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Array(value) => value | |
.into_iter() | |
.map(|v| T::to_primitive(v)) | |
.collect::<Result<Vec<T>>>(), | |
_ => Err(anyhow!("invalid type: Vec<T>")), | |
} | |
} | |
} | |
impl<K, V> Converter for HashMap<K, V> | |
where | |
K: ToString + From<String> + std::cmp::Eq + std::hash::Hash, | |
V: Converter, | |
{ | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Map( | |
self.iter() | |
.map(|(key, value)| (key.to_string(), value.to_field_value())) | |
.collect(), | |
) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Map(value) => { | |
let mut result = HashMap::with_capacity(value.len()); | |
for (k, v) in value { | |
if let Ok(k) = K::try_from(k) { | |
result.insert(k, V::to_primitive(v)?); | |
} else { | |
return Err(anyhow!("invalid type: HashMap<K, V>")); | |
} | |
} | |
return Ok(result); | |
} | |
_ => Err(anyhow!("invalid type: HashMap<K, V>")), | |
} | |
} | |
} | |
impl<T> Converter for T // MapperにもConverterを実装することで、ネストされた型も変換できるようになる | |
where | |
T: Mapper, | |
{ | |
fn to_field_value(&self) -> FieldValue { | |
FieldValue::Map(self.to_map()) | |
} | |
fn to_primitive(fv: FieldValue) -> Result<Self> { | |
match fv { | |
FieldValue::Map(value) => Ok(T::from_map(value)?), | |
_ => Err(anyhow!("invalid type: Mapper")), | |
} | |
} | |
} |
macro実装
derive macroの実装は以下のドキュメントを参考に進めました。
ベースのcrate(rust-map-macro)のディレクトリ内にmacro用のcrateを作成します。
/rust-map-macro/src# cargo new mapper_derive --lib
/rust-map-macro/src/mapper_derive/Cargo.toml
に以下を記述し、macroのcrateであることを宣言します。
[lib]
proc-macro = true
macroを実装します。
extern crate proc_macro; | |
extern crate syn; | |
use anyhow::{anyhow, Result}; | |
use syn::Data; | |
use syn::Fields; | |
#[macro_use] | |
extern crate quote; | |
use proc_macro::TokenStream; | |
#[proc_macro_derive(Mapper)] // #[proc_macro_derive(Mapper)] を書くことで、#[derive(Mapper)] を指定した時にderive_mapper() 関数が呼び出されるようになる | |
pub fn derive_mapper(input: TokenStream) -> TokenStream { // input: TokenStream に #[derive(Mapper)] を指定した型情報が入ってくる | |
let input = syn::parse_macro_input!(input as syn::DeriveInput); // inputをparseする | |
impl_mapper_macro(&input).unwrap() | |
} | |
fn impl_mapper_macro(input: &syn::DeriveInput) -> Result<TokenStream> { | |
let data_struct = match &input.data { // #[derive(Mapper)] を指定した型がstructであるかを判定 | |
Data::Struct(data_struct) => data_struct, | |
Data::Enum(_) => Err(anyhow!("invalid type: Enum"))?, | |
Data::Union(_) => Err(anyhow!("invalid type: Union"))?, | |
}; | |
let fields_named = match &data_struct.fields { // structのfield名一覧を取得する | |
Fields::Named(fields_named) => fields_named, | |
Fields::Unnamed(_) => Err(anyhow!("invalid type: Unnamed"))?, | |
Fields::Unit => Err(anyhow!("invalid type: Unit"))?, | |
}; | |
let to_field_value_token_streams: Vec<proc_macro2::TokenStream> = fields_named | |
.named | |
.iter() | |
.enumerate() | |
.map(|(i, field)| { | |
let name = match &field.ident { | |
Some(ident) => syn::Member::Named(ident.clone()), | |
None => syn::Member::Unnamed(i.into()), | |
}; // Converterを実装した型のto_field_valueを呼び、FieldValueに変換するためのmacroを書く | |
return quote! { | |
result.insert(stringify!(#name).to_string(), rust_map_macro::mapper::Converter::to_field_value(&self.#name)); | |
}; | |
}) | |
.collect(); | |
let to_primitive_token_streams: Vec<proc_macro2::TokenStream> = fields_named | |
.named | |
.iter() | |
.enumerate() | |
.map(|(i, field)| { | |
let name = match &field.ident { | |
Some(ident) => syn::Member::Named(ident.clone()), | |
None => syn::Member::Unnamed(i.into()), | |
}; | |
let ty = &field.ty; | |
return quote! { // Converterを実装した型のto_primitiveを呼び、primitive型に変換するためのmacroを書く | |
let mut #name: Option<#ty> = None; | |
if let Some(value) = __optional_map__.get_mut(stringify!(#name)) { | |
if let Some(value) = std::mem::replace(value, None) { | |
#name = Some(rust_map_macro::mapper::Converter::to_primitive(value)?); | |
} else { | |
return Err(anyhow::anyhow!("invalid type: {}", stringify!(#ty))); | |
} | |
} | |
let #name = #name.ok_or(anyhow::anyhow!("invalid type: {}", stringify!(#ty)))?; | |
}; | |
}) | |
.collect(); | |
let to_struct_token_streams: Vec<proc_macro2::TokenStream> = fields_named | |
.named | |
.iter() | |
.enumerate() | |
.map(|(i, field)| { | |
let name = match &field.ident { | |
Some(ident) => syn::Member::Named(ident.clone()), | |
None => syn::Member::Unnamed(i.into()), | |
}; | |
return quote! { // 変換後の変数を構造体にまとめるためのmacroを書く | |
#name, | |
}; | |
}) | |
.collect(); | |
let name = &input.ident; // 構造体名 | |
let (im_generics, ty_generics, _) = input.generics.split_for_impl(); // 構造体のgenerics引数 | |
Ok(quote! { | |
impl#im_generics Mapper for #name#ty_generics { // 構造体にMapperを実装する | |
fn to_map(&self) -> std::collections::HashMap<String, rust_map_macro::mapper::FieldValue> { | |
let mut result = std::collections::HashMap::new(); | |
#(#to_field_value_token_streams)* // 上で書いたmacro(to_field_value_token_streams)をバインドする | |
result | |
} | |
fn from_map(__map__: std::collections::HashMap<String, rust_map_macro::mapper::FieldValue>) -> anyhow::Result<Self> { | |
let mut __optional_map__ = std::collections::HashMap::with_capacity(__map__.len()); | |
for (key, val) in __map__ { | |
__optional_map__.insert(key, Some(val)); | |
} | |
#(#to_primitive_token_streams)* // 上で書いたmacro(to_primitive_token_streams)をバインドする | |
Ok(#name { #(#to_struct_token_streams)* }) // 上で書いたmacro(to_struct_token_streams)をバインドする | |
} | |
} | |
} | |
.into()) | |
} |
このmacroを使用するため、使用元のcrateの/rust-map-macro/Cargo.toml
に以下を記述します。
[dependencies]
mapper_derive = { path = "./src/mapper_derive" }
以下のようにmacroを使用します。
extern crate rust_map_macro; | |
use rust_map_macro::mapper::{Mapper, Time}; | |
use std::collections::HashMap; | |
#[macro_use] | |
extern crate mapper_derive; | |
fn main() { | |
let test = Test { | |
null: None, | |
boolean: true, | |
int: 1234, | |
float: 56.78, | |
string: "str".to_string(), | |
array: vec!["array1".to_string(), "array2".to_string()], | |
map: vec![("seconds".to_string(), 1234), ("nanos".to_string(), 5678)] | |
.into_iter() | |
.collect(), | |
time: Time { | |
seconds: 1234, | |
nanos: 5678, | |
}, | |
}; | |
let test_map = test.to_map(); | |
println!("map"); | |
println!("{:?}", test_map); | |
let test = Test::from_map(test_map).unwrap(); | |
println!("struct"); | |
println!("{:?}", test); | |
} | |
#[derive(Debug, Mapper)] | |
struct Test { | |
null: Option<String>, | |
boolean: bool, | |
int: i64, | |
float: f64, | |
string: String, | |
array: Vec<String>, | |
map: HashMap<String, i64>, | |
time: Time, | |
} |
実行結果は以下です。
map
{"time": Time(Time { seconds: 1234, nanos: 5678 }), "string": String("str"), "float": Double(56.78), "boolean": Boolean(true), "int": Integer(1234), "null": Null, "array": Array([String("array1"), String("array2")]), "map": Map({"seconds": Integer(1234), "nanos": Integer(5678)})}
struct
Test { null: None, boolean: true, int: 1234, float: 56.78, string: "str", array: ["array1", "array2"], map: {"seconds": 1234, "nanos": 5678}, time: Time { seconds: 1234, nanos: 5678 } }
mapの変換後もTime
の情報を保持できています。
map
...
"time": Time(Time { seconds: 1234, nanos: 5678 }),
...
"map": Map({"seconds": Integer(1234), "nanos": Integer(5678)})
...
まとめ
macroの書き方は調べてもあまり情報が出てこず、大変でした。