Help us understand the problem. What is going on with this article?

Rust勉強中 - その4 -> クレート

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回はユニットテストとコマンドライン引数について簡単に学びました。
Rust勉強中 - その3

クレート

私が参照している教材で簡単なWebサーバを構築しつつクレートの使い方を紹介しているので、それにならいます。ただし、一部プログラムを変更しています。

クレートとは

クレート(crate)は、「輸送用の箱」という意味だそうです。Rustにおけるクレートは、ライブラリや実行ファイルなどのパッケージを意味します。
Rustのクレートは、crates.ioで公開されています。

プロジェクト作成

まず、プロジェクトの作成。

$ cargo new --bin iron-sum
     Created binary (application) `iron-sum` package
$ cd iron-sum

Cargo.tomlの編集

次に、使用するクレートをRustに知らせるために、Cargo.tomlを編集します。Cargo.tomlはパッケージに関する設定をするファイルです。

Cargo.toml
[package]
name = "iron-sum"
version = "0.1.0"
authors = []
edition = ""

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
iron = "0.5.1"
mine = "0.2.3"
router = "0.5.1"
urlencoded = "0.5.0"

各クレートについて、

クレート名 説明
iron Rust向けに組み込まれたWebフレームワークで、HTTPを実現するhyperクレート上に構築されています。
mine Content-Typeヘッダを指定するために使います。このクレートはすでにヤンクされているとのことです。ただし、依存されている可能性があるためダウンロードはできます。
router ironのためのURLのルーティングを実現します。
urlencoded ironのためのURLエンコーディングを実現します。

Cargo.tomlにはコード中で利用するクレートだけ書けば良いそうです。他の依存クレートはcargoが自動で管理してくれます。(便利!)

最初のWebサーバ

main.rs
extern crate iron;
#[macro_use] extern crate mime;

use iron::prelude::*;
use iron::status;

fn main() {
    println!("Serving on http://localhost:3000...");
    Iron::new(get_form).http("localhost:3000").unwrap();
}

fn get_form(_request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(r#"
    <title>Sum Calculator</title>
    <form action="/sum" method="post">
      <input type="text" name="n"/>
      <input type="text" name="n"/>
      <button type="submit">Compute sum</button>
    </form>
    "#);

    Ok(response)
}
$ cargo run
    Updating crates.io index
  Downloaded iron v0.5.1
  Downloaded router v0.5.1
  Downloaded mime v0.2.6
  ...
   Compiling router v0.5.1
   Compiling bodyparser v0.5.0
   Compiling urlencoded v0.5.0
   Compiling iron-sum v0.1.0 (C:\Users\deta\hack\rust\iron-sum)
    Finished dev [unoptimized + debuginfo] target(s) in 1.82s
     Running `target\debug\iron-sum.exe`
Serving on http://localhost:3000...

流れは以下です。

  1. localhost:3000でサーバ待ち受け
  2. ブラウザでhttp://localhost:3000 へアクセス
  3. リクエストが来たら、HTMLで書かれたフォームを応答

クレートのインポート

main.rs
extern crate iron;
#[macro_use] extern crate mime;

use iron::prelude::*;
use iron::status;
...

extern crateはクレートを利用するための宣言です。extern crate mimeの前に#[macro_use]属性があります。これは、このクレートでエクスポートされているマクロを使用するための属性です。
次に前回も登場したuse文を使って、クレートの機能を取り込みます。use iron::prelude::*;でワイルドカードが使われています。これは、preludeの機能をすべて取り込むという意味です。Pythonでもこのようなライブラリのインポートが可能ですが、名前の衝突などが理由であまり好まれません。Rustでも通常は利用する機能の名前を書いたほうが良いみたいです。ただし、今回のように慣例としてなどの理由で例外もあります。

サーバ待ち受け

main.rs
...
fn main() {
    println!("Serving on http://localhost:3000...");
    Iron::new(get_form).http("localhost:3000").unwrap();
}
...

まず、Iron::newにリクエスト時のハンドラを指定します。次にhttpメソッドにアドレスとポートを指定してHTTPサーバとして待ち受けます。

レスポンス

main.rs
...
fn get_form(_request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(r#"
    <title>Sum Calculator</title>
    <form action="/sum" method="post">
      <input type="text" name="n"/>
      <input type="text" name="n"/>
      <button type="submit">Compute sum</button>
    </form>
    "#);

    Ok(response)
}

get_formはリクエストを処理する関数です。仮引数_requestにアンダーバー(_)が付いています。これは、この変数は使われないがRustコンパイラの警告は表示したくない場合に付けるようです。ちなみにアンダーバーを付けなかった場合は以下のように表示されます。

$ cargo run
   Compiling iron-sum v0.1.0 (C:\Users\deta\hack\rust\iron-sum)
warning: unused variable: `request`
  --> src\main.rs:12:13
   |
12 | fn get_form(request: &mut Request) -> IronResult<Response> {
   |             ^^^^^^^ help: consider prefixing with an underscore: `_request`
   |
   = note: #[warn(unused_variables)] on by default

    Finished dev [unoptimized + debuginfo] target(s) in 1.99s
     Running `target\debug\iron-sum.exe`
Serving on http://localhost:3000...

余談ですがアドバイスとしてhelp:がすごく的確でわかりやすいんですよねー。
そして、仮引数の型が&mut Requestとなっています。Request型の参照を引数にとるようです。&mut型の前に書かれています。これは可変参照と言って、この関数内で変更した内容は参照元の変数にも影響しますよという意味らしいです。今まで見てきた例では、let mut a = 0など変数の前にありましたね。この違いは要注意です!(と私が勝手に思いました)
そして、let mut response = Response::new();でレスポンスを用意します。set_mutメソッドはレスポンスのどの部分に値を設定するかを決めます。

  • 1回目はstatus::OkでHTTPステータスを設定
  • 2回目は、mime!マクロを使って、Content-Typeを設定
  • 3回目は文字列を渡して、レスポンスボディを設定

最後のレスポンスボディの文字列が、r#"~"#となっています。これは、raw string(生の文字列)といいます。raw stringは文字rと0個以上の#からなります。終端は同じ数の#で閉じます。つまりr###"で開始されていれば、"###で閉じるということですね。このraw stringは任意の文字(\"'など)をエスケープせずに文字列を生成します。
この関数の戻り値はIronResult<Response>となっています。これはResult型の一種らしいです。成功ならばOk(v)を、失敗ならばErr(e)を返します。ここでは、Ok(response)として関数の戻り値を指定しています。

URLルーティング機能の追加

今のままでは、フォームに入力してもURL以外何も変化がないです。そこで、URLルーティング機能を追加してハンドラを追加します。

追加のインポート

main.rs
extern crate iron;
extern crate urlencoded; //add
extern crate router; //add
#[macro_use] extern crate mime;

use std::str::FromStr; //add
use std::io::Write; // add
use iron::prelude::*;
use iron::status;
use router::Router; // add
use urlencoded::UrlEncodedBody; //add

新たにトレイトやクレートをインポートしました。追加したものはコメントをしています。ちなみにトレイトのインポートって言い方正しいのでしょうか?そこら辺の用語や役割が私にとってまだまだ曖昧です。

ルートの追加

main.rs
fn main() {
    let mut args   = Vec::new();

    for arg in std::env::args().skip(1) {
        args.push(arg);
    }

    if args.len() != 2 {
        writeln!(std::io::stderr(), "Usage: iron-sum <HOST> <PORT>").unwrap();
        std::process::exit(1);
    }

    let mut router = Router::new();

    router.get("/", get_form, "root");
    router.post("/sum", post_sum, "sum");

    let info = format!("{}:{}", args[0], args[1]);
    println!("Serving on http://{}...", info);
    Iron::new(router).http(info).unwrap();
}

前半部分はせっかくなので前回学んだコマンドライン引数で、ホストとポートを指定できるようにしてみました。
router.route(path, handler, id)という書き方でルートを追加します。pathはリクエストパスでglobにも対応しているそうです。handlerはリクエストハンドラ、idは一意のidでuse_for!マクロでURLを生成するときに使うそうです。
routerを先ほどのIron::newメソッドの引数に渡してあげるとURLルーティングを行ってくれます。

POSTリクエストのハンドラを追加

main.rs
fn post_sum(request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    let form_data = match request.get_ref::<UrlEncodedBody>() {
        Err(e) => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("Error parsing form data: {:?}\n", e));
            return Ok(response);
        }
        Ok(map) => map
    };

    let unparsed_numbers = match form_data.get("n") {
        None => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("form data has no 'n' parameter\n"));
            return Ok(response);
        }
        Some(nums) => nums
    };

    let mut numbers = Vec::new();
    for unparsed in unparsed_numbers {
        match i32::from_str(&unparsed) {
            Err(_) => {
                response.set_mut(status::BadRequest);
                response.set_mut(
                    format!("Value for 'n' parameter not a number: {:?}\n", unparsed));
                return Ok(response);
            }
            Ok(n) => numbers.push(n)
        }
    }

    let mut a = numbers[0];
    if numbers.len()==2 {
        let b     = numbers[1];
        a = sum(a, b);
    } else {
        for b in &numbers[1..] {
            a = sum(a, *b);
        }
    }

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(
        format!("The sum of the numbers {:?} is <b>{}</b>\n", numbers, a));
    Ok(response)
}

それぞれ、部分でリクエストデータを加工しています。
matchは処理の結果に対して、結果に合致した際の処理や、成功した場合の処理と失敗した場合の処理が指定できます。以下のような感じです。

match result {
    val1 => expr1,
    ...
    _    => exprn,
}

最後のアンダーバーはどれにもマッチしなかったという意味です。if...elseif文やswitch文に似ていますね。
matchは全ての値をチェックする必要があります。そうしないとエラーを吐くようです。試してみます。

