Search code examples
c++node.jsv8libuv

Crash when trying to call node js callback using C++ addon


I am trying to call some native api from node js and perform long running task. I am using libuv for that perposes, store js callback to perform some meaningful task in separate thread and call callback later. This is what I've done so far:

task.cpp

struct Work {
  uv_work_t request;
  Local<Function> callback;
};

static void WorkAsyncComplete(uv_work_t* req, int status)
{
    Isolate* isolate = Isolate::GetCurrent();
    v8::HandleScope handleScope(isolate);

    Work* work = static_cast<Work*>(req->data);

    const unsigned argc = 1;
    Local<Value> argv[argc] = { String::NewFromUtf8(isolate, "Hello world").ToLocalChecked() };

    Local<Function> callback = Local<Function>::New(isolate, work->callback);
    callback->Call(isolate->GetCurrentContext(), Null(isolate), argc, argv);

    work->callback.Clear();

    delete work;
}

static void WorkAsync(uv_work_t* req)
{
    Work* work = static_cast<Work*>(req->data);

    using namespace std::chrono_literals;
    std::this_thread::sleep_for(std::chrono::seconds(5));
}

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

    Work* work = new Work();
    work->request.data = work;
    
    Local<Function> callback = Local<Function>::Cast(args[1]);
    work->callback.New(isolate, callback);

    uv_queue_work(uv_default_loop(), &work->request, WorkAsync, WorkAsyncComplete);

    args.GetReturnValue().Set(Undefined(isolate));
}

void Init(Local<Object> exports, Local<Object> module)
{
    //isolate = exports->GetIsolate();
    NODE_SET_METHOD(module, "exports", RunCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

task.js:

let task = require('./build/Release/task');

task((msg) => {
  console.log(msg);
}

But I got crash in WorkAsyncComplete() when calling callback. Here is the error detail:

Debugger attached.
FATAL ERROR: v8::Function::Call Function to be called is a null pointer
 1: 00007FF6579E03DF napi_wrap+109311
 2: 00007FF657985096 v8::internal::OrderedHashTable<v8::internal::OrderedHashSet,1>::NumberOfElementsOffset+33302
 3: 00007FF657985E66 node::OnFatalError+294
 4: 00007FF65822A9BD v8::Function::Call+573
 5: 00007FFA62FD1172 load_exe_hook+258
 6: 00007FF657A37D90 uv_timer_stop+560
 7: 00007FF657A37E67 uv_timer_stop+775
 8: 00007FF657A3469B uv_async_send+331
 9: 00007FF657A33E2C uv_loop_init+1292
10: 00007FF657A33FCA uv_run+202
11: 00007FF6579400A5 v8::internal::OrderedHashTable<v8::internal::OrderedHashSet,1>::NumberOfBucketsOffset+9365
12: 00007FF6579B3867 node::Start+311
13: 00007FF65781686C RC4_options+339820
14: 00007FF6587B523C v8::internal::compiler::RepresentationChanger::Uint32OverflowOperatorFor+153532
15: 00007FFA761A7C24 BaseThreadInitThunk+20
16: 00007FFA7694D4D1 RtlUserThreadStart+33

Please advice how to fix this issue. Thanks in advance.


Solution

  • Looks like the issue is that you're using a v8::Local<...> in a long-lived object. That doesn't work because the lifetime of a Local is tied to the surrounding HandleScope: the Local becomes invalid when the HandleScope dies. See https://v8.dev/docs/embed#handles-and-garbage-collection for details. The solution is to use a v8::Persistent<...> instead of a Local.

    For the record, I'd also like to point out that while you can do C++ work in the background task, you won't be able to extend this example so that it takes a JavaScript function to execute concurrently. That would be multi-threaded programming, which JavaScript as a language doesn't support, and hence V8 as an implementation doesn't support either.