简单学习WebAssembly和JavaScript在谷歌V8引擎中的运行过程(二)

  • Post author:
  • Post category:java




创建WebAssembly模块(二)




前言

本章节记录了如何将C/C++代码编译成WebAssembly模块。生成模块的过程中使用了Emscripten工具包,这是目前来说比较完整的可以进行这一操作的工具包。同时由于WebAssembly文件是二进制格式,分析难度较大,我们利用Microsoft的开放源代码编辑器Visual Studio Code的扩展包——WebAssembly工具包,这个工具包可以预览WebAssembly二进制文件并突出显示文本表示的语法。

根据不同的目标,可以选择用三种方式创建WebAssembly模块。

第一种就是让Emscripten生成WebAssembly模块并进行实例化,同时生成模板HTML文件,这种方式我们可以直接让WebAssembly的结果在网页中显示出来。

第二种是让Emscripten生成WebAssembly模块和JavaScript模板文件,但不生成HTML文件,这个方法经常用于产品开发,只需要在HTML文件中引用这个JavaScript文件,就可以自动进行WebAssembly的编译和实例化。

第三种是只生成WebAssembly模块,这种方式是为了在动态链接时可以加载两个或以上的模块。下面我们从这三个方式来创建WebAssembly模块。



一、设备环境与工具准备

使用的系统环境为win10系统,使用VScode编写C/C++示例代码,Git使用v2.35.1版本,python使用2.7版本。

首先要保证使用的计算机已经安装了Python 2.7.18或更高级版本和Git。由于Emscripten是一组基于Python 2的脚本,Git是当今最流行的版本控制软件,它包含了许多高级工具。这两者在使用Emscripten工具包时必不可少。

具体的安装流程请参考这篇博客。


**Windows10中Emscripten 安装详解

**



二、用Emscripten编译C文件并使用HTML模板



1.编译求素数的C代码

在计算方面,WebAssembly的性能会优于JavaScript,本次我们利用WebAssembly计算1-10000的素数,并把这些素数显示到网页中。首先编写C代码,命名为primes.c。

c的代码如下:(编译和运行将在cmd中进行,此处只需选择自己熟悉的开发软件编写)

#include <stdlib.h>
#include <stdio.h>

int Isprime(int value)
{
    if ( value == 2)  return 1; //2是素数
    if ( value <= 1 || value % 2 == 0)  return 0; //能被2整除的肯定是素数
    for(int r = 3; (r * r) <= value; r += 2)  /*从3开始,到这个数的平方根为止,
                                                如果能被某个奇数整除,那肯定是素数*/
    {
        if( value % r == 0) return 0;
    }
    return 1;
}

int main()
{
    int start = 1;
    int end = 10000;
    printf("%d到%d之间的素数有:\n",start ,end );
    for(int i = start ; i <= end ; i += 2)
    {
        if( Isprime(i))
        {printf("%d," , i);}
    }
    printf("\n");
    return 0;
}



2.使用emscripten生成WebAssembly模块,JavaScript胶水代码和html文件

在cmd窗口中,使用emcc命令将C代码编译为WebAssembly模块。emcc命令接受若干输入和标记,首先包含输入文件的路径和文件名,当然如果提前打开所在文件就不用加路径。如果没有包含输出文件,那么默认会生成两个文件a.out.wasm和a.out.js,并不会生成HTML文件,如果需要生成HTML模板,那就必须使用-o标记(小写字母o),然后加上想要的文件名,此时输出文件名后缀必须为.html。此外,Emscripten还给出了几个优化标记,也可以按需使用。

我这里使用的命令为:emcc prime.c –o prime.html。

运行命令后会生成三个文件:prime.wasm,prime.js,prime.html

其中,prime.wasm文件就是经过Emscripten编译的WebAssembly模块,prime.js文件则是自动生成的为WebAssembly模块实例化的文件,prime.html文件就是网页模板。

