これはアドベントカレンダー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の構成を指定できます。具体的には、以下のとおりです。
- rust言語のバージョン。ちなみに(2023/12/22時点で)最新は1.74.1です。チャンネルという項目が該当します。
rustfmt
,clippy
,rust-analyzer
など、使用する開発支援ツールのリスト。コンポーネントという項目が該当します。- コンポーネントのセットを長々と列挙するのは面倒なので、特定のコンポーネントセットはエイリアスとして登録されています。プロファイルという項目です。
- クロスコンパイルの文脈ではコンパイル対象のアーキテクチャを指定する必要があります。ターゲットの項目でそれらを列挙できます。
いつから使えるようになったの?
このファイルを読むのは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 forrust-toolchain
- at least thetoml
variant thereof.
どうやら.toml
拡張子を明示しようという流れがcargo側で起こっていて、rustupもそれに合わせて.toml
拡張子付きのファイルをサポートしたようです。我々ユーザーもその流れに合わせてrust-toolchain.toml
を使った方がいいでしょう。
記述例
突然ですがrust-toolchain.toml
の記述例がこちら。
[toolchain]
channel = "1.70.0"
components = ["rustc", "cargo"]
profile = "minimal"
targets = []
見ればなんとなくわかりますが、一応内容を確認しておきましょう。
- 使うRust言語のバージョンは1.70.0
- 使用するコンポーネントはコンパイラ
rustc
とビルドシステム・パッケージマネージャcargo
- 後述のプロファイルでこれらは暗示されているが、ここでは例として明示
- 使用するプロファイルは
minimal
で、コンポーネントのrustc
とcargo
、rust-std
が使用される - コンパイルターゲットの指定はなし。現在のPCアーキテクチャに合うものは自動的に追加されるため、「一般的なPCをターゲットとする」みたいな感じ
以降はこのファイルを例に実際の挙動を確認していきます。ちなみに環境はRaspberry Pi 4B, Ubuntu 22.04です。
バージョン確認
rustup show
で現在のディレクトリで適用されるrustバージョンを確認することができます。
左がホームディレクトリ、右がrust-toolchain.toml
の置いてあるディレクトリでの実行結果です。バージョンが変わっているのが確認できます。
ビルド時はどうなる?
では、いちいちrustup show
で確認しないといけないのか?いやいや、そんなことはありません。
ディレクトリ移動後すぐにcargo
などのコマンドを実行してもバージョン変更が適用されます。つまり、次のような開発の流れが可能になるということです。
リポジトリをクローン
→ VSCodeで開く
→ rust-analyzerが起動 (ちょっと待つ)
→ バージョンが切り替わった状態でrust-anaylserが立ち上がる
凄い!
CIでも便利
CI環境でもこのファイルは有効です。例えばGitHub Actionsでは最初からrustup
が入っているため、即座にcargo build
を実行することができます。バージョン変更に無駄な記述をする必要はありません。
他CI環境でもrustupは同様に使えそうです。多分
ref:
- Building a Rust Project - Travis CI
- cimg/rust - CircleCI
- イメージのDockerfileではrustupを使用しています
MSRVの決定でも使える
※ MSRVはMinimum Supported Rust Versionの略です。詳しくはThe Manifest Format - The Cargo Book #The rust-version
field
ここでは実際の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/fenixやoxalica/rust-overlayを使うことで、rust-toolchain.toml
を読んで適切なtoolchainを入れることができます。すなわち、rustup
なしで同様のことが実現可能です。
つまり、Nixユーザーと非Nixユーザーの間で rust-toolachain.toml
でrustの構成を一本化できます!! 素敵ですね。
おまけおまけ: LT会でこれを話しました
Unique Visionさん主催のRust LT会でこの内容を話しました。
なかなかいい反応をもらえました。他の登壇者の内容も興味深いものが多く、とても濃く楽しい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;
}
しかしこれには問題があります。先程のRepository
traitに色々と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::Service
traitの応用ですね。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 fn
はasync_trait
マクロとの互換性がありません。現状、「1.75以降のrustしかサポートしないぜ!」という強い思想を持ったライブラリしかこの構文に対応しないことになります。バージョンを読んでcfg
マクロとかでtrait宣言を切り替えるのも手かもしれませんが、ドキュメントが壊れそうだしなんか色々めんどくさそう...
という話が件のブログ記事に書いてあります。是非読んでください。
それとももしかして、 エディション更新 の兆し、いやそれは趣旨が違うか...?