在 JS 中使用 C 程序

2021-02-08 前端大全

(點擊上方公眾號,可快速關注)

作者:EtherDream

網址:http://www.cnblogs.com/index-html/p/using_c_in_javascript.html


JavaScript 是個靈活的腳本語言,能方便的處理業務邏輯。當需要傳輸通信時,我們大多選擇 JSON 或 XML 格式。


但在數據長度非常苛刻的情況下,文本協議的效率就非常低了,這時不得不使用二進位格式。


去年的今天,在折騰一個 前後端結合的 WAF 時,就遇到了這個麻煩。


因為前端腳本需要採集不少數據,而最終是隱寫在某個 cookie 裡的,因此可用的長度非常有限,只有幾十個字節。


如果不假思索就用 JSON 的話,光一個標記欄位 {"enableXX": true} 就佔去了一半長度。然而在二進位裡,標記 true 或 false 不過是 1 個比特的事,可以節省上百倍的空間。


同時,數據還要經過校驗、加密等環節,只有使用二進位格式,才能方便的調用這些算法。


優雅實現


不過,JavaScript 並不支持二進位。


這裡的「不支持」不是說「無法實現」,而是無法「優雅實現」。語言的發明,就是用來優雅解決問題的。即使沒有語言,人類也可以用機器指令來編寫程序。


如果非要用 JavaScript 操作二進位,最終就類似這樣:


var flags = +enableXX1


雖然能實現,但很醜陋。各種硬編碼、各種位運算。


然而,對於先天支持二進位的語言,看起來就十分優雅:


union {

    struct {

        int enableXX1: 1;

        int enableXX2: 1;

        ...

    };

    int16_t value;

} flags;

 

flags.enableXX1 = enableXX1;

flags.enableXX2 = enableXX2;


開發者只需定義一個描述即可。使用時,欄位偏移多少、如何讀寫,這些細節完全不用關心。


為了能達到類似效果,起先封裝了一個 JS 版的結構體:


// 最初方案:封裝一個 JS 結構體

var s = new Struct([

    {name: 'month', bit: 4, signed: false},

    ...

]);

 

s.set('month', 12);

s.get('month');


將細節進行了隱藏,看起來就優雅多了。


優雅但不完美


但是,這總感覺不是最完美的。結構體這種東西,本該由語言提供,如今卻要用額外的代碼實現,而且還是在運行期間。


另外,後端解碼是用 C 實現的,所以得維護兩套代碼。一旦數據結構或者算法變了,得同時更新 JS 和 C,很麻煩。


於是琢磨,能否共用一套 C 代碼,同時用於前端和後端?


也就是說,需要能將 C 編譯成 JS 來運行。


認識 emscripten


能將 C 編譯成 JS 的工具有不少,最專業的要數 emscripten。


emscripten 的使用方式很簡單,和傳統 C 編譯器差不多,只不過生成的是 JS 代碼。


emcc hello.c -o hello.html

 

// hello.c

#include

#include

 

int main() {

    time_t now;

    time(&now);

    printf("Hello World: %s", ctime(&now));

    return 0;

}


編譯之後即可運行:



很有趣吧~ 大家可以嘗試下,這裡就不多介紹了。


實用缺陷


然而我們關心的不是有趣,而是實用。


事實上,即使一個 Hello World 編譯出來的 JS 也過萬行,多達數百 KB。就算壓縮再 GZIP,仍有幾十 KB。


同時 emscripten 使用了 asm.js 規範,內存訪問是通過 TypedArray 實現的。


這意味著 IE10 以下的用戶都無法運行。這也是不可接受的。


因此,我們得做如下改進:



首先寄託 emscripten 本身,看看能不能通過設置參數,來達到我們的目的。


不過一番嘗試之後,並沒有成功。那只能自己動手實現了。


減少體積


為什麼最終腳本會那麼大,裡面都放了些什麼?分析了下內容,大致有這幾個部分:



輔助功能


比如字符串和二進位轉換、提供回調包裝等。這些基本都是用不著的,我們可以給自己寫個特殊的回調函數。


接口模擬


提供文件、終端、網絡、渲染等接口。之前見過用 emscripten 移植的客戶端遊戲,看來模擬了不少接口。


初始化操作


全局內存、運行時、各種模塊的初始化。


運行時函數


純粹的 C 只能做簡單的計算,很多功能都依靠運行時函數。


不過,有些常用的函數,其背後的實現是及其複雜的。例如 malloc 和 free,對應的 JS 有近 2000 行!


程序邏輯


這才是 C 程序真正對應的 JS 代碼。因為編譯時經過 LLVM 的優化,邏輯可能變得面目全非了。


這部分代碼量不大,是我們真正想要的。


事實上,如果程序沒有用到一些特殊功能的話,把邏輯函數單獨摳出來,仍然是可以運行的!


考慮到我們的 C 程序非常簡單,所以簡單粗暴的提取出來,也是沒問題的。


C 程序對應的 JS 邏輯位於 // EMSCRIPTEN_START_FUNCS 和 // EMSCRIPTEN_END_FUNCS 之間。過濾掉運行時函數,剩下的就是 100% 的邏輯代碼了。


增加兼容


接著解決內存訪問的兼容性問題。


在很老版本的 emscripten 裡,是可以選擇是否使用 TypedArray 的。如果不用,則通過 JS Array 來實現。但如今早已去除了這個參數,只能使用 TypedArray。


首先了解下,為何要用 TypedArray。


emscripten 申請了一大塊 ArrayBuffer 來模擬內存,然後關聯了一些 HEAP 開頭的變量。



這些不同類型的 HEAP 共享同一塊內存,這樣就能高效的指針操作。


然而不支持 TypedArray 的瀏覽器,顯然無法運行。所以得提供個 polyfill 兼容下。


但經分析,這幾乎不可能實現 —— 因為 TypedArray 和數組一樣,是通過索引來訪問的:


var buf = new Uint8Array(100);

buf[0] = 123;     // set

alert(buf[0]);    // get


然而 [] 操作符在 JS 裡是無法重寫的,因此難以將其變成 setter 和 getter。況且不支持 TypedArray 的都是低版本 IE,更不用考慮 ES6 的那些特徵。


於是琢磨 IE 的私有接口。比如用 onpropertychange 事件來模擬 setter。不過這樣做效率極低,而且 getter 仍不易實現。


經過一番考慮,決定不用鉤子的方式,而是直接從源頭上解決 —— 修改語法!


我們用正則,找出源碼中的賦值操作:


HEAP[index] = val;


替換成:


HEAP_SET(index, val);


類似的,將讀取操作:


HEAP[index]


替換成:


HEAP_GET(index)


這樣,原先的索引操作,就變成函數調用了。我們就能接管內存的讀寫,並且沒有任何兼容性問題!


然後實現 8、16、32 位有無符號的版本。通過 JS 的 Array 來模擬,非常簡單。



麻煩的是模擬 Float32 和 Float64 兩個類型。不過本次 C 程序中並未用到浮點,所以就暫不實現了。


到此,兼容性問題就解決了。


大功告成


解決了這些缺陷,我們就可以愉快的在 JS 中使用 C 邏輯了。


作為腳本,只需關心採集哪些數據,這樣代碼就非常的優雅:



數據的儲存、加密、編碼,這些二進位操作,則通過 C 實現。



編譯時使用 -Os 參數優化體積,最終的 JS 精簡壓縮之後,還不到 2 KB,十分小巧精煉。



於是,這個「前後端 WAF」開發就容易多了。我們只需維護一份代碼,即可同時編譯出前後端兩個版本!


所有的數據結構和算法,都由 C 實現。前端編譯成 JS 代碼,後端編譯成 lua 模塊,供 nginx-lua 使用。



前後端的腳本,都只需關注業務功能即可,完全不用涉及數據層面的細節。


測試版


事實上,還有第三個版本 —— 本地版。


因為所有的 C 代碼都在一起,因此可以方便的編寫測試程序。


這樣就無需啟動 WebServer、打開瀏覽器來測試了。只需模擬一些數據,直接運行程序即可測試,非常輕量。


同時藉助 IDE,調試起來更容易。


小結


每一門語言都有各自的優缺點。將不同語言的優勢相互結合,可以讓程序變得更優雅、更完美。



本文屬於「前端大全」Javascript 分類。

回復 Javascript 查看更多 Javascript  技術乾貨。



【今日微信公號推薦↓】

更多推薦請看值得關注的技術和設計公眾號


其中推薦了包括技術設計極客 和 IT相親相關的熱門公眾號。技術涵蓋:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、資料庫、運維、大數據、算法、IT職場等。點擊《值得關注的技術和設計公眾號》,發現精彩!

相關焦點

  • 實戰:在Node.js和Vue.js中構建文件壓縮應用程式
    Node.js為我們提供了一個模塊來協助文件壓縮。在本文中,我們將構建一個應用程式,用戶可以在該應用程式中上傳他們想要壓縮的文件,然後使用Node.js Zlib模塊下載該文件的壓縮版本。然後,我們創建一個Express的實例,並將其保存在一個變量中。我們使用Express實例來配置我們的 cors 作為中間件。我們還需要一些Node.js的核心模塊,比如 zlib,我們將使用它來進行實際的壓縮。然後我們使用Express的實例來創建一個伺服器,它將監聽 3000 埠。
  • 在Node.js中,使用Promise.prototype.finally
    由此看來,將其放入Node.js僅是時間問題。本文將向您展示:如何使用Promise.prototype.finally()以及如何編寫自己的簡化polyfill。該.catch()函數只是.then()使用onRejected處理程序而不使用onFulfilled處理程序的便捷速記:就像.catch(),該.finally()功能是的便捷快捷方式.then()。區別在於,在兌現promise時即履行或拒絕promise時,.finally()執行功能onFinally。
  • 使用Chrome DevTools有效調試Node.js
    本文介紹如何使用最新的Google Chrome DevTools高效地調試Node.js程序。請繼續閱讀,並閱讀關於Chrome DevTools的節點調試是識別和消除軟體應用程式中的錯誤的任務,它不僅僅是在代碼中列印出值。本文介紹如何使用最新的Google Chrome DevTools高效地調試Node.js程序。
  • 在 js 開發中,如何減少 if else 語句的使用
    在 js 開發中,如何減少 if else 語句的使用代碼中嵌套的 if/else 結構往往導致代碼不美觀,也不易於理解。
  • 使用SpreadJS 實現 JavaScript 中導入和導出Excel文件
    JavaScript是一個涵蓋多種框架、直譯式、可以輕鬆自定義客戶端的腳本語言,在 Web 應用程式中,更加易於編碼和維護。訪問 SpreadJS 官網了解更多產品動態:https://www.grapecity.com.cn/developer/spreadjs 使用JavaScript實現 Excel 的導入和導出通過純JavaScript,您完全可以實現導入和導出Excel文件功能,並為最終用戶提供與這些文件進行I/O交互的界面。
  • Node.js與Python在開發Web應用程式時都是高度首選的
    Node.js由Ryan Dahl在2009年開發,是一個開放原始碼伺服器環境,主要在Windows,Linus,Unix,Mac OS X等不同平臺上運行。 使用Node.js,開發人員可以使用JavaScript編寫命令行工具和伺服器端腳本。被視為構建遊戲平臺,論壇和廣告服務等應用程式的首選技術。
  • 前端頁面開發之Node JS初學者指南
    相信大部分讀者都已經對ajax編程十分熟悉了,其中ajax中的a(asynchronous)指的是異步編成,所以在node.js世界中,一切的方法都是異步執行的,因此比如你需要程序中閱讀一個作業系統中的文件,則必須指定一個回調方法,則在讀取完這個文件後,則會執行這個回調方法的內容。  下面,就Node.js跟其他語言來做個小的對比,讀者可以可以看出其中的端倪。
  • 使用Node.Js構建IPFS應用程式|火星技術帖
    IPFS客戶端有兩種實現,一種在JavaScript中,另一種在Go中。JavaScript在這裡似乎是最好的選擇,但由於它處於比Go客戶端更早的開發狀態,因此這不是最佳選擇。我們將使用Go客戶端,並通過其API與Node連接。
  • JS逆向-Sekiro框架的簡單使用
    ,喜歡用charles的也可以使用charles瀏覽器打開紅框中的js文件,並將返回的js代碼及藍框中的js代碼內容複製到剛才的sekiro_test.js文件中,如下所示:在sekiro_test.js文件中直接使用registerAction註冊一個getaData接口,傳遞resolve和reject參數調用init方法,做如下配置:
  • nw.js桌面程序自動更新(node.js表白記)
    喜慶的話不多說,今天給大家分享一個大致3周前,初次涉足Node.js實現的nw.js桌面程序的自動更新模塊吧。  本文不做教學,僅用於打臉!希望以此得到各位大神的幫助。    從圖中不難看出,我所謂的這個自動更新機制其實僅4步而已
  • 製作跨瀏覽器兼容的Vue.js應用程式和所涉及的挑戰
    在這些框架中,最受歡迎的一個是Vue.js,這篇文章是關於Vue.js應用程式中的跨瀏覽器兼容性。那麼,對於跨瀏覽器兼容的應用程式究竟意味著什麼呢?讓我們通過一個例子來理解它。想像一下,你在辦公室工作,他們遵循BYOD(自帶設備)的趨勢。
  • 小程序開發中的一些總結
    小程序開發中的一些總結最近重新學習前端,又開始接觸小程序基礎開發。好久沒寫公眾號了,今天就寫一些最近開發過程中的總結,也算是做個筆記。可能在各位大佬眼裡有些簡單,哈哈。一.解決VS Code中小程序的wxml文件的注釋是雙斜槓的問題安裝小程序助手插件,再重啟VS Code即可。二. vscode安裝easy less使用less語法自動生成css或wxss文件。
  • Vue.js 2.5 發布,而這個會玩的團隊已經自研出用 Vue 開發小程序的...
    更好地錯誤處理在2.4及更早版本中,我們通常使用全局 config.errorHandleroption 來處理應用程式中的意外錯誤。 我們還有 renderError 組件選項來處理渲染函數中的錯誤。 但是,我們缺少處理應用程式特定部分內的泛型錯誤的機制。在2.5中,我們引入了新的 errorCaptured 鉤子。
  • 揭密vue.js的神秘之處,小程序跟vue有什麼關係
    MPvue是vue.js框架的一種開發思路,在MPvue在做美團小程序的項目中獲得實驗和驗證,並且在美團點評的小程序的項目中大範圍使用,美團為vue.js提供了一套開發組件供微信小程序的開發者使用,由餓了麼 UED (知乎專欄)設計的桌面端組件庫已經開源,滴滴也開放自己的
  • 用Python使用C語言程序(Windows平臺)
    qianyan在機器學習中,很多時候我們需要Python和C的混合編程,最重要的原因是為了性能效率的提升: 解釋型語言一般比編譯型語言慢,一般提高性能的有效做法是,先做性能測試,找出性能瓶頸部分,然後把瓶頸部分在擴展中實現。本文的目標是在windows平臺下(使用pycharm),實現python調用C語言編寫的程序。
  • 幾行JavaScript代碼構建計算機視覺程序,這裡有6個js框架
    如何使用幾行 JavaScript 代碼輕鬆構建計算機視覺應用程式?近年來,計算機視覺一直都是熱門話題,造就了無數好的應用程式。
  • Node.js中的Stream
    以文件讀寫為例,文件讀寫的時候,stream並不是一次性地把一個文件中的所有內容都讀取到內存中再進行處理(就是再寫入到另外一個文件中),而是一塊數據一塊數據的進行讀取,讀取完一塊數據就處理一塊數據(把這塊數據寫入到另外一個文件中),而不會讓它一直在內存中。相比於傳統方式,使用stream來處理數據,可以高效的使用內存,更有可能來處理大文件。再以網絡數據傳輸(網上看視頻)為例。
  • JS 中的 with 關鍵字
    中的with關鍵字,很多小夥伴們的第一印象可能就是with關鍵字的作用在於改變作用域,然後最關鍵的一點是不推薦使用with關鍵字。基本說明在js高級程序設計中是這樣描述with關鍵字的:with語句的作用是將代碼的作用域設置到一個特定的作用域中,基本語法如下:with (expression) statement;使用with關鍵字的目的是為了簡化多次編寫訪問同一對象的工作,比如下面的例子:var qs = location.search.substring
  • 讓C代碼在瀏覽器中運行——JavaScript慘遭拋棄?
    使用WebAssembly,我們可以在瀏覽器中運行一些高性能、低級別的程式語言,可用它將大型的C和C++代碼庫比如遊戲、物理引擎甚至是桌面應用程式導入Web平臺。截至目前為止,我們已經可以在Chrome、Firefox中使用WebAssembly,Edge和Safari對它的支持也基本完成。
  • 如何利用Node.js 構建分布式集群
    【IT168廠商動態】在軟體定義的世界裡,企業通過Web應用和行動應用程式來提供大部分的服務,而Node.js迅速成為時下最為流行的一個平臺之一,就和它可以搭建響應速度快、易於擴展的web應用和移動應用很很大關係,並憑藉這點成為了新的主流。作為大規模使用Node.js 的雲計算服務提供商,UCloud積累了豐富的使用經驗。