はじめに
はじめまして、Rust勉強中の人です。
Rustを書き書きしていると度々登場する手続き型マクロ。
こいつはそれはそれは便利なやつらしいですが、「一体内部で何をしてくれているんだ?」ってなったので、一つ例をとって深堀りしてみようと思った次第です。
早速掘り下げてゆく〜
今回はactix-web
で Hello World! する下記の簡単なコードを例にとって掘り下げていきます。
use actix_web::{web, App, HttpServer, Responder};
async fn hello() -> impl Responder {
"Hello, World!"
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
1. tokio::main
の目的を知る
とりあえず#[tokio::main]
を削除してcargo check
を実行してみます。
すると、コンパイラさんが次のエラーを吐きます。
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:8:1
|
8 | async fn main() -> std::io::Result<()> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
For more information about this error, try `rustc --explain E0752`.
error: could not compile `helloworld` due to previous error
前提として、HttpServer::run
は非同期メソッドです。しかし、バイナリのエントリポイントであるmain
は非同期関数にできません。ですが、どうにかしてmain
を非同期にしなければいけません。
Rustの非同期プログラミングはFuture
トレイトの上に構築されています。Future
トレイトはpoll
メソッドを実装しており、これを非同期ランタイム側から呼び出し、非同期タスクをポーリングすることで、最終的な値が解決されるという仕組みです。
しかし、Rustは標準ライブラリで、非同期ランタイムをサポートしていません。Cargo.toml
の[dependencies]
以下に依存関係として記述して、外部から取り込まなければいけません。
そこで登場するのが、非同期ランタイム tokio
ってわけ。
非同期関数になりえないmain
関数の先頭で非同期ランタイムを起動し、非同期タスクの最終的な値を解決します。
以上が大まかな#[tokio::main]
の目的・役割になります。
2. コードを解剖する
手続き型マクロの主な役割は、入力としてコードを取り込み、コードを生成し、コンパイラに出力を渡すことです。
この展開後のコードを見ることができれば重要な何かが見えてきそうですが、まさにそれの目的を果たしてくれる便利ツールがあります。
ツールをインストール
次の2つを実行して、必要なツールをインストールします。
$ cargo install cargo-expand
cargo-expand
はコード内のマクロを展開しコンパイラに渡さずに、中身を出力してくれます。
$ rustup toolchain install nightly --allow-downgrade
cargo-expand
は多くの方が普段使っているであろう安定版のstable
コンパイラではなく、 nightly
と呼ばれる毎晩リリースされるコンパイラ(詳しくはこちら)に依存してマクロを展開します。しかし、nightly
の最新のリリースでは、rustup
によってインストールされるバンドルのいくつかのコンポーネントが欠けていたり、不具合があったりする可能性があるため、--allow-downgrade
ですべての必要なコンポーネントが利用可能な最新のリリースを見つけてインストールするようにrustup
に指示します。
cargo list
、rustup show
を実行して、インストールされていることが確認できたら、次に進みます。
実行
次を実行します。
$ cargo +nightly expand
cargo +[~] [command]
とすることで、コマンド単位でツールチェーンを指定できます。
今回は先程説明したとおり、依存関係にあるnightly
を指定します。
すると、下のようにマクロ展開後のコードが出力されます。
/// ...
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use actix_web::{web, App, HttpServer, Responder};
async fn hello() -> impl Responder {
"Hello, World!"
}
fn main() -> std::io::Result<()> {
let body = async {
HttpServer::new(|| App::new().route("/", web::get().to(hello)))
.bind("127.0.0.1:8000")?
.run()
.await
};
#[allow(clippy::expect_used)]
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body)
}
#[tokio::main]
の展開後にコンパイラに渡されるmain
は同期関数になっている〜!
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body)
重要なのはこの部分で、
tokio
の非同期ランタイムを起動し、main
関数全体の処理をblock_on
することで、HttpServer::run
が返すFuture
が完了するまでブロックしています。
要は、#[tokio::main]
の役割は、非同期のmain
を定義しているかのように見せかけて、その裏では、非同期のmainを受け取り、tokioのランタイムの上でそれを実行するために必要な定型文に書き換えて、コンパイラに渡してくれているだけってことです!!!
はい、やりたかったことは以上です。
なんかまとめ方が下手すぎますが、おわりです。
さいごに
今回がQiitaへの初めての投稿でした。
順序立てて、解説を加えていく中で自分の中での理解もより一層深まった気がします。
また時間を見つけて積極的に投稿していけたらと思っています。
最後まで読んでいただきありがとうございました。
参考
今回の記事を書くにあたり、Rustの非同期処理について調べる際、こちらの記事がとても参考になりました。