WebAssembly与JavaScript交互

一、概述

WebAssembly可以视为一种采用精简的"二进制格式"的"低等级"、"类汇编"语言。目前主流的浏览器均提供了对WebAssembly的支持,虽然WebAssembly的执行性能(它能够提供near-native的执行性能)是JavaScript无法比拟的,但是在表达能力和灵活性还是不如JavaScript,所以WebAssembly的出现并不是要取代JavaScript,而是作为JavaScript的"助手",两者配合,各自发挥自身的优势,进而开发出更高质量的Web应用。

与其说WebAssembly像汇编,不如说它更像MSIL,因为它不是面向开发者的高级语言,而是高级语言的"编译目标"。虽然.NET 的运行时CLR不能直接执行C#、VB.NET和F#这样的高级语言编写的代码,而执行执行MSIL指令,但是我们可以将前者编译成后者。与之类似,我们也可以使用C、C++、C#、Rust和AssemblyScript(面向WebAssembly的TypeScript)作为开发语言,然后将代码编译成WebAssembly。

与汇编一样,虽然WebAssembly采用的是单纯的二进制格式,但是可以转换成文本形式。不仅如此,我们还可以按照这样的文本格式来编写程序(在接下来的内容中,我们提供的WebAssembly程序均采用这样的编写方式),并利用相应的工具将其编程成WebAssembly。WebAssembly文本采用一种名为S-expression的树形结构,我们定义的内容都存在与一个具体的"节点"中,每个节点通过小括号包裹起来,子节点直接内嵌于父节点中。由于模块是WebAssembly的基本部署和加载单元,所以module总是它们的根节点,如下所示的就是一个合法的最简单的WebAssembly程序。

复制代码
(module)

为了让大家对WebAssembly文本的S-expression有一个大致的了解,我们采用这样的形势定义了一个进行整数加法运算的add函数。如下面的代码片段所示,我们定义的函数通过内嵌于module节点的(func)节点表示。它具有一些子节点,其中(export "add") 表示将这个函数以名称"add"进行导出,这意味着加载此模块的JavaScript应用可以直接调用此函数。额外三个节点(param $x i32) (param y i32) (result i32)定义了函数的签名,揭示了此函数包含两个Int32(i32)的输入参数,返回值(结果)的类型也是Int32(i32)。为了提供可读性,同时也方面传参方面,我们将参数进行了命名(x和$y)

复制代码
(module
  (func (export "add") (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.add)
)

值得一提的是,WebAssembly仅仅支持4种数据类型,分别是32和64位的整数(i32和i64)和浮点数(f32和f64),连我们最常使用的字符串类型都支持。由于字符串本质上就是一组字符序列,而一个字符本质上是采用某种编码的一段字节序列,所以WebAssembly中针对字符串的处理需要自行解决编解码的工作,所以直接以S-expression文本的形式编写WebAssembly程序并不容易。

上面说WebAssembly更像MSIL,还因为执行WebAssembly的VM是一个"堆栈机(Stack Machine)",意味着它采用"压栈"的方式传递参数。而汇编面向的是真正的机器语言,是"堆栈机(Stack Machine)"和"寄存器机(Register Machine)"的结合,不仅参数传递可以采用堆栈,也可以采用寄存器。这一点可以从add函数的实现看出来,由于最终需要执行i32.add指令,我们需要在这之前调用local.get指令将两个参数压入栈中。i32.add执行后的结果也将入栈,并成为函数返回值。

在对WebAssembly以及基于S-expression的文本形式有了基本了解之后,我们通过一个简单的例子来演示一下一个WebAssembly程序大体上如何编写,我们着重关注JavaScript应用和WebAssembly之间的功能交互。我们创建一个空的目录,并创建两个文本文件app.wat和index.html,前者代表以S-expression文本形式编写的WebAssembly程序,后者是一个空的HTML页面,我们利用它提供的JavaScript程序加载并执行编译后的WebAssembly模块。

二、WebAssembly程序(app.wat)

如下所示的是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->print。如果导入的Javascript对象通过变量imports表示,意味着imports.js.print表示的就是这个导入的对象。至于其余的四个节点(param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))同样是提供了函数的签名:个i32类型的参数,分别表示第一个操作数、第二个操作数、操作符(1,2,3,4分别代表加、减、乘、除)和执行结果。由于这个函数仅仅打印运算表达式,所以没有返回值。

复制代码

(module

;; import js func print (op1, op2, op, result)

(func $print (import "js" "print") (param $op1 i32) (param $op2 i32) (param $op i32) (param $result i32))

;; int32 add (int32 x, int32 y) (func $add (param $x i32) (param $y i32) (result i32) local.get $x local.get $y i32.add)

;; int32 sub(int32 x, int32 y) (func $sub (param $x i32) (param $y i32) (result i32) local.get $x local.get $y i32.sub)

;; int32 mul(int32 x, int32 y) (func $mul (param $x i32) (param $y i32) (result i32) local.get $x local.get $y i32.mul)

;; int32 div(int32 x, int32 y) (func $div (param $x i32) (param $y i32) (result i32) local.get $x local.get $y i32.div_u) (func $main ;; call print(1, 2, add(1,2), 1) i32.const 1 i32.const 2 i32.const 1 i32.const 1 i32.const 2 call $add call $print ;; call print(1, 2, sub(1,2), 2) i32.const 1 i32.const 2 i32.const 2 i32.const 1 i32.const 2 call $sub call $print ;; call print(1, 2, mul(1,2), 3) i32.const 1 i32.const 2 i32.const 3 i32.const 1 i32.const 2 call $mul call $print ;; call print(1, 2, div(1,2), 4) i32.const 1 i32.const 2 i32.const 4 i32.const 1 i32.const 2 call $div call $print ) (start $main) )

