I'm writing a Node plugin and I'm having problems trying to call a V8 function object from a C++ worker thread.
My plugin basically starts a C++ std::thread and enters a wait loop using WaitForSingleOject(), this is triggered by a different C++ app (An X-Plane plugin) writing to a bit of shared memory. I'm trying to get my Node plugin to wake up when the Windows shared event is signaled then call a JavaScript function that I've registered from the node app, which will in turn pass the data which originated in X-Plane back to Node and the web world.
I've managed to work out how to register a JavaScript function and call it from C++, but only in the main V8 thread. I can't seem to find a way of calling the function from the std::thread.
I've tried various approaches, Locker objects (variable success), Persistent functions (didn't work), saving the main isolate object, entering/exiting the isolate, but if/when the code eventually reaches the function object it's not valid.
I get different results, ranging from crashing to freezing depending on whether I create various locker and unlocker objects.
I'm totally new to V8, so I'm not really sure I'm doing anything right. The code in question as follows:
If anyone could help at all I'll be eternally grateful!.
float* mem = 0;
HANDLE event = NULL;
Isolate* thisIsolate;
void readSharedMemory()
{
//Isolate* isolate = Isolate::GetCurrent();
//HandleScope scope(isolate);
thisIsolate->Enter();
v8::Locker locker(thisIsolate);
v8::Isolate::Scope isolateScope(thisIsolate);
//HandleScope scope(thisIsolate);
//v8::Local<Value> myVal = v8::String::NewFromUtf8(isolate, "Plugin world");
v8::Local<Value> myVal = v8::Number::New(thisIsolate, *mem);
// If it get's this far 'myFunction' is not valid
bool isFun = myFunction->IsFunction();
isFun = callbackFunction->IsFunction();
v8::Context *thisContext = *(thisIsolate->GetCurrentContext());
myFunction->Call(thisContext->Global(), 1, &(Handle<Value>(myVal)));
}
void registerCallback(const FunctionCallbackInfo<Value>& args)
{
Isolate* isolate = Isolate::GetCurrent();
v8::Locker locker(isolate);
HandleScope scope(isolate);
/** Standard parameter checking code removed **/
// Various attempts at saving a function object
v8::Local<v8::Value> func = args[0];
bool isFun = func->IsFunction();
Handle<Object> callbackObject = args[0]->ToObject();
callbackFunction = Handle<Function>::Cast(callbackObject);
isFun = callbackFunction->IsFunction();
// save the function call object - This appears to work
myFunction = v8::Function::Cast(*callbackObject);
isFun = myFunction->IsFunction();
// Test the function - this works *without* the Unlocker object below
v8::Local<Value> myVal = v8::String::NewFromUtf8(isolate, "Plugin world");
myFunction->Call(isolate->GetCurrentContext()->Global(), 1, &(Handle<Value>(myVal)));
}
void threadFunc()
{
thisIsolate->Exit();
// If I include this unlocker, the function call test above fails.
// If I don't include it, the app hangs trying to create the locker in 'readSharedMemory()'
//v8::Unlocker unlocker(thisIsolate);
event = OpenEventW(EVENT_ALL_ACCESS, FALSE, L"Global\\myEventObject");
DWORD err = GetLastError();
//thisIsolate = v8::Isolate::New();
std::cout << "Hello from thread" << std::endl;
bool runThread = true;
while (runThread)
{
DWORD dwWaitResult;
DWORD waitTime = 60000;
dwWaitResult = WaitForSingleObject(event, waitTime);
err = GetLastError();
if (dwWaitResult == WAIT_TIMEOUT)
runThread = false;
// event has been signaled - continue
readSharedMemory();
}
}
void init(Handle<Object> exports)
{
/** NODE INITILISATION STUFF REMOVED **/
// save the isolate - Is this a safe thing to do?
thisIsolate = Isolate::GetCurrent();
//Launch a thread
eventThread = std::thread(threadFunc);
}
You probably need a bit of libuv
magic to get the main node.js/v8 thread to execute your callback from another thread. This will involve:
A uv_async_t handle which acts as the wake-up call for the main v8 thread:
extern uv_async_t async;
A uv_async_init
call which binds the uv_async_t to the V8 default loop:
uv_async_init(uv_default_loop(), &async, async_cb_handler);
And a event handler which will act on the uvasync_t event on the v8 main thread:
void async_cb_handler(uv_async_t *handle) {
NotifInfo *notif;
mutex::scoped_lock sl(zqueue_mutex);
while (!zqueue.empty()) {
notif = zqueue.front();
handleNotification(notif);
delete notif;
zqueue.pop();
}
}
Finally, you'll also probably need a mutex-protected queue to be able to pass some data from the c++ addon thread to Node/V8:
extern mutex zqueue_mutex;
extern std::queue<NotifInfo *> zqueue;
When something happens in your C++ thread, just push a new item to the mutex-protected queue, and call uv_async_send
to wake up V8's default event loop (the so called 'main thread') to process the item (which can then call your Javascript callbacks)
void ozw_watcher_callback(OpenZWave::Notification const *cb, void *ctx) {
NotifInfo *notif = new NotifInfo();
notif->type = cb->GetType();
notif->homeid = cb->GetHomeId();
...
mutex::scoped_lock sl(zqueue_mutex);
zqueue.push(notif);
uv_async_send(&async);
}
(Code snippets taken from the official Node.JS addon for OpenZWave)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With