Node.JS Foundation
구성요소
Native Extension
에 대해 설명하기에 앞서, 먼저 Node.JS
가 어떻게 구성되어 있는지에 대해 알아야 합니다.
JavaScript
Node.JS
에서 실행가능한 프로그래밍 언어입니다.
V8
자바스크립트 언어로 작성된 코드를 실행해주는 엔진입니다. 자바스크립트의 객체를 생성하거나, 함수를 호출하는 방법과 같은 매커니즘을 정의합니다.
Libuv
비동기 실행과 라이프 사이클을 제공하는 C 라이브러리입니다. Node.JS
의 모든 비동기 동작은 LibUv
가 관리합니다.
기타 라이브러리
Node.JS
는 V8
이나 LibUv
를 포함한 수많은 라이브러리의 집합체이며, 이러한 라이브러리들이 정적으로 링크되어 Node.exe
를 이룹니다. Node.JS
가 어떤 라이브러리를 사용하는지 궁금하다면 Node.JS
레포지터리에 있는 deps
폴더를 살펴보세요.
Node.JS 핵심 구현
정적 링크 라이브러리들을 사용하여 Node.JS
를 구현합니다. fs
, os
, buffer
, assert
같은 빌트인 모듈도 포함됩니다.
익스텐션이란?
동작방식
Node.JS
는 결국 V8
엔진에 의하여 동작하기 때문에, Node.JS
가 의존하는 V8
엔진의 헤더와 소스코드를 그대로 사용하여 코드를 작성하면, Node.JS
에서 사용가능한 C/C++
코드가 완성됩니다.
이렇게 완성된 C/C++
코드를 포함시켜 Node.JS
를 다시 컴파일해도 상관이 없지만, 일반적으로는 동적 링크 라이브러리
Dynamically Linked Library, DLL의 형태로 컴파일한 뒤 require()
함수를 사용하여 동적으로 불러오는 방식을 취하며, 이렇게 만들어진 DLL
을 Native Extension
이라고 합니다.
구체적으로 Native Extension
이 작동하는 방식은 다음과 같습니다.
fs
모듈의 핵심 구현은C
언어로 구현된Native Extension
에 담겨있다.Native Extension
를 메모리에바인딩
하여fs
모듈을 사용할 준비를 마친다.fs
모듈이require
되고,Native Extension
의 메소드를 호출한다.- 호출된 메소드는
LibUv
에 의해 동기 또는 비동기로 수행된다. Native Extension
은V8
의 메소드를 사용하면서Node.JS
와 통신한다.
Native Extension
을 작성하기 위한 환경을 세팅하는 것은 그렇게 어려운 일이 아닙니다. Node.JS
레포지터리에 있는 deps
폴더를 살펴보면 V8
이라는 폴더가 무게감있게 자리잡고 있고, 이 안에 있는 include
폴더를 외부 라이브러리 디렉토리로 지정하면 V8
의 모든 헤더파일을 사용할 수 있습니다.
간단하죠? 이것만으로 Native Extension
를 만들 준비가 끝난 것 입니다. 대형 프로젝트이기 때문에 문서화도 잘 되어 있습니다.
//
// Native Extension ver.
#include <v8.h>
...
문제점
하지만 Native Extension
으로 개발된 라이브러리는 특정 V8
버전에 종속된다는 심각한 단점을 함께 가지고 있습니다. Node.JS
이 버전업되면서 사용하는 V8
엔진도 함께 업데이트되는 경우, V8
의 내부코드가 달라질 가능성이 큽니다.
즉, 새로운 V8
버전에서는 기존의 라이브러리가 동작하지 않게 될 가능성이 매우 높다는 것입니다.
위의 문제를 해결하기 위해 NAN
이 등장하였는데, 이것은 바로 다음 장에서 설명합니다.
NAN
문제해결?
위와같은 문제를 해결하기 위해 추상화된 Native Extension
Native Abstractions for Node가 만들어졌습니다. 인터페이스
에 가까운 형식으로 추상화를 지원하는데, 절대 바뀌지 않을 인터페이스를 정한 뒤에, 각 V8
버전에 맞춘 NAN
가 만들어지는 식입니다.
간단하게 말하자면, 각 V8
엔진의 버전마다 1:1로 대응되는 NAN
이 존재하고 있으며, 이 NAN
들은 모두 동일한 인터페이스를 유지하고 있으니, 나중에 V8
의 버전이 바뀌더라도 그에 대응되는 NAN
로 갈아끼우면 된다는 것입니다.
특히 NAN
은 Node.JS
의 초창기부터 함께해온 생태계이기 때문에, NAN
으로 작성된 코드는 모든 Node.JS
버전에서 사용할 수 있다는 것을 보장합니다. 따라서, 모든(이전) 버전에서 동작가능한 Native Extension 코드
을 만들기 위해서는 반드시 NAN
를 사용하여 작성되어야 합니다.
//
// NAN ver.
#include <nan.h>
...
아직 문제는 남아있다
이렇게만 설명하면 NAN
은 마법같은 무언가라고 생각되지만, 심각한 디메리트
도 있다는 것을 알 수 있습니다. V8
의 버전이 바뀔 때 마다, 그 V8
에 맞게끔 NAN
을 바꿔끼우고 다시 컴파일
해야 합니다. 말 그대로 코드만 호환되는 것입니다.
이것은 버전 호환성이 깨진다는 것을 의미하기 때문에, 개발자가 설정한 V8
버전을 사용하지 않는 사용자는 반드시 에러가 발생합니다.
N-API
문제종결
다시 위와같은 문제를 해결하기 위해 N-API
Next Generation Node API가 만들어졌습니다. 2016년에 논의되었고, 2017년에 실험적으로 운용되었으며, 2018년에 이르러서야 정식으로 출시되었습니다. Node.JS
의 초창기부터 함께해온 NAN
와 비교하면 엄청난 후발주자가 아닐 수 없습니다.
하지만 NAN
와 달리 N-API
는 Node.JS
자체에 포함되어 있으며, 바이너리 수준에서 호환성을 유지시켜주는 ABI
Application Binary Interface라는 것이 엄청난 장점입니다. 한번 N-API
를 사용하여 컴파일된 DLL
파일은 바이너리 호환성
Binary Compatibility를 지니기때문에, 몇 년이 지나도 최신버전에서 안정적으로 동작할 수 있음을 보장하기 때문입니다.
또한 NAN
보다 더 직관적인 인터페이스를 채택하고, Node-Addon-API
라고 불리는 C++
프로그래머를 위한 래핑 버전까지 제공합니다. 물론 NAN
도 C++
래핑을 지원하지만 Node-Addon-API
가 훨씬 더 사용하기 쉽습니다. 반드시 이전 버전도 지원해야하는 경우가 아니라면 N-API
를 사용하여 개발하는 것이 정신건강에 이롭습니다.
//
// N-API ver.
// Node Addon API ver.
#include <napi.h>
...
동작방식
한번 작성된 DLL
파일이 앞으로 나올 최신버전에서도 모두 사용될 수 있다니 N-API
는 마법의 산물일까요? 프로그래머는 마법같은 것을 믿지 않기 때문에, 그 원리를 파헤쳐야합니다. N-API
가 이런 마법같은 일을 벌일 수 있는 기본원리는 매우 간단합니다. 바로 V8의 밑바닥
를 추상화하는 것이죠.
V8
코드도 N-API
를 사용하고, 우리가 작성할 C/C++
코드도 N-API
를 사용하기 때문에, 새로운 V8
이 나오더라도 그 엔진이 N-API
를 사용하도록 노드팀이 재설계해준다면, 우리는 아무것도 할 필요가 없어지는 것이죠. 이것이 V8의 밑바닥을 추상화
Abstraction of the underlying JavaScript engine하는 것의 의미이고, N-API
의 기본 원리이며, Node.JS
에 빌트인된 이유입니다.
NAN와 비교하기
위에서 나온 N-API와 NAN의 특징을 짤막하게 정리하겠습니다.
공통점
- 기본적인 역할은 애드온 작성에 도움을 주는 추상화된 헬퍼 함수를 제공합니다.
- 추상화된 헬퍼 함수를 호출할 때 마다,
Node.JS
의 메모리와 상호작용합니다. - 따라서 새로운 노드 버전이 나오더라도 코드를 재작성하지 않게 도와줍니다.
- 둘 다
C++
로 래핑된 버전을 지원합니다.
차이점
NAN
은V8
를 추상화하고,N-API
는V8의 더 아래
를 추상화합니다.NAN
은모든 버전
에서 사용할 수 있고,N-API
는 미래의 모든최신 버전
에서 사용할 수 있습니다.NAN
은 사용법이 어렵지만,N-API
는 사용하기 쉽습니다.N-API
는한 번 컴파일
하면,여러 노드버전
에서 실행할 수 있습니다.
Getting Start
여기서는 N-API
의 C++
래핑인 Node-Addon-API
를 사용하여 Addon
을 만드는 과정을 살펴보겠습니다.
훑어보기
binding.gyp
컴파일할 소스 파일들의 경로와 컴파일 옵션을 지정하며 JSON
포맷을 따릅니다.
프로젝트의 루트에 위치해야 합니다.
node-gyp
루트폴더에 있는 binding.gyp
파일을 읽고, 타겟팅된 소스 파일들을 컴파일하는 프로그램입니다. Release(default)
또는 debug
모드로 컴파일할 수 있습니다. 디버그 모드로 컴파일하면 성능은 많이 떨어지지만 브레이크 포인트를 잡을 수 있습니다. 컴파일 작업이 성공적으로 끝나면 루트에 build
폴더가 만들어지는데, release
또는 debug
폴더 안에 있는 확장명이 .node
인 파일을 찾으면 됩니다. 이것이 DLL
파일입니다.
준비물 챙기기
먼저 아래의 프로그램들을 설치해야 합니다.
빌드 도구 :
- python 2.7 이상
- gcc / g++ / make (리눅스 사용자만 해당)
- windows-build-tools (윈도우 사용자만 해당)
npm install -g windows-build-tools
노드 패키지 (글로벌 또는 개발 의존으로 설치해야 합니다) :
여기서는 글로벌로 설치하겠습니다.
- node-gyp
npm install -g node-gyp
노드 패키지 (프로젝트 안에 로컬로 설치해야 합니다) :
- bindings
- node-addon-api
개발 시작하기
최종적으로는 아래와 같은 형태가 되어야 합니다.
root
├─ binding.gyp
├─ hello_world.cpp
├─ index.js
├─ node_modules
├─ package-lock.json
└─ package.json
- 프로젝트 폴더를 생성하고
npm
을 초기화합니다.
npm init -y
npm install bindings
npm install node-addon-api
- 다음과 같이 루트폴더에
binding.gyp
을 작성합니다.
# binding.gyp
{
"targets": [
{
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"include_dirs" : [
"<!@(node -p \"require('node-addon-api').include\")"
],
"target_name": "./hello_world",
# 여기서 타겟 소스파일을 지정합니다.
"sources": [ "hello_world.cpp" ],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}
]
}
C++
코드를 작성합니다.
//
// NAPI 헤더입니다.
// npm을 통해 node-addon-api를 로컬로 설치하면 ./node_modules 폴더에 같이 딸려옵니다.
// 이것을 설치하지 않고, 먼저 작업하면 include에 빨간줄이 그어집니다.
#include <napi.h>
//
// 자바스크립트의 String 객체를 반환하는 함수입니다.
// 파라미터는 info[n] 형태로 얻어올 수 있습니다.
Napi::String SayHi(const Napi::CallbackInfo& info) {
//
// info에는 현재 스코프 정보(env)도 들어있습니다.
// 자바스크립트 객체를 생성하려면 반드시 이것부터 가져와야합니다.
Napi::Env env = info.Env();
//
// 현재 스코프 정보(env)와 std::string을 사용하여,
// "Hi!"라는 자바스크립트 문자열을 반환합니다.
return Napi::String::New(env, "Hi!");
}
//
// 애드온 이니셜라이져입니다.
// 자바스크립트 오브젝트(exports)에 함수를 하나씩 집어넣고,
// 다 집어넣었으면 리턴문으로 반환하면 됩니다.
Napi::Object init(Napi::Env env, Napi::Object exports) {
//
// 위의 함수를 "sayHi"라는 이름으로 집어넣습니다.
exports.Set(Napi::String::New(env, "sayHi"), Napi::Function::New(env, SayHi));
//
// 다 집어넣었다면 반환합니다.
return exports;
};
//
// 애드온의 별명과, 이니셜라이져를 인자로 받습니다.
NODE_API_MODULE(hello_world, init);
node-gyp
를 사용하여 빌드합니다.
node-gyp rebuild
- 다음과 같이
index.js
를 작성합니다.
//
// hello_world.node 애드온을 찾아 가져옵니다.
const addon = require("bindings")("hello_world");
//
// 애드온 함수를 실행합니다.
// "Hi!" 라는 문자열을 가져오게 됩니다.
const str = addon.sayHi();
//
// "Hi!" 문자열이 콘솔에 출력됩니다.
console.log(str);
(옵션) CMake로 빌드하기
이제는 굳이 node-gyp
를 사용하지 않더라도, cmake
를 통해 NAPI
를 빌드할 수 있습니다.
- 먼저 아래의 도구들을 설치해주세요.
- 운영체제에 맞는 cmake, make
- npm module : cmake-js
npm install -g cmake-js
- 루트 폴더에
CMakeLists.txt
를 작성합니다.
cmake_minimum_required(VERSION 2.8.12.2)
set (CMAKE_CXX_STANDARD 11)
# 프로젝트의 이름입니다.
# 이 예제에서는 my_addon.node라는 이름으로 DLL이 생성됩니다.
project (my_addon)
include_directories(${CMAKE_JS_INC})
# 타겟 소스파일을 지정합니다.
file(GLOB SOURCE_FILES "hello.cpp")
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
# N-API 래퍼를 불러옵니다.
execute_process(COMMAND node -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
string(REPLACE "\n" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
string(REPLACE "\"" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})
# N-API 버전을 설정합니다.
# 이후버전은 이전버전도 지원하니 최신버전으로 설정해주세요.
# 2020년 3월 기준으로 5가 최신입니다.
#
# (2020-03-13 수정)
# 이전버전은 이후버전에서 안정적으로 동작하므로,
# 안정성을 원한다면 낮은 버전이 더 좋은 선택일 수 있습니다.
# 단 1, 2는 초창기에 사용된 실험용 버전이므로 사용하면 안됩니다.
add_definitions(-DNAPI_VERSION=3)
cmake-js
로 컴파일합니다.
cmake-js compile
(옵션) 크로스플랫폼 지원
기본적으로 Node.JS
는 크로스플랫폼이지만, C++
은 그렇지 않습니다. 간단한 sleep
함수조차도 윈도우냐 리눅스냐에 따라서 헤더파일과 시그너쳐가 다르기 때문이죠. 만약 N-API
로 작성된 라이브러리를 npm
에 배포하고 싶다면 어떤 운영체제를 지원할 것인지에 대해 충분히 고민해야 합니다.
크로스플랫폼을 지원하기로 결정했다면 각 환경에 맞는 소스코드를 전부 만든다음, 사용자의 운영체제마다 선택적으로 소스코드를 컴파일하도록 해야합니다. 이런 기능은 cmake나 node-pre-gyp에 포함되어 있습니다.
생각보다 어렵지않으니 크로스플랫폼에도 한번 도전해보세요 : )
(2020-03-15 추가)
크로스플랫폼 관련 포스팅을 추가했습니다.
참고자료
기본원리 참고
https://www.slideshare.net/nasottola/next-generation-napi
https://nodejs.org/api/addons.html
https://itnext.io/a-simple-guide-to-load-c-c-code-into-node-js-javascript-applications-3fcccf54fd32
https://www.nan-labs.com/blog/native-extensions-for-nodejs
https://stackoverflow.com/questions/54740947/node-js-addons-nan-vs-n-api
빌드과정 참고
https://lofty87.github.io/nodejs/20180914/nodejs-native-addon-module
https://github.com/nodejs/node-addon-api/blob/master/doc/cmake-js.md
https://github.com/nodejs/node-addon-examples/tree/master/build_with_cmake
'# Lang > NodeJS C++ Addon' 카테고리의 다른 글
[벤치마크] C++ 애드온을 도입하면 성능이 얼마나 향상될까? (2) | 2020.03.16 |
---|---|
[튜토리얼] C++ 크로스플랫폼 애드온 제작하기 (0) | 2020.03.14 |