转载

使用 C++ 扩展 Node.js

在 Node.js 的帮助下,服务器端的 JavaScript 变的非常流行。Node.js 的生态系统已经非常完善了,你几乎可以找到所有事情的扩展插件。大部分扩展插件都是 JavaScript,但是在 V8 引擎上的 Node.js 是使用 C++ 语言编写的。Kenneth Geisshirt 在 Øredev 2015 的这篇演讲,深入谈及到了使用 C++ 来开发扩展插件的问题。他也谈到了为什么你需要使用 C++ 语言来编写它们。

See the discussion on Hacker News .

Transcription below provided by Realm: a mobile database that's a replacement for SQLite & Core Data. Learn more!

Get new videos & tutorials — we won’t email you for any other reason, ever.

About the Speaker: Kenneth Geisshirt

Kenneth 拥有化学博士学位(计算机学学士),1990年代他的主要工作是在超级计算机上仿真化学实验。毕业后,他一直是一名开源世界的软件开发者。现在他正在为 Realm 的安卓组工作。在他的空闲时间,他在各种会议、集会、和社区上发表关于软件开发和开源软件的演讲。

@kgeisshirt

Realm 是当前(主要)的移动设备数据库。我们有一个 C++ 编写的存储引擎。我的工作就是桥接这个 C++ 引擎和其他语言,包括 Node.js。本次演讲我们会谈到:

  1. V8 内核和 API 的基本知识
  2. 如何封装 C++ 类,然后你可以……
  3. 编写你的扩展插件

为什么使用 C++ 来扩展?

JavaScript 和 C++ 是非常不同的语言。虽然它们都是面向对象的,但是 JavaScript 没有类的概念。而且 JavaScript 是个动态语言,而 C++ 是一个强类型语言。

Node.js 和 JavaScript 相关;如果你是个 JavaScript 的开发者,你可能从来没有用过 C++。通过编写 C++ 的扩展,你可以触及到系统资源(系统调用、IO 设备访问、GPUs)。 你可能会使用这个处理单元来做些计算:你想释放你的 CPU(C++ 作为一个编译语言会更快些,你可能需要 C++ 的性能)。Realm 项目中,四分之三的代码都是用 C++ 编写的:这是我们在不同平台和语言中共享代码的方法。当然,时常也会有些继承的代码(常常是 C、C++,甚至 Fortran)你打算在你的新项目中使用它们。

我有两个 C++ 类 : Person ,有一个 firstname() 一个 lastname() 和一个 birthday() (方法,和一个供打印的字符串);和 Book 类(你在里面存储所有的 persons, 例如,通讯录应用;方法:添加或者查询一个 person,通过地区操作符获取一个 person,删除它们或者获取 person 的数量)。第一个 name()lastname()birthday() 是 getters 和 setters. 你可以设置和获取一个 person 的名字。非常简单的类;如果你想给一个 book 增加一个 person,你就使用 person 的对象;查找功能可以返回一个 person 的对象(在 Node 中这样做很奇怪)。

Node.js 是在 V8 的基础上建立起来的。 Isolate 是 V8 的一个独立的实例;它在一个独立的实例里包含的对象不能移动到另一个中。 Handles 是对 JavaScript 对象的引用。垃圾回收会在这个对象或者句柄不再被使用的时候回收它们。 Local 处理所有在堆栈上的内存分配;生存周期是基于范围的。静态的句柄可以在多个函数调用后依旧有效,并且范围改变的时候也能有效。

函数是最常用的编程语言,它可以返回一个值;但是在 V8 中,我们不能返回对象——你可以通过 GetReturnValue() .Set() 设置返回值。你不能返回一个本地对象和一个本地句柄:本地对象只存在在本地范围内,而且是在堆栈上分配的 - 当你从函数返回的时候垃圾回收会回收它们。

在 JavaScript 你有许多不同的 “classes” 或者对象类型( StringNumberArrayObject ,……)它们在 V8 的 API 里面是 C++ 类。这些对象是最通用的。一个数字可能是个整型或者浮点型。

突破性的改变:0.10 → 0.12

在2015年2月,Node.js 0.12 发布了:它们更新了 V8 API。我使用 Node 0.10 编写了一个扩展:在它们改变后,我就不能编译成功了。本次演讲是关于 0.12+ 的。Node 语言的版本好现在又改变了 [从 0.12 (年初)增加到 5.0 (上周)]。

