ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript で C プログラムを使用する方法を詳しく説明します。

JavaScript で C プログラムを使用する方法を詳しく説明します。

黄舟
黄舟オリジナル
2017-03-09 14:40:371750ブラウズ

JavaScript で C プログラムを使用する方法を詳しく説明します:

JavaScript は、ビジネス ロジックを簡単に処理できる柔軟なスクリプト言語です。通信を送信する必要がある場合、主に JSON または XML 形式を選択します。

しかし、データ長が非常に厳しい場合、テキストプロトコルの効率は非常に低くなり、バイナリ形式を使用する必要があります。

去年の今日、フロントエンドとバックエンドを組み合わせたWAFをいじっていたときにこのトラブルに遭遇しました。

フロントエンド スクリプトは大量のデータを収集する必要があり、最終的には Cookie に隠されるため、使用できる長さは非常に限られており、わずか数十バイトです。

何も考えずに JSON を使用すると、タグフィールド 1 つだけで {"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: &#39;month&#39;, bit: 4, signed: false},
    ...
]);

s.set(&#39;month&#39;, 12);
s.get(&#39;month&#39;);

詳細が非表示になり、よりエレガントに見えます。

エレガントだが完璧ではない

ただし、これが常に最も完璧であるとは限りません。構造体のようなものは言語によって提供されるべきですが、現在は追加のコードで実装する必要があり、それはまだ実行時に行われます。

さらに、バックエンドのデコードは C で実装されているため、2 セットのコードを維持する必要があります。データ構造やアルゴリズムが変わってしまうと、JSとCを同時に更新するのは非常に面倒です。

そこで考えたのですが、フロントエンドとバックエンドの両方で C コードのセットを共有できるでしょうか?

言い換えれば、実行するには C を JS にコンパイルできる必要があります。

emscripten について知る

C を JS にコンパイルできるツールはたくさんありますが、最もプロフェッショナルなものは emscripten です。

emscripten の使用は非常に簡単で、JS コードを生成する点を除けば従来の C コンパイラと似ています。

./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 ですら 10,000 行を超え、最大で数百 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% 的逻辑代码了。

增加兼容

接着解决内存访问的兼容性问题。

首先了解下,为何要用 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 逻辑了。

作为脚本,只需关心采集哪些数据。这样 JS 代码就非常的优雅:

数据的储存、加密、编码,这些底层数据操作,则通过 C 实现。

编译时使用 -Os 参数优化体积。最终的 JS 混淆压缩之后,还不到 2 KB,十分小巧精炼。

更完美的是,我们只需维护一份代码,即可同时编译出前端和后端两个版本。

于是,这个「前后端 WAF」开发就容易多了。

所有的数据结构和算法,都由 C 实现。前端编译成 JS 代码,后端编译成 lua 模块,供 nginx-lua 使用。

前后端的脚本,都只需关注业务功能即可,完全不用涉及数据层面的细节。

测试版

事实上,还有第三个版本 —— 本地版。

因为所有的 C 代码都在一起,因此可以方便的编写测试程序。

这样就无需启动 WebServer、打开浏览器来测试了。只需模拟一些数据,直接运行程序即可测试,非常轻量。

同时借助 IDE,调试起来更容易。

小结

每一门语言都有各自的优缺点。将不同语言的优势相互结合,可以让程序变得更优雅、更完美。

以上がJavaScript で C プログラムを使用する方法を詳しく説明します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。