接下来我们定义了四个进行加、减、乘和除运算的函数add、sub、mul和div,它们与上面定义的add函数类似,不过由于缺少了(export "{funcname}")节点,所以它们仅仅是四个内部函数而已。接下来,我们定义了一个main函数,它会传入相同的参数(1、2)调用上述4个函数,并调用导入的print方法将包含结果的运算表达式打印出来。虽然命名为main,但是它也仅仅是一个普通的函数而已,所以我们需要利用(start $main)节点将其作为入口函数,这样它就会在加载的时候自动执行了。

三、编译生成app.wasm

以文本形式编写的WebAssembly程序需要编译成二进制模块才能被加载执行,这里我们使用的wat2wasm这个工具,这个工具可以从这里下载。除了将wat文件转化成wasm文件的wat2wasm,下载包里还包含了其他一些有用的工具,比如进行反向操作的wasm2wat。上面编写的app.wat文件利用如下的命令就可以编译生成WebAssembly目标文件app.wasm。

复制代码
wat2wasm app.wat -o app.wasm

四、JavaScript程序

如下所示的就是index.html文件的内容,我们着重关注其提供的JavaScript代码。我们首先定义被WebAssembly导入的用来输出运行表达式的函数print,然后按照导入路径js.print将其封装到一个导入对象中({"js":{"print":print}})。我们调用WebAssembly.instantiateStreaming函数以异步方式加载app.wasm模块,并创建对应的实例。具体下载app.wasm模块通过第一个参数提供的Promise(fetch("app.wasm"))完成,第二个参数代表导入对象。

复制代码
<!DOCTYPE html>
<html>
<head></head>
<body>
    <script>
        var url="app.wasm";
        var print = (op1, op2, op, result) => {
            switch (op) {
                case 1:
                    console.log(`${op1} + ${op2} = ${result}`);
                    break;
                case 2:
                    console.log(`${op1} - ${op2} = ${result}`);
                    break;
                case 3:
                    console.log(`${op1} * ${op2} = ${result}`);
                    break;
                case 4:
                    console.log(`${op1} / ${op2} = ${result}`);
                    break;
                default:
                    console.log("invalid operator.");
}
        };
        WebAssembly.instantiateStreaming(fetch("app.wasm"), {"js":{"print":print}});
    </script>
</body>
</html>

五、跑起来看看

我们采用相应的方式将当前目录发布为本地web站点,比如执行Python命令(python -m http.server),然后我们利用浏览器访问此站点的默认文件index.html,浏览器的控制台输出就会看到WebAssembly模块初始化输出的四个运算表达式。

六、调用导出函数

上面我们演示了WebAssembly模块在初始化的时候调用导入的JavaScript函数,现在我们来演示JavaScript应用如何调用从WebAssembly导出的函数,为此我们将app.wat的代码改写成如下的形式,仅仅保留4个导出的函数add、sub、mul和div。

复制代码
(module
  (func (export "add") (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.add)

  (func (export "sub")  (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.sub)

  (func (export "mul")  (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.mul)

   (func (export "div")  (param $x i32) (param $y i32) (result i32)
    local.get $x
    local.get $y
    i32.div_u)
)

index.html中的Javascript代码也做了如下的修改:我们在调用WebAssembly.instantiateStreaming函数成功加载WebAssembly模块并创建对应模块实例后,利用返回结果的instance属性得到这个模块实例。模块导出的成员都保存在该实例的exports属性返回的集合中,为此我们从中提取出导出的四个返回,并利用它们完成对应的运算后,调用console.log函数将包含结果的运算表达式输出到控制台上。由于WebAssembly模块不在需要调用导入的函数,所以调用instantiateStreaming函数的时候不需要在指定导入对象。

复制代码

<!DOCTYPE html> <html> <head></head> <body> <script> var url="app.wasm"; WebAssembly .instantiateStreaming(fetch("app.wasm")) .then(result => { var exports = result.instance.exports;
var result = exports.add(1,2); console.log(`1 + 2 = ${result}`); result = exports.sub(1,2); console.log(`1 - 2 = ${result}`); result = exports.mul(1,2); console.log(`1 * 2 = ${result}`); result = exports.div(1,2); console.log(`1 / 2 = ${result}`); }); </script> </body> </html>

程序运行之后,浏览器的控制台上依然会输出相同的结果。

相关推荐
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
光头程序员7 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
fmdpenny8 小时前
Vue3初学之商品的增,删,改功能
开发语言·javascript·vue.js
小美的打工日记8 小时前
ES6+新特性,var、let 和 const 的区别
前端·javascript·es6
涔溪9 小时前
有哪些常见的 Vue 错误?
前端·javascript·vue.js
程序猿online9 小时前
前端jquery 实现文本框输入出现自动补全提示功能
前端·javascript·jquery
Turtle11 小时前
SPA路由的实现原理
前端·javascript
HsuYang11 小时前
Vite源码学习(九)——DEV流程中的核心类(下)
前端·javascript·架构
傻小胖11 小时前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
蓝冰凌13 小时前
【整理】js逆向工程
javascript·js逆向