Node原生插件N-API踩坑
发布于 17 小时前 作者 kanoyami 86 次浏览 来自 分享

前言

Node的原生模块一度是被人冷落的边角,其中一部分原因在于Node程序员有一部分来自于前端,可能没有系统的C++知识,有也可能仅限于学校里学过的基本的C语法和简单的C++概念,对于晦涩的作用域,内存回收,模板的认识非常有限。并且很多人对C++有一种恐惧,可能会觉得高深难懂。诚然,C++确实是一个不那么容易学会的东西,但是有句话叫因为难才好玩。

这个世界上没有人可以精通C++

没有人可以精通C++那么也就是说,我们都差不多,都是不精通的,你和大牛也没区别。这样想,再去接触它的时候,可能就没那么抵触了。

之前说过Node原生模块不受重视的原因还在于,Node自身把C++插件的机制设计的过于复杂,需要理解V8的使用,还要明白怎么使用Node.h的方法和类型。更难过的是,随着版本的更替,一个编译好的插件发布没多久,就会因为版本的不兼容不得不重新编译以适应版本。 在这个时候,一个名为NAM(Native Abstractions for Node.js)的东西跑了出来,目的是为了帮助原生插件的开发人员不必为了每一次的版本更替而重新使用新的API,去做一些本来没有必要的工作。 一直到8.0版本的发布,Node为开发人员带来了全新的N-API。 如果想了解更多有关于C++插件开发的发展史,我推荐死月老师的

从暴力到 NAN 再到 NAPI——Node.js 原生模块开发方式变迁

N-API是什么

官方对他的描述是:

N-API (pronounced N as in the letter, followed by API) is an API for building native Addons. It is independent from the underlying JavaScript runtime (ex V8) and is maintained as part of Node.js itself. This API will be Application Binary Interface (ABI) stable across versions of Node.js. It is intended to insulate Addons from changes in the underlying JavaScript engine and allow modules compiled for one version to run on later versions of Node.js without recompilation. Addons are built/packaged with the same approach/tools outlined in the section titled C++ Addons. The only difference is the set of APIs that are used by the native code. Instead of using the V8 or Native Abstractions for Node.js APIs, the functions available in the N-API are used. APIs exposed by N-API are generally used to create and manipulate JavaScript values. Concepts and operations generally map to ideas specified in the ECMA262 Language Specification.

大意如下: N -API(读做嗯-A屁唉)是一个构建本地插件的API。它并不依赖于JavaScript运行时(例如v8)而是作为Node.js自身的组建的一部分运行。这个API将会作为应用程序二进制接口(ABI 描述了应用程序和操作系统之间,一个应用和它的库之间,或者应用的组成部分之间的低接口。)稳定在Node.js当中。它的目的是为了可以让插件更改JS引擎底层实现并允许它们为Node编译一次而无需为每个新版本的Node重新编译。

这个插件的构建和打包使用和一般C++插件相同的方法和应用。惟一的不同是这组API使用原生代码。N-API使用本地函数,而不是使用v8或者本地抽象的Node.js API。

N-API暴露的API通常是用于创建和操纵JS的值。概念和操作的一些思想也通常是被列举在ECMA262语言规范之中。

可以看到官方为了统一原生插件的开发,并给原生插件更多的自由空间而使用了ABI的模式,也就是说,开发人员可以不考虑复杂的V8类型,不去想令人沮丧的作用域销毁保留值问题,就入如同开发C一般开发Node的原生插件。

N-API意义

笔者作为一个Node的学习人员,也不敢对这个API的诞生做什么评判。但是可以在这里写写我对它的意义的理解。 N-API首先是解决的版本更替的重复编译问题,这给与了开发人员更多的信心去为他们编写的插件增加新的功能,而不是关注兼容性,这无疑增加了社区为Node开发原生插件的信心。 N-API作为一个独立不依赖JS运行时的接口,使用它开发的插件不会受到V8的限制,包括强制GC和强制使用V8类型。这样就给予了开发人员更多的可能性,我预测接下来我们甚至会看到一些科学计算的库被移植到Node中来,甚至支持通用计算异构计算也会成为可能。 简化了开发成本,由于N-API作为ABI存在,用户只需要像操作一般的C接口一样去操作它们,极大的降低了堆开发人员C++素养的要求。预计会有更多人加入到开发更高性能的原生插件中来。

让我们先用用

