05-06 16:49
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[WebApp / Node.js] How to setup Visual Studio to write Node.js C++ Addons 본문

Programming/Web App

[WebApp / Node.js] How to setup Visual Studio to write Node.js C++ Addons

cinema4dr12 2016. 7. 2. 10:54



이번 글은 Node.js의 C/C++ Addon을 작성하는 방법에 대하여 알아 보기로 하겠다.


위에 첨부한 동영상은 참고한 YouTube Tutorial이며,

Node.js의 공식 도큐먼트인 https://nodejs.org/api/addons.html#addons_wrapping_c_objects

관련 stack overflow 사이트, http://stackoverflow.com/questions/21611867/how-to-build-a-native-node-js-extension-using-node-gyp-on-win-7-x64-vs2012 이다.



개발 환경은 다음과 같다:


Node.js C/C++ Addon을 통해 C/C++을 통해 작성한 DLLNode.js의 JavaScript 간의 인터페이스를 제작할 수 있다.


Addon의 주요 구성은 V8libuv이며, 이에 대한 자세한 내용은 Node.js의 Addon 공식 도큐먼트를 참고하기 바란다.

Node.js 소스코드 다운로드

Addon을 작성하는 첫번째 단계는 Node.js의 소스코드를 다운로드하는 것이다. 소스코드는 Node.js의 웹사이트인 https://nodejs.org/를 방문하여 DOWNLOADS 메뉴에서 다운받는다. 참고로 이 글을 작성할 때 Node.js의 최신버전은 v6.2.2였다.




다운받은 소스코드를 압축해제 한 후, 적당한 경로에 복사 또는 이동한다.

환경변수 설정

소스코드의 경로를 환경변수로 설정한다. Command Line Terminal을 열고, 다음과 같이 명령어를 입력하도록 한다.


> setx NODE_HOME "{YOUR_NODE_PROJECT_PATH}\node-v6.2.2"


환경변수가 제대로 설정되었는지 확인하려면,


"Windows 시작 > 컴퓨터 > 속성(R)"을 클릭하여 시스템 정보 창을 열고, "고급 시스템 설정"을 클릭한다.






시스템 속성 창이 열리면 [고급] 탭을 선택하고 "환경 변수(N)" 버튼을 클릭한다. 환경 변수의 사용자 변수에서 NODE_HOME 값이 올바른 경로가 설정되어 있는지 확인한다.


소스코드 빌드

Command Line Terminal에서 NODE_HOME 경로로 이동하고 vcbuild.bat을 실행하여 Visual Studio Solution을 생성한다.


가령, release x64로 빌드할 경우 다음과 같이 명령을 입력한다.


> cd %NODE_HOME%

> vcbuild.bat nosign release x64

nosign 옵션은 signing을 skip하는 옵션이다. 마찬가지로 debug x64로 빌드할 경우는 다음과 같이 명령을 입력하면 된다.


> vcbuild.bat nosign debug x64


빌드 과정은 시간이 좀 소요되므로 인내심을 가지고 기다리도록 한다.




%NODE_HOME% 경로에 node.sln 파일과 .vcproj 파일들이 생성되는 것을 확인하도록 한다. 특히 %NODE_HOME%/Release 폴더 내에 node.exe 실행파일이 생성되었음을 확인한다.


Visual C++ 프로젝트 생성

이제 C++ Addon을 작성할 수 있는 모든 준비가 끝났다. Visual Studio 2015 (반드시 2015일 필요는 없다)를 실행한다.



Addon Example의 코드는 Node.js v6.2.2 Documentation의 Wrapping C++ objects를 참고하였다.


Visual Studio > 파일 > 새로 만들기 > 프로젝트를 클릭하여 아래와 같이 새 프로젝트 다이얼로그 열리면,


템플릿 > Visual C++ > 빈 프로젝트를 선택하고 "위치"와 "이름"을 입력한다. 참고로 프로젝트 이름을 "MyAddon"으로 정하였다.


프로젝트 환경 설정

node.js 소스의 헤더와 라이브러리 경로를 설정하는 과정이다.


솔루션 탐색기에서 'MyAddon' 프로젝트를 마우스 우클릭하고 속성을 클릭하여 속성 페이지 다이얼로그를 연다.


"구성속성 > 일반"에서 "대상 확장명"을 ".node"로, "프로젝트 기본값 > 구성형식"을 "동적 라이브러리(.dll)"로 설정한다.





"구성"을 "모든 구성으로", "플랫폼"을 "모든 플랫폼"으로 설정한다.




"구성 속성 > VC++ 디렉터리"의 "포함 디렉터리"에 다음 세 개의 경로를 등록한다.


$(NODE_HOME)\deps\v8\include

$(NODE_HOME)\deps\uv\include

$(NODE_HOME)\src


$(NODE_HOME)\deps\v8\include 경로에는 V8 JS Engine의 해당 헤더 파일이 존재하며,


$(NODE_HOME)\deps\uv\include 경로에는 Node.js Event Loop과 관련있는 헤더 파일이 존재하며,


$(NODE_HOME)\src 경로에는 node.h 등으 Node.js  헤더파일들이 존재한다.





"구성 속성 > VC++ 디렉터리"의 "라이브러리 디렉터리"에 다음 경로를 등록한다.


$(NODE_HOME)\$(Configuration)


$(Configuration)Release|Debug의 구성속성을 의미하는 매크로이다.





"구성 속성 > 링커 > 입력"의 "추가 종속성"에 "node.lib"을 입력한다.


코드 작성

이제 include 폴더와 src 폴더를 생성하는데, 우선 솔루션 탐색기에서 "모든 파일 표시"를 클릭한다.




솔루션 탐색기에서 프로젝트를 우측 마우스 클릭하고 "추가 > 새 항목"을 클릭한다.




프로젝트 디렉터리에 include 폴더와 src 폴더를 생성하고, "프로젝트 디렉터리 > include"에 "myobject.h" 파일을 생성한다.




이번에는 "프로젝트 디렉터리 > src"에 "myobject.cc" 파일을 생성한다.




헤더(myobject.h)와 소스(myobject.cc)를 추가한 후에 솔루션 탐색기는 다음과 같이 표시된다.





"myobject.h"를 다음과 같이 작성한다:


myobject.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
 
#include <node.h>
#include <node_object_wrap.h>
 
namespace demo {
 
    class MyObject : public node::ObjectWrap {
    public:
        static void Init(v8::Local<v8::Object> exports);
 
    private:
        explicit MyObject(double value = 0);
        ~MyObject();
 
        static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
        static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
        static v8::Persistent<v8::Function> constructor;
        double value_;
    };
 
}  // namespace demo
 
#endif
cs


"myobject.cc"를 다음과 같이 작성한다.


myobject.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// myobject.cc
#include "myobject.h"
 
namespace demo {
 
    using v8::Context;
    using v8::Function;
    using v8::FunctionCallbackInfo;
    using v8::FunctionTemplate;
    using v8::Isolate;
    using v8::Local;
    using v8::Number;
    using v8::Object;
    using v8::Persistent;
    using v8::String;
    using v8::Value;
 
    Persistent<Function> MyObject::constructor;
 
    MyObject::MyObject(double value) : value_(value) {
    }
 
    MyObject::~MyObject() {
    }
 
    void MyObject::Init(Local<Object> exports) {
        Isolate* isolate = exports->GetIsolate();
 
        // Prepare constructor template
        Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
        tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
        tpl->InstanceTemplate()->SetInternalFieldCount(1);
 
        // Prototype
        NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
 
        constructor.Reset(isolate, tpl->GetFunction());
        exports->Set(String::NewFromUtf8(isolate, "MyObject"),
            tpl->GetFunction());
    }
 
    void MyObject::New(const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
 
        if (args.IsConstructCall()) {
            // Invoked as constructor: `new MyObject(...)`
            double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
            MyObject* obj = new MyObject(value);
            obj->Wrap(args.This());
            args.GetReturnValue().Set(args.This());
        }
        else {
            // Invoked as plain function `MyObject(...)`, turn into construct call.
            const int argc = 1;
            Local<Value> argv[argc] = { args[0] };
            Local<Context> context = isolate->GetCurrentContext();
            Local<Function> cons = Local<Function>::New(isolate, constructor);
            Local<Object> result =
                cons->NewInstance(context, argc, argv).ToLocalChecked();
            args.GetReturnValue().Set(result);
        }
    }
 
