前端工程师可以用 C++ 做什么?

我觉得在 2020 年,这个问题值得再回答一次。
因为随着 Node.js N-API 的成熟,现在前端用 C++ 做出东西的难度已经大幅降低了。作为从纯写 JS 开始,逐渐在工作中涉及到了一些(现学现卖的)C++ 的前端,这里分享一些点技能树时的理解吧。


在讲具体「能做什么」之前,首先安利一下 N-API。某种程度上,个人觉得它对前端同学点亮技能树的意义,甚至更高于「青少年性行为」式的 WASM(每个人都在说,每个人都想做,每个人都以为其他人在做,然而真做过的没几个,而且多半做得不咋地)。因为只要你试过就知道,作为 Node 社区主推的原生扩展机制,基于 N-API 的 node-addon-api,几乎是目前往 JS 里嵌入 C++ 最轻松的方式了。


优点如下:

  1. 无需繁琐的 JS 侧 API,直接就能给 JS 环境扩展出 document.createElement 之类的原生方法。
  2. 独立于 JS 引擎,不用操作令人头疼的 v8::xxx 对象。
  3. 直通操作系统能力,没有沙盒限制,没有 WASM 中常见的那种动辄 trampoline 到 JS 再到 C++ 的双份快乐开销。
  4. 兼容各大系统自带的 C++ 编译工具链,不用折腾 Emscripten,还可以用 CMake.js 来替代日常爆炸的 node-gyp(见下图)。
  5. ABI 已经稳定,不会 nvm 换个 Node 版本就炸(当然未来跨平台兼容靠的应该还是 WASI,但它目前还在 Experimental 阶段)。
  6. 糖分比较高,编写和调试方便,文档很良心。

这个 N-API 用起来是什么样的呢?简单说来,如果我想用 C++ 给 JS 运行时扩展出一个朴素的原生 Print 方法,只要几行就可以实现出其本体了:

#include <napi.h>
#include <string>
#include <cstdio>

// 原生函数均接受 JS 的调用者对象,返回某种 JS 中的 Value
Napi::Value Print(const Napi::CallbackInfo& info) {
  // 把 JS 传入的第一个参数导出到 std::string
  auto x = info[0].As<Napi::String>().Utf8Value();
  printf("%s\n", x.c_str()); // 向谭浩强老师致敬
  return info.Env().Undefined(); // 返回 undefined
}

然后只要再几行胶水,就能把这个 Print 函数包装成原生模块了:

Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
  exports.Set("Print", Napi::Function::New(env, Print));
  // 其他原生方法也类似,照猫画虎即可
  exports.Set("Hello", Napi::Function::New(env, Hello));
  exports.Set("World", Napi::Function::New(env, World));
  return exports;
}

// 要导出整个模块,在整个 C++ 文件最后加一行就行
NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll)

最后只要编译出这个有效代码不过十来行的 C++ 文件,就能在 Node.js 里这样使用它了:

const { Print } = require('./build/Release/demo.node');
Print('hello world');

不过对很多人来说,C++ 的编译可能才是初学时最大的拦路虎。 这方面 Medium 上有一篇 N-API Beginners Guide 写得不错,但文中的编译工具用的是 node-gyp,这里更推荐 Node 官方基于 CMake.js 的 hello world 示例。关于构建工具的区别说来话长,简单说可以认为 CMake 和 GYP 都相当于面向 C++ 的 Webpack。大家可以先尝试用 CMake 搭一个 hello world 级的项目,搞懂 C 系语言里基于 include 复用库代码的机制是怎么一回事,然后就差不多了。


另外提到 JS 的 C++ 扩展,不得不提的自然还有《Node.js 来一打 C++ 扩展》这本书。它的深度无疑是很高的,死月也确实写得很走心,但书中主要的篇幅还是在介绍之前的 NAN 扩展机制。而对于 N-API 的 C++ 扩展,只要走通了上面的 hello world 流程,基本对着官方文档就能顺利地把桥接层搞出来。当然还是建议大家买一本死月的书对照一下,然后你就知道基于 N-API 的开发有多轻松了……并且 N-API 里还不需要像 QuickJS 和鸿蒙用的 JerryScript 引擎那样,要用 JS_DupValue 和 JS_FreeValue 这类 API 来手动平衡引用计数,整体易用性上好了很多。

好了,关于前端如何「轻松地把 C++ 用起来」,大概就只有这点东西了。下面关键的问题来了——用 C++ 可以做什么呢?

现在工业界需要 C++ 的领域,都以偏「三大浪漫」性质的东西为主,一般都比较远离应用层。但如果自己有造轮子的兴趣,那还是有不少容易和前端产生联系(甚至有可能在特定业务中落地)的方向:

  1. 基于 Skia 渲染库和 Yoga 布局库,可以自己把 Node.js 搭建成类似小程序式框架的运行时,也能给 Vue 和 React 等 UI 框架实现原生的渲染后端,或者实现部分替代无头浏览器的图像渲染等。
  2. 基于树莓派,可以操控各种嵌入式的硬件外设。对这些外设驱动的使用可以拿 N-API 自己接,不怕那些年久失修的 node-xxx 库。
  3. 基于 QuickJS 之类的嵌入式 JS 引擎,可以自己搭 JS 运行时,将其移植到一些掌机上。比如我们能不能用 TypeScript 给索尼 PSP 写游戏,把完整版的白学移植上去?其实有这个可能的……
  4. 基于 LLVM,甚至还能把 TS 的子集静态编译到机器码。个人最近做过这样的尝试,这时可以通过 Napi::External 来在 TS 的 AST 节点中存储 LLVM 的 Value 对象,还是很方便的。不过这个编译器现在还只支持 number 类型的运算,等以后完整点再拿出来吧。

总之,个人觉得走出浏览器以后,如果把 JS 式技术栈和原生相结合,能做的东西还有很多,到处都是花业余时间就能搞出个玩具来的 low-hanging fruit。具体要做什么呢?主要还是取决于个人兴趣吧。

另外还有必须说明的一点,那就是在语言特性上,C++ 真的有非常多历史问题。不要抱着做题家心态,为了成为 C++ 专家而去学习 C++ 啊……这里尤其有种不建议的行为,那就是为了目标不明的「深入学习」跑去看大型项目的 C++ 源码。特别是在初学阶段,如果上来就去研究 Chromium 和 V8 等极为复杂的项目,那大概率是事倍功半的。相比之下,反倒是从头开始找足够有趣的小 demo 做,比较容易持续找到「我能搞得定」的掌控感。像个人最近在 QConPlus 上分享的我司 GUI 框架,实际上就是从「把 QuickJS 编译到安卓上,然后尝试把它跟 Skia 接到一起」的玩具开始的。

长久以来,前端社区都在持续地把浏览器外的技术「引进来」。但社区发展到现在,也已经有很多引领业界潮流的东西值得反过来「对外输出」了。这时候如果多了解一些浏览器外的工程技术,很有利于你实现更大胆的想法。毕竟浏览器很大但计算机科学更大,这和「中国很大但世界更大」是一个道理。保持开放的心态,才不至于陷入小圈子里敝帚自珍的怪圈。我也深知个人目前水平的欠缺,还有很多要学的和想做的东西,和大家共勉吧。

原文链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值