【探索】在 JavaScript 中使用 C 程序

news2024/11/25 0:23:48

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% 的逻辑代码了。

增加兼容

接着解决内存访问的兼容性问题。

在很老版本的 emscripten 里,是可以选择是否使用 TypedArray 的。如果不用,则通过 JS Array 来实现。但如今早已去除了这个参数,只能使用 TypedArray。

首先了解下,为何要用 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 来模拟,非常简单。麻烦的是模拟 Float 类型,不过 C 程序中未用到浮点,所以就没实现。

如果支持 TypedArray,则使用原生的接口;否则,用 Array 模拟版本。

这样, 既保障了高版本浏览器的性能,又兼顾了老浏览器的功能。

大功告成

解决了这些缺陷,我们就可以愉快的在 JS 中使用 C 逻辑了。

脚本,只关心业务逻辑。例如采集哪些数据,这样代码就非常的优雅:

数据的储存、加密、编码,这些二进制操作,则通过 C 实现。

编译时使用 -Os 参数优化体积,最终的 JS 精简压缩之后,还不到 2 KB,十分小巧精炼。

于是,这个「前后端 WAF」开发就容易多了。我们只需维护一份代码,即可同时编译出前后端两个版本!

所有的数据结构和算法,都由 C 实现。前端编译成 JS 代码,后端编译成 lua 模块,供 nginx-lua 使用。

前后端的脚本,都只需关注业务功能即可,完全不用涉及数据层面的细节。

测试版

事实上,还有第三个版本 —— 本地版。

因为所有的 C 代码都在一起,因此可以方便的编写测试程序。

这样就无需启动 WebServer、打开浏览器来测试了。只需模拟一些数据,直接运行程序即可测试,非常轻量。

同时借助 IDE,调试起来更容易。

小结

每一门语言都有各自的优缺点。将不同语言的优势相互结合,可以让程序变得更优雅、更完美。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/599350.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis中的整数集合(IntSet)

Redis节省内存的两个优秀设计思想&#xff1a;一个是使用连续的内存空间&#xff0c;避免内存碎片开销&#xff1b;二个是针对不同长度的数据&#xff0c;采用不同大小的元数据&#xff0c;以避免使用统一大小的元数据&#xff0c;造成内存空间的浪费。IntSet便具备以上两个设计…

160套小程序源码

源码列表如下&#xff1a; AppleMusic (知乎日报) 微信小程序 d artand 今日更新求职招聘类 医药网 口碑外卖点餐 城市天气 外卖小程序 定位天气 家居在线 微信小程序-大好商城&#xff0c;wechat-weapp 微信小程序的掘金信息流 微信跳一跳小游戏源码 微票源码-demo 急救应急处…

MyBatis- plus

实战总结 1.批量插入性能 1.批量插入性能差的原因 使用saveBatch()方法时&#xff0c; MySQL JDBC驱动在默认情况下会无视executeBatch()语句&#xff0c;把我们期望批量执行的一组sql语句拆散&#xff0c;一条一条地发给MySQL数据库&#xff0c;批量插入实际上是单条插入&a…

2023企业真实性能测试常见面试题分析

简述性能测试流程&#xff1f; 1.分析性能需求。挑选用户使用最频繁的场景来测试&#xff0c;比如&#xff1a;登陆&#xff0c;搜索&#xff0c;下单等等。确定性能指标&#xff0c;比如&#xff1a;事务通过率为100%&#xff0c;TOP99%是5秒&#xff0c;最大并发用户为1000人…

Three.js——八、坐标、更改模型原点、移除、显示隐藏模型对象

世界坐标.getWorldPosition() 基础坐标也就是模型的.position属性 世界坐标&#xff1a;就是模型资深.position和所有父对象.position累加的坐标 用.getWorldPosition()属性需要用三维向量表示摸个坐标后方可读取 例如&#xff1a; const geometry new THREE.BoxGeometry(10…

【Qt】createEditor进不去【2023.05.07】

摘要 妈卖批&#xff0c;因为这个函数进不去&#xff0c;emo了一下午。实际上就是因为函数声明和定义的地方漏了个const关键字。 1.正确✔&#xff1a; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) cons…

Rocketmq面试(三)消息积压,增加消费者有用么?

目录 一.广播模式和集群模式的不同 二.延迟拉取 三.消费者延迟拉取消息的原因 四.增加消费者后是如何分配MessageQueue(引出负载策略) 一.广播模式和集群模式的不同 首先我们要强调一下。在广播模式&#xff08;每条消息需要被消费者组中的每个消费者处理&#xff0c;也就是…

QT/PyQT/PySide 通过富文本形式实现关键词高亮

