読者です 読者をやめる 読者になる 読者になる

Let's write β

趣味で書いたこととか、RustとLispが好き

RustでWebプログラミング No2 ~ Routerをつかって複数ルート~

rust-lang

前回の記事では、非常にシンプルなHelloWorldを表示するだけのサーバーを作成しIronの基礎を紹介しました。

今回はRouterと呼ばれるMiddlewareを利用して複数のURLに対応したサーバーを作成しました。

routerをCargo.tomlに追加

routerを利用するためにCargo.tomlに以下のようrouterをに追加します

[dependencies]

...other dependencies...

router = "*"

Router

コードの完成形は以下の様です:

extern crate iron;
extern crate router;

use iron::prelude::*;
use iron::status;
use iron::modifiers::{Redirect};
use router::{Router, url_for};

fn main() {

    fn top_handler(_: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Welcome!")))
    }

    fn redirect_handler(req: &mut Request) -> IronResult<Response> {
        let ref top_url = url_for(req, "index", ::std::collections::HashMap::new());
        return Ok(Response::with((status::Found, Redirect(top_url.clone()))))
    }

    fn greet_handler(req: &mut Request) -> IronResult<Response> {
        let ref router = req.extensions.get::<Router>();
        let ref name = router
            .unwrap()
            .find("name")
            .unwrap();
        return Ok(Response::with(
            (status::Ok,
             format!("Hello {}", name).as_str())
        ));
    }
    
    let mut router = Router::new();
    router.get("/", top_handler, "index");
    router.get("/greet/:name", greet_handler, "greeting");
    router.get("/to-top", redirect_handler, "to-top");

    Iron::new(router).http("localhost:3000").unwrap();
    println!("Listen on localhost:3000");
}

細かく見ていきましょう

Router

let mut router = Router::new();
router.get("/", top_handler, "index");
router.get("/greet/:name", greet_handler, "greeting");
router.get("/to-top", redirect_handler, "to-top");

この部分ではRouterを作成し、いくつかのルーティングを登録しています。

.getの部分はHTTPメソッドを意味していて、.post,.put, .deleteのようにその他各種HTTPメソッドに対応しています。 引数はそれぞれ、マッチするURL, その時に呼び出されるハンドラ、ルーティングIDを定義しています。

ルーティングIDはそのルーティングを後から参照する際に利用できる識別子となっています。

URL

URL中では:paramのようにすることで、後からURLの一部を取得することができます。 また、*を使うことでその部分は任意にマッチするようになります。

URLパラメータの取得

    fn greet_handler(req: &mut Request) -> IronResult<Response> {
        let ref router = req.extensions.get::<Router>();
        let ref name = router
            .unwrap()
            .find("name")
            .unwrap();
        return Ok(Response::with(
            (status::Ok,
             format!("Hello {}", name).as_str())
        ));
    }

このハンドラは/greet/:nameというURLにマッチしたときに呼び出されるようになっているハンドラです。 ハンドラ中で

let ref router = req.extensions.get::<Router>();

とすることでRouterを取得しています。extensionsはMiddlewareがリクエストを拡張するのに利用するもので、 今回の場合はRouterは自身をextensionsに保存しています。

ルータを通してURLのパラメータを取得することができます。findメソッドにURLパラメータ名(この場合は"name")を指定すると パラメータがOptionで手に入るので今回の場合は単純にunwrapしています。

url_for

URL to-topは、アクセスするとトップページにリダイレクトされるURLです。

ハンドラは以下のとおりです:

    fn redirect_handler(req: &mut Request) -> IronResult<Response> {
        let ref top_url = url_for(req, "index", ::std::collections::HashMap::new());
        return Ok(Response::with((status::Found, Redirect(top_url.clone()))))
    }

url_forを使うことでルーティングIDに対応するURLを取得することができます。

レスポンスにRedirectを利用することでユーザーをリダイレクトすることができます。

まとめ

今回はRouter Middlewareを利用して複数のルーティングに対応したHTTPサーバーを実装しました。 また、url_forを利用してルーティング登録時に設定したルーティングIDからURLを取得することができます。

