feature image

2023年12月22日 | ブログ記事

rust-toolchain.tomlを書こう

これはアドベントカレンダー2023 22日目の記事です。

モチベーション

プロジェクト内で使用するRustバージョンを揃えたい!! と思うことはよくあるかと思います。nightlyの機能を使っているプロジェクトとかだと尚更ですね。さて一体どうやるのか?2つ、検討して見ましょう。

1. .tool-versionsを書く

asdfに依存してしまいますし、GitHub ActionsなどCI環境でも取り回しが良くないです。

2. rustupのコマンドを使う

rustup override set ...といったコマンドを実行してもらえばバージョンを揃えることができます。しかし「これを実行してね」とREADMEに書くのはやや手間ですし、CI環境でも無駄なコマンドを実行する必要があります。


なかなかベストな案が出ませんね。そこで使うのがrust-toolchain.tomlです。

rust-toolchain.tomlができること

このファイルをプロジェクトのリポジトリに置くことで、そのリポジトリで使用するrustの構成を指定できます。具体的には、以下のとおりです。

いつから使えるようになったの?

このファイルを読むのはrustupなので、rustupのCHANGELOGを見ます。rustcのバージョンではないので注意してください。

rustup/CHANGELOG.md #1.24.0 - 2021-04-27 にこのような記述がありました。

Support of rust-toolchain.toml as a filename for specifying toolchains.

どうやら2021年からこのファイルはサポートされていたようです。今となっては当たり前の機能ですね。ちなみに最新のrustupバージョンは1.26.0です。

.toml拡張子は必要?

実は、.toml拡張子なしのrust-toolchainという名前のファイルでも同様のことができます。ではどちらを使うべきなのか?先程の引用と同じリリースに答えらしきものが書いてありました。

Since Cargo is migrating to explicit .toml extensions on things like .cargo/config.toml it was considered sensible to also do this for rust-toolchain - at least the toml variant thereof.

どうやら.toml拡張子を明示しようという流れがcargo側で起こっていて、rustupもそれに合わせて.toml拡張子付きのファイルをサポートしたようです。我々ユーザーもその流れに合わせてrust-toolchain.tomlを使った方がいいでしょう。

記述例

突然ですがrust-toolchain.tomlの記述例がこちら。

[toolchain]
channel = "1.70.0"
components = ["rustc", "cargo"]
profile = "minimal"
targets = []

見ればなんとなくわかりますが、一応内容を確認しておきましょう。

以降はこのファイルを例に実際の挙動を確認していきます。ちなみに環境はRaspberry Pi 4B, Ubuntu 22.04です。

バージョン確認

rustup showで現在のディレクトリで適用されるrustバージョンを確認することができます。

rpi-rustup-demo

左がホームディレクトリ、右がrust-toolchain.tomlの置いてあるディレクトリでの実行結果です。バージョンが変わっているのが確認できます。

ビルド時はどうなる?

では、いちいちrustup showで確認しないといけないのか?いやいや、そんなことはありません。

rpi-cargo-demo

ディレクトリ移動後すぐにcargoなどのコマンドを実行してもバージョン変更が適用されます。つまり、次のような開発の流れが可能になるということです。

リポジトリをクローン
→ VSCodeで開く
→ rust-analyzerが起動 (ちょっと待つ)
→ バージョンが切り替わった状態でrust-anaylserが立ち上がる

凄い!

CIでも便利

CI環境でもこのファイルは有効です。例えばGitHub Actionsでは最初からrustupが入っているため、即座にcargo buildを実行することができます。バージョン変更に無駄な記述をする必要はありません。

他CI環境でもrustupは同様に使えそうです。多分

ref:

MSRVの決定でも使える

※ MSRVはMinimum Supported Rust Versionの略です。詳しくはThe Manifest Format - The Cargo Book #The rust-version field

ここでは実際のPRを例に話を進めます。

:wrench: Set rust-toolchain by H1rono · Pull Request #88 · H1rono/traq-bot-http-rs
close #78
件のPR