从 C++ 返回值到 JavaScript 现在不一样了。参数的类型名字也改变了。isolate 是新的。在老的 API 中,当你创建一个串的时候你需要说明它的编码方式(常见的是 UTF-8)。一个扩展不可能同时支持 0.10 和 0.12+ (曾经有一个努力尝试解决这个问题,但是并不容易: 看看这篇文章 ))。

如果你想构建一个扩展,你需要写一堆的封装类(例如如果你想有个 person.cpp 类,你需要创建一个 person_wrap.cpp 文件)。然后你写一个 bindings.gyp 文件来解释编译过程(例如 目标文件名,’funstuff.cpp’;源文件;列出所有想封装的类,和封装类;OS X 特殊的扩展)。 ‘funstuff.cpp’ 用来配置好扩展;它调用了两个函数,叫做 Init。关于封装类,有一个方法配置好封装类,初始化它,然后把它加到 V8 引擎里面:InitAll。有一个叫做 Node 模块的宏,它能把所有的事情都设置好。你然后可以敲入 Node-gyp 配置,然后编译。接下来,你需要封装一个类了。

这是 BookWrap 的头文件( 代码请看视频 )。我们从 ObjectWrap 继承,然后是一个 node::ObjectWrap 。你需要一个 Init 函数,然后一个新的函数来创建一个新的对象。我们需要保持一个我们封装的对象的引用(Book* m_book))。这是一个静态的句柄,和一个函数: static v8::Persistent<v8::Function> Constructor 。JavaScript 的函数就是对象。我们需要设置这个构造函数。它被用来在这个类上调用 new。

我们有 Init() 。这个函数模版 new 会调用 BookWrap::New: ,构造一个新的对象。 Node_set_prototype_method 增加这个方法。然后,我么设置构造函数。如果我们想在 JavaScript 里列出属性的话,我们需要实现 getters 和 setters, deleters 和 enumerators (这是我的 C++ 类里面的方法)。当我需要 “funstuff” 的时候,就会调用到这个方法。

如果我们为这种类型(Book)分配一个新的对象,它调用了一个新的方法:它创建了一个封装的对象和被封装的对象,然后把它加入到 V8 运行环境中。我需要 GetReturnValue() ,然后设置返回值。我询问 IsConstructCall() :在 JavaScript 里面你可以不需要 new 来调用构造函数(作为普通函数调用)。如果你打算实现它,我需要创建另外一个分支( 不在本次的例子里面 )。在这个模块里,我只能调用 new Book() 。我要么实现它,要么抛出异常表明这样是不允许的。 偏离运行的时候我们有了一个新的对象,垃圾回收会管理它。我需要 args.length() ;如果我的构造函数需要输入参数的话,我可以那样做,如果这样,我就需要在我的构造函数里面设置它们。

在 C++ 里面方法很重要。然而,在 JavaScript 里面,你不需要方法;我们有属性,它就是方法。

这是一个长度的方法。我有 book.length( 代码请看视频 )。我通过获取 isolate 来得到当前的范围,然后为当前的 isolate 设置范围。函数的调用就是对该对象调用的引用。我解开了它 (在 Node.js 扩展里的95%的方法中 ,这是你首先要做的),而且我调用了 size 方法(例如得到通讯录里面的人数)。我然后把个数附给了新创建的一个 JavaScript 的整型(在 JavaScript V8 引擎里的对象),然后我设置了返回值。 我没有做参数检查。如果我调用 Length() 并传入 200 个参数,它也能工作,因为我没有检查。

如果我有一个可返回的查找函数(例如:我有一个 book,然后你可以通过名或姓查找这个人,然后返回那个对象),我需要能够在一个对象里面接收另一个对象,然后返回一个其他类型的对象。

在我的 person 封装对象里,我打算创建一个新的 person 封装对象,只包含一个 book 的对象。我设置好调用构造函数的调用( 第一行;如视频 )。然后我增加 person,然后返回它。我使用 EscapableHandleScope ; “escapable”:你可以使用你的本地对象然后把它放在外面。我返回这个 escapable (它把它自己从一个范围移除到另外一个范围),而不是直接返回它,这样来告知垃圾回收器范围发生改变了。文献中说你不应该在一个对象上调用 escape 两次( 虽然它没有说为什么或者会发生什么 )。这会调用到构造函数:现在我直接调用了 C++ 中间的构造函数而不是调用 JavaScript 的构造函数。调用一个 new person,但是是在 C++ 代码里面。

