Откройте для себя самый простой опыт разработки с Rust и WebAssembly. Это самый быстрый способ автоматически сгенерировать определения TypeScript из вашего кода Rust.

В этой статье

  • 💡 Мы узнаем, почему официальной цепочки инструментов Rust и WebAssembly недостаточно для TypeScript.
  • 🤹 Я покажу вам, как автоматически сгенерировать определение TypeScript с минимальными изменениями в вашем коде Rust.
  • 🧪 Вместе проведем рефакторинг реальной библиотеки WebAssembly на npm.

Пойдем.

Проблема набора текста с wasm-bindgen

Генерация типов TypeScript для модулей WebAssembly (Wasm) в Rust — непростая задача.

Я столкнулся с проблемой, когда работал над поисковой системой векторного сходства в Wasm под названием Вой. Я создал движок Wasm на Rust, чтобы предоставить инженерам JavaScript и TypeScript швейцарский нож для семантического поиска. Вот демонстрация для Интернета:

Вы можете найти репозиторий 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

Как вы можете видеть, есть много разных типов, что не очень полезно для разработчика. Давайте заглянем в код 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, чтобы передавать его туда и обратно между JavaScript и Rust через Wasm.

#[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.

Автоматическое создание привязки TypeScript с помощью Tsify

Преобразование типов данных из одного языка в другой на самом деле является распространенным паттерном, который называется Интерфейс внешних функций (FFI). Я изучил инструменты FFI, такие как Typeshare, для автоматической генерации определений TypeScript из структур Rust, но это была только половина решения. Что нам нужно, так это способ подключиться к компиляции Wasm и сгенерировать определение типа для API модуля Wasm. Так:

#[wasm_bindgen]
pub fn index(resource: Resource) -> SerializedIndex { /* snip */ }

#[wasm_bindgen]
pub fn search(index: SerializedIndex, query: Embeddings, k: NumberOfResult) -> SearchResult {
    // snip
}

К счастью, Цифы — замечательная библиотека с открытым исходным кодом для случая использования. Все, что нам нужно сделать, это получить от трейта '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 (ABI).

  • into_wasm_abi: данные передаются из Rust в JavaScript. Используется для возвращаемого типа.
  • from_wasm_abi: данные передаются из JavaScript в Rust. Используется для параметров.

Оба атрибута используют serde-wasm-bindgen для реализации преобразования данных между Rust и JavaScript.

Мы готовы построить модуль Wasm. После запуска сборки wasm-pack автоматически сгенерированное определение 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, поэтому я ожидаю, что они будут иметь тот же тип в TypeScript. Если вы знаете, почему они преобразованы в разные типы, не стесняйтесь обращаться к нам или открывать запрос на вытягивание в Voy on GitHub.

Вой — семантическая поисковая система с открытым исходным кодом в WebAssembly. Я создал его, чтобы дать возможность большему количеству проектов создавать семантические функции и улучшать пользовательский опыт для людей по всему миру. Вой следует нескольким принципам дизайна:

  • 🤏 Крошечный. Сократите нагрузку на ограниченные устройства, такие как мобильные браузеры с медленными сетями или IoT.
  • 🚀 Быстро: создайте лучший поиск для пользователей.
  • 🌳 Tree Shakable: оптимизируйте размер пакета и включите асинхронные возможности для современных веб-API, таких как Web Workers.
  • 🔋 Resumable: создавайте переносимый индекс встраивания в любом месте и в любое время.
  • ☁️ По всему миру: выполните семантический поиск на пограничных серверах CDN.

Он доступен на npm. Вы можете просто установить его с помощью своего любимого менеджера пакетов, и вы готовы к работе.

# with npm
npm i voy-search

# with Yarn
yarn add voy-search

# with pnpm
pnpm add voy-search

Попробуйте, и я буду рад услышать от вас!

Рекомендации

Want to connect?

This article was originally published on Daw-Chih's Website.