NodeJS 扩展开发实战(一):桌面通知

准备了好久的NodeJS扩展开发最近终于慢慢走上正轨道,随着对V8 API 的逐渐熟悉,以及对NodeJS v0.11 前后两个版本的变动,通过NodeJS最新的源代码和V8的Sample,感觉上手起来还是很快的。同时对C++的感觉也越来越顺手,比如对template(解决了Handle的疑惑), class(能看懂nodejs源代码),reference(解决了const v8::FunctionCallbackInfo& info的疑惑) 等的了解。

本文记录一下开发一个node-desktop-notification扩展过程中的心得。采用了Gnome桌面的libnotify库, 用到的知识如下:

libnotify 和 libnotifymm

一开始参考archlinux wiki上的代码,看到C++的部分直接就拿过来用了,直接编译成功,运行报warnning,估计是没开桌面通知,死活没出来界面,中间以为不行直接放弃了,升级fedora20后试了一把居然OK了。

很顺利的就把代码嵌入到node addon中,但是编译始终报错

无奈之下尝试了一把C语言的版本,居然直接编译通过,运行成功!

nodejs调用libnotifi效果图

回到上面,libnotify是基于c的库,而libnotifymm是对libnotify的C++封装,怪不得linux之父linus选择C而抛弃C++,看来C++却是是一团糟糕得状态啊,能用C简单清晰地实现的话,再用C++感觉却是有点画蛇添足的感觉。

编译成node addon

wiki上给的编译命令是gcc -o hello_worldpkg-config --cflags --libs libnotifyhello_world.c。对应到Node Addon中来的重点就是需要把pkg-config'命令产生的内容显示地告知编译器。分别通过cflagslibraries字段。

查阅了chromiumnode-canvas中gyp文件的书写方式,添加cflag字段就可以了

    'conditions': [
        ['OS=="win"', {
            'libraries': [
                '-lnode.lib'
            ]
        }, {
            'cflags': [
                '<!@(pkg-config libnotify --cflags)' #for libnotify
            ],
            'libraries': [
                '<!@(pkg-config libnotify --libs)'
            ]
        }]
    ]

C++ 相关的知识

static 关键字的用途

V8 相关的知识

  • C++回调参数 const FunctionCallbackInfo& info 使用info[0]取的是V8::Value类型,如果传入的是一个对象参数,并且希望解析之的话,需要调用Value类的toObject方法,如Local<Object> Opts = info[0]->ToObject(), 否则编译会报错invalid conversion from ‘v8::Value*’ to ‘v8::Object*’ [-fpermissive]
  • String::Utf8Value str(info[0])方法将JS值转换成C++中的值: Converts an object to a UTF-8-encoded character array. (通过指针),需要使用以下函数转换成C++指针。参考从v8到C++的数据类型转换
  • 上面的方法好像不是标准的C string, 因为我的node-libnotify插件无法正确解析传入的icon字符串,但是print出来的却是正确的,唯一合理的解释就是在内部String::Utf8Value得到的不是标准的C字符串, 参考Efficient way to convery v8::String to c string, 最终用了这个解决方案成功运行!
// Extracts a C string from a V8 Utf8Value.
const char* ToCString(const v8::String::Utf8Value& value) {
  return *value ? *value : "<string conversion failed>";
}

String::Utf8Value str(optsIcon);

icon = ToCString(str);  // 方法一

void *p = static_cast<void*>(&str); // 方法二, 乱码...不知道为啥
icon = static_cast<const char*>(p);

/**
 * 最终的解决方案
 * https://groups.google.com/forum/#!topic/nodejs/aNeC6kyZcFI
 */
// convert a v8::String to a (char*) -- any call to this should later be free'd
#include <stdlib.h> //calloc
#include <cstring> //strncpy
static inline char *TO_CHAR(Handle<Value> val) {
    String::Utf8Value utf8(val->ToString());

    int len = utf8.length() + 1;
    char *str = (char *) calloc(sizeof(char), len);
    strncpy(str, *utf8, len);

    return str;
}
[2014-01-03]

