数据结构和算法由 C 实现,前端编译&n
JavaScript是个灵活的脚本语言,能方便的处理业务逻辑。当需要传输通信时,我们大多选择JSON或XML格式。
但在数据长度非常苛刻的情况下,文本协议的效率就非常低了,这时不得不使用二进制格式。
去年的今天,在折腾一个前后端结合的WAF时,就遇到了这个麻烦。
因为前端脚本需要采集不少数据,而最终是隐写在某个cookie里的,因此可用的长度非常有限,只有几十个字节。
如果不假思索就用JSON的话,光一个标记字段{enableXX:true}就占去了一半长度。然而在二进制里,标记true或false不过是1个比特的事,可以节省上百倍的空间。
同时,数据还要经过校验、加密等环节,只有使用二进制格式,才能方便的调用这些算法。
优雅实现不过,JavaScript并不支持二进制。
这里的「不支持」不是说「无法实现」,而是无法「优雅实现」。语言的发明,就是用来优雅解决问题的。即使没有语言,人类也可以用机器指令来编写程序。
如果非要用JavaScript操作二进制,最终就类似这样:
varflags=+enableXX+enableXX15...虽然能实现,但很丑陋。各种硬编码、各种位运算。
然而,对于先天支持二进制的语言,看起来就十分优雅:
union{struct{intenableXX1:1;intenableXX:1;...};int16_tvalue;}flags;ableXX1=enableXX1;ableXX=enableXX;开发者只需定义一个描述即可。使用时,字段偏移多少、如何读写,这些细节完全不用关心。
为了能达到类似效果,起先封装了一个JS版的结构体:
//最初方案:封装一个JS结构体vars=newStruct([{name:month,bit:4,signed:false},...]);t(month,1);t(month);将细节进行了隐藏,看起来就优雅多了。
优雅但不完美但是,这总感觉不是最完美的。结构体这种东西,本该由语言提供,如今却要用额外的代码实现,而且还是在运行期间。
另外,后端解码是用C实现的,所以得维护两套代码。一旦数据结构或者算法变了,得同时更新JS和C,很麻烦。
于是琢磨,能否共用一套C代码,同时用于前端和后端?
也就是说,需要能将C编译成JS来运行。
认识emscripten能将C编译成JS的工具有不少,最专业的要数emscripten。
emscripten的使用方式很简单,和传统C编译器差不多,只不过生成的是JS代码。
./ml//hello.c#includestdio.h#ntmain(){time_tnow;time(now);printf(HelloWorld:%s,ctime(now));return0;}编译之后即可运行:
很有趣吧~大家可以尝试下,这里就不多介绍了。
实用缺陷然而我们关心的不是有趣,而是实用。
事实上,即使一个HelloWorld编译出来的JS也过万行,多达数百KB。就算压缩再GZIP,仍有几十KB。
同时emscripten使用了规范,内存访问是通过TypedArray实现的。
这意味着IE10以下的用户都无法运行。这也是不可接受的。
因此,我们得做如下改进:
减少体积
增加兼容
首先寄托emscripten本身,看看能不能通过设置参数,来达到我们的目的。
不过一番尝试之后,并没有成功。那只能自己动手实现了。
减少体积为什么最终脚本会那么大,里面都放了些什么?分析了下内容,大致有这几个部分:
辅助功能
接口模拟
初始化操作
运行时函数
程序逻辑
辅助功能比如字符串和二进制转换、提供回调包装等。这些基本都是用不着的,我们可以给自己写个特殊的回调函数。
接口模拟提供文件、终端、络、渲染等接口。之前见过用emscripten移植的客户端游戏,看来模拟了不少接口。
初始化操作全局内存、运行时、各种模块的初始化。
运行时函数纯粹的C只能做简单的计算,很多功能都依靠运行时函数。
不过,有些常用的函数,其背后的实现是及其复杂的。例如malloc和free,对应的JS有近行!
程序逻辑这才是C程序真正对应的JS代码。因为编译时经过LLVM的优化,逻辑可能变得面目全非了。
这部分代码量不大,是我们真正想要的。
事实上,如果程序没有用到一些特殊功能的话,把逻辑函数单独抠出来,仍然是可以运行的!
考虑到我们的C程序非常简单,所以简单粗暴的提取出来,也是没问题的。
C程序对应的JS逻辑位于//EMSCRIPTEN_START_FUNCS和//EMSCRIPTEN_END_FUNCS之间。过滤掉运行时函数,剩下的就是%的逻辑代码了。
增加兼容接着解决内存访问的兼容性问题。
首先了解下,为何要用TypedArray。
emscripten申请了一大块ArrayBuffer来模拟内存,然后关联了一些HEAP开头的变量。
这些不同类型的HEAP共享同一块内存,这样就能高效的指针操作。
然而不支持TypedArray的浏览器,显然无法运行。所以得提供个polyfill兼容下。
但经分析,这几乎不可能实现——因为TypedArray和数组一样,是通过索引来访问的:
varbuf=newUint8Array();buf[0]=1;//setalert(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、位有无符号的版本。通过JS的Array来模拟,非常简单。
麻烦的是模拟Float和Float64两个类型。不过本次C程序中并未用到浮点,所以就暂不实现了。
到此,兼容性问题就解决了。
大功告成解决了这些缺陷,我们就可以愉快的在JS中使用C逻辑了。
作为脚本,只需关心采集哪些数据。这样JS代码就非常的优雅:
数据的储存、加密、编码,这些底层数据操作,则通过C实现。
编译时使用-Os参数优化体积。最终的JS混淆压缩之后,还不到KB,十分小巧精炼。
更完美的是,我们只需维护一份代码,即可同时编译出前端和后端两个版本。
于是,这个「前后端WAF」开发就容易多了。
所有的数据结构和算法,都由C实现。前端编译成JS代码,后端编译成lua模块,供nginx-lua使用。
前后端的脚本,都只需
复合经营 开进商场的书店文化商业能复合经营 开进商场的书店文化商业能