0

我正在为 macOS 创建一个 Node C++ 插件,因此我将 Objective-C 与 C++ 和 Node Addon API 混合使用。

我想为 Node JS 提供一个函数,该函数接收一个回调,以便稍后在调用 Obj-C 观察者时调用。这就是我试图实现这一目标的方式:

#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)

}

我创建了一个JSCallback类来保存触发 JS 回调函数所需的变量。然后将其作为inClientDatafor传递AudioObjectAddPropertyListener

然后当audioOutputDeviceChanged被调用时,我尝试使用我存储的变量来触发 JS 回调。但是,当我这样做时,JS 脚本崩溃,并且只打印以下内容(无堆栈跟踪):

#
# Fatal error in v8::HandleScope::CreateHandle()
# Cannot create a handle without a HandleScope
#

我认为这可能会发生,因为当setOnAudioOutputDeviceChange返回时,它会释放(或类似的东西)变量(上下文、cb 和隔离)。因此它们在函数之外无法使用。我怎样才能解决这个问题?

如果需要,这是我使用插件的 JS 代码:

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);

这是我的binding.gyp文件,尽管我怀疑它是相关的:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "addon.mm",
      ],
      "xcode_settings": {
        "OTHER_CFLAGS": [
          "-ObjC++",
        ],
      },
      "link_settings": {
        "libraries": [
          "-framework Foundation",
          "-framework AVFoundation",
        ],
      },
    },
  ],
}
4

1 回答 1

0

您的诊断对我来说似乎是合理的:“当setOnAudioOutputDeviceChange返回时,它会释放(或类似的)变量(上下文、cb 和隔离)。因此它们在函数之外无法使用”。

这些变量由jsCb对象保存,但您将其AudioObjectAddPropertyListener()作为contextwhich is a传递给它void*,因此没有任何东西保留该对象。相反,尝试将它作为(__bridge_retained void*)jsCborCFBridgingRetain(jsCb)传递,这将在添加回调之前增加引用计数。然后你可以在你的 audioOutputDeviceChanged 函数中检索它JSCallback *jsCb = (__bridge JSCallback *)inClientData;,它既不会保留也不会释放它。最后,在移除音频属性监听器时,您应该通过__bridge_transferCFRelease/释放对象CFBridgingRelease

此处此处的文档中描述了桥接转换,您可以通过搜索 Stack Overflow 和其他地方找到更多示例。

或者,不是手动保留/释放 Obj-C 对象,您可以通过创建一个static您初始化一次的对象(例如在您的Initialize函数) 或NODE_MODULE_INITIALIZER为“上下文感知插件”创建它。然后你可以__bridge在传递这个对象时使用,void* context而不用担心保留和释放。

于 2020-09-16T18:04:37.427 回答