我们走吧。
在 Rust 中为 WebAssembly(Wasm)模块生成 TypeScript 类型并不简单。
当我在 Wasm 中开发一个名为Voy的向量相似性搜索引擎时,我遇到了这个问题。我用 Rust 构建了 Wasm 引擎,为 JavaScript 和 TypeScript 工程师提供了一把用于语义搜索的瑞士刀。这是网络演示:
您可以在 GitHub 上找到 Voy 的存储库!随意尝试一下。
该存储库包含示例,您可以看到如何在不同的框架中使用 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进行序列化和反序列化,以通过 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。
将数据类型从一种语言转换为另一种语言实际上是一种称为外部函数接口(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”。
这两个属性都将 Rust 数据类型转换为 TypeScript 定义。他们所做的不同之处在于 Wasm 的应用程序二进制接口 (ABI) 的数据流方向。
这两个属性都使用 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遵循几个设计原则:
它在 npm 上可用。您可以简单地使用您最喜欢的包管理器安装它,然后就可以开始了。
# with npm npm i voy-search # with Yarn yarn add voy-search # with pnpm pnpm add voy-search
试一试,我很高兴收到你的来信!
想要连接?
本文原载于道志网站。