吾梦可有达 發表於 2024-1-25 08:27:00

WebAssembly入门笔记[1]:与JavaScript的交互

<p>前一阵子利用Balazor开发了一个NuGet站点,对WebAssembly进行了初步的了解,觉得挺有意思。在接下来的一系列文章中,我们将通过实例演示的方式介绍WebAssembly的一些基本概念和编程模式。首先我们先来说说什么是WebAssembly,它主要帮助我们解决什么问题。</p><p>本文演示实例下载:app1&nbsp;app2&nbsp;</p><blockquote><p>一、概述<br>
二、WebAssembly程序(app.wat)<br>
三、编译生成app.wasm<br>
四、JavaScript程序<br>
五、跑起来看看<br>
六、调用导出函数</p></blockquote><h1><font size="4">一、概述</font></h1><p>WebAssembly可以视为一种采用精简的”二进制格式”的“低等级”、“类汇编”语言。目前主流的浏览器均提供了对WebAssembly的支持,虽然WebAssembly的执行性能(它能够提供near-native的执行性能)是JavaScript无法比拟的,但是在表达能力和灵活性还是不如JavaScript,所以WebAssembly的出现并不是要取代JavaScript,而是作为JavaScript的“助手”,两者配合,各自发挥自身的优势,进而开发出更高质量的Web应用。</p><p>与其说WebAssembly像汇编,不如说它更像MSIL,因为它不是面向开发者的高级语言,而是高级语言的“编译目标”。虽然.NET 的运行时CLR不能直接执行C#、VB.NET和F#这样的高级语言编写的代码,而执行执行MSIL指令,但是我们可以将前者编译成后者。与之类似,我们也可以使用C、C++、C#、Rust和AssemblyScript(面向WebAssembly的TypeScript)作为开发语言,然后将代码编译成WebAssembly。</p><p>与汇编一样,虽然WebAssembly采用的是单纯的二进制格式,但是可以转换成文本形式。不仅如此,我们还可以按照这样的文本格式来编写程序(在接下来的内容中,我们提供的WebAssembly程序均采用这样的编写方式),并利用相应的工具将其编程成WebAssembly。WebAssembly文本采用一种名为S-expression的树形结构,我们定义的内容都存在与一个具体的“节点”中,每个节点通过小括号包裹起来,子节点直接内嵌于父节点中。由于模块是WebAssembly的基本部署和加载单元,所以module总是它们的根节点,如下所示的就是一个合法的最简单的WebAssembly程序。</p><div class="cnblogs_code"><pre>(module)</pre></div><p>为了让大家对WebAssembly文本的S-expression有一个大致的了解,我们采用这样的形势定义了一个进行整数加法运算的add函数。如下面的代码片段所示,我们定义的函数通过内嵌于module节点的(func)节点表示。它具有一些子节点,其中(export "<span style="color: rgba(139, 0, 0, 1)">add</span>") 表示将这个函数以名称“add”进行导出,这意味着加载此模块的JavaScript应用可以直接调用此函数。额外三个节点(param $x i32) (param $y i32) (result i32)定义了函数的签名,揭示了此函数包含两个Int32(i32)的输入参数,返回值(结果)的类型也是Int32(i32)。为了提供可读性,同时也方面传参方面,我们将参数进行了命名($x和$y)</p><div class="cnblogs_code"><pre>(module
(func (export "<span style="color: rgba(139, 0, 0, 1)">add</span>") (param $x i32) (param $y i32) (result i32)
    local.<span style="color: rgba(0, 0, 255, 1)">get</span> $x
    local.<span style="color: rgba(0, 0, 255, 1)">get</span> $y
    i32.add)
)</pre></div><p>值得一提的是,WebAssembly仅仅支持4种数据类型,分别是32和64位的整数(i32和i64)和浮点数(f32和f64),连我们最常使用的字符串类型都不支持。由于字符串本质上就是一组字符序列,而一个字符本质上是采用某种编码的一段字节序列,所以WebAssembly中针对字符串的处理需要自行解决编解码的工作,所以直接以S-expression文本的形式编写WebAssembly程序并不容易。</p><p>上面说WebAssembly更像MSIL,还因为执行WebAssembly的VM是一个“堆栈机(Stack Machine)”,意味着它采用“压栈”的方式传递参数。而汇编面向的是真正的机器语言,是“堆栈机(Stack Machine)”和“寄存器机(Register Machine)”的结合,不仅参数传递可以采用堆栈,也可以采用寄存器。这一点可以从add函数的实现看出来,由于最终需要执行i32.add指令,我们需要在这之前调用local.get指令将两个参数压入栈中。i32.add执行后的结果也将入栈,并成为函数返回值。</p><p>在对WebAssembly以及基于S-expression的文本形式有了基本了解之后,我们通过一个简单的例子来演示一下一个WebAssembly程序大体上如何编写,我们着重关注JavaScript应用和WebAssembly之间的功能交互。我们创建一个空的目录,并创建两个文本文件app.wat和index.html,前者代表以S-expression文本形式编写的WebAssembly程序,后者是一个空的HTML页面,我们利用它提供的JavaScript程序加载并执行编译后的WebAssembly模块。</p><h1><font size="4">二、WebAssembly程序(app.wat)</font></h1><p>如下所示的是app.wat的内容,看起来很长,好在我们现在对S-expression有了基本的了解,所以理解起来没有什么问题。模块的第一个节点(func $print (import "js" "print") (param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同样是定义了一个函数,我们将其命名为$print, 子节点(import "js" "print") 表明该函数是从作为宿主的Javascript应用中导入的,具体的路径为js-&gt;print。如果导入的Javascript对象通过变量imports表示,意味着imports.js.print表示的就是这个导入的对象。至于其余的四个节点(param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同样是提供了函数的签名:个i32类型的参数,分别表示第一个操作数、第二个操作数、操作符(1,2,3,4分别代表加、减、乘、除)和执行结果。由于这个函数仅仅打印运算表达式,所以没有返回值。</p><div class="cnblogs_code"><pre><p>(module</p><p>;; import js func print (op1, op2, op, result)</p><p>&nbsp; (func $print (import "js" "print") (param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))</p><p>;; int32 add (int32 x, int32 y)
(func $<span style="color: rgba(0, 0, 255, 1)">add</span> (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.<span style="color: rgba(0, 0, 255, 1)">add</span>)
</p><p>;; int32 sub(int32 x, int32 y)
(func $sub (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.sub)</p><p>
;; int32 mul(int32 x, int32 y)
(func $<span style="color: rgba(0, 0, 255, 1)">mul</span> (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.<span style="color: rgba(0, 0, 255, 1)">mul</span>)</p><p>
   ;; int32 div(int32 x, int32 y)
   (func $<span style="color: rgba(0, 0, 255, 1)">div</span> (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.div_u)

   (func $main
    ;; call print(1, 2, <span style="color: rgba(0, 0, 255, 1)">add(1,2), 1)</span>
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1

    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    <span style="color: rgba(0, 0, 255, 1)">call</span> $<span style="color: rgba(0, 0, 255, 1)">add</span>
    <span style="color: rgba(0, 0, 255, 1)">call</span> $print

    ;; call print(1, 2, <span style="color: rgba(0, 0, 255, 1)">sub(1,2), 2)</span>
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2

    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    <span style="color: rgba(0, 0, 255, 1)">call</span> $sub
    <span style="color: rgba(0, 0, 255, 1)">call</span> $print

    ;; call print(1, 2, <span style="color: rgba(0, 0, 255, 1)">mul(1,2), 3)</span>
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 3

    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    <span style="color: rgba(0, 0, 255, 1)">call</span> $<span style="color: rgba(0, 0, 255, 1)">mul</span>
    <span style="color: rgba(0, 0, 255, 1)">call</span> $print

    ;; call print(1, 2, <span style="color: rgba(0, 0, 255, 1)">div(1,2), 4)</span>
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 4

    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 1
    i32.<span style="color: rgba(0, 0, 255, 1)">const</span> 2
    <span style="color: rgba(0, 0, 255, 1)">call</span> $<span style="color: rgba(0, 0, 255, 1)">div</span>
    <span style="color: rgba(0, 0, 255, 1)">call</span> $print
   )

   (start $main)
)</p></pre></div><p>接下来我们定义了四个进行加、减、乘和除运算的函数add、sub、mul和div,它们与上面定义的add函数类似,不过由于缺少了(export “{funcname}”)节点,所以它们仅仅是四个内部函数而已。接下来,我们定义了一个$main函数,它会传入相同的参数(1、2)调用上述4个函数,并调用导入的print方法将包含结果的运算表达式打印出来。虽然命名为$main,但是它也仅仅是一个普通的函数而已,所以我们需要利用(start $main)节点将其作为入口函数,这样它就会在加载的时候自动执行了。</p><h1><font size="4">三、编译生成app.wasm</font></h1><p>以文本形式编写的WebAssembly程序需要编译成二进制模块才能被加载执行,这里我们使用的wat2wasm这个工具,这个工具可以从这里下载。除了将wat文件转化成wasm文件的wat2wasm,下载包里还包含了其他一些有用的工具,比如进行反向操作的wasm2wat。上面编写的app.wat文件利用如下的命令就可以编译生成WebAssembly目标文件app.wasm。</p><div class="cnblogs_code"><pre>wat2wasm app.wat -o app.wasm</pre></div><h1><font size="4">四、JavaScript程序</font></h1><p>如下所示的就是index.html文件的内容,我们着重关注其提供的JavaScript代码。我们首先定义被WebAssembly导入的用来输出运行表达式的函数print,然后按照导入路径js.print将其封装到一个导入对象中({"<span style="color: rgba(139, 0, 0, 1)">js</span>":{"<span style="color: rgba(139, 0, 0, 1)">print</span>":print}})。我们调用WebAssembly.instantiateStreaming函数以异步方式加载app.wasm模块,并创建对应的实例。具体下载app.wasm模块通过第一个参数提供的Promise(fetch("<span style="color: rgba(139, 0, 0, 1)">app.wasm</span>"))完成,第二个参数代表导入对象。</p><div class="cnblogs_code"><pre>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;&lt;/head&gt;
&lt;body&gt;
    &lt;script&gt;
      var url="<span style="color: rgba(139, 0, 0, 1)">app.wasm</span>";
      var print = (op1, op2, op, result) =&gt; {
            switch (op) {
                case 1:
                  console.log(`${op1} + ${op2} = ${result}`);
                  <span style="color: rgba(0, 0, 255, 1)">break</span>;
                case 2:
                  console.log(`${op1} - ${op2} = ${result}`);
                  <span style="color: rgba(0, 0, 255, 1)">break</span>;
                case 3:
                  console.log(`${op1} * ${op2} = ${result}`);
                  <span style="color: rgba(0, 0, 255, 1)">break</span>;
                case 4:
                  console.log(`${op1} / ${op2} = ${result}`);
                  <span style="color: rgba(0, 0, 255, 1)">break</span>;
                <span style="color: rgba(0, 0, 255, 1)">default</span>:
                  console.log("<span style="color: rgba(139, 0, 0, 1)">invalid operator.</span>");
}
      };
      WebAssembly.instantiateStreaming(fetch("<span style="color: rgba(139, 0, 0, 1)">app.wasm</span>"), {"<span style="color: rgba(139, 0, 0, 1)">js</span>":{"<span style="color: rgba(139, 0, 0, 1)">print</span>":print}});
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre></div><h1><font size="4">五、跑起来看看</font></h1><p>我们采用相应的方式将当前目录发布为本地web站点,比如执行Python命令(<code>python -m http.server</code>),然后我们利用浏览器访问此站点的默认文件index.html,浏览器的控制台输出就会看到WebAssembly模块初始化输出的四个运算表达式。</p><p><img width="442" height="359" title="image" style="border: 0 currentColor; border-image: none; display: inline; background-image: none" alt="image" src="https://img2023.cnblogs.com/blog/19327/202401/19327-20240123160904679-1551756660.png" border="0"></p><h1><font size="4">六、调用导出函数</font></h1><p>上面我们演示了WebAssembly模块在初始化的时候调用导入的JavaScript函数,现在我们来演示JavaScript应用如何调用从WebAssembly导出的函数,为此我们将app.wat的代码改写成如下的形式,仅仅保留4个导出的函数add、sub、mul和div。</p><div class="cnblogs_code"><pre>(module
(func (export "<span style="color: rgba(139, 0, 0, 1)">add</span>") (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.<span style="color: rgba(0, 0, 255, 1)">add</span>)

(func (export "<span style="color: rgba(139, 0, 0, 1)">sub</span>")(param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.sub)

(func (export "<span style="color: rgba(139, 0, 0, 1)">mul</span>")(param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.<span style="color: rgba(0, 0, 255, 1)">mul</span>)

   (func (export "<span style="color: rgba(139, 0, 0, 1)">div</span>")(param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.div_u)
)</pre></div><p>index.html中的Javascript代码也做了如下的修改:我们在调用WebAssembly.instantiateStreaming函数成功加载WebAssembly模块并创建对应模块实例后,利用返回结果的instance属性得到这个模块实例。模块导出的成员都保存在该实例的exports属性返回的集合中,为此我们从中提取出导出的四个返回,并利用它们完成对应的运算后,调用console.log函数将包含结果的运算表达式输出到控制台上。由于WebAssembly模块不在需要调用导入的函数,所以调用instantiateStreaming函数的时候不需要在指定导入对象。</p><div class="cnblogs_code"><pre><p>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;&lt;/head&gt;
&lt;body&gt;
    &lt;script&gt;
      var url="<span style="color: rgba(139, 0, 0, 1)">app.wasm</span>";
      WebAssembly
            .instantiateStreaming(fetch("<span style="color: rgba(139, 0, 0, 1)">app.wasm</span>"))
            .then(result =&gt; {
                var exports = result.<span style="color: rgba(0, 0, 255, 1)">instance</span>.exports;<br>
                var result = exports.<span style="color: rgba(0, 0, 255, 1)">add</span>(1,2);
                console.log(`1 + 2 = ${result}`);

                result = exports.sub(1,2);
                console.log(`1 - 2 = ${result}`);

                result = exports.<span style="color: rgba(0, 0, 255, 1)">mul</span>(1,2);
                console.log(`1 * 2 = ${result}`);

                result = exports.<span style="color: rgba(0, 0, 255, 1)">div</span>(1,2);
                console.log(`1 / 2 = ${result}`);
            });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</p></pre></div><p>程序运行之后,浏览器的控制台上依然会输出相同的结果。</p><p><img width="442" height="359" title="image" style="border: 0 currentColor; border-image: none; display: inline; background-image: none" alt="image" src="https://img2023.cnblogs.com/blog/19327/202401/19327-20240123160904679-1551756660.png" border="0"></p><p><br></p><p>WebAssembly入门笔记:与JavaScript的交互<br>WebAssembly入门笔记:利用Memory传递字节数据<br>WebAssembly入门笔记:利用Table传递引用<br>WebAssembly入门笔记:利用Global传递全局变量<br><br>WebAssembly核心编程:wasm模块实例化的N种方式<br>WebAssembly核心编程:类型系统<br>WebAssembly核心编程: Module 与 Instance<br>WebAssembly核心编程: Memory</p><br><br>
来源:https://www.cnblogs.com/artech/p/17982714/hello_wasm_1
頁: [1]
查看完整版本: WebAssembly入门笔记[1]:与JavaScript的交互