次回はテンプレートエンジンを利用してHTMLページの表示をやってみたいと思います。

RustでWebプログラミング No.1 ~ IronのインストールとHello World~

rust-lang

Hello World

hello_worldバイナリプロジェクトを作ろう

$ cargo new hello_world --bin

サーバーはmain関数をもったプログラムに成るので、--binオプションを忘れに付けましょう。

Cargo.tomlにIronを追加する

[dependencies.iron]
version = "*"

まずCargo.tomlにironのdependenciesを追加しましょう。 これで、このアプリケーションでiron createを利用できるようになりました。

main関数を書く

次に、以下のように書くことでIronのサーバーをlocalhost:3000で立ち上げることができます。

extern crate iron;

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

fn main() {
    fn hello_world(_: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Hello, world")))
    }

    let _server = Iron::new(hello_world).http("localhost:3000").unwrap();
    println!("Lisning on port 3000");
}

こまかく見ていきましょう。

extern crate iron;

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

については通常のcrateのようにironをロードしているようです。

Handler

    fn hello_world(_: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Hello, world")))
    }

この関数は、なにやらRequestのミュータブルな参照をうけとって、 IronResultにくるんだResponseを返しているようです。

この部分は、Ironのドキュメントに依るとHandlerと呼ばれるようです。

この関数がリクエスト時に呼び出され、Requestに対応するResponseを返すことで、 ユーザーにデータが返されるようです。

pub trait Handler: Send + Sync + 'static {
    fn handle(&self, &mut Request) -> IronResult<Response>;
}

という定義で、

impl<F> Handler for F where F: Send + Sync + 'static + Fn(&mut Request) -> IronResult<Response>

が定義されているようなので、関数をHandlerとして利用できるようです。

参考

Trait iron::middleware::Handler

Response

サーバーからのリクエストに対する結果を返すときに利用するResponseですが

pub struct Response {
    pub status: Option<Status>,
    pub headers: Headers,
    pub extensions: TypeMap,
    pub body: Option<Box<WriteBody>>,
}

のような定義になっています。

Responseの作成のところでResponse::with((...))のようにして、statusbodyを設定しているようです。 これらは、Modifierという仕組みになっており、タプルのそれぞれの要素をつかってどのようにデータを変更するかを 別途定義しているようです。

Server

let _server = Iron::new(hello_world).http("localhost:3000").unwrap();

HTTPサーバーを起動するには、このようにIron::newHandlerを渡してやれば良いようです。 今回は、先ほど定義したhello_world関数をHandlerとして利用しています。

httpメソッドに渡した"localhost:3000"という文字列がパースされて、localhostの3000番ポートでHTTPサーバーが起動するようです。

起動

このようにして書いたプロジェクトをcargo runしてやれば無事http://localhost:3000でサーバーが起動します。

まとめ

今回はIronをつかって非常に単純なHTTPサーバーを起動する手順を確認しました。

RustでAPI Wrapperを簡単に書くためのライブラリを書いている

Swiftで書かれたAPKitというライブラリ:

github.com

は非常に綺麗に作られていて、便利ですし更新も活発なので非常に気に入っていて、時々コントリビュートしながら
自社プロダクトでも採用しているのですが、Rustで遊んでいる時にこれと同じようなライブラリがあったら嬉しいなぁとお思い
以下のようにプロトタイプを作っています。

github.com

以下の感じで、実装してsendRequestできるという感じの方針で進めたいと思っています。

APIKitは要所要所を抑えて適切にユーザー側で拡張できるようになっているので、非常に便利で
使いやすいので皆さんも是非iOSの開発等で利用してみてください。

extern crate api_kit;
extern crate hyper;
extern crate serde_json; 

use hyper::client::response::Response;
use hyper::client::RequestBuilder;
use api_kit::api_request::ApiRequest;
use api_kit::api_request::HttpMethod;
use hyper::header::{Headers, Accept, qitem};
use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value};
use std::io::{Read};

