paint-brush
如何在 30 秒内使用 WebAssembly 的 TypeScript 共享 Rust 类型:快速指南经过@dawchihliou
930 讀數
930 讀數

如何在 30 秒内使用 WebAssembly 的 TypeScript 共享 Rust 类型:快速指南

经过 Daw-Chih Liou6m2023/05/29
Read on Terminal Reader

太長; 讀書

💡 我们将了解为什么官方 Rust 和 WebAssembly 工具链不足以支持 TypeScript。 🤹 我将向您展示如何在对 Rust 代码进行最少更改的情况下自动生成 TypeScript 定义。 🧪 我们将一起在 npm 上重构一个真实世界的 WebAssembly 库。
featured image - 如何在 30 秒内使用 WebAssembly 的 TypeScript 共享 Rust 类型:快速指南
Daw-Chih Liou HackerNoon profile picture

使用 Rust 和 WebAssembly 发现最无缝的开发人员体验。这是从 Rust 代码自动生成 TypeScript 定义的最快方法。

在本文中

  • 💡 我们将了解为什么官方 Rust 和 WebAssembly 工具链不足以支持 TypeScript。


  • 🤹 我将向您展示如何在对 Rust 代码进行最少更改的情况下自动生成 TypeScript 定义。


  • 🧪 我们将一起在 npm 上重构一个真实世界的 WebAssembly 库。


我们走吧。


wasm-bindgen 的打字问题

在 Rust 中为 WebAssembly(Wasm)模块生成 TypeScript 类型并不简单。


当我在 Wasm 中开发一个名为Voy的向量相似性搜索引擎时,我遇到了这个问题。我用 Rust 构建了 Wasm 引擎,为 JavaScript 和 TypeScript 工程师提供了一把用于语义搜索的瑞士刀。这是网络演示:


Voy 演示


您可以在 GitHub 上找到 Voy 的存储库!随意尝试一下。


该存储库包含示例,您可以看到如何在不同的框架中使用 Voy。


我使用wasm-packwasm-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进行序列化和反序列化,以通过 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。

使用 Tsify 自动生成 TypeScript 绑定

将数据类型从一种语言转换为另一种语言实际上是一种称为外部函数接口(FFI) 的常见模式。我探索了像Typeshare这样的 FFI 工具来从 Rust 结构自动生成 TypeScript 定义,但这只是解决方案的一半。


我们需要的是一种利用 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”。


Wasm 应用程序接口


这两个属性都将 Rust 数据类型转换为 TypeScript 定义。他们所做的不同之处在于 Wasm 的应用程序二进制接口 (ABI) 的数据流方向。


  • into_wasm_abi :数据从 Rust 流向 JavaScript。用于返回类型。


  • from_wasm_abi :数据从 JavaScript 流向 Rust。用于参数。


这两个属性都使用 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[] }


所有“任何”类型都被替换为我们在 Rust 代码中定义的接口✨

最后的想法

生成的类型看起来不错,但存在一些不一致之处。如果仔细观察,您会注意到搜索函数中的查询参数被定义为 Float32Array。


查询参数在 EmbeddedResource 中定义为与“embeddings”相同的类型,因此我希望它们在 TypeScript 中具有相同的类型。


如果您知道为什么将它们转换为不同的类型,请不要犹豫,联系我们或在GitHub 上的 Voy中打开拉取请求。


Voy是 WebAssembly 中的开源语义搜索引擎。我创建它是为了让更多的项目能够构建语义特征并为世界各地的人们创造更好的用户体验。 Voy遵循几个设计原则:


  • 🤏 Tiny :减少有限设备的开销,例如具有慢速网络或物联网的移动浏览器。


  • 🚀快速:为用户创造最好的搜索体验。


  • 🌳 Tree Shakable :优化包大小并为现代 Web API(例如 Web Workers)启用异步功能。


  • 🔋可恢复:随时随地生成可移植的嵌入索引。


  • ☁️全球:在 CDN 边缘服务器上运行语义搜索。


它在 npm 上可用。您可以简单地使用您最喜欢的包管理器安装它,然后就可以开始了。


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


试一试,我很高兴收到你的来信!

参考


想要连接?

本文原载于道志网站