由於Memory儲存的是單純的二進制字節,所以原則上我們可以用來它作為媒介,在wasm模組和陣列程式之間傳遞任何型別的數據。在JavaScript API中,Memory透過WebAssembly.Memory型別表示,我們一般將它內部的緩沖區對映相應型別的陣列進行處理。WebAssembly也提供了相應的指令來提供針對Memory的讀、寫、擴容等操作(原始碼從 這裏 下載)。
一、容量限制與擴容
二、內容的讀寫
三、內容初始化
四、多Memory支持
五、批次記憶體處理
一、容量限制與擴容
Memory本質上一個可以擴容的記憶體緩沖區,在初始化的時候我們必需指定該緩衝區的初始大小,單位為Page(64K)。如果沒有指定最大允許的大小,意味著它可以無限「擴容」。WebAssembly.Memory的例項方法grow用來擴容,作為參數的整數表示擴大的Page數量,其返回值表示擴容之前的容量。在如下這個演示例項中,我們在一個Web頁面index.html初始化的時候建立了一個WebAssembly.Memory物件,並將其初始和最大尺寸設定為1和3。
<html>
<head></head>
<body>
<script>
var memory= new WebAssembly.Memory({ initial: 1, maximum: 3});
var grow = (size) => {
try{
console.log(`memory.grow(${size}) = ${memory.grow(size)}`);
}
catch(error){
console.log(error);
}
};
grow(1);
grow(1);
grow(1);
</script>
</body>
</html>
grow函式對這個WebAssembly.Memory試試擴容。我們先後3次呼叫次函式(增擴的容量為1),並將其返回值打印到控制台上。從如下的輸出可以看出,建立的Memory的初始容量為1,經過兩次擴容後,它的容量達到執行的最大容量3,進而導致第三次擴容失敗。
針對Memory的擴容也利用利用wasm的memory.grow指令來完成,該指令的輸入參數依然是擴大的容量,返回的依然是擴容前的大小。如果超過設定的最大容量,該指令會返回-1。wasm還提供了memory.size指令返回Memory當前的容量。在如下這個wat檔(app.wat)中,我們依然定義了一個初始和最大容量為1和3的Memory,兩個匯出的函式size和grow分別返回它當前容量和對它實施擴容。
(module
(memory 1 3)
(func (export"size") (result i32)
(memory.size)
)
(func (export"grow") (param $size i32) (result i32)
(memory.grow (local.get $size))
)
)
在作為宿主的index.html頁面中,我們呼叫匯出的grow函式(增擴的容量為1)對Memory實施3次擴容,並呼叫size函式輸出它當前的容量。
<html>
<head></head>
<body>
<script>
var memory= new WebAssembly.Memory(
{ initial: 1, maximum: 3});
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then((results) => {
var exports = results.instance.exports;
var grow = (size)=>console.log(
`memory.grow(${size}) = ${exports.grow(size)}`);
grow(1);
grow(1);
grow(1);
console.log(`memory.size() = ${exports.size()}`);
});
</script>
</body>
</html>
從如下的輸出可以看出,前兩次成功擴容將Memory的容量增擴到最大容量3,導致最後一次擴容失敗,返回-1。
二、內容的讀寫
我們利用Memory對其管理的緩沖區按照純字節的形式進行讀寫。WebAssembly針對具體的數據型別(i32/i64/f32/f64)提供一系列的load和store指令讀寫Memory的內容,具體的指令如下(8/16/32代表讀寫位數,s和u分別表示有符號和無符號整數):
{i32|i64|f32|f64}.load
{i32|i64}.load8_s
{i32|i64}.load8_u
{i32|i64}.load16_s
{i32|i64}.load16_u
{i32|i64}.load32_s
{i32|i64}.load32_u
{i32|i64|f32|f64}.store
{i32|i64}}.store8
{i32|i64}.store16
i64.store32
如下所示的WAT程式(app.wat)檔利用兩個匯出的函式store和load對匯入的Memory實施寫入和讀取。我們假設儲存的數據型別均為i32,所以store函式在執行i32.store指令的時候,代表寫入序號的第一個參數需要乘以4,作為指令的第一個參數(代表寫入的起始位置)。load函式在執行i32.load指令的時候也需要做類似的處理。
(module
(memory (import "imports""memory") 1)
(func (export"store") (param $index i32) (param $value i32)
(i32.store (i32.mul (local.get $index) (i32.const 4)) (local.get $value))
)
(func (export"load") (param $index i32) (result i32)
(i32.load (i32.mul (local.get $index) (i32.const 4)))
)
)
作為陣列套用的JavaScript程式可以將Memory物件的緩沖區對映為指定元素型別的陣列,並以陣列的形式對其進行讀寫。在我們的演示例項中,作為宿主套用的index.html頁面呼叫建構函式建立了一個WebAssembly.Memory物件,並將其buffer內容對應的緩沖區對映成一 個Int32Array物件,並將前三個元素賦值為1、2和3。我們將Memory物件匯入到載入的app.wasm模組中後,呼叫匯出的load函式以i32型別將Memory中儲存的12個字節讀出來。
<html>
<head></head>
<body>
<script>
var memory= new WebAssembly.Memory(
{ initial: 1, maximum: 3});
var array = newInt32Array(memory.buffer);
array[0] = 1;
array[1] = 2;
array[2] = 3;
WebAssembly
.instantiateStreaming(
fetch("app.wasm"),
{"imports":{"memory":memory}})
.then((results) => {
var exports = results.instance.exports;
console.log(`load (0) = ${exports.load(0)}`);
console.log(`load (1) = ${exports.load(1)}`);
console.log(`load (2) = ${exports.load(2)}`);
});</script>
</body>
</html>
從如下所示的三個輸出結果可以看出,wasm模組中讀取的內容與宿主套用設定的內容是一致的。
上面演示了wasm模組讀取宿主套用寫入Memory的內容,我們接下來透過修改index.html的內容呼叫匯出的store函式往Memory中寫入相同的內容,然後在宿主JavaScript程式中利用對映的陣列將其讀出來。
<html>
<head></head>
<body>
<script>
var memory= new WebAssembly.Memory(
{ initial: 1, maximum: 3});
var array = newInt32Array(memory.buffer);
WebAssembly
.instantiateStreaming(
fetch("app.wasm"),
{"imports":{"memory":memory}})
.then((results) => {
var exports = results.instance.exports;
exports.store(0, 1);
exports.store(1, 2);
exports.store(2, 3);
console.log(`array[0] = ${array[0]}`);
console.log(`array[1] = ${array[0]}`);
console.log(`array[2] = ${array[0]}`);
});</script>
</body>
</html>
宿主程式從Memory中讀取的內容體現在如下的輸出結果中。
三、內容初始化
store指令一次只能往Memory物件的緩存區寫入指定數據物件承載的全部或者部份字節,如果需要在初始化一長串字節(比如一大段文本),可以將其儲存到data p中,data p會與Memory物件自動關聯。在如下所示的WAT程式中(app.wat),我們聲明了一個data p,並用它來儲存一段文本(Hello World!),文本經過UTF-8編碼後的字節將儲存在此區域中。data指令的第一個參數 (i32.const 0)表示儲存的起始位置。
(module
(data (i32.const0) "Hello, World!")
(memory (export"memory") 1)
)
上面的WAT程式還定義並匯出了一個Memory物件,利用它與data p的自動對映機制,我們可以利用Memory來讀取儲存的文本。在如下所示作為宿主套用的index.html中,我們提取出匯出的Memory物件,並將其緩沖區對映為一個Int8Array物件,然後利用TextDescorder將其解碼成文本並輸出。
<html>
<head></head>
<body>
<script>
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then((results) => {
var exports = results.instance.exports;
var array = newInt8Array(exports.memory.buffer, 0, 13);
console.log(new TextDecoder().decode(array))
});
</script>
</body>
</html>
從如下所示的輸出結果可以看出,我們利用Memory成功讀取了儲存在data p的文本。
四、多Memory支持
WebAssembly目前的正式版本只支持「單Memory模式」,也就是說一個wasm只維護一個單一的Memory物件。雖然「多Memory」目前還處於實驗階段,但是目前主流的瀏覽器還是支持的,WAT程式中針對多Memory的程式又如何編寫呢?在如下這個演示程式中,我們定義了4個Memory,並分別將其命名為$m0、$m1、$m2和$m3,其中前兩個為匯入物件,後兩個為匯出物件。我們將這4個Memory物件的初始化容量分別設定為1、2、3、4,匯出的size函式用來返回指定Memory物件當前的容量。
(module
(memory $m0 (import "imports""memory1") 1)
(memory $m1 (import "imports""memory2") 2)
(memory $m2 (export "memory3") 3)
(memory $m3 (export "memory4") 4)
(func (export "size") (param $memory i32) (result i32)
(local $size i32)
(local.set $size (memory.size $m0))
(i32.eq (local.get $memory) (i32.const 1))
if
(local.set $size (memory.size $m1))
end
(i32.eq (local.get $memory) (i32.const 2))
if
(local.set $size (memory.size $m2))
end
(i32.eq (local.get $memory) (i32.const 3))
if
(local.set $size (memory.size $m3))
end
(local.get $size)
)
)
size函式利用第一個參數(0、1、2、3)來確定具體的Memory物件,在執行memory.size的時候, 我們會附加上Memory的命名(預設為第一個Memory)。除了指定給定的別名,也可以按照如下的方式使用Memory的序號(0、1、2和3),其他指令的使用與之類似。
(module
(memory (import "imports""memory1") 1)
(memory (import "imports""memory2") 2)
(memory (export "memory3") 3)
(memory (export "memory4") 4)
(func (export "size") (param $memory i32) (result i32)
(local $size i32)
(local.set $size (memory.size 0))
(i32.eq (local.get $memory) (i32.const 1))
if
(local.set $size (memory.size 1))
end
(i32.eq (local.get $memory) (i32.const 2))
if
(local.set $size (memory.size 2))
end
(i32.eq (local.get $memory) (i32.const 3))
if
(local.set $size (memory.size 3))
end
(local.get $size)
)
)
在執行wat2wasm對app.wat進行編譯的時候,我們需要手工添加命令列開關--enable-multi-memory以提供針對「多Memory」的支持(wat2wasm app.wat -o app.wasm --enable-multi-memory)。
<html>
<head></head>
<body>
<divid="container"></div>
<script>
var memory1 = new WebAssembly.Memory({initial:1});
var memory2 = new WebAssembly.Memory({initial:2});
WebAssembly
.instantiateStreaming(
fetch("app.wasm"),
{"imports":{"memory1":memory1, "memory2":memory2}})
.then((results) => {
var exports = results.instance.exports;
console.log(`memory1.size = ${exports.size(1)}`);
console.log(`memory2.size = ${exports.size(2)}`);
console.log(`memory3.size = ${exports.size(3)}`);
console.log(`memory4.size = ${exports.size(4)}`);
});</script>
</body>
</html>
在如上所示的作為宿主的index.html中,我們利用呼叫匯出的size函式將四個Memory的初始容量輸出到控制台上,具體的輸出結果如下所示。
利用data p對Memory的填充同樣也支持多Memory模式。如下面的程式碼片段所示,我們在app.wat中定義並匯出了三個Memory,隨後定義的三個data p透過後面指定的序號(預設為0)。我們將三個data p填充為對應的文本「foo」、「bar」和「baz」。
(module
(memory (export"memory1") 1)
(memory (export"memory2") 1)
(memory (export"memory3") 1)
(data (i32.const0) "foo")
(data 1 (i32.const0) "bar")
(data 2 (i32.const0) "baz")
)
作為宿主的index.html在獲得匯出的Memory物件後,同樣將它們的緩沖區對映為Int8Array物件,並將其解碼成字串並輸出到控制台上。
<html>
<head></head>
<body>
<divid="container"></div>
<script>
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then((results) => {
var exports = results.instance.exports;
var decoder = new TextDecoder();
var array = newInt8Array(exports.memory1.buffer, 0, 3);
console.log(`memory1: ${decoder.decode(array)}`);
array = newInt8Array(exports.memory2.buffer, 0, 3);
console.log(`memory2: ${decoder.decode(array)}`);
array = newInt8Array(exports.memory3.buffer, 0, 3);
console.log(`memory3: ${decoder.decode(array)}`);
});</script>
</body>
</html>
從三個匯出的Memory中得到的字串按照如下的形式輸出到控制台上,可以看出它們與三個data p儲存的內容是一致的。
五、批次緩沖處理
針對Memory的操作本質上就是針對字節緩沖區的操作,但是就目前釋出的正式版本來說,相關的緩沖區操作還有待完善,不過很多都在「提案」裏面了,其中就包括針對 bulk memory operations 。其中涉及如下一些有用的指令,它們已經在Web Assembly 最新的spec草案 裏了,而且主流的瀏覽器也提供了部份支持。
memory.init: 從指定的data p中指定一段記憶體片段來初始化Memory;
memory.fill: 利用指定的字節內容來填充Memory的一段連續的緩沖區;
memory.copy:連續記憶體片段的拷貝;
接下來我們來演示一下針對memory.fill指令的套用。在如下所示的WAT程式中(app.wat),我們定義並匯出了一個Memory物件。匯出的fill函式呼叫memory.fill指令往匯出的這個Memory指定的位置填充指定數量($count)的值($value)。
(module
(memory (export"memory") 1)
(func
(export"fill")
(param $offset i32)
(param $value i32)
(param $count i32)
(memory.fill
(local.get $offset)
(local.get $value)
(local.get $count))
)
)
在作為宿主的index.html頁面中,我們兩次呼叫匯出的fill函式從Memory緩沖區的初始位置開始填充兩個值255和266。
<html>
<head></head>
<body>
<divid="container"></div>
<script>
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then((results) => {
var exports = results.instance.exports;
exports.fill(0,255,2);
var array = newInt8Array(
exports.memory.buffer, 0, 8);
array.forEach((value, index, _)
=>console.log(`[${index}] = ${value}`));
exports.fill(0,256,2);
var array = newInt8Array(
exports.memory.buffer, 0, 8);
array.forEach((value, index, _)
=>console.log(`[${index}] = ${value}`));
});</script>
</body>
</html>
我們將緩沖區對映為一個Int8Array物件,並將其前8個字節輸出到控制台上。作為memory.fill指令的第二個參數,表示填充值得數據型別應該是Byte,但是wasm支持的整數型別只有i32和i64,所以這裏的參數型別只能表示為i32,但是該指令只會使用指定值的低8位元。這一點可以從輸出結果得到印證:第一次呼叫指定的值是255(00 00 00 FF,轉換成Int8就是-1),最終只會填充前面2個字節(FF FF)。第二次呼叫指定的值為256(00 00 01 00),所以填充的前兩個字節為00 00。