WebAssembly技术_JS调用C函数示例_传递参数、方法导出

1,314 阅读5分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

1. 前言

Webassembly 是一种可以在浏览器端运行二进制格式代码的技术,WebAssembly最大的优点莫过于可大幅度提升 Javascript 的性能。

WebAssembly 的设计目标:定义一个可移植,体积紧凑,加载迅速的二进制格式为编译目标,而此二进制格式文件将可以在各种平台(包括移动设备和物联网设备)上被编译,然后发挥通用的硬件性能以原生应用的速度运行。

这篇文章主要演示C代码如何编译成wasm文件,如何生成JS文件,JS代码如何调用wasm文件封装的C语言函数。分别编写了两个案例演示了整体流程,完成C函数的传参、返回值的接收等功能。

关于WebAssembly环境搭建、编译器的安装教程,在这里已经介绍过了:bbs.huaweicloud.com/blogs/33157…

2. 导出自定义函数给JS调用

下面案例里编写一个C语言代码,提供两个函数接口给JS调用。

2.1 C代码

 #include <emscripten.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 ​
 int func_square(int x) 
 {
   return x * x;
 }
 ​
 int func_sum(int x, int y) 
 {
   return x + y;
 }

说明:如果上面这样编写的C函数如果需要导出,在编译的时候需要加-s "EXPORTED_FUNCTIONS=['_func_square','_func_sum']"参数指定导出的函数。

如果不想在编译命令里指定,也可以在编写C函数时,加上EMSCRIPTEN_KEEPALIVE修饰。

