Node.js C++ Core Concept

JavaScript와 C++ 간의 효율적인 연결은 Node.js에서 중요한 문제 중 하나이다. 이러한 연결을 가능하기 위해 Node.js Core에서는 기본 C++ 클래스로 BaseObject 사용한다. BaseObject는 JavaScript 객체와 C++ 객체를 함께 묶는 주요 추상화 수단으로, 이를 통해 메모리 관리와 더불어 객체의 수명 주기를 관리할 수 있다.

BaseObject

Node.js에서 JavaScript 객체와 연관된 대부분의 클래스는 BaseObject 의 subclass이다. BaseObject의 상위 클래스는 MemoryRainer인데, V8 Heap snapshot builder에서 메모리 추적을 위해 C++ 클래스에 Annotation을 할 수 있도록 하는 Helper 클래스라고 한다. 메모리 릭에 대한 디버깅 용도로 사용할 수 있다.

모든 BaseObject는 하나의 Realm과 하나의 v8::Object 와 연관된다. v8::Object 클래스는 JavaScrip Object를 뜻한다. 이 클래스의 인스턴스에는 internal field라고 하는 공간을 할당할 수 있다. 이 공간에 BaseObject의 나 하위 Class의 포인터를 연결함으로서 JavaScript Object 와 C++ 클래스 인스턴스를 바인딩한다. Internal Field에 반드시 BaseObject의 서브클래스만 등록해야하는 것은 아니다. InternalField에 얼마나 많은 정보를 넣을지는 V8 API의 SetInternalFieldCount() 를 통해 설정할 수 있다.

base_object.cclink
21
22
23
24
25
26
27
28
BaseObject::BaseObject(Realm* realm, Local<Object> object)
: persistent_handle_(realm->isolate(), object), realm_(realm) {
CHECK_EQ(false, object.IsEmpty());
CHECK_GE(object->InternalFieldCount(), BaseObject::kInternalFieldCount);
SetInternalFields(realm->isolate_data(), object, static_cast<void*>(this));
realm->AddCleanupHook(DeleteMe, static_cast<void*>(this));
realm->modify_base_object_count(1);
}

상기 BaseObject 생성자 함수에서 확인 할 수 있듯이 SetInternalFields 를 이용해 Local 메모리 영역의 object에 BaseObject 인스턴스 자신 (this) 를 설정한다.

Accessing BaseObject

C++레벨에서 JavaScript Object로 부터 BaseObject에 대한 포인터를 얻는 방법은 Unwrap<T> 를 사용한다. 보통 JavaScript 객체의 Function Callback의 호출되었을때 arg.This()arg.Holder() 로 부터 얻을수 있다. (args.Holder()는 Node.js 내부의 모든 사용 사례에서 args.This()와 동일).

Unwrap<T>() 는 반환받는 Pointer를 미리 Casting하려는 용도의 Alias 함수이다. 내부적으로는 BaseObject::FromJSObject 에 의해서 v8::Object부터 기 지정한 internal field로 부터 Pointer를 얻어온다.

base_object.hlink
246
247
248
249
250
251
#define ASSIGN_OR_RETURN_UNWRAP(ptr, obj, ...)                                 \
do { \
*ptr = static_cast<typename std::remove_reference<decltype(*ptr)>::type>( \
BaseObject::FromJSObject(obj)); \
if (*ptr == nullptr) return __VA_ARGS__; \
} while (0)

AsyncWrap

AsyncHook은 비동기 요청(init/destory)의 수명 및 해당 콜백 활동(Before/After)을 추적하는 기능을 제공한다. 이를 위한 추가적인 정보와 도구를 제공하는 BaseObject의 서브클래스가 AsyncWrap이다. AsyncHook에서 명시되었던 것과 같이 어떤 Core 모듈이 해당 Async동작을 수행했는지를 추적하기 위한 unique id가 있다.

async_wrap.hlink
34
35
36
37
38
39
40
41
42
43
44
#define NODE_ASYNC_NON_CRYPTO_PROVIDER_TYPES(V)                                \
V(NONE) \
V(DIRHANDLE) \
V(DNSCHANNEL) \
V(ELDHISTOGRAM) \
V(FILEHANDLE) \
V(FILEHANDLECLOSEREQ) \
V(BLOBREADER) \
V(FSEVENTWRAP) \
V(FSREQCALLBACK) \
V(FSREQPROMISE) \

