Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] What's the right way to asynchronously emit events from addon to Javascript code? #1218

Closed
haoxi911 opened this issue Oct 7, 2022 · 2 comments

Comments

@haoxi911
Copy link

haoxi911 commented Oct 7, 2022

I am trying to do something like node-usb-detection but using node-addon-api. When a USB drive was unmounted from the computer, the addon code should send an event to the Javascript code.

Here is what I have:

import { EventEmitter } from 'events';

const native = ...; // load addon module
const emitter = new EventEmitter();
emitter.on('unmount', (volume: string) => {
  console.log('unmount', volume);
});
native.bindEmitter(emitter);
// Main.h
extern Napi::Function gEmitter;
extern napi_env gEnv;

// Main.cc
#include <napi.h>
#include <node_api.h>
#include "Main.h"

Napi::Function gEmitter;
napi_env gEnv;

// Q1: is it okay to cache JS emitter as a global variable in addon?
// Q2: is it necessary to cache the `env` object?
void BindEmitter(const Napi::CallbackInfo& info) {
  gEnv = info.Env();
  gEmitter = info[0].As<Napi::Function>();
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "bindEmitter"), Napi::Function::New(env, BindEmitter));
  return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
void Napi_DiskUtility::ObserveUnmount(const Napi::CallbackInfo &info) {

  [[[NSWorkspace sharedWorkspace] notificationCenter] addObserverForName:NSWorkspaceDidUnmountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
    NSDictionary *userInfo = [(NSDictionary *)notification valueForKey:@"userInfo"];
    NSString *devicePath = [userInfo valueForKey:@"NSDevicePath"];
    NSLog(@"Drive unmounted: %@", devicePath);

      // Q3: this line crashes, the error is `Fatal error in V8: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope`
      gEmit.Call({
         Napi::String::New(gEnv, "unmount"), Napi::String::New(gEnv, devicePath.UTF8String)
      });

  }]; 
}

Please refer to the 3 questions in my code. I believe I am not using handle scope properly, but I am not sure what is the right way to make it right. Ideally I want to use emitter to emit events to Javascript from any native threads.

Further more, if this demo can work, I would extend it and wrap a logging method so that all addon logs can be emitted to Javascript and be processed using the same Javascript logging package.

Is it possible? Thank you for your help!

@Julusian
Copy link

Julusian commented Oct 9, 2022

Q1) No. doing that caching means that multiple contexts (typically worker-threads, might be other ways too) will get their contexts mixed up, as this global will be shared across them all.
Q2) No. anywhere you are allowed to use it, you will have easy access to it

Q3) You aren't allowed to do this call from other threads.
Instead you can do this via a TypedThreadSafeFunction, as you can essentially call that function with some non-node data types, and in the callback you are allowed to use env and node-api types so can convert it to node-api types and do your final call.

Perhaps a useful reference is a PR I was working on to convert node-usb-detection to node-api MadLittleMods/node-usb-detection#127. I dont know how good this code is, and I think there were some memory leaks, but it did work and I think is mostly sane

@haoxi911
Copy link
Author

@Julusian Thank you! I can confirm TypedThreadSafeFunction works for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants