さあ行こう。
Rust で WebAssembly(Wasm) モジュールの TypeScript タイプを生成するのは簡単ではありません。
Wasm でVoyというベクトル類似性検索エンジンを開発していたときに、この問題に遭遇しました。私は、JavaScript および TypeScript エンジニアにセマンティック検索のためのスイス ナイフを提供するために、Rust で Wasm エンジンを構築しました。 Web 用のデモは次のとおりです。
Voy のリポジトリは GitHub にあります。ぜひお試しください。
リポジトリには、さまざまなフレームワークで Voy を使用する方法を確認できるサンプルが含まれています。
wasm-packとwasm-bindgenを使用して、Rust コードをビルドして Wasm にコンパイルしました。生成された TypeScript 定義は次のようになります。
/* tslint:disable */ /* eslint-disable */ /** * @param {any} input * @returns {string} */ export function index(resource: any): string /** * @param {string} index * @param {any} query * @param {number} k * @returns {any} */ export function search(index: string, query: any, k: number): any
ご覧のとおり、「any」型が多数ありますが、これは開発者のエクスペリエンスにはあまり役に立ちません。何が起こったのかを知るために Rust コードを調べてみましょう。
type NumberOfResult = usize; type Embeddings = Vec<f32>; type SerializedIndex = String; #[derive(Serialize, Deserialize, Debug)] pub struct EmbeddedResource { id: String, title: String, url: String, embeddings: Embeddings, } #[derive(Serialize, Deserialize, Debug)] pub struct Resource { pub embeddings: Vec<EmbeddedResource>, } #[wasm_bindgen] pub fn index(resource: JsValue) -> SerializedIndex { /* snip */ } #[wasm_bindgen] pub fn search(index: &str, query: JsValue, k: NumberOfResult) -> JsValue { // snip }
文字列、スライス、および符号なし整数は TypeScript で正しい型を生成しましたが、「 wasm_bindgen::JsValue 」は生成しませんでした。 JsValue は、wasm-bindgen による JavaScript オブジェクトの表現です。
JsValueをシリアル化および逆シリアル化して、Wasm を介して JavaScript と Rust の間でやり取りします。
#[wasm_bindgen] pub fn index(resource: JsValue) -> String { // 💡 Deserialize JsValue in to Resource struct in Rust let resource: Resource = serde_wasm_bindgen:from_value(input).unwrap(); // snip } #[wasm_bindgen] pub fn search(index: &str, query: JsValue, k: usize) -> JsValue { // snip // 💡 Serialize search result into JsValue and pass it to WebAssembly let result = engine::search(&index, &query, k).unwrap(); serde_wasm_bindgen:to_value(&result).unwrap() }
これはデータ型を変換するための公式のアプローチですが、明らかに、TypeScript をサポートするにはさらに努力する必要があります。
ある言語から別の言語へのデータ型の変換は、実際には外部関数インターフェイス(FFI) と呼ばれる一般的なパターンです。 Rust 構造体から TypeScript 定義を自動生成するTypeshareなどの FFI ツールを検討しましたが、それは解決策の半分にすぎませんでした。
必要なのは、Wasm コンパイルを利用して、Wasm モジュールの API の型定義を生成する方法です。このような:
#[wasm_bindgen] pub fn index(resource: Resource) -> SerializedIndex { /* snip */ } #[wasm_bindgen] pub fn search(index: SerializedIndex, query: Embeddings, k: NumberOfResult) -> SearchResult { // snip }
幸いなことに、 Tsify はこのユースケースにとって素晴らしいオープンソース ライブラリです。私たちがする必要があるのは、「Tsify」トレイトから派生し、 #[tsify] マクロを構造体に追加することだけです。
type NumberOfResult = usize; type Embeddings = Vec<f32>; type SerializedIndex = String; #[derive(Serialize, Deserialize, Debug, Clone, Tsify)] #[tsify(from_wasm_abi)] pub struct EmbeddedResource { pub id: String, pub title: String, pub url: String, pub embeddings: Embeddings, } #[derive(Serialize, Deserialize, Debug, Tsify)] #[tsify(from_wasm_abi)] pub struct Resource { pub embeddings: Vec<EmbeddedResource>, } #[derive(Serialize, Deserialize, Debug, Clone, Tsify)] #[tsify(into_wasm_abi)] pub struct Neighbor { pub id: String, pub title: String, pub url: String, } #[derive(Serialize, Deserialize, Debug, Clone, Tsify)] #[tsify(into_wasm_abi)] pub struct SearchResult { neighbors: Vec<Neighbor>, } #[wasm_bindgen] pub fn index(resource: Resource) -> SerializedIndex { /* snip */ } #[wasm_bindgen] pub fn search(index: SerializedIndex, query: Embeddings, k: NumberOfResult) -> SearchResult { // snip }
それでおしまい!属性「from_wasm_abi」と「into_wasm_abi」を見てみましょう。
どちらの属性も Rust データ型を TypeScript 定義に変換します。両者の違いは、Wasm の Application Binary Interface (ABI) を使用したデータ フローの方向です。
どちらの属性も serde-wasm-bindgen を使用して、Rust と JavaScript 間のデータ変換を実装します。
Wasm モジュールを構築する準備ができました。 「wasm-pack build」を実行すると、自動生成された TypeScript 定義が次のようになります。
/* tslint:disable */ /* eslint-disable */ /** * @param {Resource} resource * @returns {string} */ export function index(resource: Resource): string /** * @param {string} index * @param {Float32Array} query * @param {number} k * @returns {SearchResult} */ export function search( index: string, query: Float32Array, k: number ): SearchResult export interface EmbeddedResource { id: string title: string url: string embeddings: number[] } export interface Resource { embeddings: EmbeddedResource[] } export interface Neighbor { id: string title: string url: string } export interface SearchResult { neighbors: Neighbor[] }
すべての「any」型は、Rust コードで定義したインターフェイスに置き換えられます✨
生成された型は適切に見えますが、いくつかの不一致があります。よく見ると、検索関数のクエリ パラメーターが Float32Array として定義されていることがわかります。
クエリパラメータはEmbeddedResourceの「embeddings」と同じ型として定義されているため、TypeScriptでも同じ型であることが期待されます。
異なるタイプに変換される理由がわかっている場合は、遠慮なく連絡するか、 GitHub の Voyでプル リクエストを開いてください。
Voyは、WebAssembly のオープンソースのセマンティック検索エンジンです。より多くのプロジェクトがセマンティック機能を構築し、世界中の人々により良いユーザー エクスペリエンスを提供できるようにするために作成しました。 Voy はいくつかの設計原則に従っています。
npmで入手可能です。お気に入りのパッケージ マネージャーを使用してインストールするだけで準備完了です。
# with npm npm i voy-search # with Yarn yarn add voy-search # with pnpm pnpm add voy-search
ぜひお試しください。ご意見をお待ちしております。
接続したいですか?
この記事はもともとDaw-Chih の Web サイトに掲載されたものです。