最新Rust Webフレームワーク

 
0
このエントリーをはてなブックマークに追加
Daichi Takayama
Daichi Takayama (高山 大地)

以下の文章はIvan Cernjaさんからの許可を得て記事を翻訳したものです。

https://www.shuttle.rs/blog/2023/08/23/rust-web-framework-comparison

ウェブ開発の変わりゆく世界において、Rustは安全性と高いパフォーマンスを提供するアプリケーションの開発言語として際立っています。Rustの人気が増すにつれ、その長所を生かすウェブフレームワークも増え続けています。本記事では、各フレームワークの長所と短所を明らかにしながら、いくつかの推奨されるRustフレームワークを比較し、プロジェクトに最適な選択をするための洞察を提供します。また、Rustでのウェブアプリケーション開発のアプローチを変える可能性がある注目のフレームワークにも焦点を当てます。

多くのウェブフレームワークは一見似ていますが、その違いは細部に宿ります。テキストで主要な違いを強調することを目指していますが、シンプルな「ハロー、ワールド」を超える機能を実演する各フレームワークのコード例も紹介して、より具体的な理解を助けます。これらの例は、それぞれのGitHubリポジトリから選ばれています。

このリストが全てを網羅しているわけではなく、存在するフレームワークの中でいくつか見落としている可能性があることをご了承ください。お気に入りのフレームワークをリストに加えてほしい場合は、TwitterMastodonでぜひ私にお知らせください。

注目のRustウェブフレームワーク

Axum

AxumはRustのウェブアプリケーション開発のために特別な位置を占めるフレームワークです。非同期ネットワークアプリケーションの開発に特化した Tokio プロジェクトの一環として、Axumは非同期処理の強力なサポートを提供します。また、HTTPサーバーとしての Hyper やミドルウェアとしての Tower といったTokioエコシステムの他のライブラリとの統合を特徴としています。これにより、開発者はTokioエコシステム内の既存のライブラリやツールを効果的に再利用することが可能になります。

Axumはマクロに頼ることなく、開発者に最高レベルの体験を提供しようと努力しています。これは、アプリケーションの主要なロジックを定義する Handler トレイトのような、フレームワークの核となる抽象化をトレイトで定義することによって、Rustの型システムの力を借りて実現されています。この方法により、開発者は小さな再利用可能なコンポーネントからアプリケーションを組み立てやすくなります。

Axumにおけるハンドラは、リクエストを受け取ってレスポンスを返す関数として機能します。これは他のウェブフレームワークと似ていますが、Axumでは FromRequest トレイトを用いて、リクエストから取り出すデータの型を指定できます。返す型は IntoResponse トレイトを実装する必要があり、レスポンスのステータスコードを簡単に変えることができるタプル型など、このトレイトを実装する型がすでに多数用意されています。

Rustの型システムやジェネリクス、トレイトの非同期メソッドの使用が複雑になることがありますが、Axumはエラー発生時にその場所を明確にするヘルパーマクロを含むライブラリを提供しています。これにより、何がうまくいかなかったのかを理解しやすくなります。

Axumは多くの点で優れており、多様な機能を持つアプリケーションの開始が非常に容易です。ただし、バージョンがまだ1.0未満であるため、APIの大幅な変更が頻繁に行われ、アプリケーションが大きく影響を受けることがあります。これは0.xバージョンの一般的な問題ですが、時には細かい変更が大きな影響を及ぼし、基盤となる仕組みに対する新たな理解が必要になることもあります。Timeout レイヤーを例にすると、バージョンによっては簡単に機能したり、全てのエラーを捕捉するハンドラーが必要になったり、特定のエラーハンドラーが必要になったりします。これは重大な問題ではありませんが、作業を進める上での障害になることがあります。また、Tokioエコシステム全体を活用できるものの、直接Tokioの関数を使用するのではなく、特定の型やトレイトを介する必要がある場合があります。

