I'm creating a Node C++ addon for macOS, so I'm using Objective-C mixed with C++ and the Node Addon API.
I want to provide a function for Node JS, that receives a callback to be called later when an Obj-C observer is called. This is how I tried to achieve this:
#include <node.h>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface JSCallback:NSObject {
//Instance variables
NSString *testStr;
v8::Local<v8::Context> context;
v8::Local<v8::Function> fn;
v8::Isolate* isolate;
}
@property(retain, nonatomic, readwrite) NSString *testStr;
@property(nonatomic, readwrite) v8::Local<v8::Context> context;
@property(nonatomic, readwrite) v8::Local<v8::Function> fn;
@property(nonatomic, readwrite) v8::Isolate* isolate;
@end
@implementation JSCallback
@synthesize testStr;
@synthesize context;
@synthesize fn;
@synthesize isolate;
@end
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Function;
using v8::Context;
using v8::Value;
static OSStatus audioOutputDeviceChanged(
AudioObjectID inObjectID,
UInt32 inNumberAddresses,
const AudioObjectPropertyAddress* inAddresses,
void* __nullable inClientData
) {
JSCallback *jsCb = *((__unsafe_unretained JSCallback **)(&inClientData));
printf("%s", [jsCb.testStr UTF8String]);
jsCb.fn->Call(jsCb.context, Null(jsCb.isolate), 0, {}).ToLocalChecked();
return noErr;
}
void setOnAudioOutputDeviceChange(const FunctionCallbackInfo<Value>& args) {
AudioObjectPropertyAddress address = {
kAudioHardwarePropertyDefaultOutputDevice,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMaster,
};
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Function> cb = Local<Function>::Cast(args[0]);
// cb->Call(context, Null(isolate), 0, {}).ToLocalChecked();
JSCallback *jsCb = [[JSCallback alloc]init];
jsCb.testStr = @"a test string #002";
jsCb.context = context;
jsCb.fn = cb;
jsCb.isolate = isolate;
OSStatus status = AudioObjectAddPropertyListener(
kAudioObjectSystemObject,
&address,
&audioOutputDeviceChanged,
jsCb
);
if (status != noErr) {
NSException *e = [NSException
exceptionWithName:@"OSStatus Error"
reason:[NSString stringWithFormat:@"OSStatus Error (status: %d)", status]
userInfo:nil];
@throw e;
}
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "setOnAudioOutputDeviceChange", setOnAudioOutputDeviceChange);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}
I created the JSCallback
class to hold the variables needed to trigger the JS callback function. And then pass it as the inClientData
for AudioObjectAddPropertyListener
.
Then when audioOutputDeviceChanged
is called, I try to trigger the JS callback using the variables I stored. However, when I do so, the JS script crashes, and only prints the following (no stack trace):
#
# Fatal error in v8::HandleScope::CreateHandle()
# Cannot create a handle without a HandleScope
#
I think this could be happening because when setOnAudioOutputDeviceChange
returns, it deallocates (or something of the sort) the variables (context, cb, and isolate). And thus they're unusable outside of the function. How can I get around this?
If needed, here's my JS code that uses the addon:
const addon = require('./addon/build/Release/addon');
addon.setOnAudioOutputDeviceChange(() => {
console.info('setOnAudioOutputDeviceChange called');
});
setTimeout(() => {
// This keeps the JS script alive for some time
console.log('timedout');
}, 200000);
And this is my binding.gyp
file, though I suspect it's relevant:
{
"targets": [
{
"target_name": "addon",
"sources": [
"addon.mm",
],
"xcode_settings": {
"OTHER_CFLAGS": [
"-ObjC++",
],
},
"link_settings": {
"libraries": [
"-framework Foundation",
"-framework AVFoundation",
],
},
},
],
}