티스토리 뷰

지난 번, Node.js 용 c++ Addon을 gcc로 컴파일하기 (node-gyp 없이) 를 통해
기본적인 Addon 개발 방법 및 v8 Local 다루는 법을 알아 보았습니다.

그 후 Async Callback은 어떻게 작성할까에 대해, 틈틈이 테스트를 해보았는데
v8 API 사용에 익숙치 않다보니 오랜 시간 난항을 겪었습니다.
오랜 인고의 시간 끝에 사용법을 알게되어 포스팅 해 봅니다.

  1. v8의 Handle (memory) 관리 방법
  2. Addon 샘플 코드 (Async callback) 작성

순으로 소개하겠습니다.

※ v8 이란?
 -> Google Chrome 및 Node.js의 Javascript Engine 입니다.



1. v8의 Handle (memory) 관리 방법

이걸 먼저 소개하는 이유는, 제가 이걸 몰라서 오랫동안 고생했기 때문입니다.
미리 딱 알고나서 개발을 하면 쉽게 완성할 수 있습니다.

v8은 디자인 요소 중 하나로 효율적인 Garbage Collection을 꼽습니다.
이를 위해 Handle의 생명주기를 이해해야 정상적으로 v8 Object들을 사용할 수 있습니다.
(아래 설명은 v8 wiki의 Embedder's Guide 를 참고해서 작성했습니다.)

기본적으로 크게 두가지 Handle 종류(Template Class)가 있습니다.
Local handle과 Persistent handle 입니다. (물론 드물게 사용되는 몇개가 더 있긴 합니다.)

Local handle은
Stack에 보관되는 handle이며, 지역변수 처럼 사용되며, 그 생명주기가 HandleScope 라는 Class에 매우 의존적입니다.
이는 아래와 같은 세가지 특징으로 설명할 수 있습니다.
1. HandleScope는 동적 할당 불가, 지역변수로만 사용가능
(결국, c++에서 함수가 끝날 때, 지역 변수로 선언된 HandleScope Class의 소멸자가 불리는 결과를 만들어 냅니다. 즉 HandleScope은 c++의 Scope인 { } 안에서만 유효할 수 있습니다.)
2. HandleScope 가 지역변수로 선언된 이후부터, Local handle을 생성 가능
3. HandleScope instance의 소멸자가 호출되면 해당 HandleScope 의 유효 범위내의 handle을 garbage collector가 해제함.

결론적으로, 특정 함수 내에서 지역변수처럼 쓰면 됩니다.

Persistent handle은
heap에 할당된 JavaScript Object들을 참조할 수 있으며, 참조 시 이들이 Garbage Collector에 의해 해제되지 않도록 합니다.
대신, 명시적으로 Reset() 함수를 Call해서 추후 해제될 수 있도록 돕습니다.

Local handle은 Template 안에 있는 실제 Object를 Access할 수 있도록 '->', '*' 연산자 오버라이딩을 제공하지만, Persistent는 제공하지 않습니다.
따라서, Persistent handle은 메모리의 Lifetime 연장에만 사용 후, 다시 Local handle로 Cast 해 사용해야 합니다.
자세한 내용은 밑에서 예제 코드와 함께 다루도록 하겠습니다.

v8 소스코드에서 Handle Scope의 해제 동작을 살짝 따라가보면,
생성자에서 isolate의 handle scope data 안에 있는 메모리 포인터를 가져오는 걸로 보이며, level 값을 올려줍니다.

이후 소멸자 호출 시, level 값 감소 및 기존 보관했던 memory 주소를 복원시켜 주고HandleScopeImplementer::DeleteExtensions() 를 호출해줍니다.

DeleteExtensions는 HandleScope의 메모리 블록을 페이지 단위로 다 빌 때까지 해제해 주는 것으로 보입니다.
kHandleBlockSize 변수를 따라가보면, "fit in one page" 라는 주석과 함께, 1022 바이트로 표기하고 있습니다. (자세한 건 공부를 더 해봐야겠습니다.)


2. Addon 샘플 코드 (Async callback) 작성

첫 번째로, 파라미터로 들어온 Function Object를 Synchronous 하게 직접 Call 하는 예제를 구현하고

두 번째로, 같은 상황에서 libuv의 timer를 이용해서 Asynchronous 하게 다음 loop에서 Callback이 호출되는 상황을 구현하겠습니다.

첫 번째. (JavaScript 함수명: directCall, C++ 함수명: MethodFunc)
[6-9 라인] 우선 파라미터가 잘못 되었을 때, Exception 던져 에러 상황을 알립니다.
[14 라인] 첫 번째 argument를 "Local<Function>" 으로 Cast한 뒤,
[15 라인] "->" 연산자를 이용하여 Function class instance를 참조 후 Function Class의 Call Method를 호출합니다.

<※Local Class의 "->" 연산자 Overwriting : 링크>
<※Function Class의 "Call" Method : 링크>
<작성자 주: 왜 그런지는 모르겠으나 바로 위 링크의 Reference와 달리, 아래 15라인에서는 파라미터 한개 뺐는데도 정상 동작하네요>

// Direct call function of first parameter.
void MethodFunc(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // If parameter is invalid, throw exception.
  if (args.Length() < 1 || !args[0]->IsFunction()) {
    Local<String> err_msg = String::NewFromUtf8(isolate, "Invalid Parameter");
    Local<Value> exception = v8::Exception::Error(err_msg);
    isolate->ThrowException(exception);
    return;
  }

  // Call function directly
  Local<Function> cb = Local<Function>::Cast(args[0]);
  cb->Call(Null(isolate), 0, NULL);
}


Test를 위한 JavaScript Code
[3-8 라인] 첫 번째, positive 구문에서는 "directCall" 함수에 Parameter를 function으로 잘 넣었을 경우입니다.
잘 호출 되는지와 Synchronous call이므로, 함수 안의 내용이 화면에 먼저 출력되는지,
아니면 함수 밖의 '---------------------------' 이 먼저 출력되는지를 주목해서 확인해야 합니다.
[10-17 라인] "directCall" 함수의 Parameter를 String을 넣어 보았습니다.
try / catch가 잘 동작 하는지를 확인 합니다.

var m = require('./test.node');

console.log('----- Test 02 directCall positive -----');
m.directCall(function(text) {
  console.log('In callback: ' + text);
});
console.log('-------------------------');
console.log();

console.log('----- Test 02 directCall negative -----');
try {
  m.directCall('');
} catch (err) {
  console.error('catch: ' + err);
}
console.log('-------------------------');
console.log();


실행 결과
Positive case에서 파라미터로 넘긴 Inline 함수의 출력 구문이 먼저 불리고 '-----' 가 출력됨을 확인할 수 있습니다.
또한 그 안에서 출력한 내용이 undefined인 이유는 위에 c++ Addon에서
Call 함수 호출 시, argument를 전달하지 않았기 때문입니다.
Negative case 에서 흥미로운 점은 catch의 Parameter로 넘어 온 err를 출력하면,
c++ Addon 에서 제가 넣은 "Invalid Parameter"라는 String 외에
앞에 "Error: " 이라는 구문이 따라 붙는 점이 흥미롭습니다.

두 번째.  (JavaScript 함수명: asyncCall, C++ 함수명: MethodTimer)

- 함수 본체 구문 (MethodTimer)
[5-10 라인] 파라미터 이상있을 시 Exception을 생성해 Throw
[12-15 라인] Node.js에서 이미 동작하고 있을, libuv의 default loop을 가져 와,
 uv timer handle 초기화.
[17 라인] 함수의 첫번째 Parameter를 Local<Function>으로 Cast
[20 라인] Persistent<Function>을 new로 생성하면서 위 Local<Function>을 인자로 전달
 -> JavaScript 함수로부터 전달 된 Function Object (args[0] == Local<Function> cb)가
 MethodTimer 함수의 지역 변수 영역 (≒Handle Scope)이 소멸되더라도 (= 이 함수가 끝나더라도)
 해당 Object가 Garbage Collector에 의해 제거되지 않도록 함.
[22 라인] 생성한 Persistent<Function>을 libuv의 timer 함수에서 사용하기 위해,
 해당 포인터 값을 timer->data에 보관.
[24 라인] Async Timer를 실행.

<※ Persistent Class 생성자 : 링크>
<※ libuv의 Timer API : 링크>

void MethodTimer(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // If parameter is invalid, throw exception.
  if (args.Length() < 1 || !args[0]->IsFunction()) {
    Local<String> err_msg = String::NewFromUtf8(isolate, "Invalid Parameter");
    Local<Value> exception = v8::Exception::Error(err_msg);
    isolate->ThrowException(exception);
    return;
  }

  // Creates timer using libuv
  uv_loop_t *loop = uv_default_loop();
  uv_timer_t *timer = (uv_timer_t *)malloc(sizeof(uv_timer_t));
  uv_timer_init(loop, timer);

  Local<Function> cb = Local<Function>::Cast(args[0]);

  // Creates persistent handle for keeping parameter
  Persistent<Function> *pf = new Persistent<Function>(isolate, cb);
  // Save to uv timer handle.
  timer->data = (void *)pf;

  uv_timer_start(timer, _timer_cb, 1000, 1000);
}


- Timer 함수 구문 (_timer_cb)
libuv timer의 함수부 이기 때문에, 위의 함수(MethodTimer)는 Node.js 코드 수행 중,
잠시 호출된 Sub 함수이므로 Handle Scope에 대한 고민을 하지 않았습니다.
(아마도, 이미 Node.js 내부 코드에서 초기화되어 있었을 겁니다.)
그러나 아래 함수는 정해진 시간이 경과하여 loop iteration에 의해 호출되었기 때문에,
Handle Scope이 정의되지 않았고, 여기서 생성되는 Local handle들을 Garbage Collector가 Tracking할 수 없게 됩니다.
그래서 기본적으로 그냥 바로 Local handle 생성 시, v8 Engine이 Abort Signal을 생성하고
다음과 같은 stderr를 출력합니다.

FATAL ERROR: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope

[9 라인] HandleScope Class를 정적 변수로 선언함으로써, Local handle들이 생성될 수 있으며,
_timer_cb 함수 종료 시 Garbage Collector가 신속하게 메모리 블록을 관리할 수 있게 합니다.
[11 라인] libuv의 timer handle에 저장했던 Persistent<Function> 의 포인터를 꺼냅니다.
[20 라인] Local<Function>을 생성하는데, Persistent<Function>을 Reference handle로 Parameter에 넣어 전달합니다.
c++ 문법의 new 가 아니라 Local Class의 정적 함수인 New 함수를 이용합니다.
현재 상태는 실제 Object의 메모리 주소를 Handle 두개가 참조하고 있습니다.
모두 참조를 해제해야만 Garbage Collector가 메모리에서 제거할 수 있습니다.
[23 라인] Local<Function>을 이용해, Function Class의 Call 함수를 호출합니다.
[25-26 라인] _timer_cb가 두번 불릴 수 있도록 합니다.
[26-27 라인] _timer_cb가 두번째 호출되었을 때, Persistent<Function> 의 Reset() 함수를 호출해 메모리 참조를 초기화 합니다.
(이제 _timer_cb 함수가 종료되면, 자연스레 handle_scope의 소멸자가 호출되고
 Local<Function> callback의 메모리 참조도 초기화됩니다.
 즉, 드디어 실제 Object의 메모리를 아무도 참조하지 않게 되어 메모리 반환이 가능해집니다.)
[28 라인] Handle 자체의 메모리를 해제
[32-33 라인] timer를 중단 시키고, timer handle의 메모리를 해제.

<※ Local Class의 New 함수 : 링크>
<※ PersistentBase Class의 Reset(void) 함수 : 링크>

static int g_timer_count = 0;
static int g_timer_max = 2;

void _timer_cb(uv_timer_t *timer)
{
  Isolate *isolate = Isolate::GetCurrent();

  // Define handle scope
  HandleScope handle_scope(isolate);
  // Retrieve persistent from uv timer handle.
  Persistent<Function> *pf = (Persistent<Function> *)timer->data;

  // Create argument to forward callback.
  Local<Value> argv[1] = {
    String::NewFromUtf8(isolate, "Timer invoked")
  };

  Local<Object> global = isolate->GetCurrentContext()->Global();
  // Create Local handle from persistent handle.
  Local<Function> callback = Local<Function>::New(isolate, *pf);

  // Callback call.
  callback->Call(global, 1, argv);

  g_timer_count++;
  if (g_timer_count >= g_timer_max) {
    // Clear memory from heap.
    pf->Reset();
    delete pf;

    // Destroy timer.
    uv_timer_stop(timer);
    free(timer);
  }
}


Test를 위한 JavaScript Code
여기에서 주목해야할 점은, 5번째 줄의 'In async callback' 구문과 7번째 줄의 '---------' 구문 중
어느 코드가 먼저 실행되는가 입니다.

var m = require('./test.node');

console.log('----- Test 03 asyncCall positive -----');
m.asyncCall(function(text) {
  console.log('In async callback: ' + text);
});
console.log('-------------------------');
console.log();


실행 결과
'---------' 가 우선 출력되고, 함수 내 구문이 비동기로 나중에 호출됨을 확인할 수 있습니다.


Full Code는 위에도 적었듯이, https://github.com/z-wony/v8Practice/tree/master/02_asyncCallback 에 있습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함