Откройте для себя самый простой опыт разработки с 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
Попробуйте, и я буду рад услышать от вас!
Рекомендации
- Внешний функциональный интерфейс - Википедия
- serde-wasm-bindgen - GitHub
- Спекта - GitHub
- Структура wasm_bindgen::JsValue - wasm-bindgen
- Книга Rust и WebAssembly - Rust и WebAssembly
- ц-рс - GitHub
- Цифы - GitHub
- Тайпшер - GitHub
- Вой - GitHub
- wasm-bindgen — Rust и WebAssembly
- wasm-pack - Rust и WebAssembly
- Семантический поиск WASM в Rust - Доу-Чи Лиоу
- WebAssembly - WebAssembly.org
Want to connect? This article was originally published on Daw-Chih's Website.