作者:Timothy McCallum Second State 核心開發
這篇文章詳細解釋了 WASM 中如何實現字符串,文章有點長,建議收藏後慢慢讀~
字符串的重要性
計算機程序只用數字就可以成功執行。 然而,爲了方便人機交互,人類可讀的字符和文字是必需的。 當我們思考人類如何與 Web 上的應用程序進行交互時,情況尤其如此。 絕佳的例子是,人們在訪問Web 時選擇使用域名,而非數字 IP 地址。
正如本文的標題所宣稱的,我們將討論 WebAssembly (Wasm)中的字符串。 WASM是最近我們看到的最令人興奮的計算機編程技術之一。 Wasm 是一種接近機器的、支持多平臺的、低級的、類彙編語言(Reiser and bl ser,2017) ,它從一開始就是第一個實現形式語義學的主流編程語言(Rossberg et al. ,2018)。
WebAssembly 中的字符串
有趣的是,WebAssembly 代碼中沒有本地字符串。 更具體地說,Wasm 沒有字符串數據類型。
Wasm的MVP(只支持wasm32)有一個ILP32數據模型,目前提供以下4種數據類型,分別是:
- i32,一個32位的整數(相當於 c + + 的帶符號 long int)
- i64,一個64位的整數(相當於 c + + 的帶符號 long int)
- f32,32位浮點數(相當於 c + + 的浮點數)
- f64,64位浮點數(相當於 c + + 的 double)
雖然我們很快就會開始討論在瀏覽器中使用 Wasm,但關鍵是要始終記住,從根本上講,Wasm 的執行是用堆棧機器來定義的。 其基本想法是,每種類型的指令都會將一定數量的 i32、 i64、 f32、 f64值從堆棧中推入或彈出(MDN Web Docs ——理解 WebbAssembly 文本格式,2020)。
正如我們所看到的,上面的四種數據類型都屬於數字。 那麼,如果是這種情況,我們如何在 WebAssembly (Wasm)中促成(facilitate)字符串呢?
WebAssembly 中的字符串ーー怎樣解決?
現在,可以將高級值(如字符串)轉換爲一組數字。 如果實現了這一點,那麼我們就可以在函數之間來回傳遞這些數字集(代表字符串)。
然而,這裏有幾個問題。
對於一般的高級編碼來說,總是需要這種常量的顯式編碼 / 解碼是很麻煩的,因此這不是一個很好的長期解決方案。
此外,事實證明,這種方法目前在 Wasm 實際上不可能實現。 原因是,儘管 Wasm 函數可以接受函數中的許多值(作爲參數) ,但是目前 Wasm 函數只能返回一個值。而Wasm會有很多信息。
現在,讓我們通過看看 Rust 中的字符串的工作機制,來講一下基礎知識。
Rust字符串
字符串
Rust中的String 可以被認爲是一個保證了擁有良好的 UTF-8 Vec(Blandy and Orendorff,2017)。
& str
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]
Wasm 的“ Hello World! ”例子
有了所有這些信息,我們如何爲 Web 用 Wasm 編寫“ Hello World! ” ? 例如,我們如何在用戶界面和 Wasm 執行環境之間來回傳遞字符串?
問題的核心是… WebAssembly 需要很好地使用 JavaScript… 我們需要使用Javascript並將 JavaScript 對象傳遞到 WebAssembly,但 WebAssembly 根本不支持這一點。 目前,WebAssembly 只支持整數和浮點數(Williams,2019)。
將 JavaScript 對象硬塞進 u32以便用於 Wasm,需要費些力氣。
摔跤圖案,看起來很像甲殼類動物。
這是個巧合嗎? 我不這麼認爲。
Bindgen
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”中的第一個插槽被認爲是一個堆棧。 這個堆棧,像典型的程序執行堆棧一樣,是向下增長。
“stack” 上的臨時 JS 對象
短期的 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) {
// ...
}
wasm-bindgen 生成的 Rust
相對於上面編寫的 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。
Wasm-bindgen 生成的 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;
}
}
heap
我們可以看到, 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 對象。
長期存在的 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-pack 是一個非常棒的 Wasm 工作流工具,易於使用。
Wasm-pack (https://rustwasm.github.io/wasm-pack/) 在幕後使用wasm-bindgen。
簡而言之,wasm-pack 在編譯到 WebAssembly 的同時生成 Rust 代碼和 JavaScript 代碼。 Wasm-pack 允許您通過 JavaScript 與 WebAssembly 交流,就像它是 JavaScript 一樣(Williams,2019)。
Wasm使用 wasm32-unknown-unknown
目標編譯您的代碼。
Wasm-pack (客戶端-網頁)
下面是一個使用 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/
如果訪問服務器的地址,我們會看到下面的頁面。
當我們添加我們的名字並單擊按鈕時,得到以下響應。
Wasm-pack (服務器端- Node.js)
現在我們已經看到了使用 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。
參考文獻
- Blandy, J. and Orendorff, J. (2017). 《Rust 編程》. O’Reilly Media Inc.
- GitHub — WebAssembly. (2020). WebAssembly/interface-types. [在線] 請訪問: https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md
- GitHub — RustWasm. (2020). rustwasm/wasm-bindgen. [在線] 請訪問: https://github.com/rustwasm/wasm-bindgen/blob/master/guide/src/contributing/design/js-objects-in-rust.md
- Haas, A., Rossberg, A., Schuff, D.L., Titzer, B.L., Holman, M., Gohman, D., Wagner, L., Zakai, A. and Bastien, J.F., 2017, June. 《使用WebAssembly加快網絡速度》在第38屆ACM SIGPLAN會議上有關編程語言設計和實現的會議論文集(第185–200頁)。
- Klabnik, S. and Nichols, C. (2019). The Rust Programming Language (Covers Rust 2018). San Francisco: No Starch Press Inc.
- MDN Web Docs — Understanding WebAssembly text format. (2020). Understanding WebAssembly text format. [在線] 請訪問: https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format
- MDN Web Docs — Web APIs. (2020). Web APIs. [在線] 請訪問: https://developer.mozilla.org/en-US/docs/Web/API
- Reiser, M. and Bläser, L., 2017, October. 通過交叉編譯到WebAssembly來加速JavaScript應用程序。在第9屆ACM SIGPLAN虛擬機和中間語言國際研討會論文集(第10-17頁)中。
- Rossberg, A., Titzer, B., Haas, A., Schuff, D., Gohman, D., Wagner, L., Zakai, A., Bastien, J. and Holman, M. (2018). * 使用WebAssembly加快網絡速度。 ACM通訊,61(12),第107–115頁。
- Rustwasm.github.io. (2019). Introduction — The
wasm-bindgen
Guide. [在線] 請訪問: https://rustwasm.github.io/docs/wasm-bindgen/ [Accessed 27 Jan. 2020]. - Wasm By Example. (2019). WebAssembly Linear Memory. [在線] 請訪問: https://wasmbyexample.dev/examples/webassembly-linear-memory/webassembly-linear-memory.rust.en-us.html
- Williams, A. (2019). Rust, WebAssembly, and Javascript Make Three: An FFI Story. [在線] 請訪問: https://www.infoq.com/presentations/rust-webassembly-javascript/