rust-toolchainを導入した(あとでrust-toolchain.tomlに変えた)んですが、その時点でCIが落ちました。何故かというと、指定したバージョンが依存関係のライブラリで指定されているMSRVより低かったためです。

CIが落ちたときのログ


error: package `time-core v0.1.2` cannot be built because it requires rustc 1.67.0 or newer, while the currently active rustc version is 1.64.0
Error: Process completed with exit code 101.

ということで、このPRでMSRVを1.67.1に上げました。cargo-msrvを手作業で実行した感じですね。

...という話をした後に言うんですが、この1.67と言うのはあくまで現在のコード、依存関係でコンパイルが通る最低のRustバージョンなんですよね。前方互換性を考えるとMSRVを高くした方がいいかも?となりますし、依存関係のMSRVにも左右されます。あくまで参考指標の一つと言う感じですね。rust-toolchain.tomlの導入と同時にMSRVを1.67.1に上げた例を紹介しましたが、 もしかして浅慮だったのでは...? と少し後悔しています。

まとめ

rustup + rust-toolchain.tomlは最強です。これは間違いない。是非使ってください。

明日の担当は@YHz_ikiriさん、@Pugmaさんと@o_ER4さんと@hayatroidさん、そして@kegraさんです。お楽しみに!

ref

おまけ: Nixでは?

nix-community/fenixoxalica/rust-overlayを使うことで、rust-toolchain.tomlを読んで適切なtoolchainを入れることができます。すなわち、rustupなしで同様のことが実現可能です。

つまり、Nixユーザーと非Nixユーザーの間で rust-toolachain.tomlでrustの構成を一本化できます!! 素敵ですね。

おまけおまけ: LT会でこれを話しました

Unique Visionさん主催のRust LT会でこの内容を話しました。

UV Study : Rust LT会 (2023/12/19 19:30〜)
# 概要 RustをテーマにしたLT会です。 Rustについて、各自興味のあるテーマを持ち寄ることで知識を深めていくことを目的としています。 発表内容は Rustにまつわる技術情報であればなんでも構いません。 ### オフライン&オンラインのハイブリッド開催 当LT会はオフライン&オンラインのハイブリッド開催となります。 オフライン参加をご希望の方は、オフライン参加枠でのご応募をお願い致します。 なお、LT登壇者の方はアンケートにて、参加方法の選択をお願い致します。 ## 登壇者募集 登壇者を募集します テーマはRustに関連すればなんでもOKです! 登壇時間…
connpassページ

スライドのリンク

なかなかいい反応をもらえました。他の登壇者の内容も興味深いものが多く、とても濃く楽しいLT会でした。

おまけおまけおまけ: Rust 1.75.0についてちょっとだけポエム

分量的に ここからが本題 というレベルなので注意

12/21(多分UTC)、1.75.0でstabilizeされる機能についての記事がRust blogに公開されました。その機能は...

trait内でasync fn宣言と... -> impl Trait形式の関数宣言

ついにきた!!!!!!!!!え?何がいいのって?まず現在(1.74.1)の状況を確認しましょう。rustc<=1.74.1では以下のtrait宣言がコンパイルできませんでした。

trait Container {
    fn items(&self) -> impl Iterator<Item = i32>;
}

trait Repository {
    async fn get_users(&self) -> Vec<User>;
}

コンパイルエラー↓

error[E0562]: `impl Trait` only allowed in function and inherent method return types, not in trait method return types
 --> src/main.rs:4:19
  |
4 |     fn items() -> impl Iterator<Item = i32>;
  |                   ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information

error[E0706]: functions in traits cannot be declared `async`
 --> src/main.rs:8:5
  |
8 |     async fn get_users() -> Vec<User>;
  |     -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |     |
  |     `async` because of this
  |
  = note: `async` trait functions are not currently supported
  = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
  = note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information

Some errors have detailed explanations: E0562, E0706.
For more information about an error, try `rustc --explain E0562`.

これを回避するために我々Rustaceanは様々な策を講じてきました。H1ronoの観測範囲では大きく3つです。

