共计 15987 个字符,预计需要花费 40 分钟才能阅读完成。
WebAssembly 入门:如何与 C/C++ 我的项目联合
什么是 WebAssembly?
- 一种新型的代码,能够运行在 Web 浏览器,提供一些新个性并次要专一于高性能
- 次要不是用于写,而是 C/C++、C#、Rust 等语言编译的指标,所以你即便不晓得如何编写 WebAssembly 代码也能利用它的劣势
- 其余语言编写的代码也能以近似于原生速度运行,客户端 App 也能在 Web 上运行
- 在浏览器或 Node.js 中能够导入 WebAssembly 模块,JS 框架可能应用 WebAssembly 来取得微小的性能劣势和新的个性的同时在性能上易于应用
WebAssembly 的指标
- 快、高效、便当 — 通过利用一些通用的硬件能力,可能跨平台以近乎于原生的速度执行
- 可读、可调试 — WebAssembly 是一种低层次的汇编语言,然而它也有一种人类可读的文本格式,使得人们可编写代码、查看代码、可调试代码。
- 确保安全 — WebAssembly 明确运行在平安、沙箱的执行环境,相似其余 Web 的代码,它会强制开启同源和一些权限策略。
- 不毁坏现有的 Web — WebAssembly 被设计与其余 Web 技术兼容运行,并且放弃向后兼容性。
WebAssembly 如何与 Web 兼容的?
Web 平台能够看做有两个局部:
- 一个虚拟机(VM)用于运行 Web 利用代码,例如 JS 引擎运行 JS 代码
- 一系列 Web API,Web 利用能够调用这些 API 来管制 Web 浏览器 / 设施 的性能,来做某些事件(DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等)
长期以来,VM 只能加载 JS 运行,JS 可能足够满足咱们的需要,但现在咱们却遇到了各种性能问题,如 3D 游戏、VR/AR、计算机视觉、图片 / 视频编辑、以及其余须要原生性能的畛域。
同时,下载、解析和编译大体积的 JS 利用是很艰难的,在一些资源更加受限的平台上,如挪动设施等,则会更加放到这种性能瓶颈。
WebAssembly 是一种与 JavaScript 不同的语言,它不是为了代替 JS 而生的,而是被设计为与 JS 互为补充并能合作,使得 Web 开发者可能反复利用两种语言的长处:
- JS 是高层次的语言,灵便且极具表现力,动静类型、不须要编译步骤,并且有弱小的生态,十分易于编写 Web 利用。
- WebAssembly 是一种低层次、类汇编的语言,应用一种紧凑的二级制格局,可能以近乎原生的性能运行,并提供了低层次的内存模型,是 C++、Rust 等语言的编译指标,使得这类语言编写的代码可能在 Web 上运行(须要留神的是,WebAssembly 将在将来提供垃圾回收的内存模型等高层次的指标)
随着 WebAssembly 的呈现,上述提到的 VM 当初能够加载两种类型的代码执行:JavaScript 和 WebAssembly。
JavaScript 和 WebAssembly 能够互操作,实际上一份 WebAssembly 代码被称为一个模块,而 WebAssembly 的模块与 ES2015 的模块在具备很多独特的个性。
WebAssembly 的要害概念
为了了解 WebAssembly 是如何在 Web 运行的,须要理解几个要害概念:
- Module:通过浏览器编译成为可执行机器码的 WebAssembly 二进制文件,Module 是无状态的,相似 Blob,可能在 Window 和 Worker 之间通过
postMessage
共享,一个 Module 申明了相似 ES2015 模块相似的 import 和 export。 - Memory:一个可调整大小的 ArrayBuffer,其中蕴含由 WebAssembly 的低层次内存拜访指令读取和写入的线性字节数组。
- Table:一个可调整大小的类型化援用数组(如函数),然而处于平安和可移植性的起因,不能作为原始字节存储在内存中
- Instance:一个蕴含它在运行时用到的所有状态,蕴含 Memory、Table、以及一系列导入值的 Module,一个 Instance 相似一个 ES2015 的模块,它被加载到具备特定导入集的特定全局变量中
WebAssembly 的 JavaScript API 提供给开发者创立 Module、Memory、Table 和 Instance 的能力,给定一个 WebAssembly 的 Instance,JS 代码能够同步的调用它的 exports — 被作为一般的 JavaScript 函数导出。任意 JavaScript 函数能够被 WebAssembly 代码同步的调用,通过将 JavaScript 函数作为 imports 传给 WebAssembly Instance。
因为 JavaScript 可能齐全管制 WebAssembly 代码的下载、编译和运行,所以 JavaScript 开发者能够认为 WebAssembly 只是 JavaScript 的一个新个性 — 能够高效的生成高性能的函数。
在将来,WebAssembly 模块能够以 ES2015 的模块加载模式加载,如 <script type="module">
,意味着 JS 能够获取、编译、和导入一个 WebAssembly 模块,就像导入 ES2015 模块一样简略。
如何在利用里应用 WebAssembly?
WebAssembly 给 Web 平台增加了两块内容:一种二进制格局代码,以及一系列可用于加载和执行二进制代码的 API。
WebAssembly 目前处于一个萌芽的节点,之后必定会涌现出很多工具,而目前有四个次要的入口:
- 应用 EMScripten 来移植 C/C++ 利用
- 在汇编层面间接编写和生成 WebAssembly 代码
- 编写 Rust 利用,而后将 WebAssembly 作为它的输入
- 应用 AssemblyScript,它是一门相似 TypeScript 的语言,可能编译成 WebAssembly 二进制
移植 C/C++ 利用
尽管也有一些其余工具如:
- WasmFiddle
<!—->
- WasmFiddle++
<!—->
- WasmExplorer
然而这些工具都不足 EMScripten 的工具链和优化操作,EMScripten 的具体运行过程如下:
- EMScripten 将 C/C++ 代码喂给 Clang 编译器(一个基于 LLVM 编译架构的 C/C++ 编译器),编译成 LLVM IR
- EMScripten 将 LLVM IR 转换成 .wasm 的二进制字节码
- WebAssembly 无奈间接获取到 DOM,只能调用 JS,传入整形或浮点型的等原始数据类型,因而 WebAssembly 须要调用 JS 来获取 Web API 和调用,EMScripten 则通过创立了 HTML 文件和 JS 胶水代码来达到上述成果
将来 WebAssembly 也能够间接调用 Web API。
上述的 JS 胶水代码并不像设想中那么简略,一开始,EMScripten 实现了一些风行的 C/C++ 库,如 SDL、OpenGL、OpenAL、以及一部分 POSIX 库,这些库都是依据 Web API 来实现的,所以须要 JS 胶水代码来帮忙 WebAssembly 和底层的 Web API 进行交互。
所以,有局部胶水代码实现了 C/C++ 代码须要用到的对应的库的性能,胶水代码还同时蕴含调用上述 WebAssembly JavaScript API 的以获取、加载和运行 .wasm 文件的逻辑。
生成的 HTML 文档加载 JS 胶水代码,而后将输入写入到 <textarea>
中去,如果利用应用到了 OpenGL,HTML 也蕴含 <canvas>
元素来作为渲染指标,你能够很不便的改写 EMScripten 的输入,将其转换成 Web 利用须要的模式。
间接编写 WebAssembly 代码
如果你想构建本人的编译器、工具链,或者可能在运行时生成 WebAssembly 代码的 JS 库,你能够抉择手写 WebAssembly 代码。和物理汇编语言相似,WebAssembly 的二进制格局也有一种文本示意,你能够手动编写或生成这种文本格式,并通过 WebAssembly 的文本到二进制(text-to-binary)的工具将文本转为二进制格局。
编写 Rust 代码,并编译为 WebAssembly
多谢 Rust WebAssembly 工作组的不懈努力,咱们当初能够将 Rust 代码编译为 WebAssembly 代码。
能够参考这个链接:https://developer.mozilla.org…
应用 AssemblyScript
对于 Web 开发者来说,可是应用类 TypeScript 的模式来尝试 WebAssembly 的编写,而不须要学习 C 或 Rust 的细节,那么 AssemblyScript 将会是最好的抉择。AssemblyScript 将 TypeScript 的变体编译为 WebAssembly,使得 Web 开发者能够应用 TypeScript 兼容的工具链,例如 Prettier、VSCode Intellisense,你能够查看它的文档来理解如何应用。
如何编译将新写 C/C++ 代码编译到 WebAssembly?
通过 EMScripten 工具,可将新写的 C/C++ 代码编译为 WebAssembly 应用。
筹备条件
为了可能应用 Emscripten 工具,咱们须要装置它。首先 Clone 相干代码:
git clone https: // github . com / emscripten-core / emsdk . git
cd emsdk
而后执行如下脚本来配置 emsdk:
# 如果之前 clone 过,那么这里更新最新的代码
git pull
# 下载和装置最新的 SDK 工具
./emsdk install latest
# 为以后的 user 激活最新的 SDK 工具,在 .emscripten 文件中写入以后用户
./emsdk activate latest
# 将 SDK 相干的命令退出到 PATH,以及激活其余环境变量
source ./emsdk_env.sh
通过下面的操作咱们就能够在命令行应用 Emscripten 相干的命令了,个别咱们应用 Emscripten 时,次要有两种场景:
- 编译成 WASM 而后创立 HTML 文档来运行代码,联合 JavaScript 胶水代码来在 Web 环境运行 wasm 代码
- 编译成 wasm 代码,只创立 JavaScript 文件
生成 HTML 和 JavaScript
首先在 emsdk
目录同级创立一个文件夹:WebAssembly
,而后在文件夹下创立一份 C 代码:hello.c
如下:
#include <stdio.h>
int main() {printf("Hello World\n");
}
而后在命令行中导航到此 hello.c
目录下,运行如下命令来调用 Emscripten 进行编译:
emcc hello.c -s WASM=1 -o hello.html
上述命令解释如下:
emcc
为 Emscripten 的命令行命令-s WASM=1
则通知 Emscripten 须要输入 wasm 文件,如果不指定这个参数,那么默认会输入asm.js
-o hello.html
则通知编译器生成一个名为hello.html
的 HTML 文档来运行代码,以及 wasm 模块和对应的用于编译和实例化 wasm 的 JavaScript 胶水代码,以便 wasm 能够在 Web 环境中应用
运行如上命令之后,你的 WebAssembly
目录下应该多出了三个文件:
- 二进制的 wasm 模块代码:
hello.wasm
- 蕴含胶水代码的 JavaScript 文件:
hello.js
,通过它将原生 C 函数翻译成 JavaScript/wasm 代码 - 一个 HTML 文件:
hello.html
,用于加载、编译和实例化 wasm 的代码,并将 wasm 代码的输入展现在浏览器上。
运行代码
目前剩下的工作为在反对 WebAssembly 的浏览器中加载 hello.html
运行。
在 Firefox 52+、Chrome 57+ 和最小的 Opera 浏览器中默认反对,也能够通过在 Firefox 47+ 中的
about:config
开启javascript.options.wasm
以及 Chrome 51+、Opera 38+ 中的chrome://flags
来容许实验性的 WebAssembly 特效反对。
因为古代浏览器不反对 file://
模式的 XHR 申请,所以在 HTML 中无奈加载 .wasm
等相干的文件,所以为了可能看到成果,须要额定的本地服务器反对,能够通过运行如下命令:
npx serve .
npx 为 npm 在 5.2.0+ 之后推出的一个便捷执行 npm 命令的工具,如上述的 serve,在运行时首先检测本地是否存在,如果不存在则下载原创对应的包,并执行对应的命令,并且为一次性的操作,罢黜了先装置再容许,且须要暂用本地内存的操作。
在 WebAssembly
文件夹下运行一个本地 Web 服务器,而后关上 http://localhost:5000/hello.html 查看成果:
能够看到 咱们在 C 代码外面编写的打印 Hello World
的代码,胜利输入到了浏览器里,你也能够关上控制台看到对应的输入:
祝贺你!你胜利将一个 C 模块编译成了 WebAssembly,并将其运行在了浏览器中!
应用自定义的 HTML 模板
上述例子中是应用了 Emscripten 默认的 HTML 模板,然而很多场景下咱们都须要用到自定义的 HTML 模板,如将 WebAssembly 整合到现有的我的项目中应用时,就须要自定义 HTML 模板,接下来咱们理解一下如何应用自定义的 HTML 模板。
首先在 WebAssembly
目录下新建 hello2.c
文件,写入如下内容:
#include <stdio.h>
int main() {printf("Hello World\n");
}
在之前 clone 到本地的 emsdk 仓库代码中找到 shell_minimal.html
文件,将其复制到 WebAssembly
目录下的子文件夹 html_template
下(此文件夹须要新建),当初 WebAssembly
目录下的文件构造如下:
.
├── hello.c
├── hello.html
├── hello.js
├── hello.wasm
├── hello2.c
└── html_template
└── shell_minimal.html
在命令行导航到 WebAssembly
下,运行如下命令:
emcc -o hello2.html hello2.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html
能够看到,相比之前在参数传递上有几点变动:
- 通过设置
-o hello2.html
,编译器将会将输入hello2.js
的 JS 胶水代码以及hello2.html
的 HTML 文件 - 同时设置了
--shell-file html_template/shell_minimal.html
,通过这个命令提供了你在生成 HTML 文件时应用的 HTML 模板地址。
当初让咱们运行这个 HTML,通过如下命令:
npx serve .
在浏览器中导航到:localhosthttp://localhost:5000/hello2.html 来拜访运行后果,能够观测到和之前相似的成果:
能够看到只是短少了之前的 Emscripten 头部,其余都和之前相似,查看 WebAssembly
文件目录,会发现生成了相似的 JS、Wasm 代码:
.
├── hello.c
├── hello.html
├── hello.js
├── hello.wasm
├── hello2.c
├── hello2.html
├── hello2.js
├── hello2.wasm
└── html_template
└── shell_minimal.html
留神:你能够指定只输入 JavaScript 胶水代码,而不是一份残缺的 HTML 文档,通过在
-o
标签前面指定为.js
文件,例如emcc -o hello2.js hello2.c -O3 -s WASM=1
,而后你能够自定义 HTML 文件,而后导入这份胶水代码应用,然而这是一种更加高级的办法,罕用的模式还是应用提供的 HTML 模板:
-
Emscripten 须要大量的 JavaScript 胶水代码来解决内存调配,内存泄露以及一系列其余问题。
调用在 C 中自定义的函数
如果你在 C 代码里定义了一个函数,而后想在 JavaScript 中调用它,你能够应用 Emscripten 的 ccall
函数,以及 EMSCRIPTEN_KEEPALIVE
申明(这个申明将你的 C 函数退出到函数输入列表,具体的工作过程如下:
首先在 WebAssembly
目录下创立 hello3.c
文件,增加如下内容:
#include <stdio.h>
#include <emscripten/emscripten.h>
int main() {printf("Hello World\n");
}
#ifdef __cplusplus
extern "C" {
#endif
EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {printf("MyFunction Called\n");
}
#ifdef __cplusplus
}
#endif
Emscripten 生成的代码默认只调用 main
函数,其余函数会作为“死代码”删除掉。在函数名之前退出 EMSCRIPTEN_KEEPALIVE
申明会阻止这种“删除”产生,你须要导入 emscripten.h
头文件来应用 EMSCRIPTEN_KEEPALIVE
申明。
留神咱们在代码中增加了
#ifdef
块,确保在 C++ 代码中导入这个应用时也是能够正确工作的,因为 C 和 C++ 的命名可能存在一些混同的规定,所以上述增加EMSCRIPTEN_KEEPALIVE
申明的函数可能会生效,所以在 C++ 环境下为函数加上external
,将其当做external
函数,这样在 C++ 环境下也能够正确工作。
而后为了演示不便,HTML 文件照样应用咱们之前放到 html_template
目录下的 shell_minimal.html
文件,而后应用如下命令编译 C 代码:
emcc -o hello3.html hello3.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
留神到在上述编译中,咱们加上了 NO_EXIT_RUNTIME
参数,因为当 main
函数运行完之后,程序就会退出,所以加上这个参数确保其余函数还是还能如期运行。
而额定增加的 EXTRA_EXPORTED_RUNTIME_METHODS
则用于为 WebAssembly 的 Module 导出 ccall
办法应用,使得能够在 JavaScript 调用导出的 C 函数。
当你通过 npx serve .
运行时,仍然能够看到相似之前的后果:
当初咱们能够尝试在 JavaScript 应用 myFunction
函数,首先在编辑器中关上 hello3.html
文件,而后增加一个 <button>
元素,并在 <button>
元素点击时可能调用 myFunction
函数:
<!-- 其余内容 --->
<button class="mybutton">Run myFunction</button>
<script type='text/javascript'>
// ... 其余生成的代码
// script 标签底部
document.querySelector('.mybutton')
.addEventListener('click', function() {alert('check console');
var result = Module.ccall(
'myFunction', // name of C function
null, // return type
null, // argument types
null // arguments
);
});
</script>
<!-- 其余内容 --->
保留上述内容,从新刷新浏览器能够看到如下后果:
当咱们点击上图中的按钮时,能够取得如下后果:
首先会收到一个 alert
提醒,而后在输入外面打印了 MyFunction Called 内容,示意 myFunction
调用了,关上控制台也能够看到如下打印后果:
上述例子展现了能够在 JavaScript 中通过 ccall
来调用 C 代码中导出的函数。
如何编译曾经存在的 C 模块到 WebAssembly?
一个 WebAssembly 的外围应用场景就是将反复利用曾经存在的 C 生态系统中的库,并将它们编译到 Web 平台上应用而不必从新实现一套代码。
这些 C 库通常依赖 C 的规范库,操作系统,文件系统或者其余依赖,Emscripten 提供绝大部分上述依赖的个性,只管还是存在一些限度。
让咱们将 C 库的 WebP 编码器编译到 wasm 来理解如何编译曾经存在的 C 模块,WebP codec 的源码是用 C 实现的,可能在 Github 上找到它,同时能够理解到它的一些 API 文档。
首先 Clone WebP 编码器的源码到本地,和 emsdk
、WebAssembly
目录同级:
git clone https://github.com/webmproject/libwebp
为了疾速上手,咱们能够先导出 encode.h
头文件外面的 WebPGetEncoderVersion
函数给到 JavaScript 应用,首先在 WebAssembly
文件夹下创立 webp.c
文件并退出如下:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {return WebPGetEncoderVersion();
}
上述的例子能够很疾速的测验是否正确编译了 libwebp 的源码并能胜利应用其函数,因为上述函数无需各种简单的传参和数据结构即可胜利执行。
为了编译上述函数,咱们首先得通知编译器如何找到 libwebp 库的头文件,通过在编译时加上标记 I
,而后指定 libwep 头文件的地址来通知编译器地址,并将编译器所须要的所有 libwebp 外面的 C 文件都传给它。但有时候一个个列举 C 文件十分的繁琐,所以一种无效的策略就是将所有的 C 文件都传给编译器,而后依赖编译器本身去过滤掉那些不必要的文件,上述形容的操作能够通过在命令行编写如下命令实现:
emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
WebAssembly/webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
留神:上述的传参策略并不对在所有 C 我的项目都失效,有很多我的项目在编译前依赖 autoconfig/automake 等库来生成零碎特定的代码,而 Emscripten 提供了
emconfigure
和emmake
来封装这些命令,并注入适合的参数来抹平那些有前置依赖的我的项目。
运行上述命令之后,会产出一份 a.out.js
胶水代码,和 a.out.wasm
文件,而后你须要在 a.out.js
文件输入的目录下创立一份 HTML 文件,并在其中增加如下代码
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
上述代码中,咱们首先导入编译器编译输入的 a.out.js
胶水代码,而后在 WebAssembly 的模块初始化好了之后,通过 cwrap
函数导出 C 函数 version
应用,通过运行和之前相似的 npx serve .
命令,而后关上浏览器能够看到如下成果:
libwebp 通过十六进制的
0xabc
的 abc 来示意以后版本a.b.c
,例如 v0.6.1,则会被编码成十六进制0x000601
,对应的十进制为 1537。而这里为十进制 66049,转成 16 进制则为0x010201
,示意以后版本为 v1.2.1。
在 JavaScript 中获取图片并放入 wasm 中运行
刚刚通过调用编码器的 WebPGetEncoderVersion
办法来获取版本号来证实了曾经胜利编译了 libwebp 库到 wasm,而后能够在 JavaScript 应用它,接下来咱们将理解更加简单的操作,如何应用 libwebp 的编码 API 来转换图片格式。
libwebp 的 encoding API 须要接管一个对于 RGB、RGBA、BGR 或 BGRA 的字节数组,所以首先要答复的问题是,如何将图片放入 wasm 运行?侥幸的是,Canvas API 有一个 CanvasRenderingContext2D.getImageData
办法,可能返回一个 Uint8ClampedArray
,这个数组蕴含 RGBA 格局的图片数据。
首先咱们须要在 JavaScript 中编写加载图片的函数,将其写到上一步创立的 HTML 文件里:
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
async function loadImage(src) {
// 加载图片
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// 设置 canvas 画布的大小与图片统一
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// 将图片绘制到 canvas 上
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
当初剩下的操作则是如何将图片数据从 JavaScript 复制到 wasm,为了达成这个目标,须要在先前的 webp.c
函数外面裸露额定的办法:
- 一个为 wasm 外面的图片分配内存的办法
- 一个开释内存的办法
批改 webp.c
如下:
#include <stdlib.h> // 此头文件导入用于分配内存的 malloc 办法和开释内存的 free 办法
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {free(p);
}
create_buffer
为 RGBA 的图片分配内存,RGBA 图片一个像素蕴含 4 个字节,所以代码中须要增加 4 * sizeof(uint8_t)
,malloc
函数返回的指针指向所分配内存的第一块内存单元地址,当这个指针返回给 JavaScript 应用时,会被当做一个简略的数字解决。当通过 cwrap
函数获取裸露给 JavaScript 的对应 C 函数时,能够应用这个指针数字找到复制图片数据的内存开始地位。
咱们在 HTML 文件中增加额定的代码如下:
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap("encode", "", ["number","number","number","number",]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
get_result_size: Module.cwrap("get_result_size", "number", []),
};
const image = await loadImage('./image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
};
async function loadImage(src) {
// 加载图片
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// 设置 canvas 画布的大小与图片统一
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// 将图片绘制到 canvas 上
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
能够看到上述代码除了导入之前增加的 create_buffer
和 destroy_buffer
外,还有很多用于编码文件等方面的函数,咱们将在后续解说,除此之外,代码首先加载了一份 image.jpg
的图片,而后调用 C 函数为此图片数据分配内存,并相应的拿到返回的指针传给 WebAssembly 的 Module.HEAP8
,在内存开始地位 p,写入图片的数据,最初会开释调配的内存。
编码图片
当初图片数据曾经加载进 wasm 的内存中,能够调用 libwebp 的 encoder 办法来实现编码过程了,通过查阅 WebP 的文档,发现能够应用 WebPEncodeRGBA
函数来实现工作。这个函数接管一个指向图片数据的指针以及它的尺寸,以及一个区间在 0-100 的可选的品质参数。在编码的过程中,WebPEncodeRGBA
会调配一块用于输入数据的内存,咱们须要在编码实现之后调用 WebPFree
来开释这块内存。
咱们关上 webp.c
文件,增加如下解决编码的代码:
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {return result[1];
}
上述 WebPEncodeRGBA
函数执行的后果为调配一块输入数据的内存以及返回内存的大小。因为 C 函数无奈应用数组作为返回值(除非咱们须要进行动态内存调配),所以咱们应用一个全局动态数组来获取返回的后果,这可能不是很标准的 C 代码写法,同时它要求 wasm 指针为 32 比专长,然而为了简略起见咱们能够临时容忍这种做法。
当初 C 侧的相干逻辑曾经编写结束,能够在 JavaScript 侧调用编码函数,获取图片数据的指针和图片所占用的内存大小,将这份数据保留到 JavaScript 本人的内存中,而后开释 wasm 在解决图片时所调配的内存,让咱们关上 HTML 文件实现上述形容的逻辑:
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap("encode", "", ["number","number","number","number",]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
get_result_size: Module.cwrap("get_result_size", "number", []),
};
const image = await loadImage('./image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
api.destroy_buffer(p);
};
async function loadImage(src) {
// 加载图片
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// 设置 canvas 画布的大小与图片统一
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// 将图片绘制到 canvas 上
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
在上述代码中咱们通过 loadImage
函数加载了一张本地的 image.jpg
图片,你须要当时筹备一张图片搁置在 emcc
编译器输入的目录下,也就是咱们的 HTML 文件目录下应用。
留神:
new Uint8Array(someBuffer)
将会在同样的内存块上创立一个新视图,而new Uint8Array(someTypedArray)
只会复制someTypedArray
的数据。
当你的图片比拟大时,因为 wasm 不能裁减能够包容 input
和 output
图片数据的内存,你可能会遇到如下报错:
然而咱们例子中应用的图片比拟小,所以只须要单纯的在编译时加上一个过滤参数 -s ALLOW_MEMORY_GROWTH=1
疏忽这个报错信息即可:
emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
test-dir/webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-s ALLOW_MEMORY_GROWTH=1
再次运行上述命令,失去增加了编码函数的 wasm 代码和对应的 JavaScript 胶水代码,这样当咱们关上 HTML 文件时,它曾经可能将一份 JPG 文件编码成 WebP 的格局,为了近一步证实这个观点,咱们能够将图片展现到 Web 界面上,通过批改 HTML 文件,增加如下代码:
<script>
// ...
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
// 增加到这里
const blob = new Blob([result], {type: 'image/webp'});
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img)
api.free_result(resultPointer);
api.destroy_buffer(p);
</script>
而后刷新浏览器,应该能够看到 WebP 图片展现到 Web 端,通过将这个文件下载到本地,能够看到其格局转成了 WebP:
通过上述的流程咱们胜利编译了现有的 libwebp C 库到 wasm 应用,并将 JPG 图片转成了 WebP 格局并展现在 Web 界面上,通过 wasm 来解决计算密集型的转码操作能够大大提高网页的性能,这也是 WebAssembly 带来的次要劣势之一。
感激反对
以上便是本次分享的全部内容,心愿对你有所帮忙 ^_^
喜爱的话别忘了 分享、点赞、珍藏 三连哦~
欢送关注公众号 程序员巴士 ,一辆乏味、有范儿、有温度的程序员巴士,涉猎大厂面经、程序员生存、实战教程、技术前沿等内容,关注我,交个敌人吧!