AT Protocol(Bluesky)のためのRustライブラリを作った

前記事OCamlやってくぞ、と書いたけど結局Rustです。

Bluesky

Twitter代替の候補として噂される(?)分散型SNS Bluesky。 現状ではまだprivate betaということで招待コードが無いと使えないのだけど、先月運良くコードをいただくことができて使い始めてみている。

一通りのSNS的な動きはしているものの、まだまだ鋭意開発中ということで足りていない機能があったり 日々新機能が追加されたりと目紛しく動いている世界のようだ。

AT Protocolとエコシステムの現状 (〜2023/04)

AT Protocolについてはこちら。

atproto.com

日本語では以下の記事が詳しいでしょう。

ざっくりとした理解では、新しい分散型SNSのために「AT Protocol」が開発されていて、いま使っているBlueskyというSNSもこのAT Protocolに従って実装されているSmall-worldの一つである、という感じなのかな。

「AT Protocol」は様々なテクノロジーの総称という感じで、その中の汎用通信レイヤーとして「XRPC」というものがある。これを使ってサーバーと通信する、ということらしい。 BlueskyはWeb UIも提供されているが、DevToolsを覗いてみる限りでは確かにすべてのデータ取得や更新などこのXPRCのリクエストによって行われているようだ。

GitHubでは以下のリポジトリが公開されている。

主要なプロトコル実装としてTypeScriptで書かれた atproto があり、そのGo版ということで indigobluesky-social organizationによって管理されている。

その他の非公式の言語実装やClient, Applicationなどが以下のrepositoryにまとめられている。

言語としてTypeScriptがメインであり それを使ってすべての現存機能が動かせるので、Webフロントエンドで完結する、ということでWeb Clientが多く作られているようだ。

Rust版の実装

上記で紹介されているライブラリでRustのものは、(2023/04時点で)以下の adenosine のみ。

このrepository ownerはBlueskyの中の人ではあるが、公式としてGitHubのorganizationに入れられはしなかった、ようだ。

Lexiconとコード生成

前述したXRPCによるメッセージングは、すべて Lexicon というスキーマシステムによって型が定義されている。 JSON Schemaのように記述されるJSONによって、すべてのリクエストやレスポンス、その中のデータ型やフィールド名などが定義される。

ので、これがAPI通信の仕様のすべて、ということになる。

前述した atprotoindigoAPIライブラリは、このLexicon schemaを示すJSONファイルからコード生成によって作成されているようだ。

この仕組みで作っていない限り、どうしてもclient libraryは最新のAPIに追従できない、ということになる。AT Protocolはまだまだすごい頻度で変更が加えられていて、lexicon schemaも毎日、とはいかないまでも毎週くらいのペースでは何らかの変更がある。

上述の adenosine は、このコード生成によって作られたものではないため、実際既に最近追加されたAPIは使えないようだった。

その他にも reqwest の blocking client に依存している、とか 結局内部のxprc clientには serde_json::Value を渡していて何でも自由にできてしまう、など 微妙だなと思う点もあったので、自分でまったく別のRustライブラリを作ってみよう、ということになった。

(一応他にもそれっぽいlibraryやrepositoryを検索して先行事例を調査してみたりもしたが、ちゃんとそういったコード生成によって作られているものはなさそうだった。)

自作Rust版実装: ATrium

github.com

AT Protocolのための小さなライブラリが集まり青空を望む中庭、ということでATrium、という名前に。最初は atprs とか雑な名前で書き始めていたけど ChatGPT に「いいかんじのプロジェクト名ないですかね」と相談したところ、ATriumという名前を提案してくれたので採用した。最近 YAPC::Kyoto の会場でも聞いた単語だったし丁度いいな、と。

Lexicon schema

まずは必要になるのはLexicon schemaのJSONを読み込むためのライブラリ。 これ自身のJSON Schemaなどが存在するわけではなく、実際のvalidationは zodを使って書かれている ので、これを必死に翻訳する形で。