如果是系统的的库函数,或者是第三方库的函数需要导出给前端调用,不能修改源码声明的情况,那么就在编译的时候加上`-s "EXPORTED_FUNCTIONS=['_xxxx']" 声明即可,把要导出的函数名称在里面写好,编译就行。

 #include <emscripten.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 ​
 int EMSCRIPTEN_KEEPALIVE func_square(int x) 
 {
   return x * x;
 }
 ​
 int EMSCRIPTEN_KEEPALIVE func_sum(int x, int y) 
 {
   return x + y;
 }

2.2 将C代码编译成wasm文件

 emcc hello.c --no-entry -s "EXPORTED_FUNCTIONS=['_func_square','_func_sum']" -O3 -o hello.wasm

参数介绍:

(1)--no-entry 表示不需要导出main函数,也就是C代码里不用包含main函数,生成的wasm文件当做库给前端JS调用。

(2)"EXPORTED_FUNCTIONS=['_func_square','_func_sum']"

表示要导出的C函数名称,导出时需要在原C函数名称上加上_

(3)hello.wasm 表示指定生成的wasm文件名称

2.3 编写JS文件

这个JS代码用来加载wasm文件,做一些初始化设置。

测试时,新建一个名称为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) }))
 }

2.4 编写HTML文件

创建一个名为index.html的HTML文件,将下面贴出的代码贴进去保存。

编写的这个HTML就是主要是测试代码,里面加载了loader.js,调用loadWebAssembly方法加载wasm文件。

 <!DOCTYPE html>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Compile C to WebAssembly</title>
   <meta name="apple-mobile-web-app-capable" content="yes" />
   <meta name="apple-mobile-web-app-status-bar-style" content="black" />
   <meta name="apple-touch-fullscreen" content="yes" />
   <meta name="format-detection" content="telephone=no, email=no" />
   <script src="./loader.js"></script>
 </head>
 ​
 <body>
   <h1>Compile C to WebAssembly</h1>
   <p>The test result can be found in console.</p>
 ​
   <script>
     loadWebAssembly('./hello.wasm')
       .then(instance => {
         const func_square = instance.exports.func_square
         const func_sum = instance.exports.func_sum
         
         var malloc_inputPtr = malloc(100);
          
         console.log('10^2 =', func_square(2))
         console.log('100+100 =', func_sum(200,100))
         
       })
   </script>
 </body>
 </html>
 ​

2.5 开启HTTP服务器

使用python快速开一个HTTP服务器,用于测试。在HTML文件、wasm文件、JS文件的同级目录下,打开CMD命令行,运行下面命令。

 python -m http.server

2.6 打开谷歌浏览器测试

输入地址: http://127.0.0.1:8000/index.html 访问。

然后按下F12,打开控制台看输出结果。

image-20220218113451680

2.7 查看成功导出的C函数有哪些

在浏览器控制台源代码页面可以看到wasm转换后的文本代码,能看到导出了那些可以调用的C函数接口。

如果JS报错找不到某某函数无法调用,可以打开这个文件看一下,函数是否成功导出。

image-20220218113602770

3. 导出C函数给JS调用(方式2)

下面编写一个C代码案例,使用emcc生成js和wasm文件,自己编写一个HTML文件调用JS里提供的方法。

这个JS文件由emcc编译器自动生成,里面封装了C语言函数,可以直接通过JS文件里的方法调用C函数。

3.1 C代码

 #include <emscripten.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 ​
 int func_square(int x) 
 {
   return x * x;
 }
 ​
 int func_sum(int x, int y) 
 {
   return x + y;
 }
 ​
 void func_string(void) 
 {
   printf("成功调用C语言func_string函数.\n");
 }

3.2 将C代码编译成wasm和JS文件

 emcc hello.c -o hello.js -s EXPORTED_FUNCTIONS="['_func_square','_func_sum','_func_string','_malloc','_free']" -s WASM=1

参数介绍:

hello.c 是将要编译的源文件。

-o hello.js 指定生成的js文件名称,并且会自动生成一个同名的wasm文件。

-s EXPORTED_FUNCTIONS="['_func_square','_func_sum','_func_string','_malloc','_free']"

需要导出的函数。

编译生成的js和wasm文件:

image-20220218142005035

3.3 编写HTML文件

使用emcc编译时,JS文件和wasm文件已经生成了,接下来就编写个HTML代码,完成方法调用测试。

HTML代码里创建了3个按钮,分别调用了3个函数,测试调用C语言函数的。

注意: JS文件里导出的C函数在函数名称前面都是带了一个下划线,调用时要加上下划线。

HTML代码源码: 新建一个index.html文件,将下面代码贴进去即可。

 <!doctype html>
 <html lang="en-us">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <title>js调用c语言函数示例</title>
   </head>
   
   <body>  
   
     <script type='text/javascript'>   
       function run1()
       {
         console.log('10^10等于:',_func_square(10))
       }
       function run2()
       { 
         console.log('10+500等于:',_func_sum(10,500))
       }
       function run3()
       { 
        _func_string()   
       }
     </script>
     
     <input type="button" value="调用方法1" onclick="run1()" />
     <input type="button" value="调用方法2" onclick="run2()" />
     <input type="button" value="调用方法3" onclick="run3()" />
     
     <script async type="text/javascript" src="hello.js"></script>
   </body>
 </html>

3.4 开启HTTP服务器

使用python快速开一个HTTP服务器,用于测试。在HTML文件、wasm文件、JS文件的同级目录下,打开CMD命令行,运行下面命令。

 python -m http.server

3.5 打开谷歌浏览器测试

输入地址: http://127.0.0.1:8000/index.html 访问。

然后按下F12,打开控制台看输出结果。

image-20220218142554546

4. 数组、字符串参数传递

前面的例子都是演示整数参数传递和返回值的接收,下面代码演示,C语言与JS代码之间传递int类型指针、字符串、实现内存数据交互。

4.1 C代码

先编写C代码,提供几个测试函数。

 #include <emscripten.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 ​
 int EMSCRIPTEN_KEEPALIVE func_square(int x) 
 {
   return x * x;
 }
 ​
 int EMSCRIPTEN_KEEPALIVE func_sum(int x, int y) 
 {
   return x + y;
 }
 ​
 void EMSCRIPTEN_KEEPALIVE func_string(void) 
 {
   printf("成功调用C语言func_string函数.\n");
 }
 ​
 ​
 int* EMSCRIPTEN_KEEPALIVE int_array(int *buff)
 {
     int i=0;
     for(i=0;i<10;i++)
     {
         buff[i]=i+88;
     }
     return buff;
 }
 ​
 ​
 void EMSCRIPTEN_KEEPALIVE str_print(char *str)
 {
     printf("C语言收到JS传入的字符串:%s\n",str);
 }
 ​
 ​
 char* EMSCRIPTEN_KEEPALIVE str_cpy(char *str)
 {
     strcpy(str,"我是C代码拷贝的字符串");
     return str;
 }

4.2 将C代码编译成wasm文件

 emcc hello.c -o hello.js -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString']" -s WASM=1

参数解释:

hello.c 是将要编译的源文件。

-o hello.js 指定生成的js文件名称,并且会自动生成一个同名的wasm文件。

-s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString']"

需要导出的函数。

注意: JS与C函数之间字符串交互打印调试时,需要用到一些转换函数。这些函数默认没有导出的,需要自己手动导出。

在生成的JS代码,第1830行这个位置,可以看到编译器内置的很多函数,这些函数默认是没有导出的,如果JS需要调用这些函数,那么编译代码时,加上`-s EXPORTED_FUNCTIONS 选项导出这些函数。

