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

December 17, 2023
测试
测试
测试
测试
24 分钟阅读

1. 前言

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

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

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

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
image-20220218113451680

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

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

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

image-20220218113602770
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
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
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
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
image-20220218174557146

继续阅读

更多来自我们博客的帖子

如何安装 BuddyPress
由 测试 December 17, 2023
经过差不多一年的开发,BuddyPress 这个基于 WordPress Mu 的 SNS 插件正式版终于发布了。BuddyPress...
阅读更多
Filter如何工作
由 测试 December 17, 2023
在 web.xml...
阅读更多
如何理解CGAffineTransform
由 测试 December 17, 2023
CGAffineTransform A structure for holding an affine transformation matrix. ...
阅读更多