//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提供的函數,簡單封裝下直接調用即可。
我用的 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 文件,如果不修改,容易出現編譯錯誤。
emmake make -j 12 build_generated libssl.a libcrypto.amkdir -p ~/resource/openssl/libscp -R include ~/resource/openssl/includecp libcrypto.a libssl.a ~/Downloads/openssl/libs
創建了一個 openssl 目錄,其實是為了在 md5.c 中引用靜態庫的位置。編譯成功後,文件夾下會出現 libssl.a 和 libcrypto.a 兩個文件。
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']"選項顯式導出。
使用 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: 9ef00799a4472c71f2177fd7254faaaadedb0807
一個是程序計算的 md5 和 sha1,一個是系統上 openssl 計算的 md5 和 sha1,說明本次 Webassembly 編譯 openssl 的實踐是成功的。
md5.js (膠水代碼)<> md5.c <> openssl API
在整個實踐的過程中,最令人頭疼的問題是數據通信問題。在 C/C++ 和 JS 之間傳遞複雜數據結構很麻煩,需要操作內存來實現。
Javascript 與 C/C++ 交換數據
#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() 等一系列輔助函數用於處理各種格式的字符串在各種存儲對象中的轉換,欲知詳情請自行參考膠水代碼。
本次實踐過程中遇到的技術問題就是數據通信的問題,還有一個是思路上的問題,一直以為把 openssl 整體編譯成 .wasm 文件,就可以用了,事實證明還需要使用膠水代碼,才能在 web 中使用。那麼有個疑問 .wasm 文件本質上是個二進位文件,是否有工具可以直接運行呢 .wasm 文件,WAPM(WebAssembly Package Manager) 這是WebAssembly的包管理工具,下一篇文章一起來認識下WebAssembly 包管理工具。
https://github.com/likai1130/study/tree/master/wasm/openssl/demo
WebAssembly API(中文,解決邏輯JS調用wasm問題):https://developer.mozilla.org/zh-CN/docs/WebAssembly#API%E5%8F%82%E8%80%83Emscripten 語法學習(解決C語言調用JS語法問題):
https://emscripten.org/docs/api_reference/emscripten.h.html#c.EM_ASM_Openssl 編譯參考
https://github.com/wapm-packages/OpenSSL/blob/master/build.sh