在这里强调一下,因为N-API现在还是一个实验性的接口,在8.6.0版本中默认开启,而其他版本中需要在执行Node之前设置参数开启支持。并且因为是实验性特这,版本的变化非差快,目前你能查阅的一切示例可能都没法跑起来。

如果你想看每个版本的完整实例,请移步Node8.0.0以上的版本源码

addons-napi@Node

里面有所有示例API的使用方法,在我8.7.0版本上编译通过并且可以执行。 我们可以看到使用NAPI的方法和,构建C++插件是一样的。 所以我们先要生成一个项目目录

n-api_test/
|-------- /test.js
|-------- /addon.c
|---------/binding.gyp
|---------/common.h

这个common.h是一个包括了插件使用的预定义宏的文件,可以在源码中获得 然后开始编写binding.gyp

//binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.c" ]
    }
  ]
}

然后开始写我们的第一个插件addon

//addon.c

#include <node_api.h>
#include <string.h>
#include "./common.h"

napi_value Method(napi_env env, napi_callback_info info) {
  napi_value world;
  const char* str = "world";
  size_t str_len = strlen(str);
  NAPI_CALL(env, napi_create_string_utf8(env, str, str_len, &world));
  return world;
}

napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor desc = DECLARE_NAPI_PROPERTY("hello", Method);
  NAPI_CALL(env, napi_define_properties(env, exports, 1, &desc));
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
//common.h
#define NAPI_RETVAL_NOTHING  // Intentionally blank #define

#define GET_AND_THROW_LAST_ERROR(env)                                    \
  do {                                                                   \
    const napi_extended_error_info *error_info;                          \
    napi_get_last_error_info((env), &error_info);                        \
    bool is_pending;                                                     \
    napi_is_exception_pending((env), &is_pending);                       \
    /* If an exception is already pending, don't rethrow it */           \
    if (!is_pending) {                                                   \
      const char* error_message = error_info->error_message != NULL ?    \
        error_info->error_message :                                      \
        "empty error message";                                           \
      napi_throw_error((env), NULL, error_message);                      \
    }                                                                    \
  } while (0)

#define NAPI_ASSERT_BASE(env, assertion, message, ret_val)               \
  do {                                                                   \
    if (!(assertion)) {                                                  \
      napi_throw_error(                                                  \
          (env),                                                         \
        NULL,                                                            \
          "assertion (" #assertion ") failed: " message);                \
      return ret_val;                                                    \
    }                                                                    \
  } while (0)

// Returns NULL on failed assertion.
// This is meant to be used inside napi_callback methods.
#define NAPI_ASSERT(env, assertion, message)                             \
  NAPI_ASSERT_BASE(env, assertion, message, NULL)

// Returns empty on failed assertion.
// This is meant to be used inside functions with void return type.
#define NAPI_ASSERT_RETURN_VOID(env, assertion, message)                 \
  NAPI_ASSERT_BASE(env, assertion, message, NAPI_RETVAL_NOTHING)

#define NAPI_CALL_BASE(env, the_call, ret_val)                           \
  do {                                                                   \
    if ((the_call) != napi_ok) {                                         \
      GET_AND_THROW_LAST_ERROR((env));                                   \
      return ret_val;                                                    \
    }                                                                    \
  } while (0)

// Returns NULL if the_call doesn't return napi_ok.
#define NAPI_CALL(env, the_call)                                         \
  NAPI_CALL_BASE(env, the_call, NULL)

// Returns empty if the_call doesn't return napi_ok.
#define NAPI_CALL_RETURN_VOID(env, the_call)                             \
  NAPI_CALL_BASE(env, the_call, NAPI_RETVAL_NOTHING)

#define DECLARE_NAPI_PROPERTY(name, func)                                \
  { (name), 0, (func), 0, 0, 0, napi_default, 0 }

#define DECLARE_NAPI_GETTER(name, func)                                  \
  { (name), 0, 0, (func), 0, 0, napi_default, 0 }

$ node-gyp configure &&node-gyp build 配置并编译文件。 然后编写测试的js文件


//test.js
var assert = require('assert');
var addon = require(`./build/Release/addon.node`);
console.log(addon.hello());

$node test.js 获得正确的输出

world
(node:27082) Warning: N-API is an experimental feature and could change at any time.

总结

在最新版的NAPI设计中,甚至直接使用C作为使用语言,极大的降低了开发难度, 之前我文章中提到的利用NAPI的特性使用超过2G的连续内存空间,也是可能的,会在接下来继续完成。

回到顶部