關於字串


某些程度上,在〈建立記憶體〉中,就已經提示過如何實作字串了,當時的範例使用 (data (i32.const 0) "\48\65\6C\6C\6F"),在記憶體中置入了五個位元組,若以無號 8 位元整數來看,正好是 "Hello" 五個字元的 Unicode 號碼。

在 JavaScript 環境中,從匯出的記憶體中取得 ArrayBuffer,取得前五個位元組,使用 String.fromCharCode 轉成了 JavaScript 字串。

String.fromCharCode 是依 UniCode 號碼來判斷,而 "\48\65\6C\6C\6F" 的寫法,是用來指定每個位元組的值,英文字母 "Hello" 正好是一個位元組就可以表示各自的 Unicode 號碼,如果是中文呢?例如,'良' 這個字元?

的 Unicode 號碼是 33391,使用十六進位表示是 826F,不過別忘了,WebAssembly 採用 Little-endian,低位元組在前,高位元組在後,因此要寫成 "6F82"

例如,要在 JavaScript 中,可以使用 String.fromCharCode 產生 "良葛格" 字串,'葛' 的 Unicode 號碼是 33883,十六進位表示是 845B,'格' 的 Unicode 號碼是 26684,十六進位表示是 683C,這時就要寫為:

(module
    (memory $mem 1)
    (data (i32.const 0) "\6F\82\5B\84\3C\68")
    (export "mem" (memory $mem))
    (func $nope)
)

在 JavaScript 中,由於這三個字的 Unicode 號碼,都必須使用兩個位元組表示,因此要使用 Uint16Array

WebAssembly.instantiateStreaming(fetch('program.wasm'))
            .then(prog => {
                console.log(String.fromCharCode.apply(null, 
                    new Uint16Array(prog.instance.exports.mem.buffer, 0, 48)
                ));
            });

你可以在使用 data 時,直接以字元來表示記憶體中該有的資料,例如:

(data (i32.const 0) "Hello")

這時要注意,Wat 檔案要使用 UTF-8 編碼,因此,編譯時讀取 Wat 檔案時,讀到 "Hello" 的部份,位元組會是 "\48\65\6C\6C\6F",就每個位元組來看,剛好就是 "Hello" 中五個字元的 Unicode 號碼,這是因為 UTF-8 在英數字元部份,與原本 Ascii 編碼一致的關係。

如果你寫中文的話會如何呢?

(data (i32.const 0) "良葛格")

因為使用 UTF-8 儲存 Wat 檔案,"良葛格" 這部份被儲存為 "\e8\89\af\e8\91\9b\e6\a0\bc",也就是相當於寫為:

(data (i32.const 0) "\e8\89\af\e8\91\9b\e6\a0\bc"))

UTF-8 基本上使用三個位元組來表示編碼中文字元,例如,"\e8\89\af"'良' 這個字儲存時,採用的 UTF-8 編碼,不要將編碼與 Unicode 號碼搞錯了,同一個 Unicode 字元,可以有 UTF-8、UTF-16、UTF-32 等編碼方式,例如, 的 Unicode 號碼是 33391,而它的 UTF-8 編碼是 "\e8\89\af"

也就是這相當於:

(module
    (memory $mem 1)
    (export "mem" (memory $mem))
    (func $main
        ;; 良
        (i32.store8 (i32.const 0) (i32.const 0xE8))
        (i32.store8 (i32.const 1) (i32.const 0x89))
        (i32.store8 (i32.const 2) (i32.const 0xAF))
        ;; 葛
        (i32.store8 (i32.const 3) (i32.const 0xE8))
        (i32.store8 (i32.const 4) (i32.const 0x91))
        (i32.store8 (i32.const 5) (i32.const 0x9B))
        ;; 格
        (i32.store8 (i32.const 6) (i32.const 0xE6))
        (i32.store8 (i32.const 7) (i32.const 0xA0))
        (i32.store8 (i32.const 8) (i32.const 0xBC))                
    )
    (start $main)
)

結論就是,這時不能直接用 String.fromCharCode 來解釋記憶體中的資料,有個方便的 TextDecoder API 可以解決這件事:

WebAssembly.instantiateStreaming(fetch('program.wasm'))
            .then(prog => {
                var bytes = new Uint8Array(prog.instance.exports.mem.buffer, 0, 9);
                var string = new TextDecoder('utf8').decode(bytes);
                console.log(string);
            });