4 minute read

Node.js와 C++의 관계

Node.js는 C++로 만들어져 있다. Node.js를 이용하여 수 년동안 개발을 했다. 단순히 C++로 Node.js가 만들어졌으며 V8, Libuv 라이브러리 등을 이용하여 구현되어 있다는 사실만 알고 있었다. 최근 오픈소스 코드를 분석하다가 C++로 짜여진 코드를 봤다.

node-rdkafka Node.js에서 카프카를 이용할 수 있는 라이브러리이다. src 폴더를 보면 C++로 구성된 것을 볼 수 있다. 이 라이브러리는 Confluentlibrdkafka 라이브러리를 Node.js에 사용할 수 있게 랩핑한 것이다.

위의 코드를 봤고 C++로 구현된 부분을 직접 이용할 수 있다는 사실을 알았다.

C++ 환경에서 사용할 수 있는 API

Node.js는 C++로 만들어졌다고 했다. 그럼 Node.js에서 C++를 어떻게 사용할 수 있을까? 바로 Nodejs 제공하는 C++ Addon이 있다.

C++에서 제공하는 Node.js 헤더는 아래와 같이 있다.

  • node.h: Node.js의 C++ API를 직접 호출할 수 있다. V8 엔진과 직접 상호작용하며 Node.js의 버전별로 각각 빌드해야된다는 불편함이 있다. 제공하는 API 중에 가장 원시적인 방법이라 할 수 있다.
  • nan.h: V8과 Node.js 버전에 대응하는 추상화된 API를 제공한다. 버전별로 일관된 API로 개발할 수 있으나 Node.js의 버전별로 각각 빌드해야된다는 단점은 여전히 존재한다.
  • node_api.h: C 스타일(구조체)로 API를 제공한다. ABI(Application Binary Interface) 호환을 지원하므로 Node.js의 다양한 버전에 호환이 가능하다.
  • napi.h: C++ 스타일(객체)로 API를 제공한다. ABI(Application Binary Interface) 호환을 지원하므로 Node.js의 다양한 버전에 호환이 가능하다.

C++을 이용하여 개발한다면 napi.h 헤더를 사용하는 것을 추천한다. N-Api는 Node-API의 약자로 자세한 내용은 N-API 공식문서에서 확인 가능하다.

계산기 구현해보기

N-API를 이용하여 간단한 사칙연산 계산기를 구현해보자.

  • C++ style napi.h 사용
  • 빌드툴은 node-gyp, CMake.js 중 node-gyp를 사용할 것이다.
  • node-bindings 라이브러리로 C++파일을 Node.js에 import 한다.

환경 구성

napi.h는 node-addon-api 패키지에 포함되어 있다. 아래의 패키지들을 설치하도록 한다.

$ npm install node-addon-api node-gyp bindings

node-gyp로 빌드하기 위한 설정파일을 생성한다