良い例が役立ちますが、追跡を続けることが重要です。

それでも、Axumは私の個人的なお気に入りであり、Shuttle Launchpad で使用しているフレームワークです。その表現力と背後にある概念を高く評価しており、理解すれば直感的に解決できなかった問題はありません。Axumの概念について詳しく知りたい方は、私の Tokio + Microservicesワークショップのスライド をご覧ください。

以下は、Axumリポジトリ にあるWebSocketハンドラの簡略版で、受け取ったメッセージをエコーバックする機能を示しています。

#[tokio::main]
async fn main() {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app()).await.unwrap();
}

fn app() -> Router {
    Router::new()
        .route("/integration-testable", get(integration_testable_handler))
        .route("/unit-testable", get(unit_testable_handler))
}

async fn integration_testable_handler(ws: WebSocketUpgrade) -> Response {
    ws.on_upgrade(integration_testable_handle_socket)
}

async fn integration_testable_handle_socket(mut socket: WebSocket) {
    while let Some(Ok(msg)) = socket.recv().await {
        if let Message::Text(msg) = msg {
            if socket
                .send(Message::Text(format!("You said: {msg}")))
                .await
                .is_err()
            {
                break;
            }
        }
    }
}

Axumの要点

  • マクロフリーAPI。
  • Tokio, Tower, Hyperを駆使した充実したエコシステム。
  • 優れた開発体験を提供。
  • まだバージョン0.xであるため、APIの変更が予期せず発生する可能性がある。

Actix Web

Actix WebはRustのウェブ開発フレームワークの中でも特に人気があり、長い間にわたってその地位を確立してきました。多くの改良を重ねてきたこのオープンソースプロジェクトは、既に0番台を超えるメジャーバージョンをリリースしており、そのバージョン間での安定性が保証されています:メジャーバージョン内での破壊的な変更はありません。

  • Actix Web に言及する際、しばしば actix アクターランタイムを基盤としていると認識されがちですが、実際には4年以上前からそのような関連性はありません。現在、Actix Webでアクターが必要なのはWebSocketのみであり、非同期Rustの流れに合わせてその使用を段階的に廃止する方向で進化しています。Actixプロジェクトは、低レベルのTCPサーバー構築からHTTP/Web層、静的ファイル提供やセッション管理まで、並行処理アプリケーションを構築するための多様なライブラリを提供する広範な組織です。

一見すると、Actix WebはRustの他のウェブフレームワークと非常に似ており、マクロを用いたHTTPメソッドやルートの定義(Rocketと同様)、リクエストからのデータ抽出(Axumと類似)など、親しみやすい特徴があります。しかし、Actix WebはTokioエコシステムとの結びつきが弱く、独自の抽象化やトレイト、クレートエコシステムを備えている点で大きな違いがあります。これは二面性があり、一方でフレームワーク間の連携がスムーズに行える一方で、Tokioエコシステムに存在する豊富な機能を活用しにくい面もあります。

独自にServiceトレイトを実装している点もActix Webの特徴で、Towerの同名トレイトとは異なり互換性を持たせていません。これにより、TowerエコシステムのミドルウェアをActixで直接利用することが難しくなっています。

さらに、Actix Webで特定の機能を自身で実装する際には、フレームワークの核となるアクターモデルに直面することがあり、これが一部の開発者にとっては追加の複雑さとなる場合があります。

それでも、Actix Webコミュニティは充実したサポートを提供しており、HTTP/2やWebSocketのアップグレードサポート、一般的なウェブ開発タスク向けのクレートやガイド、そして非常に充実したドキュメントを提供しています。その速度と安定性から、Actix Webは多くの開発者にとって魅力的な選択肢であり続けています。バージョンの安定性を重視する場合には、特に優れた選択と言えるでしょう。

以下はActix Webで実装されたシンプルな WebSocketエコーサーバー の例です:

use actix::{Actor, StreamHandler};
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
use actix_web_actors::ws;

// HTTPアクターを定義
struct MyWs;

impl Actor for MyWs {
    type Context = ws::WebsocketContext<Self>;
}

// ws::Messageメッセージのハンドラ
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
            Ok(ws::Message::Text(text)) => ctx.text(text),
            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
            _ => (),
        }
    }
}

async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
    let resp = ws::start(MyWs {}, &req, stream);
    println!("{:?}", resp);
    resp
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().route("/ws/", web::get().to(index)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

Actix Webの要点

  • 独立した強固なエコシステム。
  • アクターモデルに基づいています。
  • メジャーバージョンの保証による安定したAPI。
  • 素晴らしいドキュメンテーション。

Rocket

Rocketは、開発者が直感的に使用できる豊富な機能、既知のパターンへの対応、そして全てを一つにまとめたフレームワークとして、Rustのウェブ開発界隈で長らく脚光を浴びてきました。

このフレームワークの野心的な目標は、その 公式ウェブサイト を訪れることで明らかになります。マクロを用いた直観的なルーティング、フォームデータの処理、データベースやステートの管理サポート、そして独自のテンプレートエンジンなど、Webアプリケーション開発に必要なあらゆる要素が盛り込まれています。

しかし、Rocketの野心的なアプローチは一定のコストを伴います。開発のペースはかつてほど速くなく、フレームワークのユーザーが新しい機能や改善を待ち望むこともあります。

また、Rocketはいわゆるbatteries-includedアプローチを採用しているため、Rocket独自の慣習や構造に慣れる必要があります。アプリケーションには明確なライフサイクルがあり、その構成要素は特定の形で組み合わせられています。問題が発生した際には、その背景にある原因を理解することが求められます。

Rocketは、Rustでのウェブ開発を始めるにあたって、素晴らしい選択肢です。個人的にはRocketに対して特別な愛着を持っており、その活動が再び活発になることを願っています。多くの人々にとって、RocketはRustの世界への入口であり、使用することに変わりなく楽しみがあります。ただし、私自身はRocketが提供する機能以外に依存することが多いため、実際のプロジェクトでの使用は控えています。

以下は、Rocketを用いたフォーム処理の簡略化されたアプリケーションの例で、 例示リポジトリ からの抜粋です:

#[derive(Debug, FromForm)]
struct Password<'v> {
    #[field(validate = len(6..))]
    #[field(validate = eq(self.second))]
    first: &'v str,
    #[field(validate = eq(self.first))]
    second: &'v str,
}

#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Submission<'v> {
    #[field(validate = len(1..))]
    title: &'v str,
    date: Date,
    #[field(validate = len(1..=250))]
    r#abstract: &'v str,
    #[field(validate = ext(ContentType::PDF))]
    file: TempFile<'v>,
    ready: bool,
}

#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Account<'v> {
    #[field(validate = len(1..))]
    name: &'v str,
    password: Password<'v>,
    #[field(validate = contains('@').or_else(msg!("invalid email address")))]
    email: &'v str,
}

#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Submit<'v> {
    account: Account<'v>,
    submission: Submission<'v>,
}

#[get("/")]
fn index() -> Template {
    Template::render("index", &Context::default())
}

*// NOTE: We use `Contextual` here because we want to collect all submitted form// fields to re-render forms with submitted values on error. If you have no such// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.*#[post("/", data = "<form>")]
fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
    let template = match form.value {
        Some(ref submission) => {
            println!("submission: {:#?}", submission);
            Template::render("success", &form.context)
        }
        None => Template::render("index", &form.context),
    };

    (form.context.status(), template)
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, submit])
        .attach(Template::fairing())
        .mount("/", FileServer::from(relative!("/static")))
}

Rocketの要点

  • 包括的なアプローチ。
  • 優れた開発者体験。
  • 以前ほど積極的に開発されていない。
  • 初心者には依然として素晴らしい選択肢。

