12.30 記一次完整 C++ 項目編譯成 WebAssembly 的實踐

簡介: 有 2W+ 行代碼,一篇通用的技術方案

記一次完整 C++ 項目編譯成 WebAssembly 的實踐


作者| 張翰(門柳)
出品|阿里巴巴新零售淘系技術部

本文知識點提煉:
1、把複雜的 C++ 框架編譯成 WebAssembly。
2、在 wasm 模塊裡調用 DOM API !
3、在 js 和 wasm 之間傳遞複雜數據結構。
4、對 WebAssembly 技術發展的期待。

上一篇文章《基礎為零?如何將 C++ 編譯成 WebAssembly》裡介紹了怎麼把簡單的 C++ demo 編譯成 WebAssembly,但這是遠遠不夠的。正好手頭在寫一個 C++ 的項目,功能獨立完整也足夠複雜(有 2W+ 行代碼),就順便編譯成了 WebAssembly,未必是一個合適的使用場景,主要是為了學習這項技術,親身體驗一下過程中遇到的問題。

項目背景

我現在在用 C++ 寫一個原生的響應式框架,定位和前端框架差不多,但是用 C++ 來實現,可以有更好的性能,也方便對接各種原生渲染引擎和各種語言。上層開發者仍然以 JS 為開發語言,API 和 Web Component 相似,而且可以像小程序和 Vue.js 那樣用模板+數據(聲明式+響應式)的方式開發原生 UI,框架本身就不介紹了,本文的重點是涉及 WebAssembly 的部分。

▐ 為什麼要編譯成 WebAssembly ?

框架是 C++ 寫的,本身的設計是源碼集成進各種渲染容器的,跟隨原生 SDK 發版,即使是對接瀏覽器,也是和瀏覽器內核代碼(如 UC 的 U4 內核)打包在一起,但這樣就無法運行在獨立的瀏覽器上,功能無法降級。如果說把 C++ 代碼編譯成 WebAssembly 的話,那框架就可以從遠程加載了再運行,相當於 C++ 的框架也有了動態化的能力。

簡單來講,現在這個 C++ 框架已經能運行在各種原生渲染引擎之上了,我想保持同一份 C++ 源碼,讓它能運行在乾淨的瀏覽器中。

這條鏈路應該只會用於降級的場景,重點是驗證鏈路能不能跑通,性能倒不是我最關注的問題。

需求分析

▐ 要實現的目標

首先細化一下要實現的目標,下面是一段使用響應式框架 API 開發的代碼,它可以運行在原生渲染引擎上,目標是讓這段代碼能運行在乾淨的瀏覽器裡:

<code>// 1. 自定義一個組件 

class HelloWorld extends ReactiveElement { /* ... */ }
// 2. 向環境中註冊組件,給定一個名稱
customElements.define('hello-world', HelloWorld)
// 3. 定義組件的模板
customElements.defineTemplate(HelloWorld, {
type: 'h1',
// 添加數據綁定,表示 h1 的 innerText 是由表達式 message 計算出來的
innerText: { '@binding': '`Say: ${message}`' }
})
// 4. 創建組件,傳遞初始數據
const app = new HelloWorld({ message: 'Hi~' })
// 5. 把組件掛載到 #root
customElements.mount('#root', app)
// 6. 更新組件的數據,會自動觸發 UI 的更新
app.setState({
message: "What's up!"
})/<code>

這段代碼是一個完整的例子, 1, 2, 4 是 Web Component 的標準寫法(用 ReactiveElement 代替 HTMLElement),瀏覽器已經支持,5 是用於掛載節點的語法糖, 3 和 6 是新增的 API,用於定義組件的模板和傳遞數據。方案算是對 Web Component 的增強,加入了模板和數據綁定的能力,在真實場景裡模板不會是手寫的,而是由小程序、Vue.js 所定義的模板語法編譯而來。