struct CircleCiMeRequest<'a> {
    api_token: &'a str
}

impl<'a> CircleCiMeRequest<'a> {
    fn new(token: &'a str) -> CircleCiMeRequest {
        return CircleCiMeRequest { api_token: token };
    }
}

impl<'a> ApiRequest<serde_json::Value> for CircleCiMeRequest<'a> {
    fn base_url(&self) -> &str {
        return "https://circleci.com/api/v1.1";
    }
    
    fn method(&self) -> HttpMethod {
        return HttpMethod::GET;
    }
    
    fn path(&self) -> &str {
        return "/me";
    }
    
    fn queryParameters(&self) -> Vec<(&str, &str)> {
        return vec![
            ("circle-token", self.api_token)
        ];
    }
    
    fn interceptRequest<'b>(&self, requestBuilder: RequestBuilder<'b>) -> RequestBuilder<'b> {
        let mut headers = Headers::new();
        headers.set(
                    Accept(vec![
                        qitem(Mime(TopLevel::Application, SubLevel::Json,
                                   vec![(Attr::Charset, Value::Utf8)])),
                    ])
                );
        return requestBuilder.headers(headers);
    }
    
    fn responseFromObject(&self, response: &mut Response) -> Option<serde_json::Value> {
        let mut buffer = String::new();
        response.read_to_string(&mut buffer).unwrap();
        return Some(serde_json::from_str(&buffer).unwrap());
    }
}

fn main() {
    let me = api_kit::sendRequest(&CircleCiMeRequest::new("<your circle ci api token>"));
    println!("{}", me.unwrap());
}

さて、僕が働いているAzit.incでは一緒に働けるエンジニアを募集しています!
採用情報 — 株式会社アジット|Azit Inc.

Acmeをベースにしたテキストエディタを書いている

Plan9Acmeに惚れ込んでしまって

f:id:Pocket7878_dev:20160705194800j:plain
こんなマウス
www.edikun.co.jp
を購入してしまったのですが、やはりいくらかつらみが有るというか、
ネイティブでは提供されているような機能を提供するのが難しいと感じたりする場面が有り
Acmeをベースにしたテキストエディタを自作することにしました。

github.com

コンセプト

  • Acmeをベースに、編集に本質的な機能のみを提供する
  • なるべくネイティブで提供されている機能を利用する
  • 本質的にアプリケーション内でしかできない機能以外は外部コマンドを利用してもらうようにする

最後のコンセプトはAcmeからそのまま引き継いでいる部分です、本当にこのアプリケーション内でしかできないことなのか?外部のコマンドで提供できる部分なのではないか?という事を機能を追加するにあたって考えています。

アイコン

f:id:Pocket7878_dev:20160705195524p:plain

アイコンは、plan9のウサギをベースに自作しました。

UI

f:id:Pocket7878_dev:20160705195832p:plain

カラーリングはAcme, Samから引き継いできており、左がコマンドのリストを保持しておく部分
黄色い部分がメインのテキストエディタ、青い部分がコマンドのスクラッチパッドです。

f:id:Pocket7878_dev:20160705200020p:plain

コマンドのリストは、右クリックで実行することができます。

設定

f:id:Pocket7878_dev:20160705200128p:plain

本当にエディタ側でやらないといけない機能は何でしょう?
とりあえず

  • フォント設定
  • Tabのスペースへの展開

などはエディタ側で提供しています。

今後の予定

今後は

  • シンタクスハイライト
  • Tabを提供できたらする(Sierraが出るので、Tabはいらないかも)

さて、僕が働いているAzit.incでは一緒に働けるエンジニアを募集しています!
採用情報 — 株式会社アジット|Azit Inc.

YAP(achimon)C::Asia Hachioji 2016 mid in Shinagawa でスタッフをしてきました!

uzulla.hateblo.jp
これを見た当初は「4/1だし...嘘だろ...」と思っていたのですが
buildersconでお会いした時に冗談じゃなく本当に開催するつもりだとお聞きし、
「あれはマジなんだ!」という熱い思いに打たれ微力ながらボランティアスタッフとして参加してきました!