因为本质上都是QT&#xff0c;所以我标题带了QT&#xff0c;这个思路是没问题的&#xff0c;就是用C得换个语言。 最开始想根据之前一篇博客的思路进行高亮 PyQT/PySide 文本浏览器跳转到指定行&#xff0c;并高亮指定行_qt 指定行高亮_Toblerone_Wind的博客-CSDN博客https:/…

Linux 设备树文件手动编译的 Makefile

前言 通过了解 Linux 设备树的编译方法&#xff0c;手动写了一个可以把 dts、dtsi、设备树依赖头文件等编译为设备树 dtb 的 Makefile Makefile 如下 mkfile_path : $(abspath $(lastword $(MAKEFILE_LIST))) cur_makefile_path : $(dir $(mkfile_path))DIR_ROOT : $(cur_ma…

十三届蓝桥杯国赛2022

会得噶 A 2022B 钟表C 卡牌D 最大数字dfsF 费用报销&#xff08;不是根据收据个数&#xff0c;而是根据日期dp)H 机房&#xff08;最近公共祖先lca&#xff09;I 齿轮J 搬砖&#xff08;贪心01背包&#xff09; A 2022 #include <bits/stdc.h> using namespace std; int …

Openlayers如何设置米作为作为圆形的真实半径,解决圆形半径跟随地图缩放同时缩放的失真问题

专栏目录: OpenLayers入门教程汇总目录 前言 相信找到这篇文章的同学肯定遇到了Openlayers直接设置圆形半径( radius)单位不准确的问题,而且失真严重。这是因为默认圆形半径设置的是浏览器像素大小,而不是真实地理信息中的半径长度。那么怎么进行转换成我们现实中的“米…

python+vue校园快递代取系统的设计与实现3i0v9

开发语言&#xff1a;Python 框架&#xff1a;django/flask Python版本&#xff1a;python3.7.7 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;PyCharm 本系统名为“基于vue快递代取系统”&#xff0c;系统主要适用于毕业设计&#xff0c;不…

【数据分享】1929-2022年全球站点的逐日最高气温(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff0c;其中又以气温指标最为常用&#xff01;说到气温数据&#xff0c;最详细的气温数据是具体到气象监测站点的气温数据&#xff01; 之前我们分享过1929-2022年全球气象站…

高通滤波学习(opencv)

以下代码参考视频解析 这段代码使用了二维FFT变换对输入图像进行频域处理&#xff0c;并设计了一个简单的高通滤波器。 前两行使用了numpy库中的fft2函数对输入图像image进行二维傅里叶变换&#xff08;FFT&#xff09;。接着&#xff0c;fft_shift函数将转化后的频谱数据fft…

什么是DOM和BOM?

一、什么是DOM DOM 全称是 Document Object Model&#xff0c;也就是文档对象模型。提供操作页面元素的方法和属性&#xff0c;是HTML和XML的API&#xff0c;DOM把整个页面规划成由节点层级构成的文档。 DOM 树 DOM树是Web页面的模型&#xff0c;当浏览器加载一个Web页面时&am…

A Framework for Evaluating Gradient Leakage Attacks in Federated Learning

联邦学习中梯度泄漏攻击评估框架 摘要&#xff1a; 针对问题&#xff1a;从客户端向联邦服务器共享本地参数更新也可能容易受到梯度泄漏攻击&#xff0c;并侵犯客户端关于其训练数据的隐私。 提出了一个原则性框架&#xff0c;用于评估和比较不同形式的客户端隐私泄露攻击。…

路径规划算法:基于纵横交叉优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于纵横交叉优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于纵横交叉优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

Eclipse导入项目的配置步骤说明

1.数据库创建并导入 &#xff08;1&#xff09;打开navicat&#xff0c;右击&#xff0c;选择创建数据库&#xff0c;进入新建数据库页面&#xff0c;输入数据库名称。我这里创建的是report数据库。 &#xff08;2&#xff09;右击自己创建的数据库&#xff0c;选择运行sql文件…

【C++】STL——stack OJ练习

文章目录 1. 最小栈思路分析AC代码拓展思维 2. 栈的压入、弹出序列思路讲解AC代码 3. 逆波兰表达式求值思路讲解AC代码拓展&#xff1a;中缀表达式如何转后缀 这篇文章我们来做几道stack相关的OJ题&#xff0c;练习一下stack的使用。 1. 最小栈 先来看第一道题——&#xff1a…

【人工智能】常见问题以及解答

1 什么是人工智能 人工智能&#xff08;Artificial Intelligence, AI&#xff09;是一门涉及计算机科学、数学、心理学、哲学等多个领域的交叉学科&#xff0c;旨在研究如何使计算机能够像人一样地思考、学习和行动。 在过去几十年中&#xff0c;人工智能技术得到了广泛的应用…