Rustでstruct - mapの相互変換をするための derive macroを実装した

structとmapの相互変換

任意のstructをHashMapに変換する必要があったのでderive macroで実装しました。

できたものは以下になります。

GitHub - yutakahashi114/rust-map-macro
Contribute to yutakahashi114/rust-map-macro development by creating an account on GitHub.

Rustにはreflectionがない

Rustは構造体からmap(HashMap)へ変換するための機能がないらしいです。

例えばGolangでは以下のように reflect パッケージを使うと実装できます。

Go言語でstructからmapへ変換 - Qiita
構造体のフィールド一覧を取得したくなったので、reflectを使って実装。 struct2map.gopackage main import ( “fmt” “reflect”) func StructToMap(...

Rustにはreflectionの機能がないみたいです。

D - マクロ - The Rust Programming Language

注釈: リフレクションとは、実行時に型名や関数の中身などを取得する機能のことです。 言語によって提供されていたりいなかったりしますが、実行時にメタデータがないと取得できないので、 RustやC++のようなアセンブリコードに翻訳され、パフォーマンスを要求される高級言語では、提供されないのが一般的と思われます。

調べてみると、構造体・mapからjsonへの変換や、jsonから構造体・mapへの変換ができるライブラリはありました。

GitHub - serde-rs/serde: Serialization framework for Rust
Serialization framework for Rust. Contribute to serde-rs/serde development by creating an account on GitHub.

このライブラリを使うと以下のように構造体から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,
}
view raw rust-map-macro-serde hosted with ❤ by GitHub
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を書く、みたいな感じです。

D - マクロ - The Rust Programming Language

上で触れた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_mapHashMap<String, FieldValue> => 構造体

の変換ができるようになります。

FieldValue はmap変換後のfieldの型になります。TimeTime として保持できるようにしています。

この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の実装は以下のドキュメントを参考に進めました。

D - マクロ - The Rust Programming Language

ベースの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())
}
view raw rust-map-macro-macro hosted with ❤ by GitHub

この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,
}
view raw rust-map-macro-main hosted with ❤ by GitHub

実行結果は以下です。

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の書き方は調べてもあまり情報が出てこず、大変でした。

参考文献