WebAssembly(Wasm)中的字符串

作者: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)。

編譯時的字符串——存儲在可執行文件中

字符串文本是一個指預先分配的文本的 &str,通常與程序機器代碼一起存儲在只讀內存文檔中; 程序開始執行時創建字節,一直到程序結束。 因此,修改 &str 是不可能的(Blandy 和 Orendorff,2017)。

&str 可以引用任何字符串的任何片段,因此使用 &str 作爲函數參數的一部分是合適的; 調用者可以傳遞 String&str (Klabnik 和 Nichols,2019)。

像這樣的代碼這樣:

fn my_function(the_string: &str) -> &str {
 // code ...
}

運行時的字符串ー在運行時分配和釋放

可以在運行時使用 String 創建新字符串。 可以使用以下方法將字符串文本轉換爲 StringTo 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,需要費些力氣。

file

摔跤圖案,看起來很像甲殼類動物。

這是個巧合嗎? 我不這麼認爲。

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個項。

file

一旦創建,在 Javascript 中,這個 heap 變量將在執行時存儲來自 Wasm 的所有可引用的 Javascript 值。
如果我們再看一下生成的 JavaScript,我們可以看到被導出的函數 foo 接受一個任意的參數 arg0foo 函數調用 addBorrowedObject ,將其傳遞到 arg0addBorrowedObject 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

file

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-packpkg 目錄的內容複製到我們提供 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/

如果訪問服務器的地址,我們會看到下面的頁面。

file

當我們添加我們的名字並單擊按鈕時,得到以下響應。

file

Wasm-pack (服務器端- Node.js)

現在我們已經看到了使用 html / js 和 Apache2的實際應用,讓我們繼續並創建另一個演示。 這一次是在 Node.js 的環境中,遵循 wasm-packnpm-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

file

更廣泛的應用

預計“ 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/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章