WebAssembly核心編程[4]: Memory

由於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,進而導致第三次擴容失敗。

image

針對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。

image

二、內容的讀寫

我們利用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模塊中讀取的內容與宿主應用設置的內容是一致的。

image

上面演示了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中讀取的內容體現在如下的輸出結果中。

image

三、內容初始化

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的文本。

image

四、多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的初始容量輸出到控制檯上,具體的輸出結果如下所示。

image

利用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存儲的內容是一致的。

image

五、批量緩衝處理

針對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。

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章