如何在 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% 的逻辑代码了。
增加兼容
接着解决内存访问的兼容性问题。
首先了解下,为何要用 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 程序的更多相关文章
- 如何在Javascript中利用封装这个特性
对于熟悉C#和Java的兄弟们,面向对象的三大思想(封装,继承,多态)肯定是了解的,那么如何在Javascript中利用封装这个特性呢? 我们会把现实中的一些事物抽象成一个Class并且把事物的属性( ...
- 如何在windows中编写R程序包(转载)
网上有不少R包的编译过程介绍,挑选了一篇比较详细的,做了稍许修改后转载至此,与大家分享 如何在windows中编写R程序包 created by helixcn modified by binaryf ...
- 如何在 JavaScript 中检查字符串是否包含子字符串?
如何在 JavaScript 中检查字符串是否包含子字符串? // var test4 = _.includes(string, substring); 该方法需要此文件 <script src ...
- 转 如何在C++中调用C程序
如何在C++中调用C程序? C++和C是两种完全不同的编译链接处理方式,如果直接在C++里面调用C函数,会找不到函数体,报链接错误.要解决这个问题,就要在 C++文件里面显示声明一下哪些函数是C写 ...
- 如何在JavaScript中使用高阶函数
将另一个函数作为参数的函数,或者定义一个函数作为返回值的函数,被称为高阶函数. JavaScript可以接受高阶函数.这种处理高阶函数的能力以及其他特点,使JavaScript成为非常适合函数式编程的 ...
- 前端必读:如何在 JavaScript 中使用SpreadJS导入和导出 Excel 文件
JavaScript在前端领域占据着绝对的统治地位,目前更是从浏览器到服务端,移动端,嵌入式,几乎所有的所有的应用领域都可以使用它.技术圈有一句很经典的话"凡是能用JavaScript实现的 ...
- 如何在JavaScript中正确引用某个方法(bind方法的应用)
在JavaScript中,方法往往涉及到上下文,也就是this,因此往往不能直接引用,就拿最常见的console.log("info…")来说,避免书写冗长的console,直接用 ...
- 【探索】在 JavaScript 中使用 C 程序
JavaScript 是个灵活的脚本语言,能方便的处理业务逻辑.当需要传输通信时,我们大多选择 JSON 或 XML 格式. 但在数据长度非常苛刻的情况下,文本协议的效率就非常低了,这时不得不使用二进 ...
- 如何在C++中调用C程序
注意这里的C调用C++或者C++调用C意思是.c文件中调用.cpp文件中代码,或者相反. C++和C是两种完全不同的编译链接处理方式,如果直接在C++里面调用C函数,会找不到函数体,报链接错误. 要解 ...
随机推荐
- zabbix active模式以及自定义key not Supported的解决
zabbix active模式 active模式适用场景 zabbix server端无法直连agent端,比如agent为内网机器,仅有内网ip,没有公网ip,但是内网机器能够访问server端 a ...
- python中模块和包的概念
1.模块 一个.py文件就是一个模块.这个文件的名字是:模块名.py.由此可见在python中,文件名和模块名的差别只是有没有后缀.有后缀是文件名,没有后缀是模块名. 每个文件(每个模块)都是一个独立 ...
- 每天进步一点点-Tesseract 文字识别
Tesseract 文字识别 是github上的开源文字识别软件 下载与安装 https://github.com/tesseract-ocr/tesseract/wiki 下载 https://gi ...
- 【Bootstrap】 框架 栅格布局系统设计原理
前提条件(Bootstrap 自带) 首先使用这个布局之前要定义一下代码: 这行代码如果不懂,可以搜索一下,总之大致意思就是,被定义的元素的内边距和边框不再会增加它的宽度,不加入的话排版会有问题. 不 ...
- JavaScript中的函数柯里化与反柯里化
一.柯里化定义 在计算机科学中,柯里化是把 接受多个参数的函数 变换成 接受一个单一参数(最初函数的第一个参数)的函数 并且返回 接受余下参数且返回结果的新函数的技术 高阶函数 高阶函数是实现柯里化的 ...
- Intervals POJ - 3680
传送门 给定数轴上n个带权区间$[l_i,r_i]$,权值为$w_i$ 选出一些区间使权值和最大,且每个点被覆盖次数不超过k次. 离散+拆点,最大费用可行流(跑到费用为负为止) 第一部分点按下标串起来 ...
- NX二次开发-UFUN拾取草图尺寸对话框UF_UI_select_sketch_dimensions
#include <uf.h> #include <uf_ui.h> #include <uf_sket.h> UF_initialize(); //拾取草图尺寸对 ...
- NX二次开发-遍历当前part所有component,把装配子部件设置成工作部件
NX11+VS2013 #include <uf.h> #include <uf_disp.h> #include <uf_modl.h> #include < ...
- [JZOJ 5811] 简单的填数
题意:自己搜吧... 思路: 记二元组\((x,l)\)表示当前为\(x\)且之前有\(l\)个连续数与\(x\)相同. 并且维护up和low数组表示取到最大/最小值时,连续序列的长度. 正一遍,反一 ...
- Spring随笔-核心知识DI与AOP
DI 依赖注入,使得相互依赖的组件松耦合. AOP 面向切面编程,使各种功能分离出来,形成可重用的组件.