🦀

RustのWebフレームワーク"Axum"をハンズオンの書籍や質問サイトに頼りすぎずに扱う①

2023/12/01に公開

なぜRustを選んだのか

情報系でもない普通の機械科の大学生である私がPythonやRuby,Javaなどではなく,Rust言語を使おうと思ったのか理由の述べて行きたいと思います.

技術的な側面

もともとは,将来webアプリケーション開発したいと思っていましたが,大学でマイコンを使った制御をC言語で実装するといった授業で,組み込み系や低レイヤプログラミングにも興味を持ち,Rustは低レイヤな部分からWeb開発,ゲーム開発といった汎用性があることから,将来の選択肢を広げるために選びました.
また多くの言語はGCを持っていますが,それでもメモリ安全性がどういったことなのかを学ぶことは大切なのではないかと思い,Rust言語を選びました.

マインド的な側面

インターネットやAIが発達した現代において大切なのは,わからないことや疑問に思ったことを自分で調べ,本質的に理解したり,自分がわかりやすい言葉で噛み砕くことなのではないかと思っています.
例えば,プログラミングでエラーが出た際にはエラーの内容をしっかり読んだり,ドキュメントを参照し,まずは自分の力で解決を試みることです.もちろん,様々な質問サイトで質問することや人に聞くことも大切ですが,キャリアを考えた時,自走力を身につけることは大切なのではないかと思っています.
Rustは人気で盛り上がっているものの,他の言語に比べればまだ日本語で書かれた書籍などは少なく,何か問題が発生した際には自分の力で調べ上げ,解決することが必要になります.(最初の頃はコンパイラを通すのがやっとでした.)
この自分の考えとRustの現状が自分への試練としてmatchしているのでないかと思い,Rust言語を選びました.

どのようにRustと向き合うのか

RustのWebフレームワーク"Axum"について 日本語で書かれたハンズオン形式の書籍があります.私はこれで実際に簡単なtodoアプリを開発を学びました.
ハンズオン形式はわかりやすく,挫折もしにくいため,私自身は好きですが,前述の自分自身で調べ,解決する(アプリケーションを作り上げる)力を養うにはハンズオンからは一旦距離を置き,公式ドキュメントやAxumで定義されている内容を調べながら作って行きたいと思います.
具体的には,docs.rsやgithub上のaxumのsampleコード,crates.ioなどを用いて開発していきます.

今回作成するもの: Hello Worldの表示とextract::Pathの利用

あんなに大々的に言ってたくせに想像以上に簡単な実装ですが(笑),ゆっくり自分でドキュメントを参照しながら作っていきます.

実装

まずは,AxumをCargoに追加していきます.
crates.ioで調べて,現段階においてのバージョンは0.7.1が最新なのでそれを使います.
またAxum自身は非同期ランタイムを実装していないので,実装させる必要があります. AxumのチュートリアルからAxumはtokioのみ対応していることがわかります.Axumのcrates.ioのdependenciesからバージョン1.25.0のtokioに依存していることがわかるので,こちらも追加します.

Cargo.toml
[dependencies]
axum = "0.7.1"
tokio = { version = "1.25.0", features = ["full"] }

docs.rsのexampleを参考にしてコードを書いていきます.
まずは非同期ランタイムをmain関数が使えるように#[tokio::main]属性をつけます.

main.rs
#[tokio::main]
async fn main() {
    println!("Hello World");
}

docs.rsのExampleは以下のようになっています.

docs.rsのExample
use axum::{
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));

    // run our app with hyper, listening globally on port 3000
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

これらを1つずつdocs.rsで定義を見ていきます.
最初の部分はルーティングのセットアップです.StructページのRouterを覗くと以下の定義がされています.

Routerの定義
impl<S> Router<S>
where
    S: Clone + Send + Sync + 'static,

pub fn new() -> Self

Create a new Router.
Unless you add additional routes this will respond with 404 Not Found to all requests.
route()の定義
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self

Add another route to the router.
path is a string of path segments separated by /. Each segment can be either static, a capture, or a wildcard.

new()でインスタンスの生成をし,routeでpathごとにハンドラーを呼べることがわかります.
さらにMethodRouter<S>をみてみると以下のようになっていました.

MethodRouterの定義
pub fn get<H, T>(self, handler: H) -> Self
where
    H: Handler<T, S>,
    T: 'static,
    S: Send + Sync + 'static,


pub fn post<H, T>(self, handler: H) -> Self
where
    H: Handler<T, S>,
    T: 'static,
    S: Send + Sync + 'static,


pub fn delete<H, T>(self, handler: H) -> Self
where
    H: Handler<T, S>,
    T: 'static,
    S: Send + Sync + 'static,

このことからまずは

Router::new()
.route("指定するパス", httpレスポンス(ハンドラー関数))

を記述すればいいことがわかります.
今回は単純なhello worldを出力すればいいので,MethodRouterget()になります. また,ハンドラー関数名はrootにします.
main関数でごちゃごちゃやるのは煩わしいのでapp()関数内に書くことにしました.戻り値の型は先程見たようにRouterになります.またハンドラー関数は非同期関数でないといけないことがsampleコードからもわかります.

main.rs
#[tokio::main]
async fn main() {
    let app = app();
}

fn app() -> Router {
    Router::new()
    .route("/", get(root))
}

async fn root() -> Html(&'static str) {
    todo!();
}

ここまできたら,使用するアドレスの指定とサーバにバインドする必要があります.
bindの定義を見てみると,

bind()の定義
pub async fn bind<A: ToSocketAddrs>(addr: A) -> io::Result<TcpListener> {
    let addrs = to_socket_addrs(addr).await?;

    let mut last_err = None;

    for addr in addrs {
        match TcpListener::bind_addr(addr) {
            Ok(listener) => return Ok(listener),
            Err(e) => last_err = Some(e),
        }
    }

    Err(last_err.unwrap_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidInput,
            "could not resolve to any address",
        )
    }))
}

このことから非同期関数であることが分かるので.awaitが必要であるとわかります.
したがって以下のようになります.

main.rs
#[tokio::main]
async fn main() {
    let app = app();

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("listening on: {:?}", &listener);
    axum::serve(listener, app).await.unwrap();
}

fn app() -> Router {
    Router::new()
    .route("/", get(root))
}

async fn root() -> Html<&'static str> {
    todo!();
}

ここまで出来たら,あとはroot()関数を書くでけになります.
Htmlとして返すので,docs.rsのModulesページのresponseを見てみると以下のように書かれていました.

Html型戻り値のsample
async fn html() -> Html<&'static str> {
    Html("<p>Hello, World!</p>")
}

これを真似して以下のように書きました

main.rs
#[tokio::main]
async fn main() {
    let app = app();

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("listening on: {:?}", &listener);
    axum::serve(listener, app).await.unwrap();
}

fn app() -> Router {
    Router::new()
    .route("/", get(root))
}

async fn root() -> Html<&'static str> {
    Html("
            <h1>Hello World!</h1>
            <h2>Axum!!!</h2>
        ")
}

以上で完成になりました

さらに.route()頻繁に使う機能についても再度見直してみました.
CapturesWildcardsです.
内容をみてみると.route()の第一引数で"/:id"のようにすると実際にブラウザなどで入力した際に:idの部分をキャプチャしてくれる事がわかります.また/*pathのようにするとそれ以降のパスを動的に確保してくれることがわかりました.その時の引数の型はPath<>型ということもわかります.
これらを踏まえて新たに付け足してみます.

main.rs
#[tokio::main]
async fn main() {
    let app = app();

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("listening on: {:?}", &listener);
    axum::serve(listener, app).await.unwrap();
}

fn app() -> Router {
    Router::new()
    .route("/", get(root))
    .route("/:id/*pass", get(check))
}

async fn root() -> Html<&'static str> {
    Html("
            <h1>Hello World!</h1>
            <h2>Axum!!!</h2>
        ")
}

async fn check(Path((id, url_path)): Path<(i32, String)> ) -> Html<String> {
    let text = format!("<h2>I took id: {}, path: {}</h2>", id, url_path);

    Html(text)
}

以上で完成になります.

以下のように入力すると...

このような出力になります.

次回

次はserdeを用いたJsonレスポンスを学んでいきたいと思います.

Discussion