/node-addon-example

An example of a C++ background thread app wrapped as a node addon using the napi interface

Primary LanguageC++MIT LicenseMIT

napi_cfrunloop_addon

Overview

This project is an exercise in building a C/C++ naive code Addon for node using the new N-API interface.

There are a number of examples of such Addons available out there but many of them are very simple and do not get to grips with issues that a meaningful Addon might face. So I wanted this example to be a bit more substantial than simply calling a C function.

In addition I wanted to understand how to use the C++ Object model built on top of N-API and available at https://github.com/nodejs/node-addon-api.

The Example Addon

Here is the scenario that this example addresses.

  • We have a C++ singleton class that provides an asynchronous service, in this case an interval timer service, to its clients.
  • Client code gives the instance of the class a number interval request and repeat counts and the instance counts down the intervals and calls back to the client code after every interval expiration and repeats this until the number of repeats is exhausted.
  • Multiple clients can ask for interval services simultaneously from the single instance.
  • The class instances runs as a separate thread which is initiated when the class instances is created.
  • The class instances creates a CFRunLoop and runs a CFTimer on that CFRunloop in order to count down the various time intervals. So obviously this project is for Mac/OSX only. At some point I will replace the CFRunloop with a Boost::Asio io_service.
  • The N-API Addon code must:
    • Create and start the class instance
    • organize for javascript code to pass interval requests to the class instance and its CFRunLoop
    • Pass the interval expired callback from the class instance to javascript land.
    • stop the C++ class instance thread/CFRunloop eventually

The Native Addon

The native addon provides three functions start, cancel and stop which request interval services and cancel a single request and stop the addon background thread.

start

The start function provides the Addon with an array of interval requests, and a callback function to be called by the Addon when an interval expires.

The start functions return value is a opaque value that identifies this start calls context within the Addon.

Behind the scenes the first call to start creates an instance of the C++ class, starts a background thread and CFRunloop. Subsequence start calls reuse the existing C++ class instance.

type Opaque = Any

type IntervalRequest = {
	ident : number,    // unique number to identify this request 
	interval : number, // the number of seconds the interval should last
	repeat : number,   // the number of times each interval should be repeated
	replyobj : Any     // any javascript object
}

type EventType = 'interval'|'expired'|'cancelled'|'cancelled_last'

type IntervalEvent = {	
	type : EventType			
	ident : number,				// identifying which interval the event is associated with
	thread : string 			 // a hex string which identifies the background thread
	timestamp : string			 // a text representation of the time at which the event was created
	replyobj : any				 // the reply object provided with the interval object
}
start(requests IntervalRequest[], (event IntervalEvent): void) : Opaque 

cancel

cancel(opaque: Opaque, (event IntervalEvent): void): void

The cancel function stops interval processing for a single Opaque. When the cancel is complete two events are propagated to signal this fact:

  • an event of type 'cancelled' or 'cancelled_last' is propagated to the cancel() functions callback.

  • an event of type 'cancelled' or 'cancelled_last' is propagated to the start() functions callback.

If the Opaque passed to the cancel call is the only one active the event will be cancelled_last otherwise it will be cancelled.

I chose to pass events to both the start() and cancel() callbacks as these are asynchronous and only by placing the cancelled event in the start() functions event stream is it evident that this is the last event for a given Opaque.

stop

The stop function takes a single argument, the opaque instance returned by a start function call, and stops all outstanding interval requests associated with the given Opaque, cleans up all C/C++ resources associated with the Opaque and stops the background thread and CFRunloop.

The stop() function must be called exactly once for each call to start otherwise the Addon will be left in an undefined state.

Usage

const Addon = require('napi_workerthread_addon');

      const arg0 = {
        schedule : [
          {ident : 0, interval : 2,  repeat: 15, replyobj : { time : Date.now()} },
          {ident : 1, interval : 2,  repeat: 17, replyobj : { time : Date.now()} },
          {ident : 2, interval : 2,  repeat: 19, replyobj : { time : Date.now()} },
        ]
      }

const instance = Addon.start(arg0, (event) => {
  console.log(util.inspect(event))
  if ((event.type === "cancelled_last)||(event.type === "cancelled")) {
  	Addon.stop(instance)
  }
}); // To start receiving events

// to cancel events after a period of time and before the schedule is exhausted
setTimeout(() => {
  Addon.cancel(instance, (event) => {
	  console.log(util.inspect(event))
  });
}, 7000);

The Natve Addon - Additional Functions

The native Addon provides two further functions, config and demo that illustrate additional facets of Addon construction.

config demonstrates how to pass values to, and retrieve values from, an Addon.

demo illustrates various ways of passing errors/exceptions/Error from the Addon back to js land.

Installation

This project is not yet an npm package so you will need to clone the repo or install using a URL. This is not really a problem as this project is really for demonstration purposes rather than a useable component.

Debugging with XCode

This youtube video explains how to derive an xcode project from the binding.gyp specification file and how to run and debug a nodejs test program using xcode.

https://www.youtube.com/watch?v=DND2H2-XfAc

The folder build-copy has an xcode project file (binding.xcodeproj) which is setup to run node and one of the js test programs with the main target. To use this copy build-copy to build and then local the xcode project file.

The command npm run xcode or node-gyp configure -- -f xcode will replace the build folder and recreate the xcode project file inside that folder. But this re-created project file will not have the main target setup to run a test program using node.