1

WebAssembly入门笔记[3]:利用Table传递引用

 7 months ago
source link: https://www.cnblogs.com/artech/p/17989165/hello_wasm_3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在《WebAssembly入门笔记[2]》中,我们介绍了如何利用Memory在作为宿主的JavaScript应用和wasm模块之间传递数据,但是Memory面向单纯二进制字节的读写在使用起来还是不太方便,此时我们会更多地用到另一个重要的对象Table。Table利用用来存储一组指定类型的对象,说得准确一点是对象的引用,所以可以读取出来直接消费。

一、利用Table存储wasm函数引用
二、执行call_indirect执行函数
三、利用Table存储JavaScript函数引用

一、利用Table存储wasm函数引用

就目前的版本来说,Table只支持funcref和externref两种引用类型,前者表示wasm原生函数,后者则用来存储宿主程序提供的任何JavaScript对象,所以如果存储JavaScript函数,Table元素的类型必需指定为externref。下面的实例演示了这样的场景:wasm模块将自身定义的函数存储在导出的Table中供宿主程序使用。

如下所示的采用WebAssembly Text(WAT)格式定义的app.wat文件的定义。我们定义了用来执行加、减、乘、除运算的四个函数,并将它们存储在导出的Table中。由于存储的是wasm函数,所以Table定义语句(table (export "table") funcref (elem $add $sub $mul $div))将元素类型设置为funcref。我们利用elem语句将四个函数的引用填充到Table中。(源代码

(module
   (func $add (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.add)
   )
   (func $sub (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.sub)
   )
   (func $mul (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.mul)
   )
   (func $div (param $op1 i32) (param $op2 i32) (result i32)
     (local.get $op1)
     (local.get $op2)
     (i32.div_u)
   )
  (table (export "table") funcref (elem $add $sub $mul $div))
)

上面的定义主要是为了解释wasm基于“堆栈”的参数传递方式,代码相对繁琐。如果切换如下所示的“嵌套模式”,就会简洁很多。(源代码

(module
   (func $add (param $op1 i32) (param $op2 i32) (result i32)
        (i32.add (local.get $op1) (local.get $op2))
   )
   (func $sub (param $op1 i32) (param $op2 i32) (result i32)
        (i32.sub (local.get $op1) (local.get $op2))
   )
   (func $mul (param $op1 i32) (param $op2 i32) (result i32)
        (i32.mul (local.get $op1) (local.get $op2))
   )
   (func $div (param $op1 i32) (param $op2 i32) (result i32)
        (i32.div_u (local.get $op1) (local.get $op2))
   )
  (table (export "table") funcref (elem $add $sub $mul $div))
)

在承载宿主应用的index.html中,在得到导出的Table对象之后,我们将存储(0-3)的位置作为参数调用其get方法得到对应的wasm函数。我们传入相同的参数(2,1)调用这四个函数。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then(results => {
                    var table = results.instance.exports.table;
                    document.getElementById("container").innerHTML =
                    `<p>2 + 1 = ${table.get(0)(2,1)}</p>`+
                    `<p>2 - 1 = ${table.get(1)(2,1)}</p>`+
                    `<p>2 * 1 = ${table.get(2)(2,1)}</p>`+
                    `<p>2 / 1 = ${table.get(3)(2,1)}</p>`;
                });
        </script>
    </body>
</html>

我们将包含结果的运算表达式格式化成HTML,所以页面加载后将会呈现出如下的输出。

image

二、执行call_indirect执行函数

对于存储在Table中的wasm函数,我们还可以按照如下的方式执行call_indirect指令间接执行它。执行call_indirect指定时需要以”类型“的形式确定待执行函数的签名,由于四个函数的签名都是一致的(两个参数和返回值类型均为i32类型),所以我们定义了一个名为$i32_i32_i32的函数类型。(源代码

(module
   (func $add (param $op1 i32) (param $op2 i32) (result i32)
        (i32.add (local.get $op1) (local.get $op2))
   )
   (func $sub (param $op1 i32) (param $op2 i32) (result i32)
        (i32.sub (local.get $op1) (local.get $op2))
   )
   (func $mul (param $op1 i32) (param $op2 i32) (result i32)
        (i32.mul (local.get $op1) (local.get $op2))
   )
   (func $div (param $op1 i32) (param $op2 i32) (result i32)
        (i32.div_u (local.get $op1) (local.get $op2))
   )
   (table funcref (elem $add $sub $mul $div))

   (type $i32_i32_i32 (func (param i32) (param  i32) (result i32)))
   (func (export "calc") (param $index i32) (param $op1 i32) (param $op2 i32) (result i32)
       (call_indirect (type $i32_i32_i32)  (local.get $op1) (local.get $op2) (local.get $index))
   )
)

我们定义了名为calc的导出函数执行存储在Table中的函数,该函数的第一个参数$index表示函数在Table中的位置,后续两个参数才是算数操作数。传入call_indirect指令的4个参数分别是函数类型、传入目标函数的参数和函数在Table中的位置。index.html中的JavaScript代码以如下的方式调用导出函数calc,所以页面会呈现出与上面相同的输出。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            WebAssembly
                .instantiateStreaming(fetch("app.wasm"))
                .then(results => {
                    var calc = results.instance.exports.calc;
                    document.getElementById("container").innerHTML =
                    `<p>2 + 1 = ${calc(0, 2 ,1)}</p>`+
                    `<p>2 - 1 = ${calc(1, 2 ,1)}</p>`+
                    `<p>2 * 1 = ${calc(2, 2 ,1)}</p>`+
                    `<p>2 / 1 = ${calc(3, 2 ,1)}</p>`;
                });
        </script>
    </body>
</html>

三、利用Table存储JavaScript函数引用

第一个实例演示了将wasm函数存储在Table中供JavaScript应用调用,那么是否可以反其道而行之,将JavaScript函数存储在Table中传入wasm模块中执行呢?答案是不可能,至少目前不可以。我们在前面提到过,包含函数在内的JavaScript对象只能以externref的形式存在在Table中,对于externref,wasm模块无法对其”解引用“,自然也不能直接对它进行消费。

但是我们可以将它们回传到作为宿主的JavaScript应用中执行,下面的代码很好地演示了这一点。这次我们选择在JavaScript应用中创建Table,并将其导入到wasm模块。如下面的代码片段所示,一并导入的还有一个被命名为$apply函数。这个函数具有三个参数,第一个参数类型为externref,表示存储在Table中的JavaScript函数,后面两个参数运算操作上。(源代码

(module
    (import "imports" "table" (table 4 externref))
    (func $apply (import "imports" "apply") (param externref) (param i32) (param i32))
    (func $calc (param $index i32) (param $op1 i32) (param $op2 i32)
         (call $apply (table.get (local.get $index)) (local.get $op1) (local.get $op2))
    )
    (func (export "calculate") (param $op1 i32) (param $op2 i32)
       (call $calc (i32.const 0) (local.get $op1) (local.get $op2))
       (call $calc (i32.const 1) (local.get $op1) (local.get $op2))
       (call $calc (i32.const 2) (local.get $op1) (local.get $op2))
       (call $calc (i32.const 3) (local.get $op1) (local.get $op2))
    )
)

JavaScript函数的执行实现在$calc函数中,它的第一个参数表示函数在Table中的位置。我们通过执行table.get指令得到存储在Table以externref形式存在的JavaScript函数,并将它和两个操作数作为参数调用导入的$apply函数。导出函数calculate调用$calc函数完成针对4中运算的执行。

导入的apply函数在index.html中以如下的形式定义。我们调用构造函数WebAssembly.Table创建了一个Table对象,并将初始化大小和元素类型设置为4和externref。我们调用Table对象的set方法将四个JavaScript函数存储在这个Table中,四个函数会执行加、减、乘、除运算并将表达式拼接在html字符串上,后者将会作为<div>的innerHTML,所以页面程序的输出还是与上面一致。

<html>
    <head></head>
    <body>
        <div id="container"></div>
        <script>
            var html = "";
            var apply = (func, op1, op2)=> func(op1, op2);
            const table = new WebAssembly.Table({ initial: 4, element: "externref" });

            table.set(0, (op1, op2)=> html += `<p>${op1} + ${op2} = ${op1 + op2}</p>`);
            table.set(1, (op1, op2)=> html += `<p>${op1} - ${op2} = ${op1 - op2}</p>`);
            table.set(2, (op1, op2)=> html += `<p>${op1} * ${op2} = ${op1 * op2}</p>`);
            table.set(3, (op1, op2)=> html += `<p>${op1} / ${op2} = ${op1 / op2}</p>`);

            WebAssembly
                .instantiateStreaming(fetch("app.wasm"), {"imports":{"table":table, "apply":apply}})
                .then(results => {
                    html = "";
                    results.instance.exports.calculate(4,2);
                    document.getElementById("container").innerHTML = html;
                });
        </script>
    </body>
</html>

WebAssembly入门笔记[1]:与JavaScript的交互
WebAssembly入门笔记[2]:利用Memory传递字节数据
WebAssembly入门笔记[3]:利用Table传递引用
WebAssembly入门笔记[4]:利用Global传递全局变量


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK