上一篇文章分享了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/openssl/releases/tag/OpenSSL_1_1_1d
解压后,进入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/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>
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包管理工具。

参考资料


Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通信及互联网行业有非常丰富的落地经验。Netwarps 目前在深圳、北京均设立了研发中心,团队规模30+,其中大部分为具备十年以上开发经验的技术人员,分别来自互联网、金融、云计算、区块链以及科研机构等专业领域。
Netwarps 专注于安全存储技术产品的研发与应用,主要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具有高可用、低功耗和低网络的技术特点,适用于物联网、工业互联网等场景。
公众号:Netwarps