給 Web 前端工程師看的用 Rust 開發 wasm 組件實戰

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/12/04/17874615.html
-Advertisement-
Play Games

本文先介紹了 wasm-pack 官方的教程,還有其他組件測試、發佈等的流程先不在這裡介紹了。以下用一個實際開發中的模塊來說一下開發 wasm 組件過程中遇到的問題和解決方法。 ...


什麼是wasm組件?

wasm 全稱 WebAssembly,是通過虛擬機的方式,可以在服務端、客戶端如瀏覽器等環境執行的二進位程式。他有速度快、效率高、可移植的特點。

對我們 Web 前端工程最大的好處就是可以在瀏覽器端使用二進位程式處理一些計算量大的處理,使用他比 javascript 快的特點優化性能。

目前瀏覽器對wasm的相容性如下:

https://img10.360buyimg.com/imagetools/jfs/t1/180904/35/36038/170761/64ded9bdF6f54c383/e85e037cdd4fa1fd.jpg

在移動端除了 android 4.4 和 ios 10 下不支持外,其他版本都能提供支持。還需要註意的是 wasm 有可能占用大量記憶體,使用第三方包含 wasm 調用的組件需要註意記憶體占用防止閃退。

為什麼用Rust?

wasm模塊 可以用多種語言來編譯,包括 C/C++/C#、Rust、JAVA、Go。在這裡使用 Rust 是因為他有嚴格的記憶體管理機制,從語法上儘量避免記憶體溢出,讓工程師寫出更安全的程式。

而且還有配套的工具 wasm-pack,讓使用 Rust 編寫的代碼,編譯包裝成 npm 包,讓使用這段程式的其他代碼可以像使用其他公共庫一樣調用,不需要額外學習成本。

工具安裝

  1. 安裝 rustup,他是 Rust 安裝器和版本管理工具。對於 web 前端來說相當於 nvm 這樣的工具。
    按照 rust 官網的方法安裝:https://www.rust-lang.org/zh-CN/tools/install
    同時也會安裝 cargo,他是 Rust 的構建工具和包管理器。對於 web 前端來說相當於 npm 這樣的工具。

  2. 安裝 wasm-pack,他是上文提到的把 rust 程式編譯包裝成 wasm 組件的工具。
    同樣按照 wasm-pack 官網的方法安裝:https://rustwasm.github.io/wasm-pack/installer/

  3. 使用 wasm 模板
    使用 wasm-pack 提供的模板可以快速生成 rust 的 wasm 項目。

cargo generate --git https://github.com/rustwasm/wasm-pack-template


輸入希望的項目目錄名稱,將新建目錄併在其中生成項目。

在目錄下我們可以看到幾個文件,其中一個是 Cargo.toml ,這個是 rust 項目的描述文件,對於 web 前端來說相當於 package.json 文件。

項目目錄下還有一個 src 目錄,裡面有 lib.rsutils.rs 兩個文件,其中 lib.rs 這個文件就是我們主要的邏輯入口,他引用了 wasm-bindgen 庫來輸出暴露給外部調用的介面,在函數之前加上#[wasm_bindgen]可以讓外部調用這個方法。

編譯項目

本來 rust 的項目編譯用的是 cargo build 的命令,但是我們這裡是希望編譯 wasm 組件,所以用的是 wasm-pack build 命令。

執行後會在項目目錄下的 pkg 目錄下生成編譯後的產品,是一個 npm 包的結構。需要調用這個組件的邏輯只需要像其他公共包一樣 import 就可以使用了。

實戰

以上的就是 wasm-pack 官方的教程,還有其他組件測試、發佈等的流程先不在這裡介紹了。以下用一個實際開發中的模塊來說一下開發 wasm 組件過程中遇到的問題和解決方法。

背景

需要使用的 wasm 組件是一個優化3D模型的方法,傳入一個模型的頂點信息和距離閾值,比較每個頂點位置之間的距離,如果沒達到閾值距離就合併這兩個頂點,以達到減少頂點的優化目的。

原邏輯是使用 javascript 編寫的,在模型頂點數量比較多的時候執行的時間比較長。這種大量計算的情況就很適合使用 wasm 來處理。

數據傳遞

頂點信息是存儲在一個 Float32Array 的數組中的,而 wasm 設計上除了 int 和 float 類型(對應 javascript 就是 number 類型)可以直接傳遞外,其他的類型都通過地址來傳遞。這對我們的程式來說是好消息,因為頂點信息的數據非常多,如果以值傳遞,就需要做數據複製,這個過程消耗的時間可能比我們換成 wasm 處理減
少的時間還要多。得益這個特點,我們的入參可以直接傳入。

/*---  rust ----*/

// rust 獲取 javasctipt 數據
pub fn add_attribute(&mut self, attribute: &Float32Array, item_size: u32) {
    self.attributes.push(BufferAttribute {
        array: attribute.to_vec(),
        item_size,
    });
}


