由於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 = new Int32Array(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 = new Int32Array(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 section中,data section會與Memory對象自動關聯。在如下所示的WAT程序中(app.wat),我們聲明瞭一個data section,並用它來存儲一段文本(Hello World!),文本經過UTF-8編碼後的字節將存儲在此區域中。data指令的第一個參數 (i32.const 0)表示存儲的起始位置。
(module
(data (i32.const 0) "Hello, World!")
(memory (export "memory") 1)
)
上面的WAT程序還定義並導出了一個Memory對象,利用它與data section的自動映射機制,我們可以利用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 = new Int8Array(exports.memory.buffer, 0, 13);
console.log(new TextDecoder().decode(array))
});
</script>
</body>
</html>
從如下所示的輸出結果可以看出,我們利用Memory成功讀取了存儲在data section的文本。
四、多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>
<div id="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 section對Memory的填充同樣也支持多Memory模式。如下面的代碼片段所示,我們在app.wat中定義並導出了三個Memory,隨後定義的三個data section通過後面指定的序號(默認爲0)。我們將三個data section填充爲對應的文本“foo”、“bar”和“baz”。
(module
(memory (export "memory1") 1)
(memory (export "memory2") 1)
(memory (export "memory3") 1)
(data (i32.const 0) "foo")
(data 1 (i32.const 0) "bar")
(data 2 (i32.const 0) "baz")
)
作爲宿主的index.html在獲得導出的Memory對象後,同樣將它們的緩衝區映射爲Int8Array對象,並將其解碼成字符串並輸出到控制檯上。
<html>
<head></head>
<body>
<div id="container"></div>
<script>
WebAssembly
.instantiateStreaming(fetch("app.wasm"))
.then((results) => {
var exports = results.instance.exports;
var decoder = new TextDecoder();
var array = new Int8Array(exports.memory1.buffer, 0, 3);
console.log(`memory1: ${decoder.decode(array)}`);
array = new Int8Array(exports.memory2.buffer, 0, 3);
console.log(`memory2: ${decoder.decode(array)}`);
array = new Int8Array(exports.memory3.buffer, 0, 3);
console.log(`memory3: ${decoder.decode(array)}`);
});
</script>
</body>
</html>
從三個導出的Memory中得到的字符串按照如下的形式輸出到控制檯上,可以看出它們與三個data section存儲的內容是一致的。
五、批量緩衝處理
針對Memory的操作本質上就是針對字節緩衝區的操作,但是就目前發佈的正式版本來說,相關的緩衝區操作還有待完善,不過很多都在“提案”裏面了,其中就包括針對bulk memory operations。其中涉及如下一些有用的指令,它們已經在Web Assembly最新的spec草案裏了,而且主流的瀏覽器也提供了部分支持。
- memory.init: 從指定的data section中指定一段內存片段來初始化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> <div id="container"></div> <script> WebAssembly .instantiateStreaming(fetch("app.wasm")) .then((results) => { var exports = results.instance.exports; exports.fill(0,255,2); var array = new Int8Array(exports.memory.buffer, 0, 8); array.forEach((value, index, _)=> console.log(`[${index}] = ${value}`)); exports.fill(0,256,2); var array = new Int8Array(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。