Oredev geisshirt node cover cn

使用 C++ 扩展 Node.js

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


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

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

为什么使用 C++ 来扩展? (3:08)

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

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

C++ 类的例子 (4:48)

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

V8 的概念 (6:35)

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

Receive news and updates from Realm straight to your inbox

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

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

突破性的改变:0.10 → 0.12 (10:10)

在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+ (曾经有一个努力尝试解决这个问题,但是并不容易:看看这篇文章))。

构建扩展 (12:51)

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

封装一个类 (16:03)

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

给 V8 增加一个类 (17:39)

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

实例化一个对象 (19:45)

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

方法 (22:12)

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

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

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

对象的实例化 (25:35)

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

被索引的 Getters 和 Setters (27:54)

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

访问器 (30:48)

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

回调 (32:28)

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

抛出异常 (34:07)

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

捕捉异常 (36:10)

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

NAN (37:43)

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

建议 (38:26)

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

进一步学习 (41:10)

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

About the content

This talk was delivered live in November 2015 at Øredev. The video was transcribed by Realm and is published here with the permission of the conference organizers.

Kenneth Geisshirt

Kenneth holds a Ph.D. in chemistry (and a B.Sc. in computer science), and in the 1990s he primarily worked on simulating chemical reacting on supercomputers. After graduating, he has been working as a software developer focusing on open-source software. Currently, he is working for Realm where he is part of the Android team. In his spare time, he has been speaking at meetups, conferences, and user groups and writing articles and book on topics related to software development and open source software.

4 design patterns for a RESTless mobile integration »

close