Vamos.
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:
¡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.
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".
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.
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✨
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:
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!
¿Quieres conectarte?
Este artículo se publicó originalmente en el sitio web de Daw-Chih .