AsyncHook을 도입할때 비동기 연산에 대한 디버깅을 쉽게하기 위함이었을것 같다. (TODO: 향후 History를 찾으면 다시 업데이트 하겠다.) 그러나, 최근 FS 모듈에 대한 Async File Read를 분석해보니 Tracing을 단일 Operation 단위로 처리될 수 있도록 고정되어있어 통합된 비동기 동작에 대한 Tracing 처리를 만들기가 어려웠다. 또, Node.js 에서는 공식적으로 AsyncHook에 대한 기능 사용을 권하지 않는다. 따라서, 향후 어떤 방향이든 개선이 필요한 클래스이다.

AsyncWrap 클래스에서 중요한 함수는 MakeCallback을 들수 있다. Callback을 만들기 위한 함수명 처럼 보이지만 이름과는 다르게 실제의미는 C++에서 JavaScript 쪽으로 Callback을 부르는 용도를 한다. 참고로 이러한 JavaScript와 C++ 사이의 Context Switching이 일어나는 경우 Cost가 많이 들기 때문에 되도록 적게 부르는 것이 Performance 측면에서 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void StatWatcher::Callback(uv_fs_poll_t* handle,
int status,
const uv_stat_t* prev,
const uv_stat_t* curr) {

// StatWatcher 는 AsyncWrap의 서브 클래스
StatWatcher* wrap = ContainerOf(&StatWatcher::watcher_, handle);
Environment* env = wrap->env();

// Integer::New 로 할당을 하기 때문에 HandleScope를 잡아준다.
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());

...

// C++에서 JavaScript 쪽으로 전달할 argument
Local<Value> argv[] = { Integer::New(env->isolate(), status), arr };

// wrap 인스턴스에 연결된 객체에 프로퍼티중 onchange 에 등록된 콜백을 호출
wrap->MakeCallback(env->onchange_string(), arraysize(argv), argv);
}

Libuv Handlers

libuv에서 이벤트 루프와 상호작용하는 데 사용되는 타입이 있는데 handle과 request이다. uv.h를 살펴보면 아래와 같이 정의된 타입을 확인할 수 있다.

uv.hlink
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_dir_s uv_dir_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;

/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;
typedef struct uv_random_s uv_random_t;
  • Handle은 파일 디스크립터, 타이머, 혹은 다른 리소스와 관련된 이벤트를 추적하는 데 사용된다. 이벤트 루프가 이러한 이벤트를 감지하고 처리할 수 있도록 핸들을 등록하고 관리한다.

  • Request는 비동기 작업을 수행하고 완료되었을 때 결과를 처리하는 데 사용된다. 파일 읽기, 쓰기, 혹은 네트워크 요청과 같은 작업을 수행할 때 request를 생성하여 완료되었을 때의 콜백을 등록한다.

Libuv Handle Wrapper Class

HandleWrap

HandleWrap은 AsyncWrap의 서브클래스이며 uv_handle_t 와 같은 libuv의 핸들을 감싸서 관리하는 용도로 사용된다. .ref(), .unref(),.hasRef(),.close() 등의 함수가 제공되는데, 함수 내부적으로 libuv의 uv_ref(), uv_unref() 와 같은 API를 호출한다.

1
2
3
4
5
6
7
void HandleWrap::Ref(const FunctionCallbackInfo<Value>& args) {
HandleWrap* wrap;
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());

if (IsAlive(wrap))
uv_ref(wrap->GetHandle());
}

ReqWrap

ReqWrap AsyncWrap의 서브클래스이며 uv_req_t 와 같은 libuv의 request를 쉽게 처리하는 기능을 제공한다. 예를들면 파일을 읽는 동작을 uv_fs_t request 핸들을 이용하여 전달할때 요청된 동작이 끝나면 결과 Callback이 호출되도록 등록한다.

ReqWrap::Dispatch() 메서드는 이러한 동작을 일관된 방법으로 사용할 수 있게한다.

req_wrap-inl.hlink
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
template <typename T>
template <typename LibuvFunction, typename... Args>
int ReqWrap<T>::Dispatch(LibuvFunction fn, Args... args) {
Dispatched();
// This expands as:
//
// int err = fn(env()->event_loop(), req(), arg1, arg2, Wrapper, arg3, ...)
// ^ ^ ^
// | | |
// \-- Omitted if `fn` has no | |
// first `uv_loop_t*` argument | |
// | |
// A function callback whose first argument | |
// matches the libuv request type is replaced ---/ |
// by the `Wrapper` method defined above |
// |
// Other (non-function) arguments are passed -----/
// through verbatim
int err = CallLibuvFunction<T, LibuvFunction>::Call(
fn,
env()->event_loop(),
req(),
MakeLibuvRequestCallback<T, Args>::For(this, args)...);
if (err >= 0) {
ClearWeak();
env()->IncreaseWaitingRequestCounter();
}
return err;
}
Share