image-20220218174123464

4.3 编写HTML文件

使用emcc编译时,JS文件和wasm文件已经生成了,接下来就编写个HTML代码,完成方法调用测试。

HTML代码里创建了几个按钮,分别调用了C语言代码里提供的几个测试函数。

注意: JS文件里导出的C函数在函数名称前面都是带了一个下划线,调用时要加上下划线。

HTML代码源码: 新建一个index.html文件,将下面代码贴进去即可。

 <!doctype html>
 <html lang="en-us">
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <title>js调用c语言函数示例</title>
   </head>
   
   <body>  
   
     <script type='text/javascript'>   
       function run1()
       {
         console.log('10^10等于:',_func_square(10))
       }
       
       function run2()
       { 
         console.log('10+500等于:',_func_sum(10,500))
       }
       
       function run3()
       { 
        _func_string()   
       }
       
       function run4()
       { 
         //申请空间,存放字符串
         var ptr1 = allocate(intArrayFromString("小米10至尊版"), ALLOC_NORMAL);
 ​
         //传递给C代码
         _str_print(ptr1)    
       }
       
       function run5()
       { 
          //申请空间
         var buff=_malloc(50)
         
         //调用函数
         var out_p=_int_array(buff)
         
         //打印结果
         if (out_p == 0) return;
         var str = '';
         for (var i = 0; i < 10; i++)
         {
             str += Module.HEAP32[(out_p >> 2) + i];
             str += ' ';
         }
         console.log(str);
         
         //释放空间
        _free(buff)  
       }
       
       function run6()
       { 
         //申请空间
         var buff=_malloc(50)
         
         var buff1=_str_cpy(buff);
         console.log('返回值:',UTF8ToString(buff1));
         
         //释放空间
        _free(buff)  
       }
       
     </script>
     
     <input type="button" value="求平方:传递1个整数参数,返回整数" onclick="run1()" />
     <input type="button" value="求和:传递两个整数参数,返回整数" onclick="run2()" />
     <input type="button" value="无参数和返回值函数调用.内部打印日志到控制台" onclick="run3()" />
     <input type="button" value="传入字符串参数,内部打印出来" onclick="run4()" />
     <input type="button" value="传入int类型指针,赋值数据再返回地址" onclick="run5()" />
     <input type="button" value="传入char类型字符串指针,赋值数据再返回地址" onclick="run6()" />
     <script async type="text/javascript" src="hello.js"></script>
   </body>
 </html>

4.4 开启HTTP服务器

在js、wasm、html文件存放目录下运行cmd命令。

 python -m http.server

4.5 打开浏览器测试

输入地址: http://127.0.0.1:8000/index.html 访问。

然后按下F12,依次按下页面上的按钮,打开控制台看输出结果。

image-20220218174557146