使用Emscripten編譯Eigen演算法模組為WebAssembly

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

問題場景

在Web3D開發當中,我們面臨這樣一些問題:團隊中已經有成熟的演算法模組,通常使用C/C++編寫,我們需要實現同樣的功能,如何快速實現功能,並且保持功能的一致性和長期的可維護性

技術選型

  1. 一方面我們可以使用前端Js的各類數學運算庫如:math.glgpu.js,對已有的演算法模組進行Js版本的重新實現
  2. 使用WebAssembly,通過輔助的編譯工具如emscripten將已有的演算法模組編譯為前端可直接呼叫的模組

考慮到我們已經有成熟的C++演算法模組,此時如果使用方案1,會帶來較大的人力成本,並且在開發過程中,並不能確保邏輯的一致性,因此使用選用WebAssembly

實現

根據emscripten的官方教程進行emscripten的安裝,再進行cmake的安裝

基於Emscripten,有兩種實現方案: 1. emcc直接命令列編譯,適用於依賴較少的情況,這次我們遇到的場景只需要用到Eigen,並且Eigen只需要以標頭檔案的形式引入 2. emcmake編譯 適用於依賴較多的情況,如使用Eigen,OpenCV做一些影象處理時,推薦編寫CMakeFile,維護起來更方便

下面對這兩種編譯方式進行介紹,讀者可按照上述兩種方式按需選擇

使用emcc

emcc -I ./eigen/ main.cpp -o main.js -s EXPORTED_FUNCTIONS="['_solve','_free']" -s WASM=1 -s EXPORTED_RUNTIME_METHODS="['setValue','getValue']" 上述是編譯Eigen演算法模組的emcc命令,解釋一下其中引數的含義,-I指gcc需要包含的標頭檔案路徑,main.cpp是主cpp檔案,-o指輸出,同時可以指定gcc優化級別,如果要使用wabt進行檢視,就不要使用O3了

-s指options:

EXPORTED_FUNCTIONS:指希望匯出的函式,需要加下劃線

EXPORTED_RUNTIME_METHODS :指需要匯出的Js runtime函式,根據使用的emscripten版本不同,並非所有的情況下,膠水JS程式碼都能完美的匯出所有C++模組需要用到的函式,這個時候就需要用到wabt工具進行除錯,沒有的函式需要補充上,github連結附在最後

使用cmake

``` cmake_minimum_required(VERSION 2.8) project(STEREO)

set(THIRDPARTY ${CMAKE_SOURCE_DIR}/../ThirdParty)

wasm編譯指令

set(EMSCRIPTENOPTIONS "SHELL:-s EXPORTED_FUNCTIONS=['_TriangulateDLTNView','_test'] -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] --no-entry") set(CMAKE_BUILD_TYPE Release) set(CMAKE_CXX_FLAGS_RELEASE "-O1") set(CMAKE_CXX_STANDARD 17)

配置Eigen

include_directories(${THIRDPARTY}/Eigen) add_executable(STEREO main.cpp) target_link_options(STEREO PRIVATE ${EMSCRIPTENOPTIONS}) ``` 需要注意的是,最後的target_link_options,在這裡可以連結上所需的emscripten的編譯引數 編譯成功後我們會得到:

image.png

其中main.wasm編譯出的wasm檔案,main.js是膠水js程式碼,main.js中預設通過fetch的方式進行載入,我們可以直接在html中引入

完成編譯以後,進行Js與wasm模組的資料傳遞(函式傳參),官方推薦使用arraybuffer進行傳參,這裡再介紹一種方式,

JS引數,傳入到cpp(wasm)中

function getPoint(arr) { const BYTES=8; const point = Module._malloc(arr.length * BYTES); for(let i=0;i<arr.length;i++) { Module.setValue(point+i*BYTES, arr[i], 'double') } return point; } 可以將js中的資料塊,以指標的形式傳入到cpp中

cpp計算結果,JS讀取:

我們定義的函式: const char* solve(int rayLength, double* raw){}

在Js中定義函式Pointer_stringify ``` function Pointer_stringify(ptr, length) { if (length === 0 || !ptr) return ''; // TODO: use TextDecoder // Find the length, and check for UTF while doing so var hasUtf = 0; var t; var i = 0; while (1) { assert(ptr + i < TOTAL_MEMORY); t = HEAPU8[(((ptr)+(i))>>0)]; hasUtf |= t; if (t == 0 && !length) break; i++; if (length && i == length) break; } if (!length) length = i;

        var ret = '';

        if (hasUtf < 128) {
            var MAX_CHUNK = 1024; // split up into chunks, because .apply on a huge string can overflow the stack
            var curr;
            while (length > 0) {
                curr = String.fromCharCode.apply(String, HEAPU8.subarray(ptr, ptr + Math.min(length, MAX_CHUNK)));
                ret = ret ? ret + curr : curr;
                ptr += MAX_CHUNK;
                length -= MAX_CHUNK;
            }
            return ret;
        }
        return Module['UTF8ToString'](ptr);
    }

``` 注意,在17年後,emscirpten預設取消匯出了上述函式,因此上述函式不會預設存在於膠水JS檔案中,Module物件是Js膠水程式碼中定義在全域性上的,我們可以通過這個函式完成。

呼叫函式

最後我們呼叫:我們匯出的solve函式 const rawPoint = getPoint(data) const targetPoint = _solve(2, rawPoint); const result = Pointer_stringify(targetPoint); 第一行的rawPoint就是JS資料在js和wasm共享記憶體中的指標,傳到solve函式中,最後取指標按指標地址順序讀取資料得到最後結果

總結

在面臨此類問題的時候,團隊在做技術選型的時候,需要考慮一下幾個問題,再決定是否需要使用WebAssembly: 1. 開發成本:團隊中是否有人力進行WebAssembly的開發,開發工作由前端承擔還是由後端演算法自己維護,如果由前者承擔,在市面上相關技術人員並不充足的情況下,要保持好技術沉澱,梳理出一套較為通用的編譯流程,對於大多數團隊而言,如果由後端演算法承擔,需要讓開發人員理解前端模組化知識,通常直接編譯出的Js膠水檔案可能需要修改,在定義函式,指標傳參等知識點上,演算法同學也有一些理解成本,團隊應該權衡考慮。 2. 場景選擇,在做技術選型的時候,應該首先明確功能是否應該由前端承擔,對於一些高效能運算的C++演算法模組,如果使用者的場景是用完即走,則使用者通常對WebAPP的效能要求較高,此時不適合在前端做一些很重的計算,讓使用者等待太久,如在前端用PCL庫做耗時較高的重建並不是什麼明智的選擇,首先應當考慮的還是在服務端計算,網路傳輸結果;對於一些工具類的,如三維家這種一次載入,長時使用的,使用WebAssembly去移植已有的CAD系統,顯然是一種很不錯的選擇。

wasm除錯工具wabt :http://github.com/WebAssembly/wabt 可以通過wabt對編譯出的wasm檔案進行除錯,可以看到匯出了哪些函式