Node.js에서 C/C++ 애드온 N-API, 빠르게 시작하기

Node.js에 따르면,

Node.js 애드온은 C++에 의해 작성된 동적 링크 공유 객체로, require() 함수를 사용하여 Node.js로 불러올 수 있으며 일반 Node.js 모듈처럼 사용할 수 있습니다. 이 애드온은 C/C++ 라이브러리와 Node.js에서 작동하는 자바스크립트 간의 인터페이스를 제공합니다.

nodejs 애드온을 사용하는 이유는 크게 다음과 같습니다.
1.   JS 스스로 사용하기 어려운 네이티브 api들에 접근하고 싶을 때
2.   C/C++에서 작성된 서드 파티 라이브러리와 통합하고 싶고, 이를 Node.js에서 직접 사용하고 싶을 때
3.   성능 이슈로 인해 C++ 모듈로 재작성하고 싶을 때

이 글에서는 N-API에 대해 설명하고 C/C++ 기반의 NodeJS 애드온을 구축하기 위한 사용법에 대해 설명합니다.

전체 소스코드는 https://github.com/master-atul/blog-addons-example 에서 확인할 수 있습니다.

N-API는 무엇인가?


N-API는 네이티브 애드온을 빌드하기 위한 API입니다. N-API는 자바스크립트 런타임으로부터 독립적입니다 (예: V8). 그리고 Node.js의 일부로 유지됩니다. 이 API는 ABI로 작동되어 모든 Node.js 버전에 안정성을 제공합니다. 또한, 자바스크립트 엔진의 변화로부터 안정성을 제공하도록 설계되었으며, 재컴파일 없이 Node.js의 다른 버전에서도 정상적으로 작동할 수 있도록 합니다.

ABI란?
응용프로그램과 운영체제, 응용 프로그램과 라이브러리 사이에 필요한 저수준 인터페이스.
API와 유사하지만 API보다 ABI가 저수준으로, API는 소스코드에서 사용되고, ABI는 바이너리에서 호환이 됩니다.

본질적으로, N-API는 C 또는 C++를 사용하여 NodeJS 애드온을 빌드할 수 있도록 해주고 이를 통해 빌드한 애드온은 NodeJS의 다른 버전, 환경에서 비정상 작동을 하지 않습니다.

N-API는 Node v10의 안정적인 API입니다. N-API는 Node v8과 v9에서 실험중인 기능이었습니다.

우리는 node-addon-api패키지를 사용할 것입니다. (https://github.com/nodejs/node-addon-api) 이는 N-API를 위한 헤더만 존재하는 C++ 래퍼 클래스들을 포함하는 패키지입니다. (기본적으로, C++ 오브젝트 모델을 제공하고 낮은 오버헤드에서 예외 처리 구문을 제공합니다.)

코딩 해보기


1. 명령 프롬프트를 관리자 권한으로 열어줍니다.
2. yarn이 설치되지 않았다면 "npm install -g yarn"으로 yarn을 글로벌 모드로 설치합니다.
3. "yarn global add windows-build-tools"로 윈도우 빌드 도구를 설치합니다. (npm은 비정상 작동됨)
4. 프로젝트 용 폴더를 생성한 후에 터미널에서 해당 폴더로 이동한 후 npm init으로 프로젝트를 초기화 해줍니다.
5. package.json에 "gypfile": true 를 추가해줍니다.
6. binding.gyp 파일을 생성한 후에 다음과 같이 입력합니다.

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.cc" ]
    }
  ]
}

7. 터미널에 "npm install -g node-gyp"로 node-gyp 모듈을 글로벌 모드로 설치합니다.
8. 터미널에 "npm install --save bindings"로 바인딩 모듈을 설치합니다. (모듈을 쉽게 불러올 수 있음)
9. hello.cc 파일을 생성한 후에 다음과 같이 입력합니다.

#include <node_api.h>
#include <assert.h>

napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  assert(status == napi_ok);
  return world;
}

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

napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method); //INFO: 메소드 정의
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

10. hello.js 파일을 생성한 후에 다음과 같이 입력합니다.

var addon = require('bindings')('hello');

console.log(addon.hello()); // 'world'

11. 터미널에 "node-gyp configure"를 입력하여 빌드 환경을 구성합니다. build 폴더가 생성된 것을 확인할 수 있습니다.
12. package.json은 다음과 같은 내용을 담아야 합니다. (필수: private-> 미연에 퍼블리싱 방지, gypfile)

{
  "name": "example_n-api",
  "version": "1.0.0",
  "description": "",
  "main": "hello.js",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "gypfile": true,
  "license": "ISC",
  "dependencies": {
    "bindings": "^1.5.0"
  }
}

13. 터미널에 "node-gyp build"를 입력하면 build/Release/에 hello.node 파일이 생성된 것을 확인할 수 있습니다.
14. 터미널에서 "node ./hello.js"를 입력하면 hello.node 파일이 호출되어 world 라는 내용을 출력합니다.
15. 추가적으로, 실행 파일에서 .node 파일을 실행하는 것을 확인하기 위해 "npm install -g pkg"를 입력하여 pkg를 설치합니다.
16. 패키징을 하기 위해 package.json에 다음과 같은 내용을 추가합니다. (node는 10버전일 경우를 가정하였습니다.)

{
  scripts": {
    "pkg-win-x64": "pkg . --targets node10-win-x64 --output ./build/app.exe"
  },
  "bin": {
    "app": "./hello.js"
  },
}

17. 터미널에 "npm run pkg-win-x64"를 입력하여 패키징을 수행합니다. 결과적으로 build 폴더에 app.exe 파일이 생성된 것을 확인할 수 있습니다.
18. build/Release 폴더 내의 hello.node를 build 폴더로 복사한 후에 (복사 안해도 됨) 터미널에서 ./app.exe를 실행하면 world가 출력되는 것을 확인할 수 있습니다.

N-API에 대한 자세한 정보는 https://nodejs.org/api/n-api.html 를 참조하세요.


p.s. 현재 N-API의 C++ 래핑 버전인  Node-Addon-API도 테스트 해보았는데, 파이썬 인코딩 문제로 빌드가 되지 않고 있습니다.
예제: https://github.com/nodejs/node-addon-examples