>  기사  >  웹 프론트엔드  >  JavaScript에서 C 프로그램을 사용하는 방법에 대해 자세히 설명합니다.

JavaScript에서 C 프로그램을 사용하는 방법에 대해 자세히 설명합니다.

黄舟
黄舟원래의
2017-03-09 14:40:371734검색

JavaScript에서 C 프로그램을 사용하는 방법을 자세히 가르쳐주세요 :

JavaScript는 비즈니스 로직을 쉽게 처리할 수 있는 유연한 스크립트 언어입니다. 통신을 전송해야 할 때 대부분 JSON 또는 XML 형식을 선택합니다.

그러나 데이터 길이가 매우 까다로울 경우 텍스트 프로토콜의 효율성이 매우 낮아 바이너리 형식을 사용해야 합니다.

지난해 어느 날, 프론트엔드와 백엔드를 통합한 WAF를 이리저리 헤매다가 이런 문제를 겪었습니다.

프런트 엔드 스크립트는 궁극적으로 쿠키에 숨겨진 많은 데이터를 수집해야 하기 때문에 사용 가능한 길이는 수십 바이트로 매우 제한되어 있습니다.

아무 생각 없이 JSON을 사용하면 {"enableXX": true} 태그 필드 하나만 절반 길이를 차지하게 됩니다. 그러나 바이너리에서는 참 또는 거짓을 표시하는 것이 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로 구현되므로 두 세트의 코드를 유지해야 합니다. 데이터 구조나 알고리즘이 변경되면 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.