まだ広く知られていないが魅力的なRustフレームワーク

Warp

Warpは、その美しさ、奇異さ、そして力強さで知られる、Tokioを基盤としたWebフレームワークです。他のフレームワークと比較して、Warpは独自の道を歩んでいます。

  • Warp はAxumと同様に、TokioとHyperを基に構築され、Towerミドルウェアを使用していますが、その実装方法においては大きく異なります。Warpは Filter トレイトによって特徴づけられ、このトレイトを利用してリクエストを処理するフィルターのパイプラインを構築します。フィルターは連結させたり、組み合わせたりすることができ、この柔軟性により複雑な処理フローも簡単に扱うことが可能になります。

このフレームワークは、Tokioエコシステムとの密接な連携により、追加の抽象化層なしで直接Tokioの機能を活用できる点も魅力の一つです。

Warpの関数型プログラミングアプローチは、そのスタイルに慣れ親しんだ開発者にとって、非常に魅力的で合理的です。Warpで書かれたコードは、処理の流れが一目で理解できる「物語」として読むことができます。

ただし、このフレームワークでは型が複雑になりがちであり、Rust Analyzerのインレイヒント機能をオフにすることをお勧めします。また、エラーメッセージが長く複雑になることもあり、解析が難しい場合があります。

フィルターベースの設計は強力ですが、時にはより宣言的なアプローチを好む開発者もいるかもしれません。その点で、Warpは他のフレームワークと比較して特異な存在です。

Warpは間違いなく魅力的なフレームワークですが、初心者にとって最も取り組みやすいわけではなく、また最も人気があるわけでもありません。しかし、小規模なプロジェクトや実験的なプロジェクトにおいて、新しいアイデアやアプローチを試すのに理想的な環境を提供します。

以下は、Warpのサンプルリポジトリ からのWebSocketチャットアプリケーションの簡略化された例です:

static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1);

*/// Our state of currently connected users.////// - Key is their id/// - Value is a sender of `warp::ws::Message`*type Users = Arc<RwLock<HashMap<usize, mpsc::UnboundedSender<Message>>>>;

#[tokio::main]
async fn main() {
    let users = Users::default();
    *// Turn our "state" into a new Filter...*let users = warp::any().map(move || users.clone());

    *// GET /chat -> websocket upgrade*let chat = warp::path("chat")
        *// The `ws()` filter will prepare Websocket handshake...*.and(warp::ws())
        .and(users)
        .map(|ws: warp::ws::Ws, users| {
            *// This will call our function if the handshake succeeds.*
            ws.on_upgrade(move |socket| user_connected(socket, users))
        });

    *// GET / -> index html*let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML));

    let routes = index.or(chat);

    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}

async fn user_connected(ws: WebSocket, users: Users) {
    *// Use a counter to assign a new unique ID for this user.*let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed);

    eprintln!("new chat user: {}", my_id);

    *// Split the socket into a sender and receive of messages.*let (mut user_ws_tx, mut user_ws_rx) = ws.split();

    let (tx, rx) = mpsc::unbounded_channel();
    let mut rx = UnboundedReceiverStream::new(rx);

    tokio::task::spawn(async move {
        while let Some(message) = rx.next().await {
            user_ws_tx
                .send(message)
                .unwrap_or_else(|e| {
                    eprintln!("websocket send error: {}", e);
                })
                .await;
        }
    });

    *// Save the sender in our list of connected users.*
    users.write().await.insert(my_id, tx);

    *// Return a `Future` that is basically a state machine managing// this specific user's connection.// Every time the user sends a message, broadcast it to// all other users...*while let Some(result) = user_ws_rx.next().await {
        let msg = match result {
            Ok(msg) => msg,
            Err(e) => {
                eprintln!("websocket error(uid={}): {}", my_id, e);
                break;
            }
        };
        user_message(my_id, msg, &users).await;
    }

    *// user_ws_rx stream will keep processing as long as the user stays// connected. Once they disconnect, then...*user_disconnected(my_id, &users).await;
}

