上一篇文章分享了WebAssembly概念和基本使用,经过两个代码示例的分析对WebAssembly有了大体的了解。这一篇文章分享的是基于WebAssembly的加密工具实践,咱们就以openssl的摘要算法md5和sha1为例,在Mac上编译openSSL到WebAssembly。javascript
将Openssl编译到WebAssembly整个流程是这样的,md5.c文件–>emscripten编译–>.wasm文件–>结合WebAssembly JS API–>浏览器中运行。html
//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。java
Tips: 1. 默认状况下,Emscripten 生成的代码只会调用 main() 函数,其它的函数将被视为无用代码。在一个函数名以前添加 EMSCRIPTEN_KEEPALIVE 可以防止这样的事情发生。你须要导入 emscripten.h 库来使用 EMSCRIPTEN_KEEPALIVE。 2. 内部实现调用的是openssl提供的函数,简单封装下直接调用便可。
我用的openssl版本是1.1.1d,地址: https://github.com/openssl/openssl/releases/tag/OpenSSL_1_1_1d
解压后,进入openssl-OpenSSL_1_1_1d文件夹。编译生成Makefile文件。jquery
emcmake ./Configure darwin64-x86_64-cc -no-asm --api=1.1.0
修改生成的Makefile文件,若是不修改,容易出现编译错误。git
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两个文件,github
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两个文件。web
Tips: Emscripten从v1.38开始,ccall/cwrap辅助函数默认没有导出,在编译时须要经过-s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']"选项显式导出。
使用WebAssembly JS API调用wasm。md5和sha1的代码都放在了md5.html中了,二者使用方式同样,文中只贴md5相关代码。代码地址: https://github.com/likai1130/study/blob/master/wasm/openssl/demo/md5.html算法
<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>
文件a.out,是个二进制数据
md5: 0d3c57ec65e81c7ff6da72472c68d95b
sha1: 9ef00799a4472c71f2177fd7254faaaadedb0807typescript
一个是程序计算的md5和sha1,一个是系统上openssl计算的md5和sha1,说明本次Webassembly编译openssl的实践是成功的。api
调用链以下:
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.HEAP八、Module.HEAPU8 等各类 view。附图:
计算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包管理工具。
示例代码地址:
WebAssembly API(中文,解决逻辑JS调用wasm问题。):
Emscripten 语法学习(解决C语言调用JS语法问题):
Openssl 编译参考
Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通讯及互联网行业有很是丰富的落地经验。Netwarps 目前在深圳、北京均设立了研发中心,团队规模30+,其中大部分为具有十年以上开发经验的技术人员,分别来自互联网、金融、云计算、区块链以及科研机构等专业领域。Netwarps 专一于安全存储技术产品的研发与应用,主要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具备高可用、低功耗和低网络的技术特色,适用于物联网、工业互联网等场景。公众号:Netwarps