C语言编程,打好C/C++的基础

掌握了下面的表达式,基本上就能看懂大部分指针和函数了~

char **argv
// argv: pointer to char
int (*daytab)[13]
// daytab: pointer to array[13] of int
int *daytab[13]
// daytab: array[13] of pointer to int
void *comp()
// comp: function returning pointer to void
void (*comp)()
// comp: pointer to function returning void
char (*(*x())[])()
// x: function returning pointer to array[] of pointer to function returning char
char (*(*x[3])())[5]
// x: array[3] of pointer to function returning pointer to array[5] of char

links

[2013-12-22]

学习Node addon开发

跟着node-addon-examples学习node addon开发~

基础

  • 可以直接创建并使用一个Value,比如Local<Value> foo = String::New("bar");然后引用foo,或者直接在需要调用的地方写Local<Value>::New(String::New("bar"));,两者的效果是一致的

开发中遇到的问题

  • 用node-gyp编译的时候报错; 'error: ‘FunctionCallbackInfo’ does not name a type', 切换系统的node版本为v 0.11之后即可
  • 编译出来的Node输出了源代码。估计是之前调试lib/module.js文件的时候console.log出来了加载过来的内容,但是反复排查后发现所有的console代码都已经去掉了,查看git历史记录的时候发现了被我移动到src目录下的(之前便于查看)node_natives.h文件,想到会不会是该文件的缓存导致的?删除之后重新编译了下果然OK了,再把其中的ASCII码解码成JS源代码,果然有缓存的console!
  • 实现官方demo Wrap C++ Object的时候,编译时候报error: expected class-name before ‘{’ token, 原来Node v0.11之后吧node::ObjectWrap类独立了出来,需要在自己的头文件中#include <node_object_wrap.h>才行。编译的时候,把.cc源码文件添加到gyp中的“sources”部分,不然在加载addon的时候,会报"undefined symbol"错误。target的值要和源码中NODE_MODULE(xxx, Init);的xxx保持一致。

hello world

/**
 * C++ 实现JS函数的两种情况
 */
// 用于V8内部, JS的C++实现
// Arguments继承自FunctionCallbackInfo类
// 参见http://bespin.cz/~ondras/html/classv8_1_1Arguments.html
Handle<Value> hello(const Arguments args){
    // 操作符[]取Local<Value>型的参数(JS中的参数)具有如下公共成员函数
    V8_INLINE (int Length() const)
    V8_INLINE (Local< Value > operator[](int i) const)
    V8_INLINE (Local< Function > Callee() const)
    V8_INLINE (Local< Object > This() const)
    V8_INLINE (Local< Object > Holder() const)
    V8_INLINE (bool IsConstructCall() const)
    V8_INLINE (Local< Value > Data() const)
    V8_INLINE (Isolate *GetIsolate() const)
    V8_INLINE (ReturnValue< T > GetReturnValue() const)
}
// Handle<Object> target
target->Set(String::NewSymbol("greet"), func_tmpl->GetFunction());

// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 上面的写法是Node v0.10之前的API, 下面的写法是Node v0.11之后的API
// node-gyp采用可执行的node版本来进行编译处理,之前Node是v0.10.22
// 因此无法通过编译,报错error: ‘FunctionCallbackInfo’ does not name a type
/**
 * v8::FunctionCallbackInfo 是类模板,用于处理各种类型
 * 参考:
 *  + template<class a_type> class a_class {} 
 *    http://www.cprogramming.com/tutorial/templates.html
 *  + http://www.learncpp.com/cpp-tutorial/143-template-classes/
 */
void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
 
}
// global = v8::ObjectTemplate::New();
global->Set(v8::String::New("version"), v8::FunctionTemplate::New(Version));

function arguments

Fabolous-Breathe

V8中的异常

