做者:Timothy McCallum Second State 核心開發這篇文章詳細解釋了 WASM 中如何實現字符串,文章有點長,慢慢讀~javascript
計算機程序只用數字就能夠成功執行。 然而,爲了方便人機交互,人類可讀的字符和文字是必需的。 當咱們思考人類如何與 Web 上的應用程序進行交互時,狀況尤爲如此。 絕佳的例子是,人們在訪問Web 時選擇使用域名,而非數字 IP 地址。css
正如本文的標題所宣稱的,咱們將討論 WebAssembly (Wasm)中的字符串。 WASM是最近咱們看到的最使人興奮的計算機編程技術之一。 Wasm 是一種接近機器的、支持多平臺的、低級的、類彙編語言(Reiser and bl ser,2017) ,它從一開始就是第一個實現形式語義學的主流編程語言(Rossberg et al. ,2018)。html
有趣的是,WebAssembly 代碼中沒有本地字符串。 更具體地說,Wasm 沒有字符串數據類型。java
Wasm的MVP(只支持wasm32)有一個ILP32數據模型,目前提供如下4種數據類型,分別是:node
雖然咱們很快就會開始討論在瀏覽器中使用 Wasm,但關鍵是要始終記住,從根本上講,Wasm 的執行是用堆棧機器來定義的。 其基本想法是,每種類型的指令都會將必定數量的 i3二、 i6四、 f3二、 f64值從堆棧中推入或彈出(MDN Web Docs ——理解 WebbAssembly 文本格式,2020)。jquery
正如咱們所看到的,上面的四種數據類型都屬於數字。 那麼,若是是這種狀況,咱們如何在 WebAssembly (Wasm)中促成(facilitate)字符串呢?git
如今,能夠將高級值(如字符串)轉換爲一組數字。 若是實現了這一點,那麼咱們就能夠在函數之間來回傳遞這些數字集(表明字符串)。 github
然而,這裏有幾個問題。web
對於通常的高級編碼來講,老是須要這種常量的顯式編碼 / 解碼是很麻煩的,所以這不是一個很好的長期解決方案。 apache
此外,事實證實,這種方法目前在 Wasm 實際上不可能實現。 緣由是,儘管 Wasm 函數能夠接受函數中的許多值(做爲參數) ,可是目前 Wasm 函數只能返回一個值。而Wasm會有不少信息。
如今,讓咱們經過看看 Rust 中的字符串的工做機制,來說一下基礎知識。
Rust中的String 能夠被認爲是一個保證了擁有良好的 UTF-8 Vec<u8>(Blandy and Orendorff,2017)。
Rust 中的 &str
是對其餘人擁有的一組 UTF-8文本的引用。&str
是一個寬指針(fat pointer),包含實際數據的地址及其長度。 您能夠將 &str
看做是一個保證包含格式良好的 UTF-8的 &[u8]
(Blandy and Orendorff,2017)。
字符串文本是一個指預先分配的文本的 &st
r,一般與程序機器代碼一塊兒存儲在只讀內存文檔中; 程序開始執行時建立字節,一直到程序結束。 所以,修改 &str
是不可能的(Blandy 和 Orendorff,2017)。
&str
能夠引用任何字符串的任何片斷,所以使用 &str
做爲函數參數的一部分是合適的; 調用者能夠傳遞 String
或 &str
(Klabnik 和 Nichols,2019)。
像這樣的代碼這樣:
fn my_function(the_string: &str) -> &str { // code ... }
能夠在運行時使用 String
建立新字符串。 可使用如下方法將字符串文本轉換爲 String
。 To String ()
和 String::from
作一樣的事情,所以您選擇哪一個只是風格上的區別(Klabnik 和 Nichols,2019)。
let s = "the string literal".to_string(); let s = String::from("the string literal");
將字符串轉換爲數字
下面的 Rust 代碼獲取字符串 hello
並將其轉換爲字節,而後將該字符串的兩個版本輸出到終端。
fn main() { let s: String = String::from("hello"); println!("String: {:?}", &s); println!("Bytes: {:?}", &s.as_bytes()); }
輸出
String: "hello" Bytes: [104, 101, 108, 108, 111]
有了全部這些信息,咱們如何爲 Web 用 Wasm 編寫「 Hello World! 」 ? 例如,咱們如何在用戶界面和 Wasm 執行環境之間來回傳遞字符串?
問題的核心是... WebAssembly 須要很好地使用 JavaScript... 咱們須要使用Javascript並將 JavaScript 對象傳遞到 WebAssembly,但 WebAssembly 根本不支持這一點。 目前,WebAssembly 只支持整數和浮點數(Williams,2019)。將 JavaScript 對象硬塞進 u32以便用於 Wasm,須要費些力氣。
摔跤圖案,看起來很像甲殼類動物。
這是個巧合嗎? 我不這麼認爲。
Wasm-bindgen 是 Rust 的 build time 依賴項。 它可以在編譯時生成 Rust 和 JavaScript 代碼。 它也能夠用做一個可執行文件,在命令行中稱爲 bindgen。 實際上,Wasm-bindgen 工具容許 JavaScript 和 Wasm 交流像字符串這樣的高級 JavaScript 對象。 與專門通訊的數字數據類型相反( rustwasm.github.io ,2019)。
這是如何實現的呢?
「 WebAssembly 程序的主要存儲是大量的原始字節數組、線性內存或單純的內存 (Rossberg et al. ,2018)。
Wasm-bindgen 工具抽象出線性內存,並容許在 Rust 和 JavaScript 之間使用本地數據結構(Wasm By Example,2019)。
當前的策略是讓 wasm-bindgen 維護一個「heap」。 這個「 heap」是一個由 wasm-bindgen 建立的模塊本地變量,位於 wasm-bindgen 生成的 JavaScript 文件中。
接下來的部分可能看起來有點很差懂,請堅持下去。 事實證實,這個「heap」中的第一個插槽被認爲是一個堆棧。 這個堆棧,像典型的程序執行堆棧同樣,是向下增加。
短時間的 JavaScript 對象被推送到堆棧上,它們的索引(堆棧中的位置和長度)被傳遞給 Wasm。 一個棧指針用來指出下一個項目的推送位置(GitHub ー RustWasm,2020)。
刪除只是存儲未定義 / null。 因爲這種方案的 「棧-y」 特性,它只適用於 Wasm 沒有保留 JavaScript 對象的狀況(GitHub ー RustWasm,2020)。
JsValue Wasm-bindgen 庫的 Rust 代碼庫自己使用一個特殊的 JsValue。 編寫的導出函數(以下圖所示)能夠引用這個特殊的 JsValue。 #[wasm_bindgen] pub fn foo(a: &JsValue) { // ... }
相對於上面編寫的 Rust,#[wasm_bindgen]
生成的 Rust 代碼看起來是這樣的。
#[export_name = "foo"] pub extern "C" fn __wasm_bindgen_generated_foo(arg0: u32) { let arg0 = unsafe { ManuallyDrop::new(JsValue::__from_idx(arg0)) }; let arg0 = &*arg0; foo(arg0); }
而外部可調用的標識符仍然稱爲 foo
。 調用時,wasm_bindgen-generated Rust 函數的內部代碼即 Wasm bindgen generated foo 其實是從 Wasm 模塊導出的。 Wasm bindgen-generated 函數接受一個整數參數,並將其包裝爲 JsValue
。
點要記住,因爲 Rust 的全部權屬性,對 JsValue 的引用不能持續到函數調用的生命週期以後。 所以,wasm-bindgen 生成的 Javascript 須要釋放做爲該函數執行的一部分而建立的堆棧槽。 接下來讓咱們看看生成的 Javascript。
// foo.js import * as wasm from './foo_bg'; const heap = new Array(32); heap.push(undefined, null, true, false); let stack_pointer = 32; function addBorrowedObject(obj) { stack_pointer -= 1; heap[stack_pointer] = obj; return stack_pointer; } export function foo(arg0) { const idx0 = addBorrowedObject(arg0); try { wasm.foo(idx0); } finally { heap[stack_pointer++] = undefined; } }
咱們能夠看到, JavaScript 文件從 Wasm 文件導入。
而後咱們能夠看到前面提到的「heap」模塊-本地變量被建立。 重要的是要記住這個 JavaScript 是由 Rust 代碼生成的。 若是您想了解這是如何作到的,請參閱此 mod.rs文件中的第747行。
我提供了 Rust 的一小段代碼,這段代碼能夠生成 JavaScript,代碼以下。
self.global(&format!("const heap = new Array({});", INITIAL_HEAP_OFFSET));
在 Rust 文件中,INITIAL heap offset 被硬編碼爲32。 所以,數組默認有32個項。
一旦建立,在 Javascript 中,這個 heap
變量將在執行時存儲來自 Wasm 的全部可引用的 Javascript 值。
若是咱們再看一下生成的 JavaScript,咱們能夠看到被導出的函數 foo
接受一個任意的參數 arg0
。 foo
函數調用 addBorrowedObject
,將其傳遞到 arg0
。 addBorrowedObject function
將堆棧指針位置遞減1(爲32,如今爲31) ,而後將對象存儲到該位置,同時還將該特定位置返回給調用 foo
函數。
堆棧位置存儲爲一個名爲 idx0的常量。 而後將 idx0傳遞給由 bindgen 生成的 Wasm,以便 Wasm 能夠對其進行操做(GitHub ー RustWasm,2020)。
正如咱們提到的,咱們仍然在討論「堆棧」上的 Temporary JS 對象。
若是咱們查看生成的 JavaScript 代碼的最後一行文本,咱們會看到堆棧指針位置的堆被設置爲未定義,而後自動(感謝 ++
語法)堆棧指針變量被遞增回原來的值。
到目前爲止,咱們已經介紹了一些只是臨時使用的對象,即只在一次函數調用期間使用。 接下來讓咱們看看長期存在的 JS 對象。
在這裏,咱們將討論 JavaScript 對象管理的後半部分,再次引官方的 bindgen 文檔( rustwasm.github.io,2019)。
棧的嚴格的 push / pop 不適用於長期存在的 JavaScript 對象,所以咱們須要一種更爲永久的存儲機制。
若是咱們回顧一下最初編寫的 foo
函數示例,咱們能夠看到稍微的更改就會改變 JsValue 的全部權,從而改變其生命週期。 具體來講,經過刪除 &
(在咱們編寫的 Rust 中) ,咱們使 foo
函數得到了對象的所有全部權,而不僅是借用一個refference。
// foo.rs #[wasm_bindgen] pub fn foo(a: JsValue) { // ... }
如今,在生成的 Rust 中,咱們調用 addHeapObject
,而不是 addBorrowedObject
。
import * as wasm from './foo_bg'; // imports from wasm file const heap = new Array(32); heap.push(undefined, null, true, false); let heap_next = 36; function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); const idx = heap_next; heap_next = heap[idx]; heap[idx] = obj; return idx; } T
addHeapObject
使用 heap 和 heap_next 函數來獲取一個 slot 來存儲對象。
如今咱們已經對使用 JsValue 對象有了一個大體的瞭解,接下來讓咱們關注字符串。
字符串經過兩個參數,一個指針和一個長度傳遞給 wasm。(GitHub ー RustWasm,2020)
字符串使用 TextEncoder API 進行編碼,而後複製到 Wasm 堆上。 下面是一個使用 TextEncoder API 將字符串編碼爲數組的快速示例。 你能夠在你的瀏覽器控制檯上嘗試一下。
const encoder = new TextEncoder(); const encoded = encoder.encode('Tim'); encoded // Uint8Array(3) [84, 105, 109]
只傳遞索引(指針和長度),而不是傳遞整個高級對象,是頗有意義的。 正如咱們在本文開頭所提到的,咱們可以將許多值傳遞到一個 Wasm 函數中,但只容許返回一個值。 那麼咱們如何從一個 Wasm 函數返回指針和長度呢?
目前 WebAssembly GitHub 上有一個公開的 issue,是正在實現和標準化 Wasm 函數的多個返回值。
同時導出一個返回字符串的函數,須要一個涉及到的兩種語言的 shim。 在這種狀況下,JavaScript 和 Rust 都須要就每一方如何轉換成和轉換成 Wasm (用他們各自的語言)達成一致。
Wasm-bindgen 工具能夠鏈接全部這些shim,而 #[wasm_bindgen]
宏也能夠處理 Rust shim (GitHub ー RustWasm,2020)。
這一創新以一種很是聰明的方式解決了 WebAssembly 中的字符串問題。 這當即爲無數的 Web 應用程序打開了大門,使之能夠利用 Wasm 的出色特性。 隨着開發的繼續,即多值提議的正規化,Wasm 在瀏覽器內外的功能將大大提高。
讓咱們來看一些在 WebAssembly 中使用字符串的具體例子。 這些都是你能夠本身嘗試的成功例子。
正如 bindgen 文檔所說。 「經過添加 wasm-pack,您能夠在本地 web 上運行 Rust,將其做爲更大應用程序的一部分發布,甚至能夠在 NPM 上發佈 Rust-compiled to-webassembly! 」
Wasm-pack 是一個很是棒的 Wasm 工做流工具,易於使用。
Wasm-pack (https://rustwasm.github.io/wa... 在幕後使用wasm-bindgen。
簡而言之,wasm-pack 在編譯到 WebAssembly 的同時生成 Rust 代碼和 JavaScript 代碼。 Wasm-pack 容許您經過 JavaScript 與 WebAssembly 交流,就像它是 JavaScript 同樣(Williams,2019)。
Wasm使用 wasm32-unknown-unknown
目標編譯您的代碼。
下面是一個使用 wasm-pack
在 web 上實現字符串鏈接的例子。
若是咱們啓動一個 Ubuntu Linux 系統並執行如下操做,咱們能夠在幾分鐘內開始構建這個演示。
#System housekeeping sudo apt-get update sudo apt-get -y upgrade sudo apt install build-essential #Install apache sudo apt-get -y install apache2 sudo chown -R $USER:$USER /var/www/html sudo systemctl start apache2 #Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env #Install wasm-pack curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
一旦系統設置好咱們能夠用Rust建立一個新項目
cd ~ cargo new --lib greet cd greet
而後咱們執行一些 Rust 配置,以下所示(打開 Cargo.toml 文件並在文件底部添加如下內容)
[lib] name = "greet_lib" path = "src/lib.rs" crate-type =["cdylib"][dependencies]
最後,咱們使用 wasm-pack
構建程序
wasm-pack build --target web
一旦代碼被編譯,咱們只須要建立一個 HTML 文件來進行交互,而後將 HTML 以及 wasm-pack
的 pkg
目錄的內容複製到咱們提供 Apache2 的地方。
在 ~ / greet / pkg
目錄中建立如下索引 . html 文件。
<html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> <script type="module">import init, { greet } from './greet_lib.js';async function run() {await init();var buttonOne = document.getElementById('buttonOne');buttonOne.addEventListener('click', function() {var input = $("#nameInput").val();alert(greet(input));}, false);}run();</script> </head> <body> <div class="row"> <div class="col-sm-4"></div> <div class="col-sm-4"><b>Wasm - Say hello</b></div> <div class="col-sm-4"></div> </div> <hr /> <div class="row"> <div class="col-sm-2"></div> <div class="col-sm-4">What is your name?</div> <div class="col-sm-4"> Click the button</div> <div class="col-sm-2"></div> </div> <div class="row"> <div class="col-sm-2"></div> <div class="col-sm-4"> <input type="text" id="nameInput" placeholder="1" , value="1"> </div> <div class="col-sm-4"> <button class="bg-light" id="buttonOne">Say hello</button> </div> <div class="col-sm-2"></div> </div> </body> <scriptsrc="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"> </script> </html>
將 pkg 目錄的內容複製到咱們在運行Apache2的地方
cp -rp pkg/* /var/www/html/
若是訪問服務器的地址,咱們會看到下面的頁面。
當咱們添加咱們的名字並單擊按鈕時,獲得如下響應。
如今咱們已經看到了使用 html / js 和 Apache2的實際應用,讓咱們繼續並建立另外一個演示。 這一次是在 Node.js 的環境中,遵循 wasm-pack
的 npm-browser-packages 文檔。
sudo apt-get update sudo apt-get -y upgrade sudo apt-get -y install build-essential sudo apt-get -y install curl #Install Node and NPM curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash - sudo apt-get install -y nodejs sudo apt-get install npm #Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env #Install wasm-pack curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sudo apt-get install pkg-config sudo apt-get install libssl-dev cargo install cargo-generate cargo generate --git https://github.com/rustwasm/wasm-pack-template
感興趣的話, 該demo(是用官方demo軟件生成的)的Rust代碼以下
mod utils; use wasm_bindgen::prelude::*;// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;#[wasm_bindgen] extern { fn alert(s: &str); }#[wasm_bindgen] pub fn greet() { alert("Hello, tpmccallum-greet!"); }
您可使用如下命令構建項目,最後一個參數是 npmjs. com 用戶名
wasm-pack build --scope tpmccallum
要登陸到您的 npm 賬戶,只需經過 wasm-pack 鍵入如下命令
wasm-pack login
要發佈,只需切換到 pkg 目錄並運行如下命令
cd pkg npm publish --access=public
好的,咱們已經發布了一個包。
如今,讓咱們繼續建立一個新的應用程序,咱們能夠在其中使用咱們的包。
請注意,咱們使用的是模板,因此不要爲下面的命令建立本身的應用程序名,而是使用以下所示的 create-wasm-app 文本。
cd ~ npm init wasm-app create-wasm-app
在這個階段,咱們想從 npmjs. com 安裝這個軟件包。 咱們使用如下命令來實現這一點
npm i @tpmccallum/tpmccallum-greet
如今打開 index.js
,按照名稱導入包,以下所示
import * as wasm from "tpmccallum-greet"; wasm.greet();
最後,啓動演示並訪問 localhost: 8080
npm install npm start
預計「 WebAssembly 將在其餘領域發現普遍的用途。 事實上,其餘多種嵌入方式已經在開發中: 內容傳輸網絡中的沙箱,區塊鏈上的智能合約或去中心化的雲計算,移動設備的代碼格式,甚至做爲提供可移植語言運行時的獨立引擎」 (Rossberg et al. ,2018)。
這裏詳細解釋的 MutiValue 提議頗有可能最終容許一個 Wasm 函數返回許多值,從而促進一組新接口類型的實現。
實際上,有一個提議,正如這裏所解釋的,在 WebAssembly 中添加了一組新的接口類型,用於描述高級值(好比字符串、序列、記錄和變量)。 這種新的方法能夠實現這一點,而無需提交到單一的內存表示或共享模式。 使用這種方法,接口類型只能在模塊的接口中使用,而且只能由聲明性接口適配器生成或使用。
該提案代表,它是在 WebAssembly 核心規範的基礎上進行語義分層的(經過多值和引用類型提案進行擴展)。 全部的適應都在一個自定義部分中指定,而且可使用 javascript api 進行polyfill。
參考文獻
wasm-bindgen
Guide. [在線] 請訪問: https://rustwasm.github.io/do... [Accessed 27 Jan. 2020].