有時候,您可能會想使用僅以 C 或 C++ 程式碼形式提供的程式庫。 傳統上,你可以在這裡放棄。我們再也不需要 Emscripten 和 WebAssembly (或是 Wasm)!
工具鍊
我自己的目標是想如何將一些現有的 C 程式碼編譯成 哇,LLVM 的 Wasm 後端也有一些雜訊 我開始深入研究雖然 您就能運用一些簡單的程式 如此一來,第二項 要能使用 C 的標準程式庫 多個檔案,可能會發生問題。這讓我認識到 我學到的課程:
雖然 Emscripten 過去是 C-to-asm.js 編譯器,但至今已成熟階段, 指定 Wasm,同時 在改用 Pixel 時 並將內部元件新增至官方 LLVM 後端Emscripten 也提供 與 C 標準程式庫相容的 Wasm 實作。使用 Emscripten。這項服務 出現許多隱藏性工作 模擬檔案系統、提供記憶體管理功能、將 OpenGL 與 WebGL 包裝在一起 — 許多您不需要親自開發的東西。
雖然這樣聽起來似乎是需要擔心的人身安全,但我也很擔心 :Emscripten 編譯器會移除所有不需要的項目。在我的 實驗結果顯示,系統產生的 Wasm 模組大小已針對邏輯 而 Emscripten 和 WebAssembly 團隊正在努力 日後就會更小
如要取得 Emscripten,請按照 網站或使用 Homebrew。如果你喜歡 我們也不希望在系統中安裝各種項目 超棒的軟體就是一個完善的專區 可用的 Docker 映像檔 請改採以下做法:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
編寫簡單的程式碼
以下以幾乎標準的範例,說明如何在 C 中編寫函式 計算第 n 個費波那契數:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
如果您也知道 C,函式本身不應過於驚人。即使你 不知道 C 但知道 JavaScript,希望可以 到底是什麼?
emscripten.h
是 Emscripten 提供的標頭檔案。我們只需要用到
可存取 EMSCRIPTEN_KEEPALIVE
巨集,但這個巨集
提供更多功能。
這個巨集會指示編譯器不要移除顯示中的函式
未使用的。若省略該巨集,編譯器會將函式最佳化
— 沒人在使用它。
將所有內容儲存在名為 fib.c
的檔案中。如要將檔案轉換成 .wasm
檔案
需要切換 Emscripten 的編譯器指令 emcc
:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
我們來剖析這個指令emcc
是 Emscripten 的編譯器。fib.c
是我們的 C
檔案。到目前為止都很順利。-s WASM=1
要求 Emscripten 提供我們 Wasm 檔案
而非 asm.js 檔案。
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
會指示編譯器
JavaScript 檔案提供 cwrap()
函式 - 有關這個函式的更多資訊
-O3
會指示編譯器主動進行最佳化。您可以選擇較低的
以縮短建構時間,但這樣產生的套裝組合
因為編譯器可能不會移除未使用的程式碼。
執行指令之後,您應該會看到一個 JavaScript 檔案,
a.out.js
和一個名為 a.out.wasm
的 WebAssembly 檔案。Wasm 檔案 (
「模組」) 包含經過編譯的 C 程式碼,且應相當小。
JavaScript 檔案負責載入及初始化 Wasm 模組和
提供更棒的 API如有需要,還能為您設定
堆疊、堆積和其他功能
搭載的作業系統因此 JavaScript 檔案
可使用 19 KB (約 5 KB gzip ) 稱重。
執行簡單的工作
如要載入及執行模組,最簡單的方法就是使用產生的 JavaScript
檔案。載入檔案後,您會擁有
全球 Module
由你決定使用
cwrap
敬上
來建立可處理轉換參數的 JavaScript 原生函式
轉換為適合 C 的函式,並叫用已包裝的函式。cwrap
獲得了
函式名稱、傳回類型和引數型別,並依序排列:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
如果發生以下情況: 執行這段程式碼 應該會看到「144」就是第 12 個費波那契數
神聖的努力:編譯 C 程式庫
到目前為止,我們根據 Wasm 編寫的 C 程式碼才寫得。核心肌群 WebAssembly 的應用實例是將 C 語言的現有生態系統 並允許開發人員在網路上使用這些程式庫經常提供 主要依賴 C 的標準程式庫、作業系統、檔案系統和其他 有些事物Emscripten 能夠提供大部分的這些功能 限制。
讓我們回到原本的目標:編譯 WebP 的編碼器至 Wasm WebP 轉碼器的原始碼是以 C 語言撰寫, GitHub 和部分資源 API 說明文件。這很好的起點。
$ git clone https://github.com/webmproject/libwebp
首先,我們要從WebPGetEncoderVersion()
將名為 webp.c
的 C 檔案編寫為 encode.h
:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
這個簡單的程式可用來測試我們能否取得 libwebp 的原始碼 進行編譯,因為我們不需要任何參數或複雜的資料結構, 叫用此函式。
如要編譯這個程式,我們必須告知編譯器可在哪個位置找到
使用 -I
旗標的 libwebp 標頭檔案,並傳送
libwebp。老實說,我剛剛把「所有」都拿去了
我能找到並仰賴編譯器來排除
不需要。它似乎能發揮巧思!
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
現在,我們只需要一些 HTML 和 JavaScript 即可載入全新的模組:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
從 JavaScript 擷取圖片到 Wasm
取得編碼器的版本編號固然重要,但需要對實際 更令人印象深刻,對吧?我們開始吧。
第一個要釐清的問題是:如何將圖片放到 Wasm 土地?
查看
libwebp 的編碼 API
採用 RGB、RGBA、BGR 或 BGRA 的位元組陣列。幸好 Canvas API
getImageData()
、
它提供了
Uint8ClampedArray
包含 RGBA 的圖片資料:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
現在包含多個選項也就是將資料從 JavaScript 落地複製到 Wasm 土地上為此,我們需要公開兩個額外的函式。一個是 Pod 可以分配的 為 Wasm 登陸後釋放後再次釋放的 記憶體容量:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer
會為 RGBA 圖片分配緩衝區,因此每像素 4 位元組。
malloc()
傳回的指標是下列項目的第一個記憶體儲存格位址
該緩衝區。指標傳回至 JavaScript 落地時,系統會將其視為
數字使用 cwrap
將函式公開至 JavaScript 後,我們
使用該數字尋找緩衝區開頭,並複製圖片資料。
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
最終樂章:將圖片編碼
相片現已在瓦斯姆陸地拍攝。該呼叫 WebP 編碼器
做生意!查看
WebP 說明文件、WebPEncodeRGBA
看起來都很完美這個函式會指向輸入圖片
大小也介於 0 到 100 之間也會分配
輸出緩衝區,之後我們就需要使用 WebPFree()
釋放
WebP 圖片
編碼運算的結果是一個輸出緩衝區及其長度。由於 C 中的函式不能有陣列做為傳回類型 (除非我們分配記憶體 會改為使用靜態全域陣列我知道了,不是乾淨的 C (事實上 它取決於 Wasm 指標的寬度是 32 位元 我認為這是相當不錯的做法
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
現在一切就緒,我們可以呼叫編碼函式、擷取 將指標和圖片大小放置於我們自己的 JavaScript 區域緩衝區中 釋出程序中配置的所有 Wasm-land 緩衝區。
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
視圖片大小而定,您可能會遇到 Wasm 錯誤 無法增加記憶體,以容納輸入和輸出圖片:
幸好,錯誤訊息提供這個問題的解決方法!我們只需
將 -s ALLOW_MEMORY_GROWTH=1
加入編譯指令中。
這樣就大功告成囉!我們編譯 WebP 編碼器,並將 JPEG 圖片轉碼
WebP。為證明測試過程,我們可以將結果緩衝區轉換成 blob,並使用
對 <img>
元素執行這項作業:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);
結論
如要在瀏覽器中使用 C 程式庫,並非走進公園,只須花費一次 您瞭解整個流程及資料流程的運作方式 結果或許令人興奮
WebAssembly 為網路帶來許多新契機,可供處理,編號 例如重新製作和玩遊戲提醒你,Wasm 並非萬靈丹 可套用至所有細節,但如果碰到其中一個瓶頸, Wasm 可能就會 是非常實用的工具
額外內容:要簡單實現這一點
如要嘗試避免產生的 JavaScript 檔案,您或許可以 。讓我們回到 Fibonacci 的範例如要自行載入及執行 :
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
Emscripten 建立的 WebAssembly 模組沒有記憶體可以運作
除非你能提供回憶提供 Wasm 模組的方式
anything 是使用 imports
物件,也就是
instantiateStreaming
函式。Wasm 模組可以存取
imports 物件,但不會加入其他物件。按照慣例
而 IScripting 會預期載入 JavaScript 時需要留意幾件事
環境:
- 首先是
env.memory
。Wasm 模組無人知曉 因此模型必須 具有一些記憶體可以使用輸入WebAssembly.Memory
。 代表一片線性記憶體 (可視需要擴充)。尺寸 參數位於「WebAssembly 頁面的單位中」,意即上述程式碼 可分配 1 頁的記憶體,每個頁面大小為 64 KiB。未提供maximum
理論上,記憶體理論上沒有限制 (Chrome 目前採用 2GB 的硬性限制)。大部分 WebAssembly 模組不必設定 。 env.STACKTOP
會定義堆疊的開始位置。堆疊 才能發出函式呼叫,並為本機變數分配記憶體。 我們不曾針對基本記憶體管理工作 Fibonacci 程式可將整個記憶體做為堆疊使用STACKTOP = 0
。