# ./binding.gyp
{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cpp", "calculator.cpp" ],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

target_name은 바인딩 파일 이름(addon.node 생성)이고 sources는 빌드할 파일, include_dirs은 빌드시 필요한 헤더 위치이다. 간단한 예제를 만들 것이므로 defines에서 NAPI에서 예외처리 하는 기능은 끌 것이다.

C++ 작성

소스코드 작성 부분은 간략히 이런게 있구나 정도로 보면 좋을 것이다.

Napi::ObjectWrap 객체를 상속하여 Calculator를 정의하였다.

// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

#include <napi.h>

class Calculator : public Napi::ObjectWrap<Calculator> {
 public:
  // Javascript 함수 레퍼런스
  static Napi::FunctionReference* functionRef;
  // Javasciprt 클래스 생성 및 초기화
  static Napi::Function Init(Napi::Env env);
  // 팩토리 함수
  static Napi::Object From(Napi::Env env, Napi::Value arg);
  // 생성자
  Calculator(const Napi::CallbackInfo& info);

 private:
  Napi::Value GetValue(const Napi::CallbackInfo& info);
  Napi::Value Add(const Napi::CallbackInfo& info);
  Napi::Value Subtract(const Napi::CallbackInfo& info);
  Napi::Value Multiply(const Napi::CallbackInfo& info);
  Napi::Value Divide(const Napi::CallbackInfo& info);

  double value_;
};

#endif

C++ 객체를 Javascript에서 사용하기 위해 Class를 정의한다. DefineClass는 ObjectWrap에서 상속받은 함수이다.

// calculator.cpp, Napi::Function Calculator::Init
Napi::Function Calculator::Init(const Napi::Env env) {
  // Javascript 클래스 생성
  Napi::Function func =
      DefineClass(env, "Calculator",
                  {InstanceMethod("add", &Calculator::Add),
                   InstanceMethod("subtract", &Calculator::Subtract),
                   InstanceMethod("multiply", &Calculator::Multiply),
                   InstanceMethod("divide", &Calculator::Divide),
                   InstanceMethod("getValue", &Calculator::GetValue)});

  // 클래스 생성 후 functionRef에 저장 (Persistent 함수는 GC 대상 제외시킴)
  *Calculator::functionRef = Napi::Persistent(func);

  return func;
}

functionRef에 저장된 Class를 이용하여 객체를 생성한다.

// calculator.cpp, Napi::Object Calculator::From
// 팩토리 함수
Napi::Object Calculator::From(Napi::Env env, Napi::Value arg) {
  Napi::Object obj = Calculator::functionRef->New({arg});
  return obj;
}

생성자 함수 중간을 보면 Napi::CallbackInfo로 Javascript 함수의 인자를 이용할 수 있다.

// calculator.cpp, Calculator::Calculator
// 생성자
Calculator::Calculator(const Napi::CallbackInfo& info)
    : Napi::ObjectWrap<Calculator>(info) {
  Napi::Env env = info.Env();

  // 인자가 없거나 숫자가 아닌 경우 에러 처리
  int length = info.Length();
  if (length <= 0 || !info[0].IsNumber()) {
    Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
    return;
  }

  Napi::Number value = info[0].As<Napi::Number>();
  this->value_ = value.DoubleValue();
}

Javscript의 Number, String 등을 이용하려면 Napi에 있는 객체를 이용하여야 한다.

// calculator.cpp, Napi::Value Calculator::GetValue
Napi::Value Calculator::GetValue(const Napi::CallbackInfo& info) {
  double num = this->value_;

  // c++의 double을 Napi:Number로 변환
  return Napi::Number::New(info.Env(), num);
}
// calculator.cpp, Napi::Value Calculator::Add
Napi::Value Calculator::Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  int length = info.Length();
  if (length <= 0 || !info[0].IsNumber()) {
    Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
    return env.Null();
  }

  const Napi::Number value = info[0].As<Napi::Number>();
  // Napi::Number를 C++의 double로 변환
  this->value_ = this->value_ + value.DoubleValue();

  return Calculator::GetValue(info);
}

위와 같은 방식으로 나머지 Calculator 함수도 작성하였다.

아래와 같이 Calculator 객체를 export 해준다. (Javascript의 module.export와 같은 기능)

// addon.cpp
#include <napi.h>

#include "calculator.h"

Napi::Value CreateCalculator(const Napi::CallbackInfo& info) {
  return Calculator::From(info.Env(), info[0]);
}

Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
  exports.Set("Calculator", Calculator::Init(env));
  exports.Set("createCalculator", Napi::Function::New(env, CreateCalculator));
  return exports;
}

NODE_API_MODULE(addon, InitAll)

/*
  Like...
  module.exports = {
    Caculator: CaculatorClass,
    createCalculator: (arg: number) => CaculatorClass
  };
*/

Javascript 코드 작성

// index.js

// binding.gyp의 target_name과 동일한 이름으로 모듈 바인딩
const addon = require("bindings")("addon");

console.log("Calculator", addon.Calculator);
console.log("createCalculator", addon.createCalculator);

const calculator = new addon.Calculator(0);
console.log(calculator.add(10)); // 10
console.log(calculator.subtract(5)); // 5
console.log(calculator.multiply(8)); // 40
console.log(calculator.divide(4)); // 10
console.log(calculator.getValue()); // 10

const calculator2 = addon.createCalculator(0);
console.log(calculator2.add(10)); // 10
console.log(calculator2.subtract(5)); // 5
console.log(calculator2.multiply(8)); // 40
console.log(calculator2.divide(4)); // 10
console.log(calculator2.getValue()); // 10

예제

코드 결과물은 Github Repository에서 확인할 수 있다. 또한 Node Addon Example Repository에 많은 예제가 있으니 학습시 참고하는 것을 추천한다.

빌드 및 실행

# bindings.gyp 파일을 토대로 빌드 환경 구성
$ npx node-gyp configure

# 빌드
$ npx node-gyp build

# 실행
$ node index.js

그래서 어디에 활용할 수 있을까?

앞서 말한대로 수많은 C 라이브러리를 래핑해서 Node에서 사용할 수 있다. 또한 Node.js 처리하기 힘든 고성능 작업들도 처리할 수 있다. 이러한 Node Addon은 공식적으로 C++을 지원한다. 또한 유저들이만든 Rust 언어를 사용한 node-rs이 존재한다. 다음에는 node-rs도 경험을 해볼 예정이다.

참고

Comments