上一篇文章分享了 WebAssembly 概念和根本应用,通过两个代码示例的剖析对 WebAssembly 有了大抵的理解。这一篇文章分享的是基于 WebAssembly 的加密工具实际,咱们就以 openssl 的摘要算法 md5 和 sha1 为例,在 Mac 上编译 openSSL 到 WebAssembly。
环境
- Emscripten 版本 2.0.3
- Openssl 版本 1.1.1d
- 浏览器 版本 85.0.4183.121(正式版本)(64 位)
概述
- 在 Mac 上编译 openSSL 到 WebAssembly
- 遇到的问题
- 总结
一、在 Mac 上编译 openSSL 到 WebAssembly
将 Openssl 编译到 WebAssembly 整个流程是这样的,md5.c 文件–>emscripten 编译–>.wasm 文件–> 联合 WebAssembly JS API–> 浏览器中运行。
1. md5.c 文件
//md5.c
#include <emscripten.h>
#include <openssl/md5.h>
#include <openssl/sha.h>
#include <string.h>
#include <stdio.h>
EMSCRIPTEN_KEEPALIVE
void md5(char *str, char *result,int strlen) {
MD5_CTX md5_ctx;
int MD5_BYTES = 16;
unsigned char md5sum[MD5_BYTES];
MD5_Init(&md5_ctx);
MD5_Update(&md5_ctx, str,strlen);
MD5_Final(md5sum, &md5_ctx);
char temp[3] = {0};
memset(result,0, sizeof(char) * 32);
for (int i = 0; i < MD5_BYTES; i++) {sprintf(temp, "%02x", md5sum[i]);
strcat(result, temp);
}
result[32] = '\0';
}
EMSCRIPTEN_KEEPALIVE
void sha1(char *str, char result[],int strlen) {unsigned char digest[SHA_DIGEST_LENGTH];
SHA_CTX ctx;
SHA1_Init(&ctx);
SHA1_Update(&ctx, str, strlen);
SHA1_Final(digest, &ctx);
for (int i = 0; i < SHA_DIGEST_LENGTH; i++){sprintf(&result[i*2], "%02x", (unsigned int)digest[i]);
}
}
md5.c 文件中蕴含了 md5 和 sha1 两个函数,前面会用来编译到 wasm。
Tips:
1. 默认状况下,Emscripten 生成的代码只会调用 main() 函数,其它的函数将被视为无用代码。在一个函数名之前增加 EMSCRIPTEN_KEEPALIVE 可能避免这样的事件产生。你须要导入 emscripten.h 库来应用 EMSCRIPTEN_KEEPALIVE。2. 外部实现调用的是 openssl 提供的函数,简略封装下间接调用即可。
2. Emscripten 编译
下载 openssl,生成 Makefile
我用的 openssl 版本是 1.1.1d,地址: https://github.com/openssl/op…
解压后,进入 openssl-OpenSSL_1_1_1d 文件夹。编译生成 Makefile 文件。
emcmake ./Configure darwin64-x86_64-cc -no-asm --api=1.1.0
批改生成的 Makefile 文件,如果不批改,容易呈现编译谬误。
- 将 CROSS_COMPILE=/usr/local/Cellar/emscripten/1.38.44/libexec/em 改为 CROSS_COMPILE=
- 将 CNF_CFLAGS=-arch x86_64 改为 CNF_CFLAGS=
编译 openssl
emmake make -j 12 build_generated libssl.a libcrypto.a
mkdir -p ~/resource/openssl/libs
cp -R include ~/resource/openssl/include
cp libcrypto.a libssl.a ~/Downloads/openssl/libs
创立了一个 openssl 目录,其实是为了在 md5.c 中援用动态库的地位。编译胜利后,文件夹下会呈现 libssl.a 和 libcrypto.a 两个文件,
编译 wasm
emcc md5.c -I ~/resource/openssl/include -L ~/resource/openssl/libs -lcrypto -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap","ccall"]' -o md5.js
编译胜利后,会生成 md5.js 和 md5.wasm 两个文件。
Tips:
Emscripten 从 v1.38 开始,ccall/cwrap 辅助函数默认没有导出,在编译时须要通过 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall','cwrap']" 选项显式导出。
3. 调用 wasm 文件
应用 WebAssembly JS API 调用 wasm。md5 和 sha1 的代码都放在了 md5.html 中了,两者应用形式一样,文中只贴 md5 相干代码。代码地址: https://github.com/likai1130/…
<div>
<div>
<input type="file" id="md5files" style="display: none" onchange="md5fileImport();"> 计算 md5
<input type="button" id="md5fileImport" value="导入">
</div>
</div>
<script src="jquery-3.5.1.min.js"></script>
<script src="md5.js"></script>
<script type='text/javascript'>
Module = {};
const mallocByteBuffer = len => {const ptr = Module._malloc(len)
const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, len)
return heapBytes
}
// 点击导入按钮, 使 files 触发点击事件, 而后实现读取文件的操作
$("#md5fileImport").click(function() {$("#md5files").click();})
function md5fileImport() {
// 获取读取我文件的 File 对象
var selectedFile = document.getElementById('md5files').files[0];
var name = selectedFile.name; // 读取选中文件的文件名
var size = selectedFile.size; // 读取选中文件的大小
console.log("文件名:" + name + "大小:" + size);
var reader = new FileReader(); // 读取操作就是由它实现.
reader.readAsArrayBuffer(selectedFile)
reader.onload = function() {
// 当读取实现后回调这个函数, 而后此时文件的内容存储到了 result 中, 间接操作即可
console.log(reader.result);
const md5 = Module.cwrap('md5', null, ['number', 'number']) const inBuffer = mallocByteBuffer(reader.result.byteLength)
var ctx = new Uint8Array(reader.result) inBuffer.set(ctx)
const outBuffer = mallocByteBuffer(32)
md5(inBuffer.byteOffset,outBuffer.byteOffset,inBuffer.byteLength)
console.log("md5 值 =",Array.from(outBuffer).map(v => String.fromCharCode(v)).join(''))
Module._free(inBuffer);
Module._free(outBuffer);
}
}
</script>
4. 浏览器中运行
文件 a.out, 是个二进制数据
md5: 0d3c57ec65e81c7ff6da72472c68d95b
sha1: 9ef00799a4472c71f2177fd7254faaaadedb0807
一个是程序计算的 md5 和 sha1,一个是零碎上 openssl 计算的 md5 和 sha1,阐明本次 Webassembly 编译 openssl 的实际是胜利的。
二、遇到的问题
调用链如下:
md5.js (胶水代码)<-----> md5.c <-----> openssl API
数据通信问题
在整个实际的过程中,最令人头疼的问题是数据通信问题。在 C/C++ 和 JS 之间传递简单数据结构很麻烦,须要操作内存来实现。
-
Javascript 与 C /C++ 替换数据
typescript #md5.wasm 解析后的 md5 函数在 wasm 文件中的代码 func $md5 (;3;) (export "md5") (param $var0 i32) (param $var1 i32) (param $var2 i32)
因为 wasm 目前只能够 import 和 export C 语言函数格调的 API,而且参数只有四种数据类型(i32, i64, f32, f64),都是数字,能够了解为赤裸裸的二进制编码,没法间接传递简单的类型和数据结构。 所以在浏览器中这些高级类型的 API 必须靠 JS 来封装,两头还须要一个机制实现跨语言转换简单的数据结构。
- Module.buffer
无论编译指标是 asm.js 还是 wasm,C/C++ 代码眼中的内存空间实际上对应的都是 Emscripten 提供的 ArrayBuffer 对象:Module.buffer,C/ C 内存地址与 Module.buffer 数组下标一一对应。
function md5fileImport() {var selectedFile = document.getElementById('md5files').files[0];
var name = selectedFile.name; // 读取选中文件的文件名
var size = selectedFile.size; // 读取选中文件的大小
console.log("文件名:" + name + "大小:" + size);
var reader = new FileReader(); // 这是外围, 读取操作就是由它实现.
reader.readAsArrayBuffer(selectedFile)
.....
}
在代码中咱们应用 reader.readAsArrayBuffer() 来读取文件,返回的是 ArrayBuffer 数组。但还是不能调用 C 函数,须要创立一个 typed array,如 Int8Array, UInt32Array,用其特定的格局作为这段二进制数据的 view,从而进行读写操作。
Tips:
C/C++ 代码能间接通过地址拜访的数据全副在内存中(包含运行时堆、运行时栈),而内存对应 Module.buffer 对象,C/ C 代码能间接拜访的数据事实上被限度在 Module.buffer 外部。
WebAssembly 的内存也是一个 ArrayBuffer,Emscripten 封装的 Module 提供了 Module.HEAP8、Module.HEAPU8 等各种 view。附图:
- 在 JavaScript 中拜访 C /C++ 内存
计算 md5/sha1 须要 javascript 将大量数据输出到 C /C++ 环境,而 C /C++ 无奈预知数据块的大小,此时能够在 JavaScript 中分配内存并装入数据,而后将数据指针传入,调用 C 函数进行解决。
Tips:
这种用法之所以可行,外围起因在于:Emscripten 导出了 C 的 malloc()/free()
我将分配内存空间的办法申明成了公共办法。
Module = {};
const mallocByteBuffer = len => {const ptr = Module._malloc(len)
const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, len)
return heapBytes
}
function md5fileImport() {
// 获取读取我文件的 File 对象
var selectedFile = document.getElementById('md5files').files[0];
......
var reader = new FileReader(); // 这是外围, 读取操作就是由它实现.
reader.readAsArrayBuffer(selectedFile)
reader.onload = function() {
// 当读取实现后回调这个函数, 而后此时文件的内容存储到了 result 中, 间接操作即可
const md5 = Module.cwrap('md5', null, ['number', 'number'])
const inBuffer = mallocByteBuffer(reader.result.byteLength)
var ctx = new Uint8Array(reader.result)
inBuffer.set(ctx)
const outBuffer = mallocByteBuffer(32)
md5(inBuffer.byteOffset,outBuffer.byteOffset,inBuffer.byteLength)
console.log("md5 值 =",Array.from(outBuffer).map(v => String.fromCharCode(v)).join(''))
Module._free(inBuffer);
Module._free(outBuffer);
}
}
Tips:
C/C++ 的内存没有 gc 机制,在 JavaScript 中应用 malloc() 函数调配的内存应用完结后,须要应用 free() 将其开释。
此外,Emscripten 还提供了 AsciiToString()/stringToAscii()/UTF8ArrayToString()/stringToUTF8Array() 等一系列辅助函数用于解决各种格局的字符串在各种存储对象中的转换,欲知详情请自行参考胶水代码。
三、总结
基于 wasm 的 openssl 残缺调用关系:
本次实际过程中遇到的技术问题就是数据通信的问题,还有一个是思路上的问题,始终认为把 openssl 整体编译成.wasm 文件,就能够用了,事实证明还须要应用胶水代码,能力在 web 中应用。那么有个疑难.wasm 文件实质上是个二进制文件,是否有工具能够间接运行呢.wasm 文件,WAPM(WebAssembly Package Manager) 这是 WebAssembly 的包管理工具,下一篇文章一起来意识下 WebAssembly 包管理工具。
参考资料
-
示例代码地址:
- https://github.com/likai1130/…
-
WebAssembly API(中文,解决逻辑 JS 调用 wasm 问题。):
- https://developer.mozilla.org…
-
Emscripten 语法学习(解决 C 语言调用 JS 语法问题):
- https://emscripten.org/docs/a…
-
Openssl 编译参考
- https://github.com/wapm-packa…
Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通信及互联网行业有十分丰盛的落地教训。Netwarps 目前在深圳、北京均设立了研发核心,团队规模 30+,其中大部分为具备十年以上开发教训的技术人员,别离来自互联网、金融、云计算、区块链以及科研机构等业余畛域。
Netwarps 专一于平安存储技术产品的研发与利用,次要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具备高可用、低功耗和低网络的技术特点,实用于物联网、工业互联网等场景。
公众号:Netwarps