Skip to content

Instantly share code, notes, and snippets.

@gabrielschulhof
Created June 12, 2018 18:17
Show Gist options
  • Save gabrielschulhof/d24c1f66b9f48896318e332d8a756b87 to your computer and use it in GitHub Desktop.
Save gabrielschulhof/d24c1f66b9f48896318e332d8a756b87 to your computer and use it in GitHub Desktop.
N-API multi-context addon example
#include <stdlib.h>
#include <node_api.h>
// As a convenience, wrap N-API calls such that they cause Node.js to abort
// when they are unsuccessful.
#define NAPI_CALL(call) \
do { \
napi_status status = call; \
if (status != napi_ok) { \
napi_fatal_error(#call, NAPI_AUTO_LENGTH, "failed", NAPI_AUTO_LENGTH); \
} \
} while (0)
// We store addon state in a structure instead of defining global static
// variables.
typedef struct {
napi_ref callback_function;
size_t call_count;
} AddonState;
// Node.js will call this function when the addon unloads, giving us a chance to
// free our "global static" variables and the structure holding them. This is
// currently not yet implemented in Node.js.
static void DestroyState(napi_env env, void* data, void* hint) {
AddonState* state = data;
// If we have a JavaScript callback, we call it with the special value "free"
// before destroying the reference we're holding. This illustrates the fact
// that we still have access to JavaScript even as the addon is unloaded.
if (state->callback_function != NULL) {
napi_value js_cb, string, undefined;
NAPI_CALL(napi_get_reference_value(env, state->callback_function, &js_cb));
NAPI_CALL(napi_create_string_utf8(env, "free", NAPI_AUTO_LENGTH, &string));
NAPI_CALL(napi_get_undefined(env, &undefined));
NAPI_CALL(napi_call_function(env, undefined, js_cb, 1, &string, NULL));
NAPI_CALL(napi_delete_reference(env, state->callback_function));
}
free(state);
}
// Call the previously specified callback, if any, and increment the call count.
static napi_value CallCallback(napi_env env, napi_callback_info info) {
size_t argc = 0;
void* data;
AddonState* state;
// Retrieve the addon state. This binding takes no parameters.
NAPI_CALL(napi_get_cb_info(env, info, &argc, NULL, NULL, &data));
state = data;
// If we have a reference to a JavaScript callback then retrieve it from our
// reference, call it, and increment the call count.
if (state->callback_function) {
napi_value js_cb, undefined;
NAPI_CALL(napi_get_reference_value(env, state->callback_function, &js_cb));
NAPI_CALL(napi_get_undefined(env, &undefined));
NAPI_CALL(napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
state->call_count++;
}
// Return to JavaScript the current call count.
napi_value return_value;
NAPI_CALL(napi_create_uint32(env, state->call_count, &return_value));
return return_value;
}
// Specify which JavaScript callback to call henceforth.
static napi_value SetCallback(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value js_cb;
napi_valuetype js_cb_type;
void* data;
AddonState* state;
// Retrieve the addon state and the JavaScript callback.
NAPI_CALL(napi_get_cb_info(env, info, &argc, &js_cb, NULL, &data));
state = data;
// Delete our reference to any existing JavaScript callback and reset the call
// count.
if (state->callback_function != NULL) {
NAPI_CALL(napi_delete_reference(env, state->callback_function));
state->callback_function = NULL;
state->call_count = 0;
}
// Set a new callback if js_cb is a JavaScript function, do nothing if it is
// null or undefined, and throw a fatal error if it is anything else.
NAPI_CALL(napi_typeof(env, js_cb, &js_cb_type));
if (js_cb_type == napi_function) {
NAPI_CALL(napi_create_reference(env, js_cb, 1, &state->callback_function));
} else if (!(js_cb_type == napi_null || js_cb_type == napi_undefined)) {
napi_fatal_error("SetCallback", NAPI_AUTO_LENGTH,
"Callback is neither null, nor undefined, nor a function",
NAPI_AUTO_LENGTH);
}
return NULL;
}
// Using NAPI_MODULE_INIT() instead of NAPI_MODULE() ensures that the resulting
// addon can be loaded multiple times within the same Node.js process.
NAPI_MODULE_INIT() {
// Allocate and initialize our "global static" variables.
AddonState* state = malloc(sizeof(*state));
state->callback_function = NULL;
state->call_count = 0;
// Ensure the state gets freed when this addon is unloaded by attaching it to
// the exports object we're about to populate.
NAPI_CALL(napi_wrap(env, exports, state, DestroyState, NULL, NULL));
// We pass the addon state into all bindings defined by this addon.
napi_property_descriptor properties[] = {
{ "setCallback", NULL, SetCallback, NULL, NULL, NULL, napi_enumerable, state },
{ "callCallback", NULL, CallCallback, NULL, NULL, NULL, napi_enumerable, state },
};
// Attach the above-defined properties to the "exports" object.
NAPI_CALL(napi_define_properties(env, exports,
sizeof(properties) / sizeof(*properties), properties));
return exports;
}
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.c' ]
}
]
}
const addon = require('./build/Release/binding');
console.log("Setting callback");
let callCount = 0;
addon.setCallback(() => console.log("Callback called"));
callCount = addon.callCallback();
console.log(`callCount: ${callCount}`);
callCount = addon.callCallback();
console.log(`callCount: ${callCount}`);
console.log("Setting null callback");
addon.setCallback(null);
callCount = addon.callCallback();
console.log(`callCount: ${callCount}`);
callCount = addon.callCallback();
console.log(`callCount: ${callCount}`);
console.log("Setting another callback");
addon.setCallback(() => console.log("Another callback"));
callCount = addon.callCallback();
console.log(`callCount: ${callCount}`);
callCount = addon.callCallback();
console.log(`callCount: ${callCount}`);
{
"name": "n-api-multicontext-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"install": "node-gyp rebuild"
},
"author": "",
"license": "Apache-2.0",
"gypfile": true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment