갑시다.
Rust에서 WebAssembly(Wasm) 모듈용 TypeScript 유형을 생성하는 것은 간단하지 않습니다.
Voy 라는 Wasm의 벡터 유사성 검색 엔진을 작업할 때 문제가 발생했습니다. 저는 JavaScript 및 TypeScript 엔지니어에게 의미론적 검색을 위한 스위스 칼을 제공하기 위해 Rust로 Wasm 엔진을 구축했습니다. 다음은 웹용 데모입니다.
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( 외래 함수 인터페이스 )라는 일반적인 패턴입니다. Rust 구조체에서 TypeScript 정의를 자동 생성하기 위해 Typeshare 와 같은 FFI 도구를 탐색했지만 이는 솔루션의 절반에 불과했습니다.
우리에게 필요한 것은 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(Application Binary Interface)를 사용한 데이터 흐름의 방향입니다.
두 속성 모두 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[] }
모든 "any" 유형은 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
한번 시도해 보세요. 여러분의 의견을 듣게 되어 기쁩니다!
연결하고 싶으신가요?
이 기사는 원래 Daw-Chih 웹사이트 에 게시되었습니다.