/*---  javascript ----*/

// javascript 傳遞數據到 rust
for (const name of attributeNames) {
  const attr = attrArrays[name]
  bg.add_attribute(attr.array, attr.itemSize)
}


而計算後的結果,wasm 也提供了返回數組的指針和數組長度的方法,javascript 可以讀取 wasm 的記憶體空間,根據這兩個值構造新的頂點信息Float32Array。

/*---  rust ----*/

// 返回指定數據的記憶體指針位置
pub fn get_attribute_ptr(&self, index: usize) -> *const f32 {
  self.attributes[index].array.as_ptr()
}

// 返回指定數據的長度
pub fn get_attribute_length(&self, index: usize) -> usize {
  self.attributes[index].array.len()
}


/*---  javascript ----*/

// javascript 或取 rust 記憶體空間中的指定部分,構建Float32Array
const ptr = bg.get_attribute_ptr(i)
const length = bg.get_attribute_length(i)

const buffer = new attr.array.constructor(wasm.getMemory().buffer, ptr, length)


數據類型

合併頂點計算的邏輯中,有一段是這樣的:每個頂點的位置、UV等信息,經過給定的精度計算後,生成一個特征值,之後比較每個頂點的特征值,如果是相同的話就表示這兩個頂點可以合併。

原 javascript 版本的代碼是逐個信息按順序,加上分隔號,拼成一個字元串。

rust 版本的代碼如果也按同樣的方法處理,因為頂點的信息量是不定的,有可能只有位置信息,也有可能有UV、法線、顏色等信息,所以生成的特征值字元串長度也不確定。

rust 對於可變長度的字元串使用 String 類型,每次對字元串使用push_str方法增加內容。得到的結果 wasm 版本的執行速度跟 javascript 版本相差不大,甚至在某些情況下耗時還更多,經過逐個過程作排查,發現是在生成特征值和在表中查詢特征值這個過程中花費的時間比較多。

根據程式的意圖,特征值並不一定要是字元串,只需要在不同輸入值的時候能夠輸出相關的值就可以,這跟生成 hash 值的需求是一樣的,於是考慮將特征值生成替換成 hash 值計算。

因為在存儲特征值的表使用了std::collections::hash_map類型,於是 hash 值也使用了其下的std::collections::hash_map::DefaultHasher類來計算

use std::collections::hash_map::DefaultHasher;

...

let mut hasher = DefaultHasher::new();

for j in 0..self.attributes.len() {
  ...

  let value = (attr.array[i * attr.item_size as usize + index as usize]
    * self.shift_multiplier)
    .trunc() as i32;
    
  hasher.write_i32(value);
    
  ...
}

let hash = hasher.finish();


需要註意的是對寫入不同類型的內容,需要調用不同的方法,頂點信息中的值是正負值都用,經過精度計算後取整得到的值類型是i32,所以用write_i32來寫入內容。

生成的 hash 值為u64,作為hash_mapkey記錄對應頂點的序號。

替換特征值的類型之後,wasm 版本的耗時達到了 javascript 版本的 1/2,基本符合 wasm 設計的性能範圍。

適配打包工具

wasm-pack 工具打包出來的 npm 包,可以直接在webpack下載入並調用運行。

我們原本的項目使用 vite 構建,vite 對import wasm 組件策略和 webpack 的不一樣,vite 載入會返回一個載入方法,調用載入方法會返回一個 Promise,resolve 後才會返回跟 webpack 載入一樣的 wasm 組件。

我們要對 wasm-pack 生成的產物作一些修改,假設我們的 wasm 組件命名為 merge_vertice_wasm,生成的主 js 文件應該會命名為merge_vertice_wasm.js,內容如下:

import * as wasm from './merge_vertice_wasm_bg.wasm'
import { __wbg_set_wasm } as wasm_bg from './merge_vertice_wasm_bg.js'
__wbg_set_wasm(wasm);
export * from './merge_vertice_wasm_bg.js'


為相容 vite 的載入策略,修改成下麵的內容

import * as wasm from './merge_vertice_wasm_bg.wasm'
import * as wasm_bg from './merge_vertice_wasm_bg.js'

let memory
if (wasm.default) {
  wasm.default({
    './merge_vertice_wasm_bg.js': wasm_bg,
  }).then(_wasm => {
    memory = _wasm.memory
    wasm_bg.__wbg_set_wasm(_wasm)
  })
} else {
  memory = _wasm.memory
  wasm_bg.__wbg_set_wasm(wasm)
}
export * from './merge_vertice_wasm_bg.js'

export function getMemory() {
  return memory
}


就可以在 webpack 和 vite 下都可以順利載入並運行了。

其中增加了getMemory的方法供外部獲取 wasm 組件的記憶體空間。

wasm調用javascript方法

當我們在調試和測試性能表現時,需要列印日誌,由於我們的 wasm 跑在瀏覽器環境中,我們需要調用 javascript 的方法,比如console.logconsole.time