回避策1. associated type

impl Traitの型をassociated typeとして具体的に与える方法です。

trait Container {
    type Items: Iterator<Item = i32>;
    fn items(&self) -> Self::Items;
}

trait Repository {
    type FutUsers: Future<Output = Vec<User>>;
    fn get_users(&self) -> Self::FutUsers;
}

tower::Serviceではこの方式を採用しています。

pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future
    where
        <Self::Future as Future>::Output == Result<Self::Response, Self::Error>;

    fn poll_ready(
        &mut self,
        cx: &mut Context<'_>
    ) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}

しかしこれには問題があります。先程のRepositorytraitに色々とasync fnを足したくなったとしましょう。

// <=1.74.1でコンパイルが通らないコード
trait Repository {
    async fn get_users(&self) -> Vec<User>;
    async fn get_user_by_id(&self, id: i32) -> Option<User>;
    async fn create_user(&self, user: User) -> ();
    // ...
}

associated typeを使ってコンパイルを通すとしたらどう書くでしょうか?

trait Repository {
    type FutGetUsers: Future<Output = Vec<User>>;
    type FutGetUserById: Future<Output = Option<User>>;
    type FutCreateUser: Future<Output = ()>;

    fn get_users(&self) -> Self::FutGetUsers;
    fn get_user_by_id(&self, id: i32) -> Self::FutGetUserById;
    fn create_user(&self, user: User) -> Self::FutCreateUser;
}

これを書くこと(implブロックも!)を考えると指が痛くなってきませんかね。書いてる私はもちろん指が痛いです。まあこんなに指が痛くなる実装は嫌なので、traitを細かく分けるのが一応の回避策というところです。

trait Repository<Query> {
    type Output;
    type Future: Future<Output = Output>;
    fn execute(&self, query: Self::Query) -> Self::Future;
}

struct RepositoryImpl;
struct GetUsersQuery;

impl Repositroy<GetUsersQuery> for RepositoryImpl {
    type Output = Vec<User>;
    type Future = ...;
    fn execute(&self, _: GetUsersQuery) -> Self::Future { ... }
}

tower::Servicetraitの応用ですね。implブロックが細切れになるところも嬉しいところです。しかし記述量は依然として多い...

回避策2. traitオブジェクトを使う

trait Container {
    fn items(&self) -> Box<dyn Iterator<Item = i32>>;
}

trait Repository {
    fn get_users(&self) -> Pin<Box<dyn Future<Output = Vec<User>>>>;
}

GC付きの言語ならこのような雰囲気になりがちですが、Rustで書くととても富豪に感じられますね。私だけかな?またこれにもやはり問題があります。追加の型境界を加えづらいことです。

例えば、以下のコードがコンパイルできません。

struct ContainerImpl;

impl Container for ContainerImpl {
    fn items(&self) -> Box<dyn Iterator<Item = i32>> {
        Box::new(vec![1, 2, 3].into_iter())
    }
}

fn main() {
    let container = ContainerImpl;
    let items = container.items();
    println!("{}", items.len());
}

エラーメッセージ↓

error[E0599]: the method `len` exists for struct `Box<dyn Iterator<Item = i32>>`, but its trait bounds were not satisfied
   --> src/main.rs:16:26
    |
16  |       println!("{}", items.len());
    |                            ^^^
    |
   ::: /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/iter/traits/iterator.rs:74:1
    |