async fn user_message(my_id: usize, msg: Message, users: &Users) {
    *// Skip any non-Text messages...*let msg = if let Ok(s) = msg.to_str() {
        s
    } else {
        return;
    };

    let new_msg = format!("<User#{}>: {}", my_id, msg);

    *// New message from this user, send it to everyone else (except same uid)...*for (&uid, tx) in users.read().await.iter() {
        if my_id != uid {
            if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) {
                *// The tx is disconnected, our `user_disconnected` code// should be happening in another task, nothing more to// do here.*}
        }
    }
}

async fn user_disconnected(my_id: usize, users: &Users) {
    eprintln!("good bye user: {}", my_id);

    *// Stream closed up, so remove from the user list*
    users.write().await.remove(&my_id);
}

Warpの要点

  • 関数型アプローチ。
  • 非常に表現力豊か。
  • Tokio, Tower, Hyperに近い強固なエコシステム。
  • 初心者に優しいとは言えない。

Tide

  • Tide は、async-stdをベースにした極めてシンプルなWebフレームワークです。このフレームワークは、限られたAPI面積を通じて、直接的かつシンプルなウェブ開発体験を提供します。Tideでのハンドラ関数は、Request オブジェクトを受け取り、tide::Result を介して Response オブジェクトを返す非同期関数です。データの抽出やレスポンスのフォーマットは開発者の裁量に委ねられています。

このアプローチは、開発者にとって追加の作業を意味するかもしれませんが、同時にHTTPリクエストとレスポンスの処理をより直接的にコントロールする能力を提供します。特定の場合においては、この近接性が事柄を単純化することにつながり得ます。

Tideのミドルウェア設計は、Towerフレームワークで見られる概念と似ていますが、async trait crate の公開により、実装が大幅に容易になっています。非同期Rustエコシステムに深く関わる開発者によって構築されているため、Rustの最新の非同期機能、例えばNightlyリリースで導入された トレイト内の非同期関数 などが迅速に取り入れられています。

サンプルリポジトリ からのユーザーセッションの例。

#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    femme::start();
    let mut app = tide::new();
    app.with(tide::log::LogMiddleware::new());

    app.with(tide::sessions::SessionMiddleware::new(
        tide::sessions::MemoryStore::new(),
        std::env::var("TIDE_SECRET")
            .expect(
                "Please provide a TIDE_SECRET value of at \
                      least 32 bytes in order to run this example",
            )
            .as_bytes(),
    ));

    app.with(tide::utils::Before(
        |mut request: tide::Request<()>| async move {
            let session = request.session_mut();
            let visits: usize = session.get("visits").unwrap_or_default();
            session.insert("visits", visits + 1).unwrap();
            request
        },
    ));

    app.at("/").get(|req: tide::Request<()>| async move {
        let visits: usize = req.session().get("visits").unwrap();
        Ok(format!("you have visited this website {} times", visits))
    });

    app.at("/reset")
        .get(|mut req: tide::Request<()>| async move {
            req.session_mut().destroy();
            Ok(tide::Redirect::new("/"))
        });

    app.listen("127.0.0.1:8080").await?;

    Ok(())
}

Tideの要点

  • ミニマリスティックアプローチ。
  • *async-std*ランタイムを使用。
  • シンプルなハンドラー関数。
  • 非同期機能の実験場。

Poem

「プログラムは詩と同じ、書かれなければ存在しない」— ダイクストラ

  • *Poem*のReadmeは、この引用で始まります。Poemは、充実した機能を提供しつつも、手軽に使えるWebフレームワークだと自己紹介しています。この大胆な宣言に応える形で、Poemはその使いやすさと機能性を実証しています。表面上はAxumに似ており、ハンドラ関数に特定のマクロを使用する点が主な違いですが、同時にTokioとHyper上に構築され、Towerミドルウェアとの完全な互換性を持ちながら、独自のミドルウェアトレイトを提供しています。

