🦀

他の言語からRustを呼び出すときに便利なtypeshareの紹介

2022/12/18に公開

この記事は Rust Advent Calendar 2022 18日目の記事です。

みなさんはRust書いてますでしょうか?私も最速を目指して日々実装しています。

この記事では、RustでWebやスマホアプリ向けのライブラリを実装したときに頭を悩ませる、型の二度書きを解消してくれるツールである、typeshareの紹介をしていきたいと思います。

ここで書かれたコードはここに上がっています。

Rustは他の言語から呼ばれることが多い

自分だけかもしれないですが、Rustを採用するプロジェクトではすべてをRustで実装するのではないことが多いです。Webアプリではメインはtypescriptで実装、スマホアプリだったらメインはそのプラットフォームやdart(Flutter)でバックエンドの場合はPythonやRubyなどで実装して、速度・移植性・安全性などが必要な場合にワンポイントリリーフ的に採用するケースが多いです。

Rustで定義した型を他の言語で再度定義する必要が出てしまう

Rustをその他の言語(以下ホスト言語と書きます)から呼び出す場合には、その返り値の型や引数の型に関しては以下の様に扱うのが一般的かと思います。

  • シンプルな型(整数や文字列)のみでやり取りする
  • JSONなどでシリアライゼーションしたバイト列でやり取りする

後者の場合には利便性を考慮するとホスト言語側にもそのRustの型に対応したホスト言語の型が存在すると、LSPなどのサポート的にも楽になるところが多いです。

ただし、その場合はRust側で定義した型をホスト側で定義する必要があります。(逆にホスト言語の型をRustで実装する場合もあります)

use serde::Serialize;

#[derive(Debug, Serialize)]
struct SimpleStruct {
    id: u32,
    name: String,
}

この型は以下のようなJSONにシリアライズされます。

{
  "id": 42,
  "name": "Hello"
}

ホスト言語がtypescriptの場合対応した以下のような型を宣言しなければいけません。

export interface SimpleStruct {
	id: number;
	name: string;
}

これはかなり手間ですし、Rustのenumは直和型になっているので以下のような型を書くことが出来ます。

#[derive(Debug, Serialize)]
#[serde(tag = "type", content = "content")]
pub enum Command {
    List,
    Detail(u32),
}

このような型はtypescriptだと以下のような型になるのですが、慣れていないホスト言語の場合はすぐに型のマッピングがわかるわけではありません。(僕もTypescriptは詳しくないので、以下の例はtypeshareに吐き出させたものを載っけています)

export type Command = 
	| { type: "List", content?: undefined }
	| { type: "Detail", content: number };

typeshareを利用する

ここで、typeshareを利用するとRustで定義した型からホスト言語の型を生成することが出来ます。

インストール

まずはtypeshare-cliをインストールします。

> cargo install typeshare-cli

以下のようになっていればインストールは成功しています。

> typeshare --version
typeshare 1.0.1

Rustの型定義にtypeshare用のタグを付ける

typeshareを使うためにはRustの型定義にこれはtypeshareで公開する型であるということを明示する必要があります。

以下の手順でそれを行います。

プロジェクトのCargo.tomlにtypeshareを追加する

[package]
name = "typeshare-example"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
typeshare = "1"

型定義にタグを追加する

use typeshare::typeshare;
use serde::Serialize;


#[derive(Debug, Serialize)]
pub struct SimpleStruct {
    pub id: u32,
    pub name: String,
}

#[derive(Debug, Serialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum Command {
    List,
    Detail(u32),
}

これで、typeshareを利用する準備は完了です。

typeshareを実行する

ホスト言語がtypescriptの場合は以下のようなコマンドをRustのプロジェクトルートで実行すると、typescriptのファイルを出力してくれます。

typeshare . --lang=typescript --output-file=my_typescript_definitions.ts
/*
 Generated by typeshare 1.0.0
*/

export interface SimpleStruct {
	id: number;
	name: string;
}

export type Command = 
	| { type: "List", content?: undefined }
	| { type: "Detail", content: number };

この出力されたファイルを利用する部分などで読み込んで利用する事ができます。

おまけ

その他の言語への対応

typeshareはその他の言語への対応も進んできています。

issueレベルだと

  • dart
  • python(pydantic)

への対応が議論されています。

利用例リポジトリ

https://github.com/higumachan/typeshare-example

Discussion