74  |   pub trait Iterator {
    |   ------------------ doesn't satisfy `dyn Iterator<Item = i32>: ExactSizeIterator`
    |
   ::: /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:195:1
    |
195 | / pub struct Box<
196 | |     T: ?Sized,
197 | |     #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
198 | | >(Unique<T>, A);
    | |_- doesn't satisfy `Box<dyn Iterator<Item = i32>>: ExactSizeIterator`
    |
    = note: the following trait bounds were not satisfied:
            `dyn Iterator<Item = i32>: ExactSizeIterator`
            which is required by `Box<dyn Iterator<Item = i32>>: ExactSizeIterator`

For more information about this error, try `rustc --explain E0599`.

これを通すためには、traitの宣言を書き換える必要があります。Iterator<Item = i32> + ExactSizeIteratorの境界をそのままtraitオブジェクトにするのは不可能(Trait object types - The Rust Referenceを読むとわかるはず)なので記述が面倒です。略

ちなみにassociated typeを用いる方法であればかなりやりやすいです。

trait Container {
    type Items: Iterator<Item = i32>;
    fn items(&self) -> Self::Items;
}

struct ContainerImpl;

impl Container for ContainerImpl {
    type Items = std::vec::IntoIter<i32>;
    fn items(&self) -> Self::Items {
        vec![1, 2, 3].into_iter()
    }
}

fn f<T: Container>(container: T)
where
    T::Items: ExactSizeIterator,
{
    let items = container.items();
    println!("{}", items.len());
}

回避策3. async-traitマクロ

これはasnyc fnでのみ使える話です。これを使うとasync fnの機能を先取りした気分になれます。

use async_trait::asnyc_trait;

#[async_trait]
trait Repository {
    async fn get_users() -> Vec<User>;
}

しかし外部にAPIの一部として#[async_trait]をつけたものを公開すると少しドキュメントがわかりづらくなってしまいます。以下はaxum::extract::FromRequestからの引用です。

pub trait FromRequest<S, M = ViaRequest>: Sized {
    type Rejection: IntoResponse;

    // Required method
    fn from_request<'life0, 'async_trait>(
        req: Request<Body>,
        state: &'life0 S
    ) -> Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait>>
       where 'life0: 'async_trait,
             Self: 'async_trait;
}

元のソースは以下のようになっています。

// コメントと一部マクロは省略
#[async_trait]
pub trait FromRequest<S, M = private::ViaRequest>: Sized {
    type Rejection: IntoResponse;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection>;
}

マクロの導入によってライフタイムがわかりづらくなってしまっていますね。このへんは微妙に感じてしまいます。

このような問題が解決されます!!!!!

1.75.0凄い!!!!!!!!!!!

...と手放しに喜べたらよかったんですがね...

何がいけないのか?まず、impl Traitで戻り値の型を記述すると型境界がそれで固定されてしまいます。

trait Container {
    fn items(&self) -> impl Iterator<Item = i32>;
}

fn f(container: &impl Container) {
    let items = container.items();
    println!("{}", items.len());
}

エラーメッセージ↓

error[E0599]: no method named `len` found for associated type `impl std::iter::Iterator<Item = i32>` in the current scope
 --> src/main.rs:7:26
  |
7 |     println!("{}", items.len());
  |                          ^^^ method not found in `impl Iterator<Item = i32>`

traitオブジェクトと同じ問題が発生していますね。

また、async fnasync_traitマクロとの互換性がありません。現状、「1.75以降のrustしかサポートしないぜ!」という強い思想を持ったライブラリしかこの構文に対応しないことになります。バージョンを読んでcfgマクロとかでtrait宣言を切り替えるのも手かもしれませんが、ドキュメントが壊れそうだしなんか色々めんどくさそう...

という話が件のブログ記事に書いてあります。是非読んでください。

Announcing `async fn` and return-position `impl Trait` in traits | Rust Blog
Empowering everyone to build reliable and efficient software.

それとももしかして、 エディション更新 の兆し、いやそれは趣旨が違うか...?

H1rono_K icon
この記事を書いた人
H1rono_K

趣味プログラマー(大学生)

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年12月11日
DIGI-CON HACKATHON 2023『Mikage』
toshi00 icon toshi00
2023年12月13日
HgameOPについて語る
noc7t icon noc7t
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
2024年12月11日
Nixで実行環境のライセンス違反を予防する話
comavius icon comavius
2024年8月29日
クロスコンパイルRust
H1rono_K icon H1rono_K
2023年12月25日
オレオレRustプロジェクトテンプレート
H1rono_K icon H1rono_K
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記