Poemのミドルウェアトレイトは特にユーザーフレンドリーで、 Endpoint に対してトレイトを直接実装するか、または Endpoint を引数に取る非同期関数を簡単に作成することができます。これは、TowerとそのService Trait と長時間取り組んだ後には、特に新鮮なアプローチと言えるでしょう。

広範囲にわたるエコシステムとの互換性を誇りながら、Poem自体も豊富な機能を備えています。OpenAPIやSwaggerドキュメンテーションの完全サポートはもちろん、HTTPベースのWebサービスだけでなく、Tonicを用いたgRPCサービスやLambda関数での使用も可能です。OpenTelemetry、Redis、Prometheusのサポートを含めると、エンタープライズ向けアプリケーション開発に必要な全てを提供します。

Poemがまだ0.xのバージョンであるとしても、その発展には目を離せません。勢いを保ちながら安定した1.0リリースを目指しているPoemは、注目に値するフレームワークです。

以下は、Poemの例リポジトリ にあるWebSocketの簡略版です:

#[handler]
fn ws(
    Path(name): Path<String>,
    ws: WebSocket,
    sender: Data<&tokio::sync::broadcast::Sender<String>>,
) -> impl IntoResponse {
    let sender = sender.clone();
    let mut receiver = sender.subscribe();
    ws.on_upgrade(move |socket| async move {
        let (mut sink, mut stream) = socket.split();

        tokio::spawn(async move {
            while let Some(Ok(msg)) = stream.next().await {
                if let Message::Text(text) = msg {
                    if sender.send(format!("{name}: {text}")).is_err() {
                        break;
                    }
                }
            }
        });

        tokio::spawn(async move {
            while let Ok(msg) = receiver.recv().await {
                if sink.send(Message::Text(msg)).await.is_err() {
                    break;
                }
            }
        });
    })
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let app = Route::new().at("/", get(index)).at(
        "/ws/:name",
        get(ws.data(tokio::sync::broadcast::channel::<String>(32).0)),
    );

    Server::new(TcpListener::bind("127.0.0.1:3000"))
        .run(app)
        .await
}

Poemの要点

  • 豊富な機能を搭載。
  • Tokioエコシステムとの互換性。
  • 使いやすい。
  • gRPCおよびLambdaに適応可能。

注目すべきフレームワーク

Pavex

最初に触れたように、多くのRust Webフレームワークは表面上似ており、差異は細部にあると言えます。しかしながら、Pavexはその常識を打ち破る存在です。Pavex は、Zero To Production の著者、Luca Palmieriによって開発されています。彼の豊富な経験と洞察が、Pavexの開発に活かされています。

Pavexは自身を、RustでAPIを構築するための「特別なコンパイラ」と位置付けています。アプリケーションが実現すべき機能の高レベルな記述から始まり、設定済みで起動準備が整ったAPIサーバーSDKクレートを生成することができます。

開発の初期段階にありながらも、Pavexは既に大きな注目を集めています。更なる詳細や進捗については、Luca Palmieriの ブログ を参照してください。

まとめ

RustのWebフレームワークの世界は、その多様性において豊かです。一つのフレームワークが全てのニーズを満たすわけではなく、プロジェクトの要件に応じて最適な選択をすることが重要です。Web開発をこれから始める方には、ActixやAxumを特に推奨します。これらのフレームワークは新規ユーザーにとって親しみやすく、充実したドキュメントが用意されています。個人的には、長年にわたりAxumを愛用してきましたが、Pavexの新たな提案や、Poemにおける新しいアプローチにも関心があります。

すべての主要なRust WebフレームワークはShuttleでサポートされているため、Shuttleのドキュメント にあるサンプルを試して、自分のプロジェクトに最適なフレームワークを見つけることをお勧めします!

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。