数据结构和算法由 C 实现,前端编译&n

数据结构和算法由 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使用。

前后端的脚本,都只需









































复合经营 开进商场的书店文化商业能
复合经营 开进商场的书店文化商业能



转载请注明:http://www.92nongye.com/xxnr/204612470.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了