Rust で書いた型定義を様々な言語に変換できる Typeshare を試してみた

この記事は「はてなエンジニア 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 のビルトイン型を各言語の型に変換してくれますが、サポートされていない型があるようです。試してみたところ、i64u64 はコード生成時にエラーになってしまいました。
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 が必要です。