通过ThrowException函数抛出异常的实例,异常实例通过异常对象Exception的五个成员函数生成,分别为RangeError, ReferenceError, SynctaxError, TypeError, Error,抛出异常的方法如下

ThrowException(Exception::TypeError(String::New("xxx")));
[2013-12-22]

学习使用GYP

GYP在NodeJS,V8,chromium等项目都有广泛的运用,学习GYP便于后续对NodeJS扩展的开发。

参考文档:

GYP的书写规范

GYP文件的内容其实是Python中的字典,支持单引号和双引号,最后一个元素末尾也允许添加。因此它不是严格的JSON。

GYP格式

GYP文件的顶层可以分成五个key:

  1. 'variables'
  2. 'includes',需要被引用的文件列表,带有.gypi后缀
  3. 'target_defaults',该文件内所有targets属性的默认设置。
  4. 'targets',该gyp文件构建输出的目标位置的列表,每个targets包含了描述生成目标所必须的的信息
  5. 'conditions',能修改该gyp定义的全局字典内容的条件定义列表。比如添加和平台相关的目标文件。

target部分

  1. target_name
  2. type
  3. msvs_guid
  4. dependencies,列出该target依赖的其他target,确保其他target比该target先构建
  5. defines,C预定义(相当于-D参数)
  6. include_dirs,头文件所在的位置(相当于-I)
  7. `conditions,条件代码块,用于制定不同平台下target的不同设置
[2013-12-16]

使用mocha进行CoffeeScript代码测试

mocha看上去相当不错的样子,想到开发大型系统。调用API的时候难免会出问题,这个时候要是有自动测试程序的话就会非常容易排查出问题。

注意事项:

  • mocha会自动查找当前目录下的test子目录,对起test子目录进行测试。
  • mocha默认不支持coffee,需要加--compilers coffee:coffee-script才能正常使用。

语法介绍

mocha支持BDDTDDexports三种流行的测试接口,默认采用BDD

exports 实际上就是BDD,只是写法上省略了describe, it关键字

参考:

BDD(Behavior Drive Development)

describe(intro, fn)

describe函数主要用于将要测试的内容包在一起,便于分组查看,在函数参数中可以嵌套使用describe

it(desc, fn)

it是mocha测试的核心,其内的函数如果抛出了任何异常,都会被it捕获,提示测试失败

before()

before语句写在it()语句之前,在it内部写before是无效的。

beforeEach对后续的每个it()语句生效

after()

mocha 中的after在完成测试之后自动执行,对于异步测试,也就是手动调用done()函数表明测试完成才执行。

beforeEach()

afterEach()

输出格式

设定--reporter(-R)参数来控制mocha的输出内容和样式

dot

默认样式,点列出测试文件,只显示总测试结果

spec

将测试文件中的描述语句也呈现出来

nyan

一个火车头~

tap

纯文字,是Test-Anything-Protocol的消费者

landing

以飞机降落的模型,直观展示测试失败位置

list

类似于spec,但是将层级关系展开,不如spec直观

progress

json

使用实例

使用Makefile来实现mocha测试

上文用的是Cakefile,但是相对来说,不如Makefile来得通用,看了下Makefile的语法,感觉清晰不少,express、connect等框架也是用的Makefile,于是尝试使用Makefile来实现自动化测试

Tips

  • 规则后面的指令前需使用Tab,不能使用空格
  • 如果规则名和目录下文件名重名(比如test),则会报make: 'test' is up to date.错误。解决办法是设置.PHONY参数,在其中指定重名的规则,如.PHONY: test。 参考Makefile中的PHONY
  • 在指令前面加@,不输出指令内容,便于查看整洁干净的测试的结果
[2013-12-05]


NodeJS学习笔记 -- cluster

cluster简介

cluster模块fork出来的进程之间并不会共享数据,实际上每个子work之间都是整个程序重新运算一遍。

参考以下文章:

共享端口原理

Then net.Server.listen checks to see if process.env.NODEWORKERID is set. If so, then the current process is a child created cluster. Instead of trying to start accepting connections on this port, a file handle is requested from the parent process: 调用listen方法的时候,如果检测到当前进程是cluster fork出来的进程,就不是监听这个端口,而是从父进程中请求一个处理句炳

[2013-12-04]

Shell编程技巧与陷阱

基础

读取输入

cat >  hello.sh<<END

读取用户输入,直到碰到END(自定义终止符号)

管道

  • tee 将输入输出到屏幕,并且写入到后面跟的文件中去(用于保存中间数据
  • 管道永远连接标准输出
  • 使用\来对长Shell语句进行换行

重定向

重定向是覆盖写

cmd > /dev/null 2>&1

所有屏幕上的标准输出内容重定向到销毁机中,2>&1,2号管道在1号管道输出(&等价)。

  • 标准输出指向/dev/null
  • 2号输出等价与1号输出,使得屏幕上的所有输出(标准输出,错误输出)都重定向到/dev/null
  • 如果不做上述操作时,后台执行的该程序会占用标准输出和标准输入,导致无法正常退出

完全停用输入和输出

cmd /dev/null 2>&1 &

cat < a.c > a.c

结果为空!而不是a.c的内容\n 原因: 重定向在底层即为,所有的shell是在底层把所有的文件准备好之后才开始执行,<以只读的方式打开a.c,还没有开始读>相当于以覆盖的方式fopena.c,此时内容为空,导致读到的内容为空!

cat << a.c >> a.c

死循环!\n 原因:追加写,先只读打开a.c,然后追加写打开a.c,当写入内容时,读管道又检测到待读内容,产生死循环

重定向之书写顺序

  • >file 2>&1 标准输出到file文件中,并让错误输出等价于标准输出:结果全输出到file中
  • 2>&1 > file 标准输出等价于错误输出, 都指向screen,然后标准输出指向file,错误输出依然指向screen: 结果只有标准输出到file中

cmd &> /dev/null

&表示后台执行cmd,

循环

    for i in `seq 1 3`; do
        echo $i
    done
    
    # c 风格的for循环,核心为2个括号
    LIMIT=10
    for ((a=1;
        a<=LIMIT;
        a++))
    do
        echo "$a"
    done

调式

bash -x 和 echo

进阶

awk 是所有shell命令中效率最高的

  • 反引号(```): 执行该符号内的命令,并将其输出作为自符串替换当前位置(推荐使用$()来代替)
  • [] 里面使用 eq 等参数比较大小, [[]]里面直接使用 == > < 来判断

