【问题标题】:Storing a JS Callback to be called later in Node C++ Addon存储稍后在 Node C++ 插件中调用的 JS 回调
【发布时间】:2020-09-11 02:59:53
【问题描述】:

我正在为 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 回调函数所需的变量。然后将其作为inClientData 传递给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",
        ],
      },
    },
  ],
}

【问题讨论】:

    标签: c++ node.js objective-c objective-c++ node-addon-api


    【解决方案1】:

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

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

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

    或者,除了手动保留/释放 Obj-C 对象之外,您还可以通过创建一个您初始化一次的 static 对象来重构您的代码,以便 Obj-C 对象与整个 Node 模块一样长(例如在您的Initialize 函数中)或在NODE_MODULE_INITIALIZER 中为“context-aware addon”创建它。然后您可以在将此对象作为void* context 传递时使用__bridge,而不必担心保留和释放。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-07
      • 1970-01-01
      • 1970-01-01
      • 2018-12-03
      • 1970-01-01
      • 2017-09-15
      相关资源
      最近更新 更多