paint-brush
Cómo compartir tipos de Rust con TypeScript para WebAssembly en 30 segundos: una guía rápidapor@dawchihliou
930 lecturas
930 lecturas

Cómo compartir tipos de Rust con TypeScript para WebAssembly en 30 segundos: una guía rápida

por Daw-Chih Liou6m2023/05/29
Read on Terminal Reader

Demasiado Largo; Para Leer

💡 Aprenderemos por qué la cadena de herramientas oficial de Rust y WebAssembly no es suficiente para TypeScript. 🤹 Te mostraré cómo generar automáticamente una definición de TypeScript con un cambio mínimo en tu código Rust. 🧪 Refactorizaremos una biblioteca WebAssembly del mundo real en npm juntos.
featured image - Cómo compartir tipos de Rust con TypeScript para WebAssembly en 30 segundos: una guía rápida
Daw-Chih Liou HackerNoon profile picture

Descubra la experiencia de desarrollador más fluida con Rust y WebAssembly. Esta es la forma más rápida de generar automáticamente definiciones de TypeScript a partir de su código Rust.

En este articulo

  • 💡 Aprenderemos por qué la cadena de herramientas oficial de Rust y WebAssembly no es suficiente para TypeScript.


  • 🤹 Te mostraré cómo generar automáticamente la definición de TypeScript con un cambio mínimo en tu código Rust.


  • 🧪 Refactorizaremos juntos una biblioteca WebAssembly del mundo real en npm.


Vamos.


El problema de escribir con wasm-bindgen

La generación de tipos TypeScript para módulos WebAssembly (Wasm) en Rust no es sencilla.


Me encontré con el problema cuando estaba trabajando en un motor de búsqueda de similitud de vectores en Wasm llamado Voy . Desarrollé el motor Wasm en Rust para proporcionar a los ingenieros de JavaScript y TypeScript una navaja suiza para la búsqueda semántica. Aquí hay una demostración para la web:


Demostración de voy


¡Puedes encontrar el repositorio de Voy en GitHub ! Siéntete libre de probarlo.


El repositorio incluye ejemplos que puede ver cómo usar Voy en diferentes marcos.


Usé wasm-pack y wasm-bindgen para construir y compilar el código de Rust en Wasm. Las definiciones de TypeScript generadas se ven así:


 /* 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


Como puede ver, hay muchos tipos de "cualquiera", lo que no es muy útil para la experiencia del desarrollador. Echemos un vistazo al código de Rust para averiguar qué sucedió.


 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 }


La cadena, el segmento y el entero sin signo generaron los tipos correctos en TypeScript, pero " wasm_bindgen::JsValue " no. JsValue es la representación de wasm-bindgen de un objeto de JavaScript.


Serializamos y deserializamos el JsValue para pasarlo de un lado a otro entre JavaScript y Rust a través de 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() }


Es el enfoque oficial para convertir tipos de datos, pero evidentemente, debemos hacer un esfuerzo adicional para admitir TypeScript.

Generación automática de enlace de TypeScript con Tsify

La conversión de tipos de datos de un idioma a otro es en realidad un patrón común llamado interfaz de función externa (FFI). Exploré herramientas de FFI como Typeshare para generar automáticamente definiciones de TypeScript a partir de estructuras de Rust, pero fue solo la mitad de la solución.


Lo que necesitamos es una forma de acceder a la compilación Wasm y generar la definición de tipo para la API del módulo Wasm. Como esto:


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


Afortunadamente, Tsify es una increíble biblioteca de código abierto para el caso de uso. Todo lo que tenemos que hacer es derivar del rasgo "Tsify" y agregar una macro #[tsify] a las estructuras:


 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 }


¡Eso es todo! Echemos un vistazo a los atributos "from_wasm_abi" y "into_wasm_abi".


Wasm ITB


Ambos atributos convierten el tipo de datos de Rust a la definición de TypeScript. Lo que hacen diferente es la dirección del flujo de datos con la interfaz binaria de aplicación (ABI) de Wasm.


  • into_wasm_abi : los datos fluyen de Rust a JavaScript. Se utiliza para el tipo de devolución.


  • from_wasm_abi : los datos fluyen desde JavaScript a Rust. Se utiliza para parámetros.


Ambos atributos usan serde-wasm-bindgen para implementar la conversión de datos entre Rust y JavaScript.


Estamos listos para construir el módulo Wasm. Una vez que ejecuta "wasm-pack build", la definición de TypeScript generada automáticamente:


 /* 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[] }


Todos los tipos "cualquiera" se reemplazan con las interfaces que definimos en el código de Rust✨

Pensamientos finales

Los tipos generados se ven bien, pero hay algunas inconsistencias. Si observa detenidamente, notará que el parámetro de consulta en la función de búsqueda se define como Float32Array.


El parámetro de consulta se define como del mismo tipo que "incrustaciones" en EmbeddedResource, por lo que espero que tengan el mismo tipo en TypeScript.


Si sabe por qué se convierten a diferentes tipos, no dude en comunicarse o abrir una solicitud de extracción en Voy en GitHub .


Voy es un motor de búsqueda semántica de código abierto en WebAssembly. Lo creé para potenciar más proyectos para crear características semánticas y crear mejores experiencias de usuario para personas de todo el mundo. Voy sigue varios principios de diseño:


  • 🤏 Minúsculo : reduzca la sobrecarga para dispositivos limitados, como navegadores móviles con redes lentas o IoT.


  • 🚀 Rápido : cree la mejor experiencia de búsqueda para los usuarios.


  • 🌳 Tree Shakable : optimice el tamaño del paquete y habilite las capacidades asincrónicas para la API web moderna, como Web Workers.


  • 🔋 Reanudable : genere un índice de incrustaciones portátiles en cualquier lugar y en cualquier momento.


  • ☁️ En todo el mundo : ejecute una búsqueda semántica en servidores perimetrales de CDN.


Está disponible en npm. Simplemente puede instalarlo con su administrador de paquetes favorito y estará listo para comenzar.


 # with npm npm i voy-search # with Yarn yarn add voy-search # with pnpm pnpm add voy-search


¡Pruébalo, y me alegra saber de ti!

Referencias


¿Quieres conectarte?

Este artículo se publicó originalmente en el sitio web de Daw-Chih .