被索引的 Getters 和 Setters

你有 book[4]。如果你打算实现它,你需要 getters 和 setters。你有一个 UInt32,这意味着索引不能是负数(它是 unsigned)。在有些编程语言中(Python),你可以用负数索引访问一个数组,它会从后往前计算,而不是从前往后,从左边,到另外一边。但是你在 Node js 里面不允许这样做。而且,它是一个 32 位的整型,你的数组不可能那么大(仅仅四千万个元素)。我需要验证输入:如果我访问一个不存在的元素,我会得到一个 C++ 的异常( 不是个好主意,因为用户会不知道哪里出错了;它总会说 segmentation fault )。我需要验证我的输入,然后抛出异常。然后我设置好返回值,然后返回我的对象。Setters 也很类似。这有一个退出的参数你需要设置。你可以使用 JavaScript 里面的 delete;你可以说 delete,然后是对象。Enumerators 很容易;它们生成了所有允许的索引的列表。

访问器对于已知的属性是非常有用的。我尝试着为属性的句柄命名,但是 它们就不工作了 。 在 C++ 里面,你常只有少数的 getters 和 setters (仅仅是你的 C++ 类里才有);你只想封装它们。访问器很容易使用。你用访问器设置好它们:你在你的 Init 里面包含它,在那里你也会设置好你的类。

因为 JavaScript 里面的函数也是对象,它和你想的不一样。类型是函数对象。你需要设置一个调用,然后调用函数对象。这就是说,如果你有函数对象,你就可以调用它。如果匿名函数返回些东西,你也可以从返回值中得到它们。如果你想同时有异常和返回值,就比较麻烦了:你需要记住那个函数(即使是匿名函数),在 JavaScript 里面只有对象;你有一个 V8 类叫做 Function 的可以用来代表它们。

从 C++ 中抛出异常到你的 JavaScript 中是不可以的( 它们不理解 )。如果你想在这些 isolate 中抛出一个异常,有个函数叫 ThrowException。它设置了 JavaScript,Node.js 或者 V8 引擎的状态为 “exception”。当你从 C++ 返回到 V8 的时候,重新执行了 JavaScript,突然它变成了一个异常。它只设置了异常状态;然后返回到 V8,他就会变成一个 JavaScript 的异常。

如果你有一个匿名函数,或者一段代码你想给用户抛出一个 JavaScript 的异常,而且你想在 C++ 中捕捉它,处理它。如果你有一个 transactional manager 而且有一个 transaction 的话,这会非常有用:例如,你抛出一个异常,你想回滚。你需要 TryCatch 。在函数调用后,你可以问,这是一个 Exception thrown?: HasCaught() 。你可以重新抛出一个 JavaScript 的异常,然后再次抛出它,从 C++ 回到 V8。这就是你在 Node.js 引擎里做的事情。这对封装 C++ 类来说是足够的了。

如果你想桥接 Node.js 0.10 和 0.12+,这里有一个本地的 Node 的抽象,叫做 NAN,它尝试着创建些宏。你可以为两个版本都使用同样的源代码。

当你有 C++ 的类,而且你想封装它们并且作为扩展使用的时候,你不需要一个一个的封装它们( 这次演讲里面我是这么做的 )。而且,因为 JavaScript 不是强类型语言,你需要做输入检查。如果你期望的是串,使用前请手工检查它们。单元测试很重要。当然, C++ 和 JavaScript 都是面向对象语言,但是一个是和类一起,一个就是类:这对于从 JavaScript 转到 C++ 领域的开发者来说会难一些。跨语言函数调用调试也很困难。

请看 我的例子 . 这也有一个 Google documentation about V8 , JavaScript: The Good Parts ,或者其他现代 C++ 的课本。

See the discussion on Hacker News .

Get new videos & tutorials — we won’t email you for any other reason, ever.

原文  https://realm.io/cn/news/oredev-kenneth-geisshirt-extending-node-js-cpp/
正文到此结束
Loading...