原标题:在 Javascript 中使用 C 程序
✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦
Java 是个灵活的脚本语言,能方便的处理业务逻辑。当需要传输通信时,我们大多选择 JSON 或 XML 格式。
但在数据长度非常苛刻的情况下,文本协议的效率就非常低了,这时不得不使用二进制格式。
去年的今天,在折腾一个 前后端结合的 WAF 时,就遇到了这个麻烦。因为前端脚本需要采集不少数据,而最终是隐写在某个 COOKIE 里的,因此可用的长度非常有限,只有几十个字节。
如果不假思索就用 JSON 的话,光一个标记字段 {"enableXX": true} 就占去了一半长度。然而在二进制里,标记 true 或 false 不过是 1 个比特的事,可以节省上百倍的空间。
同时,数据还要经过校验、加密等环节,只有使用二进制格式,才能方便的调用这些算法。
1优雅实现
不过,Java 并不支持二进制。
这里的「不支持」不是说「无法实现」,而是无法「优雅实现」。语言的发明,就是用来优雅解决问题的。即使没有语言,人类也可以用机器指令来编写程序。
如果非要用 Java 操作二进制,最终就类似这样:
var flags &#61; &#43;enableXX1 <<16 | &#43;enableXX2 <<15 | ...
虽然能实现&#xff0c;但很丑陋。各种硬编码、各种位运算。
然而&#xff0c;对于先天支持二进制的语言&#xff0c;看起来就十分优雅&#xff1a;
union {
struct {
int enableXX1: 1;
int enableXX2: 1;
...
};
int16_t value;
} flags;
flags.enableXX1 &#61; enableXX1;
flags.enableXX2 &#61; enableXX2;
开发者只需定义一个描述即可。使用时&#xff0c;字段偏移多少、如何读写&#xff0c;这些细节完全不用关心。
为了能达到类似效果&#xff0c;起先封装了一个 JS 版的结构体&#xff1a;
// 最初方案&#xff1a;封装一个 JS 结构体
var s &#61; new Struct([
{name: &#39;month&#39;, bit: 4, signed: false},
...
]);
s.set(&#39;month&#39;, 12);
s.get(&#39;month&#39;);
将细节进行了隐藏&#xff0c;看起来就优雅多了。
2优雅但不完美
但是&#xff0c;这总感觉不是最完美的。结构体这种东西&#xff0c;本该由语言提供&#xff0c;如今却要用额外的代码实现&#xff0c;而且还是在运行期间。
另外&#xff0c;后端解码是用 C 实现的&#xff0c;所以得维护两套代码。一旦数据结构或者算法变了&#xff0c;得同时更新 JS 和 C&#xff0c;很麻烦。
于是琢磨&#xff0c;能否共用一套 C 代码&#xff0c;同时用于前端和后端&#xff1f;
也就是说&#xff0c;需要能将 C 编译成 JS 来运行。
3认识 emen
能将 C 编译成 JS 的工具有不少&#xff0c;最专业的要数 emen。
emen 的使用方式很简单&#xff0c;和传统 C 编译器差不多&#xff0c;只不过生成的是 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;
}
编译之后即可运行&#xff1a;
很有趣吧~ 大家可以尝试下&#xff0c;这里就不多介绍了。
4实用缺陷
然而我们关心的不是有趣&#xff0c;而是实用。
事实上&#xff0c;即使一个 Hello World 编译出来的 JS 也过万行&#xff0c;多达数百 KB。就算压缩再 GZIP&#xff0c;仍有几十 KB。同时 emen 使用了 asm.js 规范&#xff0c;内存访问是通过 TypedArray 实现的。
这意味着 IE10 以下的用户都无法运行。这也是不可接受的。因此&#xff0c;我们得做如下改进&#xff1a;
减少体积
增加兼容
首先寄托 emen 本身&#xff0c;看看能不能通过设置参数&#xff0c;来达到我们的目的。不过一番尝试之后&#xff0c;并没有成功。那只能自己动手实现了。
5减少体积
为什么最终脚本会那么大&#xff0c;里面都放了些什么&#xff1f;分析了下内容&#xff0c;大致有这几个部分&#xff1a;
辅助功能
接口模拟
初始化操作
运行时函数
程序逻辑
辅助功能
比如字符串和二进制转换、提供回调包装等。这些基本都是用不着的&#xff0c;我们可以给自己写个特殊的回调函数。
接口模拟
提供文件、终端、网络、渲染等接口。之前见过用 emen 移植的客户端游戏&#xff0c;看来模拟了不少接口。
初始化操作
全局内存、运行时、各种模块的初始化。
运行时函数
纯粹的 C 只能做简单的计算&#xff0c;很多功能都依靠运行时函数。
不过&#xff0c;有些常用的函数&#xff0c;其背后的实现是及其复杂的。例如 malloc 和 free&#xff0c;对应的 JS 有近 2000 行&#xff01;
程序逻辑
这才是 C 程序真正对应的 JS 代码。因为编译时经过 LLVM 的优化&#xff0c;逻辑可能变得面目全非了。
这部分代码量不大&#xff0c;是我们真正想要的。
事实上&#xff0c;如果程序没有用到一些特殊功能的话&#xff0c;把逻辑函数单独抠出来&#xff0c;仍然是可以运行的&#xff01;
考虑到我们的 C 程序非常简单&#xff0c;所以简单粗暴的提取出来&#xff0c;也是没问题的。
C 程序对应的 JS 逻辑位于 // EMEN_START_FUNCS 和 // EMEN_END_FUNCS 之间。过滤掉运行时函数&#xff0c;剩下的就是 100% 的逻辑代码了。
6增加兼容
接着解决内存访问的兼容性问题。
在很老版本的 emen 里&#xff0c;是可以选择是否使用 TypedArray 的。如果不用&#xff0c;则通过 JS Array 来实现。但如今早已去除了这个参数&#xff0c;只能使用 TypedArray。
首先了解下&#xff0c;为何要用 TypedArray。
emen 申请了一大块 ArrayBuffer 来模拟内存&#xff0c;然后关联了一些 HEAP 开头的变量。
这些不同类型的 HEAP 共享同一块内存&#xff0c;这样就能高效的指针操作。
然而不支持 TypedArray 的浏览器&#xff0c;显然无法运行。所以得提供个 polyfill 兼容下。
但经分析&#xff0c;这几乎不可能实现 —— 因为 TypedArray 和数组一样&#xff0c;是通过索引来访问的&#xff1a;
var buf &#61; new Uint8Array(100);
buf[0] &#61; 123; // set
alert(buf[0]); // get
然而 [] 操作符在 JS 里是无法重写的&#xff0c;因此难以将其变成 setter 和 getter。况且不支持 TypedArray 的都是低版本 IE&#xff0c;更不用考虑 ES6 的那些特征。
于是琢磨 IE 的私有接口。比如用 onpropertychange 事件来模拟 setter。不过这样做效率极低&#xff0c;而且 getter 仍不易实现。
经过一番考虑&#xff0c;决定不用钩子的方式&#xff0c;而是直接从源头上解决 —— 修改语法&#xff01;
我们用正则&#xff0c;找出源码中的赋值操作:
HEAP[index] &#61; val;
替换成:
HEAP_SET(index, val);
类似的&#xff0c;将读取操作:
HEAP[index]
替换成:
HEAP_GET(index)
这样&#xff0c;原先的索引操作&#xff0c;就变成函数调用了。我们就能接管内存的读写&#xff0c;并且没有任何兼容性问题&#xff01;
然后实现 8、16、32 位有无符号的版本。通过 JS 的 Array 来模拟&#xff0c;非常简单。麻烦的是模拟 Float 类型&#xff0c;不过 C 程序中未用到浮点&#xff0c;所以就没实现。
如果支持 TypedArray&#xff0c;则使用原生的接口&#xff1b;否则&#xff0c;用 Array 模拟版本。
这样&#xff0c; 既保障了高版本浏览器的性能&#xff0c;又兼顾了老浏览器的功能。
7大功告成
解决了这些缺陷&#xff0c;我们就可以愉快的在 JS 中使用 C 逻辑了。
脚本&#xff0c;只关心业务逻辑。例如采集哪些数据&#xff0c;这样代码就非常的优雅&#xff1a;
数据的储存、加密、编码&#xff0c;这些二进制操作&#xff0c;则通过 C 实现。
编译时使用 -Os 参数优化体积&#xff0c;最终的 JS 精简压缩之后&#xff0c;还不到 2 KB&#xff0c;十分小巧精炼。
于是&#xff0c;这个「前后端 WAF」开发就容易多了。我们只需维护一份代码&#xff0c;即可同时编译出前后端两个版本&#xff01;
所有的数据结构和算法&#xff0c;都由 C 实现。前端编译成 JS 代码&#xff0c;后端编译成 lua 模块&#xff0c;供 nginx-lua 使用。
前后端的脚本&#xff0c;都只需关注业务功能即可&#xff0c;完全不用涉及数据层面的细节。
测试版
事实上&#xff0c;还有第三个版本 —— 本地版。
因为所有的 C 代码都在一起&#xff0c;因此可以方便的编写测试程序。
这样就无需启动 WebServer、打开浏览器来测试了。只需模拟一些数据&#xff0c;直接运行程序即可测试&#xff0c;非常轻量。
同时借助 IDE&#xff0c;调试起来更容易。
8小结
每一门语言都有各自的优缺点。将不同语言的优势相互结合&#xff0c;可以让程序变得更优雅、更完美。
✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦
原文&#xff1a;http://www.cnblogs.com/index-html/p/using_c_in_java.html
点击“阅读原文”&#xff0c;看更多
精选文章
责任编辑&#xff1a;