LoginSignup
17
10

公開APIのインターフェースで利用している外部クレートはRe-exportする(と良さそう)

Last updated at Posted at 2020-12-10

この記事はRust Advent Calendar 2020の 10 日目の記事です。

こんにちは、@tasshi です。
同日にうっかりもう 1 つアドベントカレンダーを登録していて1
慌てて書いているうちにこちらは 1 日遅れとなってしまいました。

まだ 11 日未明なのでセーフだと信じて強い気持ちで投稿します。

概要

Rust の Re-export の使い方について調べていたところ、気になるトピックがありました。

Guidance around reexporting · Issue #176 · rust-lang/api-guidelines

ざっくりと説明すると、
「公開している API のインターフェースで外部クレートを利用している場合は、
その外部クレート全体を Re-export(再公開)するのが良い」

という内容です。

現在も Open issue のため今後方針が変わる可能性もありますが、現時点ではこれが一番良さそうということで記事に書き起こしてみました。

Re-exporting(再公開)について

pubキーワードとuseキーワードを組み合わせると、スコープに持ち込んだ名前を外に対して公開することができます。
これを**Re-exporting(再公開)**と呼びます。

pub mod public_module {
    pub mod submodule_in_public {
        pub fn my_function() {}
    }
}

mod private_module {
    pub mod submodule_in_private {
        pub fn my_function() {}
    }
}

pub use crate::private_module::submodule_in_private; //非公開サブモジュールからRe-export
pub use crate::public_module::submodule_in_public; //公開サブモジュールからRe-export

TRPL (en): Bringing Paths Into Scope with the use Keyword - The Rust Programming Language
TRPL (ja): use キーワードでパスをスコープに持ち込む - The Rust Programming Language 日本語版

Re-export は、以下のような場面で効果を発揮します。

  • 階層の深いサブモジュールから外部公開するものを浅い階層へ取り出す
  • 非公開サブモジュールで実装を隠蔽しつつ公開用インターフェースのみ公開する
  • 外部モジュールをクレートの一部として公開する

Rust のモジュールシステムについては κeen さんの記事を読むのがおすすめです。
Rust のモジュールの使い方 2018 Edition 版 | κeen の Happy Hacκing Blog

公開 API のインターフェースで他のクレートの型を受け取る/返す

さて、本題に入ります。
公開 API が外部モジュールの提供する型を受け取ったり、返したりする場合を考えます。

ここでは架空のクレートimgconverterを例として進めていきます。
imgconverterimage::DynamicImageを受け取って何かしらの変換を行う関数convert_imageを提供しています。

imgconverter/lib.rs
use image::DynamicImage;

pub fn convert_image(image: DynamicImage) -> DynamicImage {
    // なんらかの処理
}

これをアプリケーション側で利用してみましょう。
main.rsではimage::DynamicImageを生成し、convert_image関数で変換を行います。
Cargo.toml には crates.io からimageクレートを探して最新バージョンを持ってきます。2

app/main.rs
use image::DynamicImage;
use imgconverter::convert_image;

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let dst_image = convert_image(src_image);
}
app/Cargo.toml
# 一部抜粋
[dependencies]
image = "0.23.12"

それではコンパイルしてみましょう。

❯ cargo build
   Compiling app v0.1.0
error[E0308]: mismatched types
 --> src/bin/main.rs:6:35
  |
6 |     let dst_image = convert_image(src_image);
  |                                   ^^^^^^^^^ expected enum `image::dynimage::DynamicImage`, found enum `image::DynamicImage`
  |
  = note: perhaps two different versions of crate `image` are being used?

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `app`.

コンパイルエラーとなってしまいました。

原因

実はimgconverterは少し古いバージョンのimageクレートに依存していました。

imgconverter/Cargo.toml
# 一部抜粋
[dependencies]
image = "0.19.0"

引数として与えられたimage::DynamicImageのバージョンが0.23.12であるのに対して、
convert_imageの要求するimage::DynamicImageのバージョンは0.19.0です。

要求するバージョンと与えられたバージョンとが違うためコンパイルエラーとなっています。

依存する型を Re-export する

先ほどのエラーは、ライブラリクレートの依存関係を確認して適切なバージョンのimageクレートを使うことで防ぐことができます。
しかし、クレートの利用者に毎回適切なバージョンのimageクレートを見つけさせるのは開発体験が良いとは言えません。

利用者をバージョン違いのストレスから開放するために依存している型を Re-export してあげましょう。

新しいimgconverterではimage::DynamicImageをそのまま再公開しています。

imgconverter/lib.rs
pub use image::DynamicImage;

pub fn convert_image(image: DynamicImage) -> DynamicImage {
    // なんらかの処理
    return image;
}

アプリケーション側ではimgconverter::DynamicImageを利用することで適切なバージョンのDynamicImageを利用できます。

app/main.rs
use imgconverter::{convert_image, DynamicImage};

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let dst_image = convert_image(src_image);
}

これでmain.rsが正常にビルドできるようになりました。

返り値にさらに操作を加える

まだ十分ではありません。

変換されたdst_imageにさらに画像処理を加えてみましょう。
image::imageopsにはDynamicImageに対する様々な画像処理関数が用意されています。34

今回はimage::imageops::flip_horizontalを利用して画像を左右反転します。
さてどうなるでしょうか。

app/main.rs
use image::imageops;
use imgconverter::{convert_image, DynamicImage};

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let dst_image = convert_image(src_image);
    imageops::flip_horizontal(&mut dst_image);  // コンパイルエラー
}

これもバージョン違いでエラーになります。

このように直接インターフェースで利用している型だけを Re-export しても、そのクレートの他の機能を使うことができません。

依存する型を含むクレート全体を Re-export する

これを防ぐためにタイトルで出てきたクレート全体の Re-export を行います。

imgconverterではimageを再公開します。

imgconverter/lib.rs
pub use image; //imageクレート全体をRe-export
use image::DynamicImage;

pub fn convert_image(image: DynamicImage) -> DynamicImage {
    // なんらかの処理
    return image;
}

アプリケーション側では再公開されたimgconverter::imageを利用します。

app/main.rs
use imgconverter::convert_image;
use imgconverter::image::{imageops, DynamicImage};

fn main() {
    let src_image = DynamicImage::new_rgb8(800, 600);
    let mut dst_image = convert_image(src_image);
    imageops::flip_horizontal(&mut dst_image);
}

これで利用者はインターフェースで利用されているクレートのバージョンを意識しなくて良くなりました。

検証コード

上記のバージョン違いのコードも含まれているのでビルドはできません。
tasshi-playground/rust-re-export-dependencies: Investigation for usage of "Re-exporting"

参考ページ

余談

これって依存ライブラリ同士で Re-export しているクレートのバージョンが違った場合どうするんですかね。

  1. kintoneで他のユーザの予定を確認するJSカスタマイズを作る - Qiita

  2. image - crates.io: Rust Package Registry

  3. image::imageops - Rust

  4. 正確にはimageクレートの提供する画像系の型全般に対する画像処理関数群です

17
10
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
17
10