会場は日本マイクロソフト様が快く貸し出してくださいました!

f:id:Pocket7878_dev:20160703082102j:plain:w400

イベントに参加することはあってもスタッフになったこともなかった自分ですが思い切って
部屋リーダーに立候補しB部屋の副リーダーをやらせていただきました。

会場は

緊張してしまってあまり声が出ていなかったので、部屋リーダーの仕事が上手くできていなかったような気がします。
何事も無くうまく回せたのは、同じ部屋のリーダーをしてくださった@motchangさん, @mahoyaya さん, @__papix__ さん, @silfsquare さん, @stefafafan さん, @__papix__ さん皆さんのおかげです
本当に有難うございました。

B部屋で開催されていた
mackee (mackee) · GitHub
さんの
github.com
では3Dプリンタを持参いただいており、開演前に3Dプリンタの周辺に大勢の人が集まっていました。
3Dプリンタをさらに改良するために部品を3Dプリントしたり、オープンソースハードウェアとして機械の機構自体を改良したりと
非常に楽しそうなお話であり、思わず3Dプリンタのキットを購入しそうになりました!
やっていくとまだまだ改良したい点がどんどん出てくる分野というのはとても楽しそうで是非いつか挑戦してみたいです。

また、
soudai (soudai sone) · GitHub
さんの

github.com

ではRDBに潜む色々な闇について紹介してくださり、自分のやっているような小中規模なアプリケーションではなかなか考える場面のない
SQLのチューニングなどの話についてなまのDBエンジニアの方の声が聞けたのは非常に貴重な機会でした。
RDBの闇に皆さん興味があったようで、B部屋がほぼ満員になるとても良いトークでした!!


来年も開催されるとか!?

来年こそは是非トークも応募したいと思っております!!


また、僕が共同創業者をしている
株式会社アジット|Azit Inc.
では一緒に働けるエンジニアを募集しています!

Plan9PortのMacOSX環境での日本語入力のサポートを試みている話

Plan9

Plan9PortのAcmeに前回の記事でも触れましたがぞっこんなわけですが、
どうしても日本語入力ができなくて嫌だったので、iTerm2やdevdrawのIME対応を試みていたプロジェクトなどを
参考に日本語入力に暫定的にですが対応したという話です。

GitHub - pocket7878/plan9port at ime-support

そもそものPlan9Portの構造の話

Plan9portはもともと、各プログラムを地道に変更するという方法ではなく、VMのように各ホストOSとの対話の
部分をdevdrawというプログラムによって抽象化することによって動作しています。

devdrawはウィンドウ内の描画の処理や、ホストOSのマウス、キーボードなどの動きを各アプリケーション側に
抽象的なインタフェースで提供し、アプリケーション側はdevdrawから来た司令をそのまま処理して描画しておけば問題ないように作られています。

そのため、OSXのキーボードのイベントやIMEなどに対応させるには、このdevdrawを改造する必要があります。

IME対応するためにやったこと

devdrawとIMEの関係

もともとdevdrawでは、キーボードについてはOSXと最低限のキーボードのイベントの通信だけをしており、
NSResponderのkeyDown:メッセージで受け取った処理はCmdキーの時にキーコードをチェックしたりplan9がつかっているコードに
少し変換などをしてそのままIMEを介さずにコードをアプリケーション側に送っていました。

この状態ではそもそもIMEを経由していませんから、変換されないのは当然です。そこで、まずはIMEを通して入力を
試みることにしました。

中国語のIME対応に試みているコードが見つかった

そのためには、NSTextInputClientというプロトコルを実装しつつ、キーボードからのイベントをIMEに一度送ってやる必要があります。
その辺りのベースのコードは中国語のIMEに対応を試みてくださっていた

acme中文设置

を参考にさせていただきました。
(なにぶんIMEについて全く無知な状態から始めていたので、NSTextInputClientというプロトコルの存在を学ばせていただくところから
始まりました。この参考となるコードがなければ何もできていなかったと思うので非常に感謝しています)

