node.js的C++入门
最近的任务是把计划库的API用JavaScript语言调用起来,需要用Node.js的C++扩展,本文简单归总一下node.js addons官方文档https://nodejs.org/api/addons.html
1. 基本知识介绍
在node.js中,除了用js写代码以外,还可以使用C++编写扩展,这有点类似DLL,动态链接进js代码中。使用上也相当方便,只需用require包含,这和一般的js模块并没有什么区别。C++扩展为js和C++代码的通信提供了一个接口。
要编写node.js的C++扩展,需要了解一些基本知识:
1. V8: Google出品的大名鼎鼎的V8引擎,它实际上是一个C++类库,用来和 JavaScript 交互,比如创建对象,调用函数等等。V8的API大部分都声明在v8.h头文件中。
2. libuv:一个C实现的事件循环库,node.js使用libuv来实现自己的事件循环、工作线程和所有的异步行为。它是一个跨平台的,高度抽象的lib,提供了简单易用的、POSIX-like的方式来让操作系统和系统任务进行交互。比如和文件系统、sockets、定时器和系统事件。libuv还提供了POSIX threads线程级别的抽象来增强标准事件循环中不具备的复杂异步能力。我们鼓励C++扩展的作者思考如何通过转换I/O或其他耗时操作到非阻塞系统操作来避免阻塞事件循环。
3. node.js内部lib,node.js本身提供了很多C/C++ API来给扩展使用,比如最重要的一个:node::ObjectWrap类。
4. node.js包含了很多静态链接库,比如OpenSSL。这些库都放在node.js代码树的deps/目录下。只有V8和OpenSSL标识符被有意地被node.js重复导出来被各种扩展使用。
下面快速地来看一个实例。
2. 第一个例子HelloWord
下面的例子是一个简单的C++扩展,其功能相当于js的如下代码:
module.exports.hello = () => 'world';
首先创建一个hello.cc:
// hello.cc
#include <node.h> namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value; void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
} void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
} NODE_MODULE(addon, init)
} // namespace demo
这个最简单的例子,已经出现了一些我们完全没有接触过的东西。大致解释一下:
1. 函数Method的参数类型是FunctionCallbackInfo<Value>&,FunctionCallbackInfo
2. Isolate,英文意思是“隔离”,在这里Isolate指的是一个独立的V8 runtime,可以理解为一个独立的V8执行环境,它包括了自己的堆管理器、GC等组件。后续的很多操作都要依赖于这个Isolate,后面我们会看到在很多操作中,都会使用Isolate的实例作为一个上下文传入。
(注:一个给定的Isolate在同一时间只能被一个线程访问,但如果有多个不同的Isolate,就可以给多个线程同时访问。不过,一个Isolate还不足以运行脚本,你还需要一个全局对象,一个执行上下文通过指定一个全局对象来定义一个完整的脚本执行环境。因此,可以有多个执行上下文存在于一个Isolate中,而且它们还可以简单安全地共享它们的全局对象。这是因为这个全局对象实际上属于Isolate,而却这个全局对象被Isolate的互斥锁保护着。)
3. 返回值需要用args.GetReturnValue().Set()来设置。
4. 向外导出方法需要在扩展的初始化函数中使用NODE_SET_METHOD(exports, Method_Name, Method);。如果有多个方法需要导出,就写多个NODE_SET_METHOD。
注意到node.js的C++扩展都必须按以下形式导出一个初始化函数(该函数名字可以随便设置一个):
void Initialize(Local<Object> exports);
NODE_MODULE(module_name, Initialize)
NODE_MODULE这行后面并没有分号(;),因为它并不是一个函数,你可以认为这是一个声明。module_name必须匹配最后生成的二进制文件的文件名(不包括.node后缀)。在hello.cc这个例子中,初始化函数是init,扩展模块名是addon。
构建(Building)
写好源代码后我们就要把它编译成二进制的addon.node文件了。binding.gyp文件用来描述我们模块的构建配置,这个文件的内容是JSON形式的:
{
"targets": [
{
"target_name": "addon",
"sources": [ "hello.cc" ]
}
]
}
实施构建操作需要用到node-gyp,如果尚未安装的话,需要运行(可能要用到sudo):
npm install -g node-gyp
来全局安装node-gyp。
编写完binding.gyp文件,我们使用:
node-gyp configure
来生成对应项目在当前平台的build目录。这将会在build目录下生成一个Makefile(Unix-like系统)或者一个vcxproj文件(Windows系统)还有一部分其他文件。接着,运行:
node-gyp build
来生成一个编译过的addon.node文件,这个文件会被放在build/Release/目录下。
build成功后,这个二进制的C++扩展就可以在node.js中使用require包含进来:
1 // hello.js
2 const addon = require('./build/Release/addon');
3 console.log(addon.hello()); // 'world'
由于扩展的二进制文件的存放位置会根据编译方式不同而变化(有可能放在build/Debug/目录),所以可以用这种方式来引入扩展:
1 try {
2 return require('./build/Release/addon.node');
3 } catch (err) {
4 return require('./build/Debug/addon.node');
5 }
链接node.js依赖
node.js使用一些静态链接库,比如V8、libuv和OpenSSL。所有扩展都必须链接V8,还有可能需要链接一些其他的库。典型情况下,使用#include <...>来include这些库(比如链接V8就是#include <v8.h>),node-gyp会自动找到这些库。然而,有几个注意事项需要说明:
1. node-gyp运行时,它会检测node.js的版本并且下载全部源码文件或者只是下载头文件。如果下载了全部源码文件,扩展就可以使用node.js的所有依赖,如果仅仅下载了头文件,则只有node.js导出的那些东西可以被使用。
2. node-gyp可以使用--nodedir选项来指定本地node.js映像,使用这个选项时,扩展可以使用全部的node.js依赖。
使用require加载C++扩展
经过编译的node.js C++扩展的后缀名是.node(类似.so和.dll),require()函数会查找这些.node文件像初始化动态链接库那样初始化它们。
当使用reqiure()时,.node后缀可以被省略。需要注意的是,node.js在使用reqiure()加载模块时,会优先加载js后缀的文件。比如说一个目录下有一个addon.js和一个addon.node,当使用require('addon')时,node.js会优先加载addon.js。
函数参数
C++扩展可以暴露函数和对象出来让node.js访问。当从js中调用C++扩展中的函数时,实参和返回值必须映射到C/C++事先声明好的代码中。以下代码展示了C++扩展代码如何读取从js传递过来的函数实参和如何返回值:
// addon.cc
#include < node.h > namespace demo {
using v8: :Exception;
using v8: :FunctionCallbackInfo;
using v8: :Isolate;
using v8: :Local;
using v8: :Number;
using v8: :Object;
using v8: :String;
using v8: :Value; // This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo < Value > &args) {
Isolate * isolate = args.GetIsolate(); // Check the number of arguments passed.
if (args.Length() < 2) {
// Throw an Error that is passed back to JavaScript
isolate - >ThrowException(Exception: :TypeError(String: :NewFromUtf8(isolate, "Wrong number of arguments")));
return;
} // Check the argument types
if (!args[0] - >IsNumber() || !args[1] - >IsNumber()) {
isolate - >ThrowException(Exception: :TypeError(String: :NewFromUtf8(isolate, "Wrong arguments")));
return;
} // Perform the operation
double value = args[0] - >NumberValue() + args[1] - >NumberValue();
Local < Number > num = Number: :New(isolate, value); // Set the return value (using the passed in
// FunctionCallbackInfo<Value>&)
args.GetReturnValue().Set(num);
} void Init(Local < Object > exports) {
NODE_SET_METHOD(exports, "add", Add);
} NODE_MODULE(addon, Init)
} // namespace demo
编译成功后,这个扩展可以被node.js使用require()包含并使用:
1 // test.js
2 const addon = require('./build/Release/addon');
3 console.log('This should be eight:', addon.add(3, 5));
回调函数
一种很常见的做法是从js传递回调函数给C++调用,下面这个示例展示了如何做:
// addon.cc
#include < node.h > namespace demo { using v8: :Function;
using v8: :FunctionCallbackInfo;
using v8: :Isolate;
using v8: :Local;
using v8: :Null;
using v8: :Object;
using v8: :String;
using v8: :Value; void RunCallback(const FunctionCallbackInfo < Value > &args) {
Isolate * isolate = args.GetIsolate();
Local < Function > cb = Local < Function > ::Cast(args[0]);
const unsigned argc = 1;
Local < Value > argv[argc] = {
String: :NewFromUtf8(isolate, "hello world")
};
cb - >Call(Null(isolate), argc, argv);
} void Init(Local < Object > exports, Local < Object > module) {
NODE_SET_METHOD(module, "exports", RunCallback);
} NODE_MODULE(addon, Init) } // namespace demo
解释:
1. 传递回调函数,其实和传递普通参数没什么大的区别,使用
Local<Function> cb = Local<Function>::Cast(args[0]);
可以获得这个回调函数。然后需要显式声明这个回调函数的参数个数和参数数组:
const unsigned argc = 1;
Local<Value> argv[argc] = { String::NewFromUtf8(isolate, "hello world") };
调用这个回调函数需要传入isolate、参数个数argc、参数数组argv:
cb->Call(Null(isolate), argc, argv);
2. Init函数和之前有点不同,上面这个扩展的Init()使用了两个参数的形式(之前都是单参数),其中第二个参数接受一个module对象:
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", RunCallback); // 相当于直接导出整个模块作为方法
}
这将允许扩展使用单个函数的形式代替之前往exports中添加函数作为属性的方式来完全地重写exports。因此可以直接用扩展的名字作为函数名来调用,这适用于此扩展只对外暴露一个方法的情况:
1 // test.js
2 const addon = require('./build/Release/addon');
3 addon((msg) => {
4 console.log(msg); // 'hello world'
5 });
node.js的C++入门的更多相关文章
- Node.js API快速入门
Node.js API 快速入门 一.事件EventEmitter const EventEmitter = require('events'); class MyEmitter extends Ev ...
- Node.js学习笔记(2) - Node.js安装及入门hello world
今天来简单的记录一下Node.js的安装配置以及简单的入门 一.Node.js的安装 1.windows下的安装 windows下的安装很简单,只需要去官网http://nodejs.org中,找到w ...
- Node.js+Express配置入门
Node.js是一个Javascript运行环境(runtime).实际上它是对Google V8引擎进行了封装.V8引 擎执行Javascript的速度非常快,性能非常好.Node.js对一些特殊用 ...
- Node.js web快速入门 -- KoaHub.js
介绍 KoaHub.js -- 基于 Koa.js 平台的 Node.js web 快速开发框架.可以直接在项目里使用 ES6/7(Generator Function, Class, Async & ...
- node.js,express入门看详细篇
先最简单的代码 安装 npm install express app.js 代码内容 const express = require('express') const app = express() ...
- Node.js安装和入门 - 2行代码让你能够启动一个Server
转自:http://josh-persistence.iteye.com/blog/1979552 备忘 Node.js是一个轻松构建快速,可扩展的网络应用平台建立在Chrome的JavaScrip ...
- node.js安装与入门使用
一个基于 Chrome V8 引擎的 JavaScript 运行环境. Node.js 的包管理器 npm,是全球最大的开源库生态系统. 提供事件驱动和非阻塞I/O API,可优化应用程序的吞吐量和规 ...
- Node.js 模块系统入门
在编程领域中,模块是自包含的功能单元,可以跨项目共享和重用.它们使开发人员的生活更加轻松,因为我们可以使用它来增加应用程序的功能,而不必亲自编写这些功能.它还让我们可以组织和解耦代码,从而使应用程序更 ...
- Node.js 之 express 入门 ejs include公共部分
1. 直接进入express安装 因为之前有一篇文章我已经讲过怎么安装node了 而网上的教程也是非常多.所有直接进入到express.教程简陋 由于我比较笨 所有只要写到我自己明白就行. 这里有个教 ...
随机推荐
- 树莓派安装raspbian并配置开发环境
1.烧录系统 首先准备好我们要烧录的raspbian系统,可以在树莓派官网中下载https://www.raspberrypi.org/downloads/ 这里我们选择 2018-11-13-ras ...
- 定位布局 Stack 层叠组件 Stack 与 Align Stack 与 Positioned 实现
一.Flutter Stack 组件 Stack 表示堆的意思,我们可以用 Stack 或者 Stack 结合 Align 或者 Stack 结合 Positiond 来实现页面的定位布局 align ...
- 谈谈一些有趣的CSS题目-- 单行居中,两行居左,超过两行省略
开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...
- 消息队列(五)--- RocketMQ-消息存储1
问题 : 部署时如何知道自己是 broker 还是 NameServer topic 订阅信息放在哪里 broker 的作用到底是什么 纪录是如何持久化的 群发的时候,是如何储存消息的 send 方法 ...
- IoT生态不完善、与智能电视区别不大,荣耀智慧屏概念大于实际
编辑 | 于斌 出品 | 于见(mpyujian) 前两天,华为荣耀略显"低调"地在北京召开了一场小型的媒体沟通会.在这场沟通会上,荣耀却颇为"重磅"地推出了坊 ...
- 架设传奇时打开DBC数据库出错或读取DBC失败解决方法
架设传奇时打开DBC数据库出错或读取DBC失败解决方法 DBC右键-属性-高级-管理员身份运行 即可
- Linux虚拟机(CentOS)安装gcc, g++
1. 确保自己的虚拟机联网 点击那个三角形可以选择连接网络 如果还是连不了网,参考https://www.cnblogs.com/xingbo/p/6100554.html 2.联网后,使用命令 ...
- HTML去除多余空白区域的方法
在head标记中添加html,body{height:100%;}即可. 实例如下 <head><style>html,body{height:90%;}</style& ...
- php中截取中文不乱吗
php截取中文的使用是随处可见的,譬如,博客首页显示简介,可能会用到,或一些相册简介会用到,以前不知道,还傻傻的自己去写函数用来做“智能截取”,效果还不十分好,幸运的是,今天因为一位同学做项目,让我一 ...
- Bugku-CTF社工篇之简单的个人信息收集