この記事は「はてなエンジニア Advent Calendar 2022」の13日目の記事です。
初めまして、id:SlashNephy です。入社エントリーを書いていないのですが、今年ひっそりと はてな に新卒入社して、Web アプリケーションエンジニアをやらせていただいております。今日は Rust で書いた型定義を様々な言語に変換できる Typeshare のご紹介です。
Typeshare は Rust で書いた型定義から TypeScript / Kotlin / Swift / Go *1 といった言語の型定義を生成できる CLI ツールです。主に FFI (Foreign Function Interface) で便利に型定義を使い回すことを目的に開発されているそうです。
インストール
cargo コマンドで CLI をインストールできます。
$ cargo install typeshare-cli
Typeshare を使ってみる
まずは cargo init --lib
でパッケージを作成しておきます。
Typeshare は #[typeshare]
アトリビュートを付与した、構造体・列挙型の型定義を変換対象として扱います。src/lib.rs
に以下のような構造体・列挙型を作ります。
use typeshare::typeshare; #[typeshare] pub struct User { id: u32, name: String, discriminator: String, } #[typeshare] #[serde(tag = "type", content = "value")] pub enum UserResponse { Ok(User), Err(String), }
Rust の型定義を各言語に変換するには、上で作成したパッケージのルートで次のコマンドを実行します。
$ typeshare-cli . --lang=typescript --output-file=typeshare.d.ts $ typeshare-cli . --lang=kotlin --output-file=typeshare.kt $ typeshare-cli . --lang=swift --output-file=typeshare.swift $ typeshare-cli . --lang=go --output-file=typeshare.go --go-package main
TypeScript の出力ファイル (typeshare.d.ts) は、以下のようになっています。
export interface User { id: number; name: string; discriminator: string; } export type UserResponse = | { type: "Ok", value: User } | { type: "Err", value: string };
Rust の列挙型が、TypeScript ではユニオン型として表現されているのが分かります。Swift や Go の出力ファイルには、Codable
の実装や MarshalJSON
のグルーコードも記述されていました。
気になったところ
実際に使ってみて、気になったところについて記しておきます。
- 現時点では関数のバインディングは生成できないようです。今後に期待です。
- README を見ると対応しそうな雰囲気もあります。
- Typeshare は Rust のビルトイン型を各言語の型に変換してくれますが、サポートされていない型があるようです。試してみたところ、
i64
やu64
はコード生成時にエラーになってしまいました。
thread 'main' panicked at 'failed to parse a rust type: ["i64"]'
Typeshare は相互運用性を重視しているので、それぞれの言語で扱える数値型に変換されます。
例えば JavaScript (ES2020) では 9_007_199_254_740_991
より大きな整数は BigInt
で扱うようになっているので、 U53
という特別な整数型を使うことで明示的に number
を使うようにできます。
- 複数の associated type を持つ、列挙型は現時点ではサポートされていないようです。例えば以下のような定義はコード生成時にエラーになります。
#[typeshare] enum Tuple<T> { Pair(T, T), Triple(T, T, T), }
thread 'main' panicked at 'multiple unnamed associated types are not currently supported'
いかがでしたか?
1Password の開発元の AgileBits 社では、積極的に Rust への置き換えが進んでいて 1Password 8 では Rust がメインに使われているそうなので AgileBits 社ならではの OSS だと感じました。
本当は関数のバインディングを生成して、Rust との FFI をやってみるぞ!と思っていたのですが、今回は型定義が共有できるようになったよ、というところまでを書きました。
Rust の型定義が流用できると TypeScript のフロントや、Swift / Kotlin で書かれたモバイルアプリケーションから Rust で書かれた WebAssembly やネイティブコードを呼び出したりする場面で便利になるのかなと想像しました。
関数のバインディングも生成できるようになれば、活用先が広がりそうですね。(特に Kotlin では JNI の手続きが大変なので楽をしたいですね。。。)
今回の検証に使用したコードは GitHub に置いておきます。
はてなエンジニア Advent Calendar 2022 昨日の12日目は id:ma2saka さんでした。明日の14日目は id:masayosu さんです。
*1:デフォルトの features に Go は含まれていないので、Go のサポートを追加する場合はインストール時のオプションに --all-features が必要です。