この実装ではIMEにキーボードのイベントを送信するためにNSResponderのinterpretKeyEventというメッセージを利用していました。

iTermへのAquaSKK対応のパッチが非常に参考になった

このソースを読んだ時に、たしかAquaSKKのiTerm対応でそれがあまりbetterではないという事が

mzp.hatenablog.com

で解説されていたということを思い出し、私の実装の方ではNSTextInputContextのhandleEventを利用することにしました。

また、記事で紹介されているiTermへのパッチでのhandleEvent及び、キャッシュしたイベントをdoCommandBySelectorメッセージで
処理する手法は非常に参考になりました。

devdrawとIMEの苦しい関係

なにぶん、devdrawというのは先ほど説明したように抽象的な層なので、通常のアプリケーションのように「自分が処理できるキーコード」
という概念をそもそも持っていません.どんなキーコードを扱えるのかは、実際にdevdrawを使うアプリケーション次第なのです。

なので、IME側では一切特別なキーボード処理をすることをやめ、変換の部分だけを担当してもらうことにしました。
そのためには:

  • insertText:replacementRange
  • doCommandBySelector:
  • setMarkedText:selectedRange:replacementRange:

の3っつのメソッドを適切に扱えば良いことがわかりました。setMarked~はIMEが変換を開始した時に呼び出してくるもので、
変換途中だとわかるように下線がついていたり、背景色が違っていたりするNSAttributedStringを渡してくれます。
つまり、これはIMEが変換を試みているということを意味しています。

insert~はIMEが確定したタイミングで呼ばれてくるものです、そしてdoCommandBySelectorは、それ以外のキーバインドのような
もので、アプリケーション側にmoveForward:とかinsertTabとかをやってみてほしいという依頼を出してくるものです。

つまり、doCommandBySelectorに来た時点で変換ではなく、なにか特殊なキーである可能性が高いと考えられます。
そのため、この場合はiTermへのパッチを参考にさせていただき、キャッシュしておいたもともとのNSEventを
既存のdevdrawのキーボード処理を参考にアプリケーションに送信しました。

謎の16文字制限

ここまででIMEからざっくりと文字入力はできるようになっていました、しかし快適になったと思って使っているとある事実に
気が付きました。それは変換を確定せずに16文字連続で打っていくと途中で変換途中の文字列が適切に更新されなく成るということでした。

この変換途中の文字列の置換には\bというbackspaceをひとつ前の変換文字列の長さだけ送り出し、更新後の文字を挿入するという方法をとっています
(なにぶん、アプリケーションごとに文字をどう扱っているかは違うので範囲置換とかの共通の方法はなさそうなのです)

これが16文字を超えたタイミングで急にうまく行かなくなって途中までしか消えなくなったりするようになっていました。

ここでしばらく苦しんだのですが、結論としてはdevdrawはもともとIMEを想定していなかったために、
devdrawにやってきたキーボードイベントを「リングバッファ」に保管して定期的にアプリケーションに送信していました。
このリングバッファがIMEが入ってきたことによって一気に大量の文字が流れ込んできてデータが取りこぼされるようになっていました。

この仕様自体はIMEを想定しないならば、時間的にも空間的にも効率が良いので良いのですが、私の方法でn文字置換するために大体2*n文文字を送っているのでは取りこぼされてしまいます。

そこで、リングバッファを諦めて、リンクドリストを利用してキーボードイベントをキューするように実装をしなおしました。

こうすることで実質どれだけ長くキーボードイベントが一気にきてもあふれること無く送信されるようになりました。

現在の苦悩

これで、現状IMEから入力をなんとなく行うことはデキるようになりましたが、現状幾つかの欠点があります。

未確定状態で途中でキャンセルの基準がない

