《WebAssembly 权威指南》(6)在浏览器中运行遗留代码
譯者注:這篇文章是《WebAssembly 權(quán)威指南》一書的第六章,介紹了如何使用 WebAssembly 在瀏覽器中運行遺留代碼,即已經(jīng)存在的 C/C++ 代碼庫。文章以一個實際的例子,即使用 Emscripten 工具將 C++ 代碼編譯為 WebAssembly 模塊,并在瀏覽器中使用 JavaScript 調(diào)用它。文章詳細說明了 Emscripten 的工作原理、編譯選項、運行時環(huán)境和調(diào)試方法。
現(xiàn)在,我們再仔細地看看在瀏覽器中調(diào)用 C/C++ 代碼的過程。大多數(shù)編程語言的代碼都不是為了在瀏覽器中以下載的形式運行。但是,正如香港騎士(譯者注:The Hong Kong Cavaliers,電影《天生愛神》中的主角 Buckaroo Banzai 的樂隊名稱)的隊長那樣,偶爾你會發(fā)現(xiàn)自己出現(xiàn)在某個意想不到的新地方,而那里只有你。
我們對在瀏覽器中調(diào)用 C/C++ 代碼感興趣的原因是多方面的。但取代 JavaScript 并不是其中之一。至少對大多數(shù)人來說不是。相反,我們有大量的用 C 和 C++ 等語言編寫的遺留代碼。其中有很多是非常有用的,如果能在我們的網(wǎng)絡(luò)應(yīng)用程序中使用這些代碼,那就太好了。其中一些可能是將組織與遺留系統(tǒng)聯(lián)系在一起的。能夠通過瀏覽器分發(fā)這些代碼將是一個很大的進步。
此外,有些問題根本不適合用 JavaScript 來寫。可以用另一種語言來編寫應(yīng)用程序的這一部分,而不需要一個單獨的運行時,這是非常吸引人的。而且,正如我們的最后一個用例所表明的那樣,對于敏感和棘手的軟件(如加密算法)來說,有來自可信來源的可信代碼提供證明才是有價值的。能夠簡單地重新編譯來自你所認識的人的現(xiàn)有代碼,他們知道自己在做什么,這也是一種有用的能力。
在上一章中,我們展示了使用常規(guī)的支持 WebAssembly 的 C 編譯器(如 clang)和一些頭和庫的依賴性管理來實現(xiàn)基本的集成是可能的。然而,必須提供我們自己的標準庫版本,并手動將 C 代碼連接到所提供的 JavaScript 上,這樣做很快就會過時。
幸運的是,Emscript 項目?[1]?奠定了基礎(chǔ),它比其他方式更容易。這并不奇怪,因為它的主要開發(fā)者 Alon Zakai 和 Luke Wagner 一直是這項工作的幕后推手,從 asm.js 開始,延伸到 WebAssembly MVP,再到推動規(guī)范的發(fā)展 ,一直持續(xù)到今天。Emscripten 工具鏈在這一過程中發(fā)揮了重要作用。
該項目是基于 LLVM 平臺的。在前面的章節(jié)中,我指出它最初有一個自定義的后端,用來生成 asm.js 的可優(yōu)化的 JavaScript 子集。一旦 WebAssembly 平臺被定義,一個新的后端就能生成 Wasm 二進制文件。
不幸的是,這只是解決方案的一部分。還需要支持數(shù)據(jù)進出內(nèi)存,鏈接模塊,包裝現(xiàn)有的庫,等等。一個提供用戶界面或監(jiān)聽網(wǎng)絡(luò)請求的 C 語言程序經(jīng)常在一個相當緊密的循環(huán)中響應(yīng)輸入活動。鑒于瀏覽器默認為單線程環(huán)境,這種主循環(huán)會出現(xiàn)操作上的不匹配。Emscripten 工具鏈已被修改,以解決在試圖將本地 C/C++ 移植到 Web 環(huán)境中運行時可能出現(xiàn)的許多類型的問題。與大多數(shù)主題一樣,本書不可能全面介紹這個項目的所有內(nèi)容,但我將嘗試讓你快速入門。
適當?shù)?"Hello, World!"
所以,首先要承認:我們本可以在第二章中就在瀏覽器中擁有一個有效的、未經(jīng)修改的 "Hello, World!" 的例子,早在 2000 年就有了。最后一次,我們將向你展示例 6-1 中的代碼。
例 6-1. 典型的 "Hello, World!" 程序用 C 語言表達
#include?<stdio.h> int?main?()?{printf?("Hello,?World!\n");return?0; }使用 Emscripten C 語言編譯器(安裝說明見附件?[2]),我們只需要告訴它編譯 C 代碼并生成一些 JavaScript 腳手架。之后,它就會在 Node.js 中未經(jīng)修改地運行。
brian@tweezer?~/g/w/s/ch06>?emcc?hello.c?-o?hello.js? brian@tweezer?~/g/w/s/ch06>?ls?-laF total?520 drwxr-xr-x?7?brian?staff?224?Mar?1?14:45?./ drwxr-xr-x?7?brian?staff?224?Mar?1?13:02?../ -rw-r--r--?1?brian?staff?121457?Mar?1?13:05?bootstrap.min.css? -rw-r--r--?1?brian?staff?76?Mar?1?13:02?hello.c? -rw-r--r--?1?brian?staff?388?Mar?1?13:07?hello.html? -rw-r--r--?1?brian?staff?121686?Mar?1?14:45?hello.js -rwxr-xr-x?1?brian?staff?11711?Mar?1?14:45?hello.wasm*? brian@tweezer?~/g/w/s/ch06>?node?hello.js Hello,?World!例 6-2 中的 HTML 文件并不是由這個過程生成的,它與我們之前看到的文件有明顯的不同。有一個單一的?<script>?元素來加載我們生成的 JavaScript。我們沒有使用到目前為止使用過的 utils.js 文件。相反,我們有一個由前面的命令產(chǎn)生的更長的 JavaScript 文件。看看這個文件的清單!它超過了 120KB。它超過了 120 千字節(jié)!超過 2000 行代碼。如果你瀏覽下這個文件,你很快就會迷失。這就是我不想在前面的章節(jié)中從這里開始的原因。
例 6-2. 一個與我們所見過的 HTML 文件大不相同的文件
<!DOCTYPE?html> <html?lang="en"><head>?<meta?charset="utf-8"?/>?<link?rel="stylesheet"?href="bootstrap.min.css"?/>?<title>Hello,?World!</title>?</head>?<body>?<div?class="container">?<h1>Hello,?World!</h1>?</div>?<script?src="hello.js"></script>??</body> </html>然而,如果我們通過 HTTP 提供這個目錄,打開瀏覽器,并打開 JavaScript 控制臺,你會看到非常類似圖 6-1 的東西。
圖 6-1. 你看到的 Hello, World!如果你對?hello.wasm?文件使用 wasm-objdump 命令,你會注意到有一個導出的?main ()?函數(shù)。生成的代碼很快就超出了我們顯示整個文件的能力,所以我將只強調(diào)導出部分。
...Export?[13]:-?memory?[0]?->?"memory"-?func?[3]?<__wasm_call_ctors>?->?"__wasm_call_ctors"-?func?[5]?<main>?->?"main"-?func?[6]?<__errno_location>?->?"__errno_location"-?func?[50]?<fflush>?->?"fflush"-?func?[47]?<stackSave>?->?"stackSave"-?func?[48]?<stackRestore>?->?"stackRestore"-?func?[49]?<stackAlloc>?->?"stackAlloc"-?func?[44]?<emscripten_stack_init>?->?"emscripten_stack_init"-?func?[45]?<emscripten_stack_get_free>?->?"emscripten_stack_get_free"-?func?[46]?<emscripten_stack_get_end>?->?"emscripten_stack_get_end"-?table?[0]?->?"__indirect_function_table"-?func?[53]?<dynCall_jiji>?->?"dynCall_jiji" ...你看,為了能讓這個 “Hello, world!” 運行生成了相當多的腳手架。這些細節(jié)相當復雜,但如果你想通過它來追蹤,我建議用 wasm2wat 生成相應(yīng)的 Wat 文件。從那里,追蹤?main ()?函數(shù)(在前面的代碼樣本中編號為 5)。你將會看到如例 6-3 所示內(nèi)容。
例 6-3. Wat 的 main 方法
... (func?(;5;)?(type?5)?(param?i32?i32)?(result?i32)?(local?i32) call?4 local.set?2 local.get?2 return)? ...最終,你會發(fā)現(xiàn)自己回到了生成的 JavaScript 文件。在那里有一個叫做?fd_write?的函數(shù),如例 6-4 所示。它被添加到一個名為?wasi_snapshot_preview1?的命名空間。顧名思義,這是一個我們將在后面的討論中涉及的預(yù)覽,但主要的一點是,Emscripten 工具鏈正在生成代碼,以解決我們在前面章節(jié)中看到的一些底層麻煩。我們將在第 10 章中發(fā)現(xiàn)與 Rust 生態(tài)系統(tǒng)類似的工具鏈。
例 6-4. printf 解決方案的一部分
... function?_fd_write?(fd,?iov,?iovcnt,?pnum)?{//hack?to?support?printf?in?SYSCALLS_REQUIRE_FILESYSTEM=0varnum?=?0;for?(vari?=?0;?i?<?iovcnt;?i++)?{var?ptr?=?HEAP32?[(((iov)?+?(i?*?8))?>>?2)];var?len?=?HEAP32?[(((iov)?+?(i?*?8?+?4))?>>?2)];for?(varj?=?0;?j?<?len;?j++)?{SYSCALLS.printChar?(fd,?HEAPU8?[ptr?+?j]);}num?+=?len;}HEAP32?[((pnum)?>>?2)]?=?numreturn?0; } ...當然,我們沒有必要深入了解這一切到底是如何進行的。重要的是你要明白,我們實際上不是在典型的標準庫意義上調(diào)用?printf (),而是這個函數(shù)被改寫成了調(diào)用生成的代碼。在瀏覽器中,它將把字符路由到與開發(fā)者工具相關(guān)的 JavaScript 控制臺。在 Node.js 環(huán)境中,它將被路由到底層系統(tǒng)控制臺。在這個階段,重要的是,我們的傳統(tǒng)應(yīng)用程序不必修改就可以在這個新環(huán)境中運行,但我們也沒有被直接運行本地 C 和 C++ 的可怕前景所困擾。我們在可移植性、安全性和性能之間取得了重要的平衡,這正是 WebAssembly 的意義所在。
生成的代碼在 JavaScript 中有一個 Module 對象,它定義了我們的 WebAssembly 代碼將占用的運行環(huán)境。在 JavaScript 文件的頂部有一些注釋,描述了這個對象和它作為兩個世界之間的接口的作用。然而,為了保持事情的可控性,我們將專注于其中更小的部分。
我們可以選擇的方法之一是使用編譯器指令來打開或壓制某些生成行為。例如,我們可能不希望我們的 C 程序在加載 JavaScript 代碼時立即運行。如果你嘗試在沒有?INVOKE_RUN=0?指令的情況下進行編譯,你會看到典型的問候語,就像你在前面的例子中一樣。在下面的片段中,注意到在 Node.js 中加載代碼時,沒有任何東西被打印到命令行。
brian@tweezer?~/g/w/s/ch06>?emcc?hello.c?-o?hello.js?-s?INVOKE_RUN=0? brian@tweezer?~/g/w/s/ch06>?node?hello.js brian@tweezer?~/g/w/s/ch06>很明顯,如果你抑制了自動執(zhí)行,你將希望能夠指示應(yīng)用程序何時可執(zhí)行。這可以通過另一個指令來實現(xiàn):
brian@tweezer?~/g/w/s/ch06>?emcc?hello.c?-o?hello.js?? -s?INVOKE_RUN=0?-s?EXPORTED_RUNTIME_METHODS="['callMain']"在例 6-5 中,你可以看到我們調(diào)用?main ()?函數(shù)來響應(yīng)一個按鈕的點擊。
例 6-5. 一個延遲的?main ()?方法調(diào)用
<!DOCTYPE?html> <html?lang="en"><head>?<meta?charset="utf-8"?/>?<link?rel="stylesheet"?href="bootstrap.min.css"?/>?<title>Hello,?World!</title>?</head>?<body>?<div?class="container">?<h1>Hello,?World!</h1>?<button?id="press">Press?Me</button>?</div>?<script?src="hello.js"></script>?<script> var?button?=?document.getElementById?("press");?button.onclick?=?function?()?{try?{?Module.callMain?(); }?catch?(re)?{};};</script>??</body> </html>在圖 6-2 中,可以看到當按鈕被按下時,友好的信息被打印到控制臺。Firefox 沒有顯示每條相同的信息,但它顯示我已經(jīng)在右邊按了七次按鈕。你的瀏覽器可能會在每次調(diào)用時顯示一條打印信息。
圖 6-2. 按下按鈕所觸發(fā)的 Hello, World!移植第三方代碼
我們現(xiàn)在要深入研究將一些現(xiàn)有的代碼引入瀏覽器。這段代碼從未打算在 web 上運行,它所做的事情通常不會在瀏覽器中運行,例如向文件系統(tǒng)寫入。不要擔心,我們不會破壞瀏覽器的安全模型,但你會看到這段 C++ 代碼基本上可以不加修改地運行。
Emscripten 有大量的選項用于將第三方代碼移植到 WebAssembly 中。它可以有效地替代 cc、make 和 configure,這通常使移植過程變得簡單。在現(xiàn)實中,你很可能要通過自己的方式來解決遇到的問題,但你可能會驚訝于這個過程是如此的簡單。該項目的網(wǎng)站?[3]?有很多幫助文檔。然而,我最喜歡的主題介紹是 Robert Aboukhalil 的?Level Up With WebAssembly 材料?[4]。他告訴你如何將幾個不同的開源項目移植到 WebAssembly,以便在瀏覽器上運行。這包括像俄羅斯方塊、乒乓和吃豆人等游戲。與其嘗試重新創(chuàng)造他已經(jīng)完成的杰作,我將專注于一個相對簡單和干凈的項目。
我花了一些時間來尋找好的候選代碼。我想找一些內(nèi)容豐富但又不至于過于復雜的代碼。最終,我在?Arash Partow 的網(wǎng)站?[5]?上找到了他收集的優(yōu)雅、干凈、適當授權(quán)和有用的 C++ 代碼。在那里你會發(fā)現(xiàn)相當多有趣的材料。我原本打算使用計算幾何庫,但 Bitmap 庫更適合于這本書。
首先,從?Partow 的網(wǎng)站?[6]?下載代碼。下載 ZIP 文件, 解壓縮,你會看到三個文件。Makefile 是一個老式的 Unix 構(gòu)建文件,它有組裝有關(guān)軟件的指示。我們稍后將探討這個過程。bitmap_image.hpp?文件是主庫,bitmap_test.cpp?是一個全面的測試集合,用于生成一堆有趣的 Windows 位圖圖像。這段代碼不需要任何特定平臺的庫。
brian@tweezer?~/g/w/s/c/bitmap>?ls?-alF? total?536 -rw-r--r--@?1?brian??staff???770B?Dec?31??1999?Makefile -rw-r--r--@?1?brian??staff???242K?Dec?31??1999?bitmap_image.hpp -rw-r--r--@?1?brian??staff????20K?Dec?31??1999?bitmap_test.cpp我把一些注釋和許可證的細節(jié)從例 6-6 中刪除了,為了節(jié)省空間。剩下的是構(gòu)建測試程序的規(guī)則結(jié)構(gòu),即?bitmap_test。Makefile 的工作方式是建立一個目標,然后是建立目標的依賴關(guān)系和規(guī)則。作為一種慣例,通常有一個 All 規(guī)則,指定前面提到的目標文件名。它依賴于?.cpp?和?.hpp?文件。如果這兩個文件中的任何一個被修改了,我們的可執(zhí)行文件就需要被重新構(gòu)建。要做到這一點,make 工具將用 OPTIONS 變量中的選項執(zhí)行 COMPILER 變量中的文件。作為一個 C/C++ 程序,它還需要與 LINKER_OPT 變量中指定的庫鏈接。在這種情況下,我們要與標準的 C++ 庫和基本的數(shù)學函數(shù)集合進行鏈接。在庫方面,這是最獨立的了。clean 目標只是刪除了衍生的結(jié)果。
Makefile 通常對空格和制表符比較敏感。確保使用制表符來開始縮進的規(guī)則行。本書倉庫中的代碼就是這樣做的,但如果你以任何方式修改它,你要確保使用制表符。
例 6-6. 測試程序的 Makefile
COMPILER OPTIONS LINKER_OPT =?-c++ =?-ansi?-pedantic-errors?-Wall?-Wall?-Werror?-Wextra?-o =?-L/usr/lib?-lstdc++?-lm all:?bitmap_test bitmap_test:?bitmap_test.cpp?bitmap_image.hpp$(COMPILER)?$(OPTIONS)?bitmap_test?bitmap_test.cpp?$(LINKER_OPT) clean:?rm?-f?core?*.o?*.bak?*stackdump?*~只要你安裝了一個正常的 C++ 環(huán)境,你就應(yīng)該能夠構(gòu)建測試程序。
brian@tweezer?~/g/w/s/c/bitmap>?make c++?-ansi?-pedantic-errors?-Wall?-Wall?-Werror?-Wextra?-o?bitmap_test?? bitmap_test.cpp?-L/usr/lib?-lstdc++?-lm?brian@tweezer?~/g/w/s/c/bitmap>?ls?-alF? total?944 drwxr-xr-x@??6?brian??staff?????192?Mar??6?14:35?./ drwxr-xr-x@?11?brian??staff?????352?Mar??6?13:56?../ -rw-r--r--@??1?brian??staff?????770?Dec?31??1999?Makefile -rw-r--r--@??1?brian??staff??247721?Dec?31??1999?bitmap_image.hpp -rwxr-xr-x???1?brian??staff??205032?Mar??6?14:35?bitmap_test* -rw-r--r--@??1?brian??staff???20479?Dec?31??1999?bitmap_test.cpp現(xiàn)在這個測試程序要求在當前目錄下有一個?image.bmp?文件的例子。我只是在網(wǎng)上找了一個,然后用這個名字。運行該程序后,你將得到一大堆生成的圖像,如圖 6-3 所示。
圖 6-3. 由 bitmap_test 可執(zhí)行文件生成的圖像好的,所以它可以工作。它是干凈的代碼。我不打算教你 C++,也不打算帶你看代碼,但我會給你看一些工作實例,你可以在完全不理解發(fā)生了什么的情況下進行嘗試。
首先要做的是。我們需要修改 Makefile 以使用 Emscripten 編譯器,而不是你用來構(gòu)建測試程序的東西。這就像更新 COMPILER 變量一樣簡單,如例 6-7 所示。
例 6-7. 為我們的測試程序更新 Makefile,以便用 Emscripten 編譯。
... COMPILER??????=?-em++ ...Makefile 的 clean 步驟并沒有刪除可執(zhí)行文件。所以手動刪除?bitmap_test(或者更好的是修改 Makefile!),現(xiàn)在重新運行 make。你應(yīng)該看到類似下面的東西。
brian@tweezer?~/g/w/c/bitmap>?make em++?-ansi?-pedantic-errors?-Wall?-Wall?-Werror?-Wextra?-o?bitmap_test???bitmap_test.cpp?-L/usr/lib?-lstdc++?-lm brian@tweezer?~/g/w/c/bitmap>?ls?-alF total?1848 drwxr-xr-x@?8?brian?staff????256????Mar??6?15:21?./ drwxr-xr-x?12?brian?staff????384????Mar??6?14:47?.../ -rw-r-r--@??1?brian?staff????771????Mar??6?15:20?Makefile -rw-r-r--@??1?brian?staff?247721????Dec?31?1999?bitmap_image.hpp -rw-r--r--?1?brian?staff?248314????Mar??6?15:21?bitmap_test -rw-r-r--@??1?brian?staff??20479????Dec??31?1999?bitmap_test.cpp -rwxr-xr-x?1?brian?staff?296743????Mar??6?15:21?bitmap_test.wasm*. -rw-r-r--@??1?brian?staff?120054????Mar??6?14:39?image.bmp呃,這很容易。不幸的是,我們還沒有完全完成。雖然這確實是在編譯,但由于各種原因,它是無法工作的。其中第一個原因是,該庫希望能夠?qū)懭胛募到y(tǒng)。這一點應(yīng)該不足為奇,因為這是不可能的。然而,有一個非常酷的文件系統(tǒng)抽象,它可以通過添加編譯器指令來寫入本地存儲。現(xiàn)在,就像處理?printf () ?的調(diào)用一樣,Emscripten 工具鏈將模擬一個文件系統(tǒng)。通過在你的 Makefile 中添加指令?-s FORCE_FILESYSTEM=1?來解鎖這一支持。我將在下面向你展示最終的形式。
第二個問題是,默認生成的 Memory 實例將不允許增長。如果我們期望這個庫能在內(nèi)存中生成一些相當大的圖像,那么它就需要足夠的內(nèi)存。所以,我們可以使用另一個指令來允許它這樣做。這是我在第 4 章中向你展示的如何手動操作的東西。這是 Emscripten 可以為我們處理的細節(jié)。為了對這個過程有更多的控制,我們將告訴 Emscripten 不要自動退出程序,并導出?main ()?方法,這樣我們就可以在需要時調(diào)用它。因為我們不是生成一個獨立的二進制文件,我們還要告訴 Emscripten 編譯器生成一個叫做?bitmap_test.js?的 JavaScript 文件。bitmap_test?規(guī)則的命令現(xiàn)在應(yīng)該如例 6-8 所示。
例 6-8. 修改后的 Makefile 帶有我們所有的 Emscripten 選項
bitmap_test:?bitmap_test.cpp?bitmap_image.hpp $(COMPILER)?$(OPTIONS)?bitmap_test.js?bitmap_test.cpp?$(LINKER_OPT)?-s?FORCE_FILESYSTEM=1?-s?ALLOW_MEMORY_GROWTH=1??-s?INVOKE_RUN=0?-s?EXPORTED_RUNTIME_METHODS="['callMain']"這就解決了妨礙該例子工作的具體問題。然而,還有一個問題。這個程序運行了 20 個相對耗時的測試。由于 JavaScript 是一個單線程的環(huán)境,當 WebAssembly 模塊運行時,瀏覽器很可能會抓狂,因為花了太長時間。
我們最終會解決這個問題,但目前,我只是要刪除對其余測試的調(diào)用,只調(diào)用我最喜歡的?test20 ()。
main ()?方法現(xiàn)在如例 6-9 所示。
例 6-9. 調(diào)用一個測試的 main 方法
int?main?()?{test20?();return?0;? }如果你重新運行 make 命令,你應(yīng)該看到生成的 Wasm 和 JavaScript 文件。我將生成一些基本的 HTML 腳手架供我們使用。在例 6-10 中,你可以看到我有一個按鈕和一個?<canvas>?元素,我們將用它來渲染位圖。現(xiàn)在,把這個文件和你的 Wasm 和 JavaScript 文件保存在同一個目錄中,并像我們在書中所做的那樣,通過 HTTP 提供給它。
例 6-10. 為我們的位圖生成器提供 HTML 腳手架
<!DOCTYPE?html> <html?lang="en"><head>?<meta?charset="utf-8"?/>?<title>C++-rendered?Image?in?the?Browser</title>?</head>?<body>?<div?class="container">?<h1>C++-rendered?Image?in?the?Browser</h1>?</div>?<button?id="load">Load</button>?<canvas?id="output"></canvas>?<script?src="bitmap_test.js"></script>?<script> var?button?=?document.getElementById?("load");?button.onclick?=?function?()?{Module.callMain?();console.log?("Done?rendering.");};</script>??</body> </html>一旦你把 HTML 加載到你的瀏覽器,打開開發(fā)者控制臺并按下按鈕。這將生成各種文件,并將它們寫到 "磁盤" 上。這將需要一點時間,我料想你的瀏覽器會抱怨這一點。只要告訴它在它要求的時候等待就可以了。一旦完成,你應(yīng)該看到信息被打印到控制臺。此時,在控制臺中,你可以做一些可能讓你吃驚的事情,如圖 6-4 所示。
圖 6-4. 在瀏覽器中向文件系統(tǒng)寫文件我們的第三方代碼使用標準 C++ 庫來向 "文件系統(tǒng)" 寫入。Emscripten 在瀏覽器的本地存儲上提供了一個抽象層,使之成為可能。從 C++ 中,我們可以很容易地把它讀回來。在 JavaScript 中,這并不難,如例 6-11。
例 6-11. 使用文件系統(tǒng)抽象從 "磁盤" 中讀取 "文件" 的 JavaScript
<-?undefined>>?image <-?Uint8Array?(2880054)?[66,?77,?54,?242,?43,?0,?0,?0,?0,?0,?...]我們剛剛通過調(diào)用?FS.readFile ()?函數(shù)得到了一個 Uint8Array。這將使我們很容易處理來自文件的字節(jié)。只有一個問題。瀏覽器不支持顯示 Windows 位圖文件!幸運的是,這是一種有記錄的格式,有人為我們做了件好事,提供了相應(yīng)的代碼。我們可以依靠一些現(xiàn)有的 C 或 C++ 代碼,但只是為了向你展示一些選擇,我們將使用 JavaScript 代碼?[7]。
幸運的是,在第 4 章你已經(jīng)具備了理解例 6-12 中的大部分內(nèi)容的能力。我們把從?FS.readFile ()?函數(shù)中返回的 ArrayBuffer 傳遞給一個叫做?getMBP ()?的方法。這將在緩沖區(qū)周圍創(chuàng)建一個 DataView,并在把它們?nèi)M一個更容易理解的 JavaScript 表示法之前拉出各種圖像細節(jié)。
一旦讀入位圖文件,我們通過同一網(wǎng)站的?convertToImageData ()?函數(shù)將 JavaScript 結(jié)構(gòu)轉(zhuǎn)換成 ImageData 實例。之后 ,我們設(shè)置?<canvas>?的大小以匹配其高度和寬度,并使用其?putImageData ()?方法來渲染像素。
例 6-12. 用 JavaScript 讀回我們的位圖文件,并在?<canvas>?中渲染它
<script>//?Code?taken?from?https://tinyurl.com/bitmap-in-javascript?//?Written?by?Ian?Elliottfunction?getBMP?(buffer)?{var?datav?=?new?DataView?(buffer);var?bitmap?=?{};bitmap.fileheader?=?{};bitmap.fileheader.bfType?=?datav.getUint16?(0,?true);bitmap.fileheader.bfSize?=?datav.getUint32?(2,?true);bitmap.fileheader.bfReserved1?=?datav.getUint16?(6,?true);bitmap.fileheader.bfReserved2?=?datav.getUint16?(8,?true);bitmap.fileheader.bfOffBits?=?datav.getUint32?(10,?true);bitmap.infoheader?=?{};bitmap.infoheader.biSize?=?datav.getUint32?(14,?true);bitmap.infoheader.biWidth?=?datav.getUint32?(18,?true);bitmap.infoheader.biHeight?=?datav.getUint32?(22,?true);bitmap.infoheader.biPlanes?=?datav.getUint16?(26,?true);bitmap.infoheader.biBitCount?=?datav.getUint16?(28,?true);bitmap.infoheader.biCompression?=?datav.getUint32?(30,?true);bitmap.infoheader.biSizeImage?=?datav.getUint32?(34,?true);bitmap.infoheader.biXPelsPerMeter?=?datav.getUint32?(38,?true);bitmap.infoheader.biYPelsPerMeter?=?datav.getUint32?(42,?true);bitmap.infoheader.biClrUsed?=?datav.getUint32?(46,?true);bitmap.infoheader.biClrImportant?=?datav.getUint32?(50,?true);var?start?=?bitmap.fileheader.bfOffBits;bitmap.stride?=?Math.floor?((bitmap.infoheader.biBitCount??*bitmap.infoheader.biWidth?+?31)?/?32)?*?4;bitmap.pixels?=?new?Uint8Array?(buffer,?start);return?bitmap;}//?Code?taken?from?https://tinyurl.com/bitmap-in-javascript?//?Written?by?Ian?Elliottfunction?convertToImageData?(bitmap)?{var?canvas?=?document.createElement?("canvas");var?ctx?=?canvas.getContext?("2d");var?width?=?bitmap.infoheader.biWidth;var?height?=?bitmap.infoheader.biHeight;var?imageData?=?ctx.createImageData?(width,?height);var?data?=?imageData.data;var?bmpdata?=?bitmap.pixels;var?stride?=?bitmap.stride;for?(var?y?=?0;?y?<?height;?++y)?{for?(var?x?=?0;?x?<?width;?++x)?{var?index1?=?(x+width*(height-y))*4;var?index2?=?x?*?3?+?stride?*?y;data?[index1]?=?bmpdata?[index2?+?2];data?[index1?+?1]?=?bmpdata?[index2?+?1];data?[index1?+?2]?=?bmpdata?[index2];data?[index1?+?3]?=?255;}}return?imageData;}var?button?=?document.getElementById?("load");button.onclick?=?function?()?{Module.callMain?();var?canvas?=?document.getElementById?("output");var?context?=?canvas.getContext?('2d');var?image?=?FS.readFile?("./test20_julia_set_vga.bmp");var?bmp?=?getBMP?(image.buffer);var?imageData?=?convertToImageData?(bmp);canvas.width?=?bmp.infoheader.biWidth;canvas.height?=?bmp.infoheader.biHeight;context.putImageData?(imageData,?0,?0);console.log?(image);};</script></body> </html>調(diào)用我們的 C++ 應(yīng)用程序,并在通過 JavaScript 讀回結(jié)果后在畫布上渲染,其結(jié)果如圖 6-5 所示。
圖 6-5. 在畫布中渲染位圖文件的結(jié)果我希望你至少有一點印象。要在瀏覽器中運行這段 C++ 代碼,我們只需要做很少的事情,這一點非常酷!在性能和線程方面仍有一些問題,但你已經(jīng)從兩個數(shù)字相加的過程一路走來,走了很長一段路。
我們可以執(zhí)行命令中添加一個參數(shù),以選擇要運行的測試。目前,我們不擔心在樣本圖像中讀取的測試。
為了接受命令行上的參數(shù),我們需要將?main ()?方法修改為如例 6-13 所示。
例 6-13. 修改了?main ()?方法,以接受測試選擇的參數(shù)
int?main?(int?argc,?char?**argv) {int?which?=?20;if?(argc>?1)?{std::string::size_type?sz;which?=?std::stoi?(argv?[1],?&sz);}switch?(which)?{case?0:case?1:case?2:case?3:case?4:case?5:case?6:case?7:case?8:case?10:case?11:case?12:case?13:case?16:printf?("Sorry,?%?s?requires?reading?in?a?file?which?we?are?not?supporting?yet.\n",?argv?[1]);break;case?9:test09?();break;case?14:test14?();break;case?15:test15?();break;case?17:test17?();break;case?18:test18?();break;case?19:test19?();break;case?20:test20?();break;default:printf?("Sorry,?%?s?is?an?unknown?test?number.\n",?argv?[1]);}return?0; }我們首先注意到的是,main ()?的簽名已經(jīng)被修改為接受一個整數(shù)作為命令行參數(shù),實際上是一個字符串數(shù)組。請記住,在 C/C++ 中,這是作為一個指針指向一堆指針,這就是為什么有兩個星號的原因。我們可以像對數(shù)組那樣對它們進行索引。
默認情況下,第一個參數(shù)將是可執(zhí)行文件的名稱。由于我們從 0 開始計數(shù),第一個傳入的參數(shù)將在 1 的位置。我們設(shè)置一個默認的測試數(shù)字為 20,因為我已經(jīng)表示這是我最喜歡的測試。然而,如果你傳入一個代表數(shù)字的字符串,它將被轉(zhuǎn)換為一個整數(shù)。一旦我們確定了是否使用默認值,我們就在這個值上切換。如前所述,我們跳過需要輸入圖像的測試。還有其他幾個你可以運行的。
{{<callout note 提示>}}
如果你要在本地代碼和 WebAssembly 之間來回轉(zhuǎn)移,你可能想在這時維護兩個不同的 Makefile。你可以創(chuàng)建靈活的 Makefiles,支持兩個目標。你可以用?-f <file>?參數(shù)來指定使用哪個文件,如下面的例子所示。
{{}}
如果你愿意,可以重新編譯本地可執(zhí)行文件并嘗試新的參數(shù)處理:
brian@tweezer?~/g/w/s/c/bitmap>?make?-f?Makefile.orig c++?-ansi?-pedantic-errors?-Wall?-Wall?-Werror?-Wextra?-o?bitmap_test?? bitmap_test.cpp?-L/usr/lib?-lstdc++?-lm brian@tweezer?~/g/w/s/c/bitmap>?./bitmap_test?1 1?requires?reading?in?a?file?which?we?don't?support?yet. brian@tweezer?~/g/w/s/c/bitmap>?./bitmap_test?9 brian@tweezer?~/g/w/s/c/bitmap>?ls?-laF total?7608 drwxr-xr-x@?14?brian?staff?????448?Mar??7?22:55?./ drwxr-xr-x??12?brian?staff?????384?Mar??6?14:45?../ -rw-r--r--@??1?brian?staff?????893?Mar??7?17:43?Makefile -rw-r--r--@??1?brian?staff?????776?Mar??7?20:10?Makefile.orig -rw-r--r--@??1?brian?staff??247721?Dec?31??1999?bitmap_image.hpp -rwxr-xr-x???1?brian?staff?205264??Mar??7?22:55?bitmap_test* -rw-r--r--@??1?brian?staff???20954?Mar??7?20:16?bitmap_test.cpp -rw-r--r--???1?brian?staff??249546?Mar??7?20:16?bitmap_test.js? -rw-r--r--@??1?brian?staff??120054?Mar??6?14:39?image.bmp -rw-r--r--???1?brian?staff????3127?Mar??7?17:26?index.html -rw-r--r--???1?brian?staff?3000054?Mar??7?22:55?test09_color_map_image.bmp好消息是,我們不需要對 JavaScript 代碼做太多的改動!因為簽名已經(jīng)改變了。現(xiàn)在我們可以傳入字符串,以調(diào)用?main ()?方法。與其在 HTML 中輸出只有適度差異的 JavaScript,在圖 6-6 中你可以看到從開發(fā)者控制臺輸出的帶參數(shù)的可執(zhí)行程序的調(diào)用結(jié)果。
圖 6-6. 在瀏覽器中用命令行參數(shù)調(diào)用我們的位圖生成器除了根據(jù)命令行參數(shù)選擇測試外,你可能還想把測試當作函數(shù)來運行。然而,這需要更多工作。
首先在我們的測試文件中添加一個名為?run_test ()?的方法用來接受一個參數(shù)。在這一點上沒有必要重復實際的代碼,所以我們將只是打印出一個字符串,表明請求運行的是哪個測試。如例 6-14,你可以看到這個函數(shù)的定義。
void?run_test?(int?i)?{printf?("Running?test?%?d!\n",?i); }默認情況下,只有?main ()?方法被導出,因為那是我們需要啟動程序的唯一函數(shù)。我們需要添加一個?EXPORTED_FUNCTIONS?指令,如下所示。函數(shù)名的定義有一個前導下劃線字符。如果你想讓?main ()?仍然可以被調(diào)用,你需要把它也包括進來,但在例 6-15 中我們沒有這樣做。
例 6-15. 修改后的 Makefile 導出更多方法
bitmap_test:?bitmap_test.cpp?bitmap_image.hpp$(COMPILER)?$(OPTIONS)?bitmap_test.js?bitmap_test.cpp?$(LINKER_OPT)?-s?FORCE_FILESYSTEM=1?-s?ALLOW_MEMORY_GROWTH=1?-s?INVOKE_RUN=0?-s?EXPORTED_FUNCTIONS="['_main',?'_run_test']" -s?EXPORTED_RUNTIME_METHODS="['callMain']"不幸的是,這樣做是行不通的,因為我們使用的是 C++。生成的函數(shù)名會被編譯器進一步篡改,其原因不值得在此贅述。為了避免這個問題,我們需要告訴編譯器抑制這種行為并使用 C 語言連接。要使用這種行為,我們需要修改函數(shù)定義,使之看起來如例 6-16 所示。
例 6-16. 導出一個函數(shù)以便從 JavaScript 中調(diào)用,并使用 C 語言鏈接
extern?"C" void?run_test?(int?i)?{printf?("Running?test?%?d!\n",?i); }這應(yīng)該可以解決這個問題,但我還要做一個改動,向你展示另一個選擇。
Emscripten 工具鏈中有一個便捷的方法叫 cwrap,它可以為調(diào)用一個特定的 C 函數(shù)生成一個 JavaScript 函數(shù)。我們把它添加到例 6-17 的?EXPORTED_RUNTIME_METHODS?指令中。
例 6-17. 更新了 Makefile 以使用 cwrap
bitmap_test:?bitmap_test.cpp?bitmap_image.hpp$(COMPILER)?$(OPTIONS)?bitmap_test.js?bitmap_test.cpp?$(LINKER_OPT)?-s?FORCE_FILESYSTEM=1?-s?ALLOW_MEMORY_GROWTH=1?-s?INVOKE_RUN=0?-s?EXPORTED_FUNCTIONS="['_main',?'_run_test']"?-s?EXPORTED_RUNTIME_METHODS="['callMain',?'cwrap']"如果你重新構(gòu)建和加載 HTML,你將能夠從 JavaScript 開發(fā)者控制臺調(diào)用這個函數(shù)。其結(jié)果如圖 6-7 所示。
圖 6-7. 從 JavaScript 中直接通過 cwrap () 調(diào)用我們的函數(shù)請注意,cwrap ()?的調(diào)用返回一個適當?shù)?JavaScript 函數(shù),我們可以像往常一樣使用。你可以把 switch 語句移到這個方法中,同樣能用來調(diào)用任意的測試。
作為練習,嘗試添加一個方法,寫出一個名為?image.bmp?的位圖文件。從你的 C++ 代碼中導出這個方法并從瀏覽器中調(diào)用它。這可以讓你在需要它的測試中讀回該文件。你可以修改 switch 語句以允許調(diào)用這些方法。
最后,想象一些其他的用戶界面元素,允許你挑選要運行的測試。一旦運行,設(shè)想在?<canvas>?元素中顯示一個文件列表。你幾乎擁有做這件事所需要的所有組件,所以請試一試吧!
libsodium
在本章結(jié)束之前,我想請你注意一個叫做 libsodium 的項目。我們不打算直接用它做任何事情,但它展示了通過 WebAssembly 將 C 和 C++ 等語言與瀏覽器混合的能力。
它基于網(wǎng)絡(luò)和加密庫(NaCl),這是一個高性能的現(xiàn)代加密庫,由深諳此道的人編寫。NaCl 的許多功能還不一定能用于 JavaScript 運行時。新的密碼套件,包括帶有附加數(shù)據(jù)的認證加密(AEAD),可能會在它們被移植到 JavaScript 或通過操作系統(tǒng)提供給瀏覽器之前發(fā)布在這里。
第二個動機是,NaCl 庫的作者知道他們在做什么。用一個糟糕的實現(xiàn)來破壞一個加密庫的功效是非常容易的。即使是比較兩個哈希值是否相同這樣微妙的事情,如果實施不當也會泄露細節(jié)。令人沮喪的是,這種比較的正確實現(xiàn)將與開發(fā)人員通常比較兩個哈希值的方式相悖。我的觀點是,NaCl 代碼庫是有出處的。如果一個沒有背景了解這些細節(jié)的 JavaScript 開發(fā)人員試圖實現(xiàn)這些功能,那么它很有可能存在這些漏洞。當你擁有一個可信賴的代碼庫時,能夠重新編譯并直接使用它是考慮本章主題的另一個原因。
所以,libsodium 旨在通過 WebAssembly 將 NaCl 庫導出到 JavaScript 環(huán)境中,而不需要重寫代碼或在性能上妥協(xié)。它被作為 WebAssembly 項目來維護。我認為,一旦人們對以這種方式使用 WebAssembly 有了更好的認識,我們就會看到更多的項目可以作為本地庫或 WebAssembly 模塊使用,這取決于你的配置需求。這將是代碼重用的一個好機會。我們將在第 10 章看到這種方法的另一個例子。
在那之前,還有更多關(guān)于 WebAssembly 的知識需要學習。
引用鏈接
[1]?Emscript 項目:?https://emscripten.org/
[2]?附件:?../appendix/
[3]?該項目的網(wǎng)站:?https://emscripten.org/docs/compiling/Building-Projects.html
[4]?Level Up With WebAssembly 材料:?https://www.levelupwasm.com/
[5]?Arash Partow 的網(wǎng)站:?https://www.partow.net/
[6]?Partow 的網(wǎng)站:?http://www.partow.net/programming/bitmap/index.html
[7]?使用 JavaScript 代碼:?https://www.i-programmer.info/projects/36-web/6234-reading-a-bmp-file-in-javascript.html
獲取更多云原生社區(qū)資訊,加入微信群,請加入云原生社區(qū),點擊閱讀原文了解更多。
總結(jié)
以上是生活随笔為你收集整理的《WebAssembly 权威指南》(6)在浏览器中运行遗留代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SCN HeadRoom 事件分析
- 下一篇: HCIE-12.9 杭州战报