wasm-bindgen 庫提供了 web-sys 的組件,讓 rust 可以調用這些方法。

首先需要在cargo.toml中添加 web-sys 的依賴,並聲明需要用到的特性:

[dependencies]
wasm-bindgen = "0.2.84"

[dependencies.web-sys]
version = "0.3.64"
features = ["console"]


這樣在下次編譯的時候,cargo 就會自動處理這些依賴,將會下載並構建。

然後在我們的 rust 文件中,加入對 web-sys 的引用:

extern crate web_sys;


就可以調用 javascript 的 console 下的方法了:

// 調用console.log
web_sys::console::log_1(&JsValue::from(logContent));

// 調用console.time(label)
web_sys::console::time_with_label(label);

// 調用console.timeEnd(label)
web_sys::console::time_end_with_label(label);


原 javascript 版本優化模型耗時:

https://img14.360buyimg.com/imagetools/jfs/t1/109410/21/37527/8537/64dedd1cFe4c8c5c4/596fc2d36cc9fe5c.jpg

wasm 版本優化模型耗時:

https://img12.360buyimg.com/imagetools/jfs/t1/188745/32/36809/10529/64dedd1cF49a8b5cc/8dea820d278ad577.jpg

總結

以上為根據官網文檔把模型合併頂點優化方法遷移為 wasm 版本的開發經歷,從安裝工具到發佈、調試的整個過程。

中間因為對 rust 數據類型的不熟悉和對不同前端構建工具對 wasm 組件處理的不同不夠清晰,在開發過程中遇到的問題和解決方法。

rust 版本的代碼邏輯基本上是從 javascript 版本翻譯過來的,其中應該還有在 rust 環境下的優化手段,將在之後的學習中繼續迭代。

以上的完整代碼在http://xingyun.jd.com/codingRoot/LemonTea-lib/merge-vertice-wasm/倉庫,歡迎提優化修改建議。

引用

rust 官方文檔:https://doc.rust-lang.org/book/

rust wasm 官方介紹文檔:https://rustwasm.github.io/docs/book/

wasm-pack 官方文檔:https://rustwasm.github.io/docs/wasm-pack/

作者:京東零售 胡俊文

來源:京東雲開發者社區 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 面對今日頭條、抖音等不同產品線的複雜數據質量場景,火山引擎 DataLeap 數據質量平臺如何滿足多樣的需求? ...
  • 如果你是商家,當你要進行廣告投放的時候,假如平臺推送的用戶都是你潛在的買家,那你就可以花更少的錢,帶來更大的收益。這背後有一項技術支撐,那就是用戶畫像。 ...
  • 本文分享自華為雲社區《GaussDB(DWS)查詢優化技術大揭秘》,作者: 胡辣湯。 大數據時代,數據量呈爆髮式增長,經常面臨百億、千億數據查詢場景,當數據倉庫數據量較大、SQL語句執行效率低時,數據倉庫性能會受到影響。本期《GaussDB(DWS)查詢優化技術大揭秘》的主題直播中,我們邀請到華為雲 ...
  • 現代資料庫系統能夠存儲和處理大量數據。因此,由任何一個用戶單獨負責處理與管理資料庫相關的所有活動的情況相對較少。通常,不同的資料庫用戶需要對資料庫的某些部分具有不同級別的訪問許可權:某些用戶可能只需要讀取特定資料庫中的數據,而其他用戶則必須能夠插入新文檔或修改現有文檔。同樣,應用程式可能需要獨特的許可權 ...
  • C(Chapter) C-01.資料庫概述 1.為什麼要用資料庫 持久化(persistence):把數據保存到可掉電式存儲設備(硬碟)中以供之後使用。大多數情況下,特別是企業應用,數據持久化是將記憶體中的數據保存到硬碟上加以"固化",而持久化的實現過程大多使用各種關係資料庫來完成。 持久化的主要作用 ...
  • 本文分享自華為雲社區《深入理解HarmonyOS UIAbility:生命周期、WindowStage與啟動模式探析》,作者:檸檬味擁抱。 UIAbility組件概述 UIAbility組件是HarmonyOS中一種包含UI界面的應用組件,主要用於與用戶進行交互。每個UIAbility組件實例對應最 ...
  • 只有不斷學習和成長,才能適應這個快速變化的世界。 1. 懶載入 1.1 React 懶載入 React 中懶載入 Lazy 與 Suspense 需要搭配使用。 React.lazy 定義: React.1azy 函數能讓你像渲染常規組件一樣處理動態引入的組件。其實就是懶載入。 為什麼代碼要分割? ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 測試發現了一個問題,簡單描述問題就是通過函數刪除一個數組中多個元素,傳入的參數是一個數組索引。 然後發現實際效果有時刪除的不是想要的內容。 具體 Bug 代碼實現: const arr = [1,2,3,4,5,6,7]; cons ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...