看起來用 JS 寫個 polyfill 就可以搞定。但是模板的運算和數據綁定怎麼實現?裡面是可以包含循環、分支和表達式的,前端框架的做法是把它編譯成 js 代碼,如果寫 polyfill,很可能又寫出了一個前端框架,或者基於現有前端框架做封裝,但是這樣就和原生框架的行為不一致了。

▐ 遇到的問題

想用響應式框架跑通上面的例子,就是要實現這麼一個調用鏈路:

<code>demo.js  [響應式框架]  DOM API/<code>

在原生框架中,ReactiveElement class 和 customElements 上的接口都是由 C++ 實現的,類似於 DOM API,有 ES6 的類,也有普通函數,接受的參數有 class(Function),String 和 Object 等各種類型。然而 wasm 目前只可以 import 和 export C 語言函數風格的 API,而且參數只有四種數據類型(i32, i64, f32, f64),都是數字,可以理解為赤裸裸的二進制編碼,沒法直接傳遞複雜的類型和數據結構。所以在瀏覽器中這些高級類型的 API 必須靠 JS 來封裝,中間還需要一個機制實現跨語言轉換複雜的數據結構。

WebAssembly 是一種編譯目標,雖然運行時是 wasm 和 JS 之間互相調用,但是寫代碼的時候感知到的是 C++ 和 JS 之間的互相調用。文中說的 JS 和 C++ 的調用,實際意思是 JS 和 wasm 模塊之間的調用。
另外,如果要實現在瀏覽器裡渲染和更新 UI 的話,就必須要用到 DOM API,眾所周知 WebAssembly 調用不了 DOM API,也不是個靠譜的用法。原生框架提供了 C++ 的 ComponentRenderer 抽象類來對接渲染引擎,不同的渲染引擎分別實現這個類然後注入框架,不管靠譜不靠譜,在瀏覽器裡也只能靠 JS 實現這個渲染器了,封裝 DOM 操作然後把接口傳給 C++ 調用,而且也要傳遞複雜的數據結構。如果想精確更新某個特定組件節點,還得解決 C++ 組件和 JS 組件一對一 binding 的問題。

總結下來,是兩個問題:

  1. 在 C++ 和 JS 之間傳遞複雜數據結構。(數據通信)
  2. 實現 C++ 和 JS 複雜數據類型的一對一綁定。(類型綁定)
  1. 無需多說,性能優化永無止境。

等上面的基礎設施建設完成後,可以為 WebAssembly 的落地掃清大部分障礙。

最後結合本文的例子,設想在未來使用 WebAssembly 的更合理的架構:

記一次完整 C++ 項目編譯成 WebAssembly 的實踐

左邊是目前在瀏覽器裡運行 WebAssembly 的層次結構,業務邏輯 JS 運行與框架之上,wasm 模塊要裹上一層 js glue 才可以運行,瀏覽器是集 JS 腳本引擎、WebAssembly 運行時、渲染引擎與一體的運行環境,平臺的原生能力透過瀏覽器(或 webview)的殼透出到 JS 環境中。這個架構裡最關鍵的是瀏覽器,集中實現了各種引擎,功能穩定而且標準化,但是也增大了定製和改造的難度,要引入就都得引入。

右邊是一個理想的運行 WebAssembly 的層次結構,在最底層的還是平臺原生能力,再往上就不是簡單的對接瀏覽器了,而是將 JS 引擎和 WebAssembly 運行時分開,這兩個都可以算作腳本引擎,至於渲染引擎,它應該運行與腳本引擎的下方,屬於平臺原生能力的一部分(圖中未畫出來)。有了獨立的 WebAssembly 運行時,平臺原生能力就能夠直接以 wasi 的形式透出,開發和運行 wasm 的時候就不再受 JS 的影響,也有助於實現標準化,向上透出的接口是語言無關的,依然可以被 JS 調用,也可以支持其他語言,也支持其他編譯好的 WebAssembly 模塊。


分享到:


相關文章: