1. 程式人生 > >【探索】在 JavaScript 中使用 C 程式

【探索】在 JavaScript 中使用 C 程式

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

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

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

因為前端指令碼需要採集不少資料,而最終是隱寫在某個 cookie 裡的,因此可用的長度非常有限,只有幾十個位元組。

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

同時,資料還要經過校驗、加密等環節,只有使用二進位制格式,才能方便的呼叫這些演算法。

優雅實現

不過,JavaScript 並不支援二進位制。

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

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

var flags = +enableXX1 << 16 | +enableXX2 << 15 | ...

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

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

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 <stdio.h>
#include <time.h>  

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 來模擬,非常簡單。麻煩的是模擬 Float 型別,不過 C 程式中未用到浮點,所以就沒實現。

如果支援 TypedArray,則使用原生的介面;否則,用 Array 模擬版本。

這樣, 既保障了高版本瀏覽器的效能,又兼顧了老瀏覽器的功能。

大功告成

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

指令碼,只關心業務邏輯。例如採集哪些資料,這樣程式碼就非常的優雅:

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

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

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

所有的資料結構和演算法,都由 C 實現。前端編譯成 JS 程式碼,後端編譯成 lua 模組,供 nginx-lua 使用。

前後端的指令碼,都只需關注業務功能即可,完全不用涉及資料層面的細節。

測試版

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

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

這樣就無需啟動 WebServer、開啟瀏覽器來測試了。只需模擬一些資料,直接執行程式即可測試,非常輕量。

同時藉助 IDE,除錯起來更容易。

小結

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

相關推薦

VS2010 C++創建DLL圖解

-a rar cls ret ria endif -s pan 項目 標簽: dllc++2010threadlibraryc 本文章已收錄於: .embody { padding: 10px 10px 10px; margin: 0 -20px; b

RegExpJavaScript正則表達式判斷匹配規則以及常用方法

返回 空字符串 tro true 正則表達式 str 本地 大小 表示範圍 字符串是編程時涉及到的最多的一種數據結構,對字符串進行操作的需求幾乎無處不在。 正則表達式是一種用來匹配字符串的強有力的武器。它的設計思想是用一種描述性的語言來給字符串定義一個規則,凡是符合規則的字

前端javascript的陣列及操作方法

建立: 物件的例項建立:var aList = new Array(1,2,3); 直接建立:var aList = [1,2,3,'a'] 陣列的api:增刪改查/反轉排序/合成字串 1.增:放到最後 var list = ['穿山甲','水娃','蛇精'] list.push

例項javascript的window物件的onbeforeunload、onload、onunload的區別(在新版本的瀏覽器兩個關閉事件可能不會觸發)

onload是在頁面載入的時候觸發,與之相反onunload是在頁面關閉(解除安裝)的時候觸發,至於onbeforeunload是指頁面將要關閉的時候觸發(一般用於彈出是否確認關閉的時候) 上程式碼 <!DOCTYPE HTML PUBLIC "-//W3C//DT

實用JavaScriptVideo使用詳解過程(多視訊列表迴圈播放)

做過一個專案裡有用到插入多個視訊類似列表迴圈播放的,視訊資訊是從後臺資料庫傳入的 核心JS程式碼片段 <script type="text/javascript"> var videoArray=new Array(); var relative

探索JavaScript 使用 C 程式

JavaScript 是個靈活的指令碼語言,能方便的處理業務邏輯。當需要傳輸通訊時,我們大多選擇 JSON 或 XML 格式。 但在資料長度非常苛刻的情況下,文字協議的效率就非常低了,這時不得不使用二進位制格式。 去年的今天,在折騰一個 前後端結合的 WAF 時,就遇到了這個麻煩。 因為前端指令碼需要採集不少

Chrome如何在C++增加給JavaScript呼叫的API

本文示例說明了如何在Chrome瀏覽器中增加JavaScript API。為了簡化,先假設是在已有的namespace中增加一個新的API,文章的最後將指出如果增加一下全新的namespace所需注意的事項。 在繼續之前,請確保你的Chrome瀏覽器中已安裝了一些擴充

前端小小白的學習之路 JavaScript的十個難點,你有必要知道。

計算 tel isa 耗時 一個 支持 rip 每次 javascrip 1. 立即執行函數 立即執行函數,即Immediately Invoked Function Expression (IIFE),正如它的名字,就是創建函數的同時立即執行。它沒有綁定任何事件,也無需

.Net淺談C#的值類型和引用類型

rem 理解 amp div net 親情 實例 函數 大小 在C#中,值類型和引用類型是相當重要的兩個概念,必須在設計類型的時候就決定類型實例的行為。如果在編寫代碼時不能理解引用類型和值類型的區別,那麽將會給代碼帶來不必要的異常。很多人就是因為沒有弄清楚這兩個概念從而在編

JavaScriptJavascriptdocument.execCommand()的用法

停止 ouya browser 剪貼板 form 折騰 bookmark 水平線 man Javascript中document.execCommand()的用法 合並轉載: 轉載出處1 轉載出處2 document.execCommand()方法處理Html數據時常用語法

分析淺談C#Control的Invoke與BeginInvoke在主副線程的執行順序和區別(SamWang)

info start result 初步 總結 inter blank rap 傳遞   今天無意中看到有關Invoke和BeginInvoke的一些資料,不太清楚它們之間的區別。所以花了點時間研究了下。   據msdn中介紹,它們最大的區別就是BeginInvoke屬於

C++如何執行C++程式

Linux下編譯C++程式時,gcc命令也可以使用,不過要增加-lstdc++選項,否則會發生連結錯誤。例如編譯單個原始檔: gcc main.cpp -lstdc++ 編譯多個原始檔: gcc main.cpp module.cpp -lstdc++ 不過 GCC

leetcode字串的第一個唯一字元(C、Python解答)

題目: 字串中的第一個唯一字元 給定一個字串,找到它的第一個不重複的字元,並返回它的索引。如果不存在,則返回 -1。 案例: s = "leetcode" 返回 0. s = "lovelee

JavaScript:語法javaScript的Object.defineProperty()和defineProperties()

2017-09-21 釋出 ECMAS-262第5版在定義只有內部採用的特性時,提供了描述了屬性特徵的幾種屬性。ECMAScript物件中目前存在的屬性描述符主要有兩種,資料描述符(資料屬性)和存取描述符(訪問器屬性),資料描述符是一個擁有可寫或不可寫值的屬性。

慕課網JavaScriptOOP(上)

1.概念與繼承 面向物件程式設計(Object-oriented programming,OOP)是一種程式設計範型,同時也是一種程式開發的方法。物件指的是類的例項。它將物件作為程式的基本單元,將程式和資料封裝其中,以提高軟體的重用性、靈活性和擴充套件性。 面向物件特點:繼承、封

慕課網JavaScript函式和this

1.函式概述 JS中函式比較特殊,函式也是物件中的一種。常叫做函式物件。 所以JS函式可以像其它物件那樣操作和傳遞,所以我們也常叫JS中的函式為函式物件。 函式的返回值依賴return,一般的函式呼叫:沒有return就會預設在所有程式碼執行完返回一個undefined; 如

小白javascript的“文件已完成載入後執行 document.write,整個 HTML 頁面將被覆蓋”

document.write已經遇到過好幾次了,還是得弄弄清楚。 W3cschool上面有一道練習題,如下(圖片1) 結果輸入的時候h1跟p段落全部被覆蓋,只有語句“糟糕!文件消失了” 圖片中出現

探索機器指令翻譯成 JavaScript

前言 前些時候研究指令碼混淆時,打算先學一些「程式流程」相關的概念。為了不因太枯燥而放棄,決定想一個有趣的案例,可以邊探索邊學。 於是想了一個話題:嘗試將機器指令 1:1 翻譯 成 JavaScript,這樣就能在瀏覽器中,直接執行等價的邏輯。 為了簡單起見,這裡選擇古董級 CPU —— MOS 6502。

JAVAjava實現map集合的資料存取詳解三種方法。Android程式設計師也是要會寫的

長期維護的Android專案,裡面包括常用功能實現,以及知識點詳解, 當然還有java中的知識點。具體請看github: https://github.com/QQ986945193/DavidAndroidProjectTools 好了,說正題

轉載淺談C的malloc和free

在C語言的學習中,對記憶體管理這部分的知識掌握尤其重要!之前對C中的malloc()和free()兩個函式的瞭解甚少,只知道大概該怎麼用——就是malloc然後free就一切OK了。當然現在對這兩個函式的體會也不見得多,不過對於本文章第三部分的內容倒是有了轉折性的認識,所