繰り返しになりますがdevdrawはアプリケーションそのものではないために、変換途中でアプリケーションから変換を中断したいという
ことの明確な基準がありません。そのため、現状では変換途中にマウスクリックが発生したらそのまま確定させてしまうようにしました。
しかし、これでは問題があり、acmeの様なFocus follow mouseなアプリケーションでは、クリックを必要とせずにフォーカスのあっている
テキスト領域が変わります。しかし、devdrawからはそのことを知ることはできません。
そのため、未確定状態からどこかにカーソルを動かすには、一度確定するか、マウスをクリックした上で再開する必要が生まれています。

Samでは相変わらずバッファ溢れが起きているような感じがする


Acmeはバックエンドというような概念なしのエディタですが、SamはSamTermというフロントエンドとSamのバックエンドが互いに
通信をしあって描画が行われています。そのため、devdrawの溢れと同じ問題が発生している予感があり、Acmeでは正常に
変換が実施されるタイミングでSamではクチャっとなってしまう場面が見受けられます。

追記
入力中に途中で多く戻りすぎたりというふうに見えていたのでバファ溢れかと思っていたのですが、
どうもsamterm上で、無私すべき入力とそうでない入力の区別を雑然とつけていたようで、Kcmdというコマンドキーの
値よりも大きいキーはすべてKcmdとの組み合わせの可能性があるとして無視するようになっており、そのため
変換の頭に発生するnのようなマルチバイト文字が無視されているためIME的には1文字入力しているのに、されておらず
次の に になるタイミングで1文字削除してとやろうとすると多く削除される形になってしまうというのがその原因でした。

変換の部分変換に未対応かも

IMEでは入力の全体ではなく一部にフォーカスを当てて変換を行うことは多々ああります。
(そのためにselectedRangeとmarkedRangeは明確にNSTextInputClientでは区別されています)が、まだそこには明確には対応していません
なので、そのような変換への上手い対処法も考察中です。



ざっと走り書きですがここ数日やっていることの途中報告です。現状ではまだ幾つか不満なところがありますが
日本語入力が全くできない状態からここまでこれただけでもかなり自分的にはテンションが上がっています。

Acmeにキーバインドを追加する

Plan9

Plan9Portを最近良く使っています。AcmeというPlan9のエディタがあり、マウスのクリックをいろいろな場面で利用して非常に
便利なのですが、一方でキーバインドは最小限に抑えられており、どうしても僕には少し使いづらい場面がありました。

そこで、Acmeソースコードを読んで、キーバインドを追加することに成功したので、その手順をざっと自分用のメモとして残しておきます。

text.cの中を見よう

ソースコードのファイルの中にtext.cというファイルがあります、
その中のtextypeという以下の関数を見てみましょう

void
texttype(Text *t, Rune r)
{
	uint q0, q1;
	int nnb, nb, n, i;
	int nr;
	Rune *rp;
	Text *u;

	if(t->what!=Body && t->what!=Tag && r=='\n')
		return;
	if(t->what == Tag)
		t->w->tagsafe = FALSE;

	nr = 1;
	rp = &r;
	switch(r){
        case 0x02: // ^B emacs like left
	case Kleft:
		typecommit(t);
		if(t->q0 > 0)
			textshow(t, t->q0-1, t->q0-1, TRUE);
		return;
        case 0x06: // ^F emacs like right
	case Kright:
		typecommit(t);
		if(t->q1 < t->file->b.nc)
			textshow(t, t->q1+1, t->q1+1, TRUE);
		return;

キーバインドを加えよう

実は、この中のcase文がキーバインドを決定している箇所です。

コメント付きで私がCtrl-BやCtrl-Fをつけわ得ている箇所がわかるでしょうか。このような形でcaseの中にキーバインドを定義してやると
自動的に反映されます。

キーのコードを調査しよう

キーのコードを調査するときには私は xev というX11のプログラムを使っています。xevの使い方は割愛しますが、xevで調査したコードをそのまま
利用すればキーバインドが設定されます。

ざっと、自分が忘れないようにのメモなので走り書きでスミマセン。いつか機会があったら清書します(善処します;;)

僕が働いているAzit.incでは一緒に働けるエンジニアを募集しています!
採用情報 — 株式会社アジット|Azit Inc.