40
8

More than 5 years have passed since last update.

serdeの `derive` feature と名前空間の困った関係

Posted at

最近のserdeには derive というfeatureがあります。これは互換性の上で厄介だという話をします。

両方インポートする方式

serdeを使うときは、以下のように書くことが多かったかと思います:

Cargo.toml
[dependencies]
serde = "1.0.97"
serde_derive = "1.0.97"
main.rs
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};
// 1.29以前の書き方:
// extern crate serde;
// #[macro_use] extern crate serde_derive;
// use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
pub struct Foo {}

// 以下、 Deserialize traitとSerialize traitを使うコード

derive featureを使う方式

一方、以下のように書くこともできます:

Cargo.toml
[dependencies]
serde = { version = "1.0.97", features = ["derive"] }
main.rs
use serde::{Deserialize, Serialize};
// 1.29以前の書き方:
// #[macro_use] extern crate serde;
// use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
pub struct Foo {}

// 以下、 Deserialize traitとSerialize traitを使うコード

これは実はかなり古くて、2017年5月のv0.9.12から存在する機能のようです。ただserdeのドキュメントに書かれているサンプルがこの方式に書き換えられたのは割と最近のようです。

derive の困ったところ

さてこの derive ですが、ライブラリ間の相互運用性で問題が指摘されています

まず、以下のコードは serdederive featureが無効なときだけコンパイルが通ります。

// コード1: derive featureが無効なときだけ動く
use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Foo {}

一方、以下のコードは serdederive featureが有効なときだけコンパイルが通ります。

// コード1: derive featureが有効なときだけ動く
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Foo {}

これは名前解決の動作に由来します。Rustではモジュールとは別に名前空間という概念があります。「型・トレイト・モジュール」「値・変数・関数」「マクロ・deriveマクロ・属性」の3つは区別されるので、同じモジュール内に同じ名前で共存できます。(C++ではないC言語の名前空間と似てますね)

  • serde_derive のルートモジュールには、マクロ名前空間の Serialize とマクロ名前空間の Deserialize が定義されています。
  • serde は、型名前空間の Serialize と型名前空間の Deserialize を定義しています。
  • それに加えて、 derive featureが有効なら、 serde_derive に由来するマクロ名前空間の Serialize/Deserialize がそこに合流します。

では、同じ名前の use が複数あったらどうなるでしょうか。Rustの規則は以下のようになっていました。

  • 単独インポートとglobインポートであれば、単独インポートが優先される。
  • globインポート同士の場合、実体(Def)が同じなら統合される。そうでなければ、どちらも無効。
  • 単独インポート同士の場合、衝突する。

単独インポートは、実際には複数名前空間からのインポートにも対応しています。その場合は可能な名前空間全てからインポートしますが、上記の「単独インポート同士の場合、衝突する。」という規則はそのままです。つまり、

use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize, Serialize};

と書いた場合、次のようになります。

  • derive feature が無効な場合: 1行目は型名前空間、2行目はマクロ名前空間からインポートする。衝突はせず合流する。
  • derive feature が有効な場合: 1行目は型名前空間+マクロ名前空間、2行目はマクロ名前空間からインポートする。同じ名前空間の同じ名前を複数回単独インポートしようとしているので、衝突する。

一方、 serde からのインポートだけを書いた場合、 derive featureが無効だったらマクロがインポートされなくなってしまいます。つまり、この2つはどちらも、 derive featureに対してポータブルではないということです。

(名前解決の挙動について詳しくは拙ブログに詳しくまとめましたので興味があればどうぞ)

完全にポータブルに書く方法

完全にポータブルに書くにはいくつかの方法があります。一番手っ取り早くて堅牢なのは別名インポートすることです。

use serde::{Deserialize, Serialize};
use serde_derive::{Deserialize as De, Serialize as Ser};

#[derive(Ser, De)]
pub struct Foo {}

serde_derive 側をglob importにしてもそこそこうまく機能します。ただこれは、将来deriveが増えたときに余計なものまでインポートするかもしれません。

use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use serde_derive::*;

一応、以下のように書けば Deserialize/Serialize だけうまく合流させることができるはずです。

use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use self::__derive_helper::*;

mod __derive_helper {
    pub use serde_derive::{Deserialize, Serialize};
}

#[derive(Deserialize, Serialize)]
pub struct Foo {}

derive を使うべきか、使わざるべきか

完全にポータブルにするには上記のような方法がありますが、基本的には derive featureを使っていけばいいと思います。

featureはadditiveに解決されます。つまり、ある依存元Aは derive を必要としていて、別の依存元B は必要としていないとき、 derive は有効になります。このような性質から、基本的には「derive が有効になっているとコンパイルが通らないライブラリ」のほうが淘汰されることになるはずです。

もちろん、本来的には、依存される側である serde 側に「derive が有効なときも、 derive を必要としていない依存元に対する互換性が壊れない」という性質を守る責務があります。とはいえ、これは名前解決に由来する不可避な現象なので、致し方ないかなと思います。

まとめ

  • serde には derive featureがあり、これを使うと直接 serde_derive に依存しなくてよくなる
  • featureは本来足し算だが、名前解決の問題で「derive が有効になるとコンパイルできない」ということが起こりえる
  • そのようなライブラリはやがて淘汰されるはずなので、 derive feature を使っていって問題ないはず
40
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
8