EmacsでのRustの開発環境
今までRustの開発はrust-modeに委ねていたのだけど、ふとツイッターでrusticなるものを知った。
試しに入れてみたら開発環境の構築がかなり簡単に片付いて色々リッチになったのでメモ。
設定は以下の通り。
;; Rust (require 'rustic) (setq-default rustic-format-trigger 'on-save) (setq rustic-rustfmt-bin "~/.cargo/bin/rustfmt") (add-to-list 'rustic-rustfmt-config-alist '("edition" . "2018")) (setq lsp-rust-analyzer-server-command '("~/usr/local/bin/rust-analyzer"))
実を言うとそこまでリッチにしなくても良い感はあって、ある程度補完が効いて型情報などが取れればいい感があるのだけど、今回環境構築をするついでにformatterが自動で効くようにした。
これは2, 3行目のところで、ファイルを保存した際にCargoでインストールしたrustfmt
が走るようになっている。
4行目のadd-to-list
のところについて、rusticのformatterはなぜかデフォルトでeditionが2015となっていて、async
構文を見るとformatterがエラーを吐いてしまう。
なのでconfigにeditionが2018であることを明記する必要があった。
LSPのサーバーにはrust-analyzer
を使っていて、自分はHomebrew経由でインストールしているのでパスをこのように記述した。
rust-analyzerのインストールは以下の通り。
brew install rust-analyzer
自分の今のところのRustの開発環境はこんな具合。
Rust楽しんでいくぞ〜〜〜
Actix-webで作ったAPIサーバーにJSONをPOSTする
TL; DR
serde_json
は、curlコマンドでシングルクオーテーションとダブルクオーテーションの順序を気にするらしく
curl -X POST -H "Content-Type:application/json" \ master -d "{'username': 'komi'}" \ http://localhost:8000/search
はダメで
curl -X POST -H "Content-Type:application/json" \ master -d '{"username": "komi"}' \ http://localhost:8000/search
はオッケーらしい。
状況
Actix-webでAPIサーバーを開発していて、POSTするAPIを書いてみたのだが、テストコードは動いてるのに手元でcurl
で叩いてみるとなぜか動かない現象に遭遇した。
コードは以下の通り。
use diesel::prelude::*; use diesel::r2d2::{self, ConnectionManager}; use dotenv::dotenv; use std::env; pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; pub fn make_pool() -> DbPool { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let manager = ConnectionManager::<PgConnection>::new(database_url); r2d2::Pool::builder() .build(manager) .expect("Failed to create pool.") } #[post("/search")] pub async fn search_user(info: web::Json<UserData>, pool: web::Data<DbPool>) -> impl Responder { let conn = pool.get().expect("couldn't get db connection from pool"); let res = Schedule::get_schedule(info.username.clone(), &conn); match res { Ok(content) if content.len() > 0 => web::Json(Info { result: true }), _ => web::Json(Info { result: false }), } }
ターミナルで叩いたコマンドは
curl -X POST -H "Content-Type:application/json" \ master -d "{'username': 'komi'}" \ http://localhost:8000/search
エラーメッセージは以下の通り。
[2020-08-24T12:20:50Z DEBUG actix_web::types::json] Failed to deserialize Json from payload. Request path: /search [2020-08-24T12:20:50Z DEBUG actix_web::middleware::logger] Error in response: Deserialize(Error("key must be a string", line: 1, column: 2)) [2020-08-24T12:20:50Z INFO actix_web::middleware::logger] 127.0.0.1:53552 "POST /search HTTP/1.1" 400 0 "-" "curl/7.64.1" 0.000690
どうやらJSONのロードに失敗しているらしい。
しかしテストコードは動いている。
なぜだろうかと考えて、Actix-webのサンプルなどを叩いてみたところ、そちらは動く。
ひょっとしたらr2d2
が何か悪さをしているのかと調べてみるが、これは問題ないらしい。
そうこうして調べてるうちに、テキトーにコピペしてみたコマンドが動いたり動かなかったりするのを発見。
結局のところ原因は何だったかというと、シングルクオーテーションとダブルクオーテーションの順序だった。
つまり
"{'username': 'komi'}"
はダメで、
'{"username": "komi"}'
はオッケーらしい。
自分は今までシングルクオーテーションとダブルクオーテーションの順序などあまり気にしたことがなかったので、今回このようなケースにハマって微妙な時間を溶かした。
そりゃ確かにテストコードは通ってもコマンドは動かない。
ということで原因は見つかって、自分もちょっと学びになった。
Actix-webでAPIサーバーを書いてみた
現在ちょっとしたカレンダーアプリを作っており、バックエンドにはRustを採用している。
フレームワークにはActix-webを採用しており、今回バックエンドサイドはある程度書き上がったのでその所感をまとめてみようと思う。
ミニマム
とにかく全体的に感じたのが、とにかくミニマムで取り回しやすいなという感じがした。
マクロ等を利用したルーティングやURIディスパッチ、各種ミドルウェアの実装などをほんの数行で実現でき、簡易的なAPIサーバーを実装するにはかなり良いと思う。
例えばマクロによるルーティングとURLディスパッチについて、自分も今作っているカレンダーアプリではこんなコードがある。
#[get("/user/{username}")] pub async fn schedule_content( info: web::Path<UserData>, pool: web::Data<DbPool>, ) -> impl Responder { let conn = pool.get().expect("couldn't get db connection from pool"); let res = Schedule::get_schedule(info.username.clone(), &conn); match res { Ok(contents) => web::Json(contents), Err(_) => panic!("Not found schedule"), } }
URLで/user/{username}
として任意の文字列usernameに対して適当なコンテンツを打ち返すというのがこの関数の動作だけども、
#[get(...)]
という属性マクロを用いてルーティングを定義し、info: web::Path<UserData>
として引数にURLのディスパッチを行う
ということができている。
例えばこれをPythonのFlaskとかでやろうとするならもう少しコード量が必要になると思われる。
別にPythonのFlaskでも良い感はあるが、やはりRustには型による安心感がある。
実際、今回は{username}
という任意の文字列をUserData
という型(構造体)に押し込めており、こちらとして事前に用意しておいたメソッドを適用させることなどができる。
もちろん、場合によってはディスパッチされる型を整数型として受け取ることも可能である。
こうした型の安全性とシンプルなコードを高いレベル両立できるのはやはりすごいと思う。
拡張性
今回のバックエンドとしてはそこまで複雑な動作をさせているわけではないが、地味にDBへの連携などの処理がある。
その点、Actix-webはHTTPサーバーとしての処理とRust内部でのデータ処理のフェイズを綺麗に分けられているので、DieselなどのORMとの橋渡しが思ったよりスムーズにできた。
正直なところDieselがORMとしてどうなのかという点については多少の文句はあるのだけど(やはりPythonのSQLAlchemyが最高な気がする)、Actix-webに関しては言えばそこらへんを非常に良く分業できているので、開発していてとてもユーザー体験が良かったと思う。
実際、チュートリアルにもDieselとの接続について簡単なデザインパターンが載っており、これは初学者にとっては心強いと思う。
これ以外に、CSRF対策なども数行挟み込めばなんとかなる。
use actix_web::middleware::csrf; fn handle_post(_req: HttpRequest) -> &'static str { "This action should only be triggered with requests from the same site" } fn main() { let app = Application::new() .middleware( csrf::CsrfFilter::build() .allowed_origin("https://www.example.com") .finish()) .resource("/", |r| { r.method(Method::GET).f(|_| httpcodes::HttpOk); r.method(Method::POST).f(handle_post); }) .finish(); }
他にもHTTP/2.0とかWebSocketあたりの先進的なWebの技術についてもしっかりカバーされており、やはり強い。
非同期
やはりActix-webについてはこれが嬉しい。
async/awaitを特に意識することなくサクッと実装でき、これは何よりも嬉しいポイントだと思う。
開発者サイドからは非同期処理特有のめんどくさい部分がRustのasync構文によってtokioのランタイムをフル活用でき、ストレスなく非同期処理を実装できる。
困ってないけど困りそうなポイント
今回はActix-webでAPIサーバーを実装したわけで、割と開発がサクサク進んだので特に困ったポイントなどは無いのだけど、こういうケースだと少し困るかな、という点を上げておこうと思う。
DjangoやRailsの場合、データベースやフロントの方までフレームワークが管理しており、フレームワークが敷いたレールの上をしっかり歩めば良いというのが根幹の思想としてある。
一方で、今回扱ったActix-webについてはそうした完全に全部をカバーするという思想ではなく必要な機能を必要な分だけ使うというイメージである。
なので、例えばテンプレートエンジンなど込みで一つのフレームワークに乗っかって全て完結させたいんだーという場合において、Actix-webを含めて現存するRustのフレームワークでは実現が難しいポイントだと思う。
ただ、これについては個人的には問題ないと思っていて、というのもRustはとにかくシンプルにプロダクトを作成するという点においてかなり強みを発揮すると考えている。
故にRustは疎結合な構成のWebアプリで使われると思っていて(そっちの方が型周りの良さが出てくると思う)、今のところそういう機能モリモリなMVCフレームワークはないし、これからもそういうのは登場する必要はないかなと思う。
今後もWeb開発においてRustは局所的に高速に非同期処理したい場合で活躍していくと思う。
まとめ
今回はActix-webで実装をしてみた所感については簡単にまとめてみた。
色々偉そうな感じのことを言ってしまっており、もしかしたら間違いがあるかもしれないので、その場合は後学やコミュニティのためにも指摘してもらえると嬉しい。
今後もRustで開発していこうと思う。
DieselでPostgreSQLに接続
最近はずっとRustで簡単なWebアプリを作っているのだけど、PostgreSQLへの接続にちょっと困ったのでメモ。
環境
- Rust: 1.45.0
[dependencies] actix-web = "2.0.0" actix-rt = "1.1.1" diesel = { version = "1.4.5", features = ["postgres", "chrono", "r2d2"] } dotenv = "0.15.0" serde = "1.0.114" chrono = { version = "0.4.13", features = ["serde"] } r2d2 = "0.8.9"
やったこと
cargo install diesel_cli
その後、作業ディレクトリの中で.env
というファイルを作り、その中に
DATABASE_URL=postgres://[username]:[password]@localhost/[some_name]
と書き込む。
このときusername
とpassword
はローカル環境なら端末のユーザー名とパスワード。
次にPostgreSQLサーバーを立てる。
PostgreSQLのインストールで、Macの場合は
brew install postgresql
とする。
これを入れることで
# PostgreSQLサーバー起動 postgres -D /usr/local/var/postgres # PostgreSQLサーバーへ接続するためのクライアント psql -h [ホスト名] -U [ユーザー名] -p [パスワード] [テーブル名] # テーブル作成 createdb -h [ホスト名] -U [ユーザー名] -p [パスワード] [テーブル名]
などのコマンドが打てる。
ローカル環境の場合はpsqlコマンドの-h
や-U
などのオプションはデフォルトでlocalhostに繋がるようになっており、手元で簡単に試すのであればpsql [テーブル名]
などすれば良い。
さて、今回はDieselでPostgreSQLにつなぐことが目的なので、サーバーを立てる。
postgres -D /usr/local/var/postgres
これを裏でやっておいて、作業ディレクトリでDieselのセットアップをする。
diesel setup
こうすると先ほど後ろで走らせておいたPostgreSQLサーバーに通信が飛んだのがわかると思う。
あとはDieselのチュートリアルの通りにやればよく、まずSQLのコマンドを生成する。
diesel migration generate create_posts
これをやると作業ディレクトリにmigrationというディレクトリが作られ、中にup.sql
とdown.sql
というファイルが作られる。
それぞれ
CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT 'f' )
DROP TABLE posts
と書き込む。
あとは
diesel migration run
とコマンドを打てば、裏側で走らせてるPostgreSQLサーバーが走ってCREATE TABLEしてくれる。
反省
最初に作業をしていたとき、ローカルでPostgreSQLサーバーを立てるのがめんどくさくて、.env
の中を
DATABASE_URL="test.db"
としており、実際にdiesel setup
などをすると若干怪しい動きをしつつも作業ディレクトリにtest.db
というファイルが生成されていたのでイケると勝手に勘違いしていた。
その後、作業を続けてActix-webでHTTPサーバー立てようとしたときデータベースへの繋ぎ込みがうまくいかないことが原因となってHTTPサーバー自体も立ち上がらず...
そんなこんなでかなり時間を食ってしまった(CircleCIもめちゃくちゃコケた)ので、これからはローカル開発でもサボらずにサーバーを立てようと思う。
インスタンスでNginxを使って簡単にデプロイする
ローカルのマシンで
python3 -m http.server 5000
をやると5000番ポートにサーバーを立てることができる。
ブラウザでlocalhost:5000を見てみるとディレクトリにあるファイルが一覧で見れる。
さて、これをインスタンスでも同じことをやろうと思う。
まず、インスタンスを簡単に準備する。
自分はGCPでCompute Engineを使って用意した。
とりあえずPythonをインストールする。
sudo apt install python3
これでできたあと、Pythonでローカルサーバーを立てる。
python3 -m http.server 5000
結論から言うと、このままだとブラウザで http://[外向けIP]
としてもアクセスできない。
このアドレスにアクセスできるようにNginxを利用する。
まずNginxをインストールする。
sudo apt install nginx
このあと、 /etc/nginx/conf.d/[何かテキトーな名前].conf
というファイルを作り、そこにNginxの設定を書き込む。
server { listen 80; server_name [外向けIP]; // ここ注意 location / { proxy_pass http://localhost:5000; } }
それぞれの意味として
- 1行目 server: Nginxサーバーについての設定
- 2行目 listen: Nginxはデフォで80番ポートを見に行くとかなんとか
- 3行目 server_name: ドメインとかを書くらしい、今回はIPを直打ち
- 4行目 localtion / : ルーティングで
/
にアクセス来た時にプロキシとして飛ばす設定 - 5行目 proxy_pass: プロキシで飛ばすアドレス。ポート番号注意。
という具合。
この設定が書けたらNginxを起動する
sudo service nginx start
そうして、Pythonでサーバーを立てる。
ポート番号はNginxの設定ファイルと合うようにする。
python3 -m http.server 5000
これでブラウザを見に行くと中身が見れる。
これで簡単にデプロイができた。
RustのWebフレームワークを触ってみた感想
最近はRustにハマっている。
前までPythonとCommon Lispをずっと触っていた人間で、完全に動的型付け言語の脳になっていたのだけど、少し勉強してみようくらいの気持ちでRustを書き始めた。
最初はそんなモチベだったのだけど、気がついたらもうコードを書くときはRustくらい型がしっかりしていないと不安になるようになってしまった。
とにかくRustは書いてて楽しい言語だと思う。
そんなRustだが、サーバーエンジニアリングの言語として触るならどんな感じになるか知りたくなり、今回RustのWebフレームワークを触ってみることにした。
どのフレームワークか
RustのWebフレームワークは結構たくさんある。
ここらへんの比較については以下のページにて比較がなされている。
流石は勢いのある言語ということもあり、かなりたくさんのWebフレームワークが作られている。
ひとまず、rust web framework
と検索してみたらRocketが出てきた(Simple, Fast, Type-Safeと謳っている)のと、あと上記のフレームワーク比較で非同期処理ができるActix-webを触ってみることにした。
Rocket
とりあえずチュートリアルを回してみる。
ホントに触ってみたらRustのそのままの感じが前面に出てるような感じで、触りやすい印象。
ある程度のお作法はあるものの、そこまで癖がなく簡単に書ける。
実際、ルーティングについても#[hogehoge]
というようにアトリビュートをつけるだけで簡単に実装ができる。
#[get("/")] fn index() -> &'static str { "Hello Rocket!" } fn main() { rocket::ignite() .mount("/", routes![index, hoge, hello, bar]) .launch(); }
これ以外にもテストやStateなどもとても触りやすい形式となっており、かなり素直にWebアプリが実装できるようになっている。
何よりチュートリアルがめちゃくちゃ整っている。
非同期処理ができないのが難点だけども、シンプルなWebアプリをRustで書いてみようとするならRocketはかなり良さそうな印象。
ちなみにちょっと触っててコンパイラがwarningを出してたので直してPRを出したらマージされた。
Actix-web
Rocketのチュートリアルが終わったので今度はActix-webを触る。
このActix-webというのはRust的にちょっと歴史があるものらしく、Rustの安定版に非同期処理が実装されたのがつい去年の話らしい。
そんなわけだけど、安定版に実装されるまでに非同期処理のランタイムを実装する試みがたくさんあったらしく、Actixはそのうちの1つのtokioというランタイムを用いたアクターモデルの非同期処理フレームワークという位置付けらしい。(多分認識が間違ってるかも)
で、そのActixの非同期処理機構をWeb系にガンガン拡張していって出来上がったのがActix-webというものらしい。
そんな背景だが、とりあえず触ってみる。
Rocketと同様にまずチュートリアルを回してみる。
触った感触としては、基本的にはかなりシンプルな設計だと感じた。
ただ、かなり実践的な用途を考えているらしく、データベースへの連携だったりHTTP/2への対応、graceful shutdownによるホットデプロイメントだったり、かなり豪華な仕様となっている。
まだチュートリアルを全部回しきってはいないが、ここらへんはWeb開発において結構重要そうな云々がフレームワークとしてまとまっているので、是非ともこれからもActix-webを触ってみようと思う。
まとめ
今回はRustのWebフレームワークとしてRocketとActix-webを触ってみた。
どちらも触りやすい感じで、同時にチュートリアル等が整っているので始めやすかった。
今のところは手元でローカルホストでちょっと動きを確かめる程度の感じになっているが、ある程度Actix-webが使い慣れてきたら勉強の意味も兼ねてGCPなりでインスタンスを立てて何かWebアプリを作ってみようと思う。
Common Lispで気に入ってた部分がRustにもあり、今のところは全体的に楽しくコードを書けているので、周りでまだRustを触ってない人は是非一緒にRustを勉強をしましょう。
では、今回はここらへんで。
RustでVec<&str>を返そうとして困った
そんなこんなでRustの練習がてら今はRustでLispインタプリタの実装に取り掛かっている。
基本的な要件はラムダ式が扱えてリスト処理、簡単な算術演算ができるようにすること。
前にこの仕様でPythonで実装した。
これを今回Rustでやろうというわけである。
リストをreturnする
Lispインタプリタを実装する第一歩として、トークナイザを実装することがある。
これはいたって簡単で、Lispは(function arg0 arg1 ...)
というようにカッコの頭に関数がきてその後に引数が来る。
つまりトークナイザの実装としては
(func arg0 arg1) => ["(", "func", "arg0", "arg1", ")"]
というように空白区切りでリストへ変換するだけの作業となる。
そんなこんなでこれをRustで実装しようとしたのだが、これにちょっと手こずった。
結論としては以下のコードを実装すれば良い。
fn tokenize(s: &str) -> Vec<String>{ let spreaded = s.replace("(", " ( ") .replace(")", " ) "); let tokens: Vec<String> = a.split(" ") .map(|item| item.to_string()) .collect(); return tokens; }
色々試した
上記のコードで動かせばいい感じになるが、あいにくRustユーザーとしてはまだまだ未熟なので色々試行錯誤していた。
最初に書いたコードはこれ。
fn tokenize(s: &str) -> Vec<&str>{ let tokens: Vec<&str> = s.replace("(", " ( ") .replace(")", " ) ") .split(" ") .collect(); return tokens; }
これをやるとerror[E0515]: cannot return value referencing temporary value
と怒られた。
ローカル変数屁の参照を返すなボケと言っている。
これを直すために&str
とString
を調べてみると、&str
はsliceで配列への参照、String
はVectorらしい。
ということで今回のケースでは中身に文字列が入ったリスト(ベクトル)をreturnするためにVecの中の文字列の型を&str
ではなくString
として記述するべきらしい。
ということで色々試行錯誤する(ここで何度も型を間違えて3時間くらい格闘した)
そんなこんなで色々ググったりして格闘してるうちに上記の動くコードを書くことができた。
反省
ずっとLispとPythonという型を明示的に書かない言語を長いこと触っていなかったためにかなり手こずった。
具体的にRustで型を使いこなすにはそれなりに鍛錬が必要な気がするのでこれからはRustで色々書いていこうと思う。
ただ、今回&str
とString
を間違えていたらなかなかに危険なプログラムが出来上がっていただろうから、Rustの所有権のありがたみとCargoの面倒見の良さに感謝していたりもする。
Rustがんばっていきたい。