I ran into a frustrating problem today. I'm working with node-ffi
to run C++ code within my electron application. In general I've had good experiences, but I started working with multithreading today and ran into some difficulty. The ffi
callback that I pass in is called from the thread just fine. However when I end my loop and try to join
the loop thread to the main thread it completely freezes the electron app.
Full disclaimer: I'm pretty new to C++, and would appreciate any feedback on my code to improve it, especially any red flags you think I should be aware of.
Here are two repos that demonstrate the error I ran into:
Electron Project - https://github.com/JakeDluhy/threading-test
C++ DLL - https://github.com/JakeDluhy/ThreadedDll
And here's an overview of what I'm doing:
In my dll, I expose functions to begin/end a session and start/stop streaming. These call the reference of a class instance to actually implement the functionality. Essentially, it's a C wrapper around the more powerful C++ class.
// ThreadedDll.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#ifdef THREADEDDLL_EXPORTS
#define THREADEDDLL_API __declspec(dllexport)
#else
#define THREADEDDLL_API __declspec(dllimport)
#endif
THREADEDDLL_API void beginSession(void(*frameReadyCB)());
THREADEDDLL_API void endSession();
THREADEDDLL_API void startStreaming();
THREADEDDLL_API void stopStreaming();
#ifdef __cplusplus
}
#endif
// ThreadedDll.cpp
#include "ThreadedDll.h"
#include "Threader.h"
static Threader *threader = NULL;
void beginSession(void(*frameReadyCB)())
{
threader = new Threader(frameReadyCB);
}
void endSession()
{
delete threader;
threader = NULL;
}
void startStreaming()
{
if (threader) threader->start();
}
void stopStreaming()
{
if (threader) threader->stop();
}
Here's what the Threader
class looks like:
// Threader.h
#pragma once
#include <thread>
#include <atomic>
using std::thread;
using std::atomic;
class Threader
{
public:
Threader(void(*frameReadyCB)());
~Threader();
void start();
void stop();
private:
void renderLoop();
atomic<bool> isThreading;
void(*frameReadyCB)();
thread myThread;
};
// Threader.cpp
#include "Threader.h"
Threader::Threader(void(*frameReadyCB)()) :
isThreading{ false },
frameReadyCB{ frameReadyCB }
{
}
Threader::~Threader()
{
if (myThread.joinable()) myThread.join();
}
void Threader::start()
{
isThreading = true;
myThread = thread(&Threader::renderLoop, this);
}
void Threader::stop()
{
isThreading = false;
if (myThread.joinable()) myThread.join();
}
void Threader::renderLoop()
{
while (isThreading) {
frameReadyCB();
}
}
And then here's my test javascript that uses it:
// ThreadedDll.js
const ffi = require('ffi');
const path = require('path');
const DllPath = path.resolve(__dirname, '../dll/ThreadedDll.dll');
// Map the library functions in the way that FFI expects
const DllMap = {
'beginSession': [ 'void', [ 'pointer' ] ],
'endSession': [ 'void', [] ],
'startStreaming': [ 'void', [] ],
'stopStreaming': [ 'void', [] ],
};
// Create the Library using ffi, the DLL, and the Function Table
const DllLib = ffi.Library(DllPath, DllMap);
class ThreadedDll {
constructor(args) {
this.frameReadyCB = ffi.Callback('void', [], () => {
console.log('Frame Ready');
});
DllLib.beginSession(this.frameReadyCB);
}
startStreaming() {
DllLib.startStreaming();
}
stopStreaming() {
DllLib.stopStreaming();
}
endSession() {
DllLib.endSession();
}
}
module.exports = ThreadedDll;
// app.js
const ThreadedDll = require('./ThreadedDll');
setTimeout(() => {
const threaded = new ThreadedDll();
console.log('start stream');
threaded.startStreaming();
setTimeout(() => {
console.log('stop stream');
threaded.stopStreaming();
console.log('end session');
threaded.endSession();
}, 1000);
}, 2000);
And it is in app.js that the main electron process runs. I would expect to see
start stream
Frame Ready (3800)
stop stream
end session
But it shows no end session
. However if I remove the line frameReadyCB()
within the c++ it works as expected. So somehow the ffi callback reference is screwing up the multithreading environment. Would love to get some thoughts on this. Thanks!