main.rs
fn post_sum(request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    let form_data = match request.get_ref::<UrlEncodedBody>() {
//        Err(e) => {
//            response.set_mut(status::BadRequest);
//            response.set_mut(format!("Error parsing form data: {:?}\n", e));
//            return Ok(response);
//        }
        Ok(map) => map
    };
    ...
$ cargo run localhost 3000
   Compiling iron-sum v0.1.0 (C:\Users\deta\hack\rust\iron-sum)
error[E0004]: non-exhaustive patterns: `Err(_)` not covered
  --> src\main.rs:39:27
   |
39 |     let form_data = match request.get_ref::<UrlEncodedBody>() {
   |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ pattern `Err(_)` not covered
   |
   = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms

error: aborting due to previous error

For more information about this error, try `rustc --explain E0004`.
error: Could not compile `iron-sum`.

To learn more, run the command again with --verbose

Err(e)をコメントアウトしてみると、ちゃんと「Err(_)忘れてまっせ」というメッセージまで出して教えてくれます。
ところで、一部処理で

    let unparsed_numbers = match form_data.get("n") {
        None => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("form data has no 'n' parameter\n"));
            return Ok(response);
        }
        Some(nums) => nums
    };

NoneSomeというのがあり、気になったので少し調べました。これは結果にOption型が返ってきた場合の処理みたいです。Optionは値がないか、いくつかのTの値があるかを保持しています。なので、Noneは何も持っていない場合の処理で、Some(nums)はnumsを持っている場合の処理ということでしょうか。
このようにResult型ではOk(v)とErr(e)を、Option型ではSome(T)とNoneを持つように、Rustでは複数の異なる型を持つ型を自分で定義できます。このような型をenum(列挙型)といいます。

request.get_ref::<UrlEncodedBody>()::<urlEncodedBody>()は型パラメータといい、指定された型(ここではUrlEncodedBody)でメソッド(get_ref)を実行してくれという意味だと私の中で理解しました。

最後にすべてのソースコードを載せておきます。

main.rs
extern crate iron;
extern crate urlencoded;
extern crate router;
#[macro_use] extern crate mime;

use std::str::FromStr;
use std::io::Write;
use iron::prelude::*;
use iron::status;
use router::Router;
use urlencoded::UrlEncodedBody;

fn sum(mut a: i32, b: i32) -> i32 {
    assert!(a != 0 && b != 0);
    a = a+b;
    a
}

fn get_form(_request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(r#"
    <title>Sum Calculator</title>
    <form action="/sum" method="post">
      <input type="text" name="n"/>
      <input type="text" name="n"/>
      <button type="submit">Compute sum</button>
    </form>
    "#);

    Ok(response)
}

fn post_sum(request: &mut Request) -> IronResult<Response> {
    let mut response = Response::new();

    let form_data = match request.get_ref::<UrlEncodedBody>() {
        Err(e) => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("Error parsing form data: {:?}\n", e));
            return Ok(response);
        }
        Ok(map) => map
    };

    let unparsed_numbers = match form_data.get("n") {
        None => {
            response.set_mut(status::BadRequest);
            response.set_mut(format!("form data has no 'n' parameter\n"));
            return Ok(response);
        }
        Some(nums) => nums
    };

    let mut numbers = Vec::new();
    for unparsed in unparsed_numbers {
        match i32::from_str(&unparsed) {
            Err(_) => {
                response.set_mut(status::BadRequest);
                response.set_mut(
                    format!("Value for 'n' parameter not a number: {:?}\n", unparsed));
                return Ok(response);
            }
            Ok(n) => numbers.push(n)
        }
    }

    let mut a = numbers[0];
    if numbers.len()==2 {
        let b     = numbers[1];
        a = sum(a, b);
    } else {
        for b in &numbers[1..] {
            a = sum(a, *b);
        }
    }

    response.set_mut(status::Ok);
    response.set_mut(mime!(Text/Html; Charset=Utf8));
    response.set_mut(
        format!("The sum of the numbers {:?} is <b>{}</b>\n", numbers, a));
    Ok(response)
}

fn main() {
    let mut args   = Vec::new();

    for arg in std::env::args().skip(1) {
        args.push(arg);
    }

    if args.len() != 2 {
        writeln!(std::io::stderr(), "Usage: iron-sum <HOST> <PORT>").unwrap();
        std::process::exit(1);
    }

    let mut router = Router::new();

    router.get("/", get_form, "root");
    router.post("/sum", post_sum, "sum");

    let info = format!("{}:{}", args[0], args[1]);
    println!("Serving on http://{}...", info);
    Iron::new(router).http(info).unwrap();
}
$ cargo run localhost 3000
    Finished dev [unoptimized + debuginfo] target(s) in 0.13s
     Running `target\debug\iron-sum.exe localhost 3000`
Serving on http://localhost:3000...

今回はここまで。
クレートの使い方が簡単にですが分かりました。まだトレイトやクレートの中身、型、列挙型などあいまいな部分多しです。ただRustはやっていて楽しいし、勉強になりますよね。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away