本章节所需要准备的内容
1、最新版本的Chrome浏览器、最新版本的FireFox浏览器,其他(反正我不推荐)
2、一台能打字的电脑
正式开始
C语言部分
首先,我们先从C语言部分入手。
在这里,我们先创建一个空文件夹命名为 "WebAssemblyChap1" ,然后我们分别创建两个文件(helloworld.c , helloworld.h),如下图所示:
然后,我们打开 helloworld.c
输入以下内容:
#include "helloworld.h" void sayHi() { alert("hello,world!"); }
这是一个很简单的C程序,不过看到这里,有些会 C/C++ 的同学可能就会说了:你怎么不按常理出牌?
C语言里必须要有 main 函数! C语言里怎么可能有 alert 函数,你是不是写了个 JavaScript 代码骗我们?!
我们来解答以下上面的问题:
1、没有 main 函数的 c 文件就是一个库,它并不是一个可以直接被执行的程序。
2、虽然 C语言没有 alert 函数,不代表我们不可以注入一个。
我们接着来看,现在我们打开 helloworld.h
输入以下内容:
extern void alert(char* str);
呐,alert 函数的定义不就在头文件里写这么。
好的,以上两个文件都写完了对吧~
那么接下来就是 emcc 的主场了。
我们打开 控制台/终端 切换到咱们的项目目录下,然后输入以下命令:
emcc helloworld.c -s BINARYEN=1 -s SIDE_MODULE=1 -O3 -o helloworld.wasm
嗯。这个命令的基本意思是,我们希望将 helloworld.c 及其引用的文件全部编译成 helloworld.wasm 文件。
-s BINARYEN=1 的意思是编译成 wasm 否则它会弄成 Asm.js。。。
-s SIDE_MODULE=1 的意思是不要自动插入乱七八糟的东西进去,因为 Escripten 默认会有一些辅助函数并自动生成一个辅助 js 文件。我们这里主要是看原生 js 怎么写,所以不需要它的“好意”了。
-O3 (注意是英文字母O不是0)意思是最佳优化。
其他具体内容请参见:emcc
好的,当我们敲下回车后,目录中自动出现了一个名为 helloworld.wasm 的文件,如下图所示:
以上,我们的 C语言以及 emcc 的部分就此完成了。
javascript 部分
那么,接下来我们就来看看怎么在 javascript 中引入这个文件呢?
首先,我们还是在这个目录下创建一个名为 helloworld.html 的文件。
并写上 Html5 规范的默认空文档,如图所示:
接下来,我们再写上一个 script 标签。
在此标签中我们将写一些引入 helloworld.wasm 、 编译 、执行相关的代码。
我们将涉及到的知识点:
1、Fetch API - 它旨在替代传统 XMLHttpRequest ,而且它使用了 Promise API,更符合异步操作规范。
2、WebAssembly API - 浏览器上的 WebAssembly 相关操作对象。
3、TypedArray - 类型化数组,用于方便的操作二进制数据,嗯……和 C语言打交道怎么能少了它?
好的,如果以下代码有任何看不懂的地方,直接看以上的知识点链接可以了解更为详细的内容哦!
let imports = { env: { // 内存空间 memoryBase: 0, memory: new WebAssembly.Memory({initial: 256 }), // 变量映射表 tableBase: 0, table: new WebAssembly.Table({initial: 0, element: "anyfunc" }), // 注入函数 _alert: function (p) { // 这一块说明请看文章 alert(utf8ToString(p)); } } };
我们先定义一个 imports 对象,这个对象包含函数、WebAssembly.Memory 对象等。
对于 Emscripten 生成的 WASM 文件来讲,里面包含的东西可以直接参照以上代码去写,当然了。你要注入的函数是需要进行增减的。
那么你可能注意到了,我们在 alert 函数名前加了一个 _ 下划线,这是因为通过 C代码生成的文件被解析后会自动加上一个 _ ,具体原因可能是因为避免重名。你只要注意一下就可以了。
嗯,我们看到 alert 函数内调用了一个 utf8ToString 函数并将我们注入函数的形参 p 传入进去了,这是什么意思?
我们先来看看它的实现:
function utf8ToString(p) { let h = new Uint8Array(imports.env.memory.buffer); let s = ""; for (let i = p; h[i]; i++) { s += String.fromCharCode(h[i]); } return s; }
为什么我们要这么做?
因为我们传入的字符串并不是字符串。这怎么理解啊?
原因是在于目前 WebAssembly 标准中只允许在互操作时进行数值类型的交换。
所以,我们这里需要直接去读内存才能得到我们所传的字符串。
那么 p 的意思就是字符串在指定内存区域的 offset 值。
这些很好理解吧。那么这里怎么判断字符串有多长呢?
我们可以看到上述代码的 for 循环终止条件是 h[i] ,这里的 h 就是我们读取的内存,h[i] 判断的是当前读取的字节是否不为0。
在 C 中,一个字符串后会跟着一个 \0 字节,代表这个字符串已经结束了。
看到这里,你应该可以理解了吧~~~
OK,接下来就是正餐。
fetch("helloworld.wasm"). then(response => response.arrayBuffer()). then(bytes => WebAssembly.instantiate(bytes, imports)). then(mod => mod.instance). then(instance => { let exports = instance.exports; exports._sayHi(); });
我们都干了写什么呢?嗯……按照代码行数来一条一条的解读吧。
1、加载 helloworld.wasm
2、读取其 ArrayBuffer
3、编译并实例化
4、将实例化后的 instance 对象传出
5、在导出函数中调用 _sayHi() 函数 (这里同样需要注意函数名被加了一个下划线的问题)
写完以上几个内容之后,我们就可以运行 helloworld.html 来查看效果了。
不过需要注意的是,我们必须在服务器环境中运行,否则因跨域而报错的哟!
下面就来看看效果吧!~
OK,本章就到此结束咯。
下一章我们将讲解如何进行更深层次的 C/C++ 与 javascript 的互操作。