    void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
        Isolate* isolate = args.GetIsolate();
 
        MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
        obj->value_ += 1;
 
        args.GetReturnValue().Set(Number::New(isolate, obj->value_));
    }
 
}  // namespace demo
cs


프로젝트를 Build 해 보자. 에러가 날 것이다. 그 이유는, "myobject.h"를 찾지 못하기 때문이다.


이 헤더가 있는 경로를 추가하도록 한다. "프로젝트 속성 다이얼로그 > 구성속성 > C/C++ > 추가 포함 디렉터리"에 프로젝트 디렉터리 내 include 폴더를 등록한다.






이제 다시 프로젝트를 빌드 해 보면 에러가 나지 않을 것이다. 여기까지 성공했다면 이제 "addon.cc" cpp 소스를 새로 추가한다.


addon.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// addon.cc
#include <node.h>
#include "myobject.h"
 
namespace demo {
 
    using v8::Local;
    using v8::Object;
 
    void InitAll(Local<Object> exports) {
        MyObject::Init(exports);
    }
 
    NODE_MODULE(addon, InitAll)
 
}  // namespace demo
 
cs


다시 프로젝트를 빌드 해 본다. 에러가 나오면 어디선가 실수를 한 것이다.


NOTE: 코드에 대한 설명은 본 튜토리얼의 범위를 벗어난다. 각자의 C/C++ 지식을 동원하여 코드 분석을 해 보기 바란다.

디버깅 옵션 설정

디버깅을 위해 "명령" 값을 지정한다. MyAddon 프로젝트 속성 다이얼로그 > 구성 속성 > 디버깅 > 명령에 "$(NODE_HOME)\$(Configuration)\node.exe"을 입력한다.


그리고 작업 디렉터리는 "$(OutDir)"로 지정한다. $(OutDir)는 빌드 출력파일의 경로를 의미한다. 즉, node.exe를 실행하는 위치는 지정한 것이다.


테스트

Visual Studio에서 [로컬 Windows 디버거] 버튼을 클릭하거나, F5를 눌러 디버깅 모드로 실행한다. node.js 콘솔이 팝업되는데 다음 명령들을 입력하여 테스트 해보도록 한다.


// test.js
> const addon = require('./MyAddon');

> var obj = new addon.MyObject(10);
> console.log(obj.plusOne()); // 11
> console.log(obj.plusOne()); // 12
> console.log(obj.plusOne()); // 13



위와 같은 결과가 나오면 성공이다!!

Template 내보내기

만약 Node.js Addon을 작성할 때마다 이전까지의 과정을 계속 반복해야 한다면 생산성이 떨어질 것이다. 따라서, 지금까지 작성한 것을 Template으로 저장한다면 향후 Addon을 다시 만들더라도 개발환경 재설정을 할 필요가 없다. Template을 만드는 방법은 다음과 같다.


"Visual Studio 메뉴 > 파일(F) > 템플릿 내보내기(E)"를 클릭한다.




아래 다이얼로그가 열리면 [다음(N)>] 버튼을 클릭한다.




아래와 같이 다이얼로그가 나오면,


  • 템플릿 : NodeAddon

  • 아이콘 이미지 : %NODE_HOME%\src\res\node.ico

  • 템플릿을 자동으로 Visual Studio로 가져오기 : 체크


기타 옵션은 각자가 원하는대로 지정한다.




[마침(F)] 버튼을 클릭하여 템플릿 내보내기를 완료한다. 템플릿 내보내기가 정상적으로 진행되었다면 Visual Studio에서 새 프로젝트 생성 시 아래 이미지와 같이 NodeAddon 템플릿이 등장할 것이다.





이제 NodeAddon 템플릿으로 새로운 Node Addon 프로젝트를 생성해 보고 테스트해 보도록 한다.

Comments