变量常识

  • 作用域,默认全局,限定在函数内部,用local来声明
  • 继承性: 使用export导出变量,让子shell继承该变量
  • 特殊变量
    • 环境变量:$PATH;$PWD;$LINENO(当前语句行号)
    • 位置参数:$1, $2, $3 传入的第N个参数
    • 特殊参数:$#(位置参数的数量), $*, $@(所有位置参数的内容), $?(命令执行后返回的状态,0表示成功), $$(当前shell的PID), $!(后台执行的最后一个进程号), $0(当前执行的进程名)
  • 整数计算: $(( 内部所有的运算操作都能执行 ))
  • 浮点运算: 使用awk来实现,(格式化输出变量运算结果)

数组

  • 赋值:
  • 访问:$(arr[x])
  • 删除: unset
  • 长度: $(#arr)

进程替换 <() >()

diff 2 个服务器的文件

vimdiff <(ssh server1 cat conf)<(ssh server2 cat conf)

还原所有备份文件

for file in ls .; do cp $file "$file.bak" done

[2013-11-18]

NodeJS源码详解——process对象

简单学习了Google的V8之后,决定开始学习NodeJS源代码的一个系列。

process对象

process对象中的大部分属性都是使用c++实现的,在node.cc(2300-2514)中的void SetupProcessObject()函数中定义。process在C++中是一个JS对象(ObjectTemplate类型对象o_tmpl的实例,通过o_tmpl->NewInstance()生成),并作为参数传入到Node的初始化JS代码的数组格式——node_native("src/node.js")中去,可以在"build/src/node_natives.h"目录下查看(./configure && make -j 3)。

参考

  • src/node.cc:2546-2603void Load()
  • src/node_javascript.cc:39-41Handle<String> MainSource()

node_natives.h源代码示例

    namespace node {
        const char node_native[] = {47, 47, 32, 67, 112 ......}  // "src/node.js"
        const char console_native[] = {47, 47, 32, 67, 112 ......} 
        const char buffer_native[] = {47, 47, 32, 67, 112 ......}
        .....
    }
    struct _native {  const char* name;  const char* source;  size_t source_len;};
    static const struct _native natives[] = {
        { "node", node_native, sizeof(node_native)-1 },
        { "dgram", dgram_native, sizeof(dgram_native)-1 },
        { "console", console_native, sizeof(console_native)-1 },
        { "buffer", buffer_native, sizeof(buffer_native)-1 },
        ....
    }
    // using v8::String::NewFromOneByte to genarate String type for Handle

核心的process.binding

  1. 通过env->binding_cache_object()获取已加载模块的缓存列表,如果存在,就返回该模块的JS对象
  2. 否则,通过Set env->module_load_list_array()返回的对象的属性,添加待加载的模块名字到process.moduleLoadList对象中
  3. 通过get_builtin_module(*module_v)获得模块
  4. 通过mod->register_context_func(exports, unused, env->context());加载模块到exports对象中去,并使用cache->Set("xxx", exports);设定xxx模块的缓存对象exports。
  5. 通过args.GetReturnValue().Set(exports);,将exports对象(加载的模块)作为process.binding的结果返回

get_builtin_module 函数

返回的是一个node_module_struct类的实例,其数据结构和函数为

struct node_module_struct {
    int version;
    void *dso_handle;
    const char *filename;
    node::addon_register_func register_func;
    node::addon_context_register_func register_context_func;
    const char *modname;
};
node_module_struct* get_builtin_module(const char *name) {
    char buf[128];
    node_module_struct *cur = NULL;
    snprintf(buf, sizeof(buf), "node_%s", name);
    /* TODO: you could look these up in a hash, but there are only
     * a few, and once loaded they are cached. */
    for (int i = 0; node_module_list[i] != NULL; i++) {
        cur = node_module_list[i];
        if (strcmp(cur->modname, buf) == 0) {
            return cur;
        }
    }
    return NULL;
}
// node_buffer 所返回的register_context_func方法
void Initialize(Handle<Object> target,
            Handle<Value> unused,
            Handle<Context> context) {
    Environment* env = Environment::GetCurrent(context);
    HandleScope handle_scope(env->isolate());
    target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "setupBufferJS"),
                FunctionTemplate::New(SetupBufferJS)->GetFunction());
} 

该实例是从node_module_list[]数组中去读取对应的node模块(以node_XXX为key)。然后调用返回的实例的初始化方法。比如调用process.Bing("node_buffer")加载时,过程如下

  • 读取node_module_list中名为node_buffer的node_module_struct类的实例(node_buffer.cc)为cur
  • 调用cur的register_context_func函数,传入一个新的JS对象(exports),JS undefined值和context,其对应的函数源代码如下所示。在这个新JS对象中设定各种方法
  • Binging函数(C++)通过args.GetReturnValue().Set(exports)返回这个新建并设定好各种属性的JS对象--即process.binding所返回的对象.
[2013-10-24]

学习V8

本文记录学习V8过程中的一些资源

V8 源代码结构剖析

V8中实现了ECMA的全部,其中的eval为全局函数,因此是在v8中实现的(而不是node中实现)

src 目录

natives.js

主要实现了ECMA中的全局函数

[2013-10-21]

学习C++

本文用于记录学习C++过程中的一些资源,和心得

[2013-10-21]