このJSON自体もかなり柔軟性があり、色んな入力が有り得るので大変…。いちおう type の tag を使って判別可能なようにはなっているので、それを使って enum に振り分けることでどうにか serde で正しく読み込めるようになった。 Option なfieldが多く、一度 deserialize してから元のJSONに同等に戻せるようにすると記述が大変だったので、serde_withskip_serializing_none を使った。

unionで新しい定義を作られるところが、単純に enum に変換すると一段深くなってしまうしtagがずれても困るし…ということで仕方なくいちいちコピーしてしまっているが、もっといい方法あるだろうか。…macroを使えば良いのか?

コード生成

ともかくLexicon schemaを正しく読み込むことができれば、あとはそこからRustコードに落とし込んでいける。 細かいところはまだ実装できていないが、基本的な object などはだいたい書けている、と思う。

Lexiconではdefsの名前は camelCase だが Rustコードでは 型名は PascalCase で field名やmodule名は snake_case にしたい、などの変換が必要だったので、 heck というライブラリを使用した。

正しくnamespaceをマッピングさせていれば ref はそのまま定義済みの型として参照できるが、 union はまぁ厄介で、それ用の enum を追加で定義して Lexicon解析のとき同様に $type tagで判別して振り分けるようにする必要があった。

とりあえず正しいコードとして生成されているかどうかは cargo build で確認できて 通ればまぁ動くだろう、という判断ができて便利だった。

API設計

型の定義だけならまだラクだが、Lexiconには query もしくは procedure のXRPCリクエストの定義が含まれる。 これは関数の実装を提供することになるが、そのためにはHTTP requestを実行するためのclientが必要になる、がAPIライブラリとしてはそういったものは含めたくない、と考えた。

実はRustでHTTP clientをマトモに使ったことが無かったが、 hyper だったり isahc だったり surf だったり、いろいろあるらしい。ライブラリでClientも含めることになると、どれを使うか判断する(もしくはすべてサポートするなどの)必要がでてきてしまう。

ので、それらは回避して HttpClient というTraitを定義して、それを実装するものを外部から注入する形にした。 各XRPCリクエストは「HttpClient をSupertraitとする、デフォルト実装を持つTrait」となり、外部で実装したClientがそれらの実装を宣言すれば各リクエストが使えるようになる、ということになる。

#[async_trait::async_trait]
pub trait HttpClient {
    async fn send(&self, req: http::Request<Vec<u8>>) -> Result<http::Response<Vec<u8>>, Box<dyn Error>>;
}

最初こういうのもライブラリとして存在しているのでは、と探して http-client というのがあったので使おうとしたが、同梱されている HyperClient を使ってみたら依存のtokioのバージョンが古すぎて動かず、またこのTraitで使われている http-types も良いけど http を使ったほうがより汎用的で良いですよ、とChatGPTが教えてくれたので自前で簡単に定義することにした。

ともかく、この形にすることで、ライブラリのユーザーは一手間はかかるけど自分の好きなHTTP clientを使って各種XRPCリクエストを送ることができるようになっている。はず。

とりあえずこうして作った APIライブラリをまず atrium-api という名前でpublishした。

docs.rs https://crates.io/crates/atrium-api

Lex

ちなみに Lexicon schema解析のための型定義だけのもので一つのライブラリとして切り出して作っていて、まだ publishはしていないけどやる予定。 上記の atrium-api の設計が気に入らない、という人はこれを使って自分でコード生成を作って別のAPIライブラリを作ってくれれば良いと思います。

CLI

作成したAPIの動作実験も兼ねて、 mattn/bsky のようなCLIも作ってみている。 これはこれで便利、なのかな? バイナリ配布だけしてみても良いかもしれない。

今後

まだまだ未完成で未実装のところも多いので埋めていく予定。 あとは本家のLexicon更新を検知して自動でコード生成してpushして publishまで…が自動化できても良さそうだけど そこまでは無理か? ちょっと調べてやってみたいところ。

あとは…折角Rustで動かせるようになるならTauriとか使ってGUIアプリまで作れると良いのかな… さすがにそんな余裕は無さそうだけども。

ともかくちゃんと使ってもらえるかもしれない(現時点で22 starsとかだけど)OSSっぽいものを初めて作った気がするので、まずは色々整備していきたいところ。