利用:emrun –no_browser –port 8080 prime.html命令,启动本地服务器,


提示:需要注意的是,不能直接打开html文件:


然后打开浏览器,输入:https://localhost:8080/prime.html网址,就可以看到结果

结果




三、用Emscripten生成WebAssembly和JavaScript代码

前一章节创建WebAssembly模块的方法,更多是让我们快速验证代码或者再运行下一步之前验证模块逻辑的有效性。但是在产品开发的过程中,会使用自己编辑的或者项目中已有的网页,这样一来,在网页中引用WebAssembly模块会更加方便,由于WebAssembly目前不能直接与DOM进行交互,也没办法在html代码中独自加入,只能通过JavaScript API进行交互,所以这一种方法,我们生成了可以连接WebAssembly模块的JavaScript代码,我们只需要在html文件中将指向这个文件的引用包含进去,在网页加载时,这个文件就会自动下载WebAssembly模块并进行实例化。



1.求裴波那契数列的C代码

对于裴波那契数列而言,实现的方式有递归和非递归两种,我采用了递归的方式实现。

#include<stdio.h>
#include<stdlib.h>

#define N 40 //输出40位裴波那契数列
  
int Fbi(int i)  // 斐波那契的递归函数 
{
    if( i < 2 )
    return i == 1;  
    return Fbi(i - 1) + Fbi(i - 2);   
}  
    
int main()
{
   int i;
   printf("递归显示斐波那契数列:\n");
   for(i = 1;i < N;i++)  
   printf("%10d ", Fbi(i));  
    
   return 0;
}



2.编译运行

接下来要进行的就是编译环节,为了按照我们的要求生成WebAssembly模块和JavaScript代码,不生成HTML模板,需要将输出文件的后缀改为.js。

运行以下命令:emcc fib.c –o fib.js。

这个时候需要注意一点,就是文件夹的位置,如果我们在cmd窗口中打开了fib.c所在的文件夹,那么才不需要文件路径。

编译之后会生成两个文件:fib.wasm和fib.js

接下来,需要我们自己设计一个html网页。在HTML文件中,通过包含src属性,script标签可以用来包含JavaScript代码,前者这个属性会告诉浏览器在哪里找到代码文件。Script标签可以放到HTML文件的head或者body标签中,但就使用习惯而言,将script标签放在body标签的结尾处为最佳结果。这并不是语法要求,而是在浏览器加载过程中,在JavaScript下载好之前会暂停DOM的构造,如果我们先构造DOM,那么网页不会显示为一片空白。浏览器加载页面的详细过程,会在后面讲述。

fib.html的文件内容

<!DOCTYPE html>
<head>
    <meta charset="utf-8"/>
    <link href="favicon.ico" rel="shortcut icon">
</head>
<body>
    我为求裴波那契数列的WebAssembly模块创建的html页面。

    <script src="fib.js">        </script>
</body>
</head>

启动本地服务器后:emrun –no_browser –port 8080 fib.html

,打开http://localhost:8080/fib.html网址,会看到结果,如图3-3.4所示。在这个网页中,并没有像上一章节那样显示出WebAssembly模块的结果,需要我们打开网页控制台,在控制台窗口中查看。

在这里插入图片描述




四、用Emscripten只生成WebAssembly文件

这种方式中,我们只用Emscripten创建WebAssembly文件,不包含其他任何文件,在这种情况下,我们需要自己创建HTML文件,还要编写下载 和实例化WebAssembly模块的JavaScript代码。

我们可以告知Emscripten创建一个副模块,这种生成方式常用于动态链接,其中可以下载多个模块,然后在运行时将它们链接在一起,从而组合成为一个单元进行工作,这种方式很像其他语言中的依赖库。当然在这种做法中,Emscripten就不会在WebAssembly模块中将任何C标准库函数与代码包含在一起,比如print等函数。

我们可能会因为三个原因使用副模块的创建方法。首先是实现动态链接,在这种情况下,所有的副模块中只有一个被编译为主模块包含C的标准库函数。其次是模块的设计逻辑中不需要C的标准库,但是只要模块和JavaScript代码之间传递了任何整型或浮点型数据,那么就需要内存管理这一功能,这就会使用某些C的标准库函数,比如malloc和free,这可能会导致一些比较难处理的Bug。最后一种就是我们要分析浏览器会如何下载并编译WebAssembly模块和实例化的过程,这会是很有用的技巧。



1.将传入值加一后返回的c代码

我们创建一个简单的函数,实现接受一个整型变量,将这个值加1,再向使用者返回结果的功能。创建add.c文件

int add(int value )
{
    return value+1;
}

这个时候,我们不能使用任何的标准库



2.编译运行

这种情况下,我们的编译命令需要做一些调整,

使用:emcc add.c -s SIDE_MODULE=2 -O1 -s EXPORTED_FUNCTIONS=[‘Add’] -o add.wasm来编译add.c文件。

命令里面的,-s SIDE_MODULE=2,这个标记的作用是告诉Emscripten不用在生成的模块中包含像C标准库这样的内容,也不用生成任何其他的文件。-O1(大写字母O)是优化标记,如果不添加优化标记,默认不优化,在这个环节,如果不进行优化,就会在试图加载WebAssembly模块时产生链接错误,这个模块的代码中需要若干函数和全局变量,但是我并没有提供,通过移除多余导入可以解决这个问题,所以使用了优化。在Emscripten中,优化等级从下到上分别为:-O0、-O1、-O2、-Os、-Oz、-O3。此外,还需要将函数add指定为导出函数,这样才能够被JavaScript调用,否则在编译过程函数名会发生变化。为了实现这一要求,需要添加-s EXPORTED_FUNCTION[ ],然后包含函数名。最后,输出文件的后缀应为.wasm。如果不指定文件名,Emscripten会创建一个名为a.out.wasm的文件。

然后创建一个html文件如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="apple-mobile-web-app-capable" content="yes" />
        <meta name="apple-touch-fullscreen" content="yes" />
        <meta name="format-detection" content="telephone=no, email=no" />
        <script src="../loader.js"></script>
    </head>
    <body>
        这是生成WebAssembly模块的第三种方法。
        <script >
            loadWebAssembly('./add.wasm')
                .then(instance => {
                    const add = instance.exports.add
    
                    console.log(add(17))
            })
        </script>
    </body>
</html>

其中,loader.js文件内容如下,需要将其包含在文件目录下:

function loadWebAssembly(filename, imports = {}) {
  return fetch(filename)
    .then(response => response.arrayBuffer())
    .then(buffer => {
      imports.env = imports.env || {}
      Object.assign(imports.env, {
        memoryBase: 0,
        tableBase: 0,
        __memory_base: 0,
        __table_base: 0,
        memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
        table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })
      })
      return WebAssembly.instantiate(buffer, imports)
    })
    .then(result => result.instance )
}

function loadJS (url, imports = {}) {
  return fetch(url)
    .then(response => response.text())
    .then(code => new Function('imports', `return (${code})()`))
    .then(factory => ({ exports: factory(imports) }))
}

WebAssembly 目前只设计也只实现了 javascript API,只有通过 js 代码来编译、实例化才可以调用其中的接口。这也很好的说明了 WebAssembly 并不是要替代 javascript ,而是用来增强 javascript 和 Web 平台的能力的。

步骤写出来了,其实就是 【加载文件】->【转成 buffer】->【编译】->【实例化】

这一过程可以参考GitHub中

大佬的教程

然后按照我们熟悉的步骤,启动本地服务器,运行即可。

在这里插入图片描述




总结

以上就是WebAssembly模块的简单创建方法,具体可参考书目《WebAssembly实战》,里面有更加详细完整的教程。

ps:我是先看了这本教程然后才开始进行其他分析整理



版权声明:本文为qq_42275400原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。