Implementation of napi from c++ addon

Posted by Lukey on Fri, 18 Feb 2022 01:57:11 +0100

Node. The napi of JS greatly facilitates the writing of c++ addon, making users no longer need to face complex v8. This paper analyzes the use of napi and what napi does through an example.

1. Functions exported to js

#include <node_api.h>
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

The above code is a general pattern when using napi. We only need to implement the Init function (of course, it can also be called other names). Next, let's look at the implementation of Init.

napi_value Init(napi_env env, napi_value exports) {
  napi_value func;
  // Create a function and set it to the value of the getArray property of the exports object
  napi_create_function(env,
                      NULL,
                      NAPI_AUTO_LENGTH,
                      newArray,
                      NULL,
                      &func);
  napi_set_named_property(env, exports, "getArray", func);
  return exports;
}

napi_create_function is also an api provided by NaPi. Its function is to create a function. For details, please refer to the NaPi documentation. Then export this function to js for use. The name is getArray. When js executes getArray, it will execute newArray function.

2. Implementation of newarray

static napi_value newArray(napi_env env, napi_callback_info info) {
   size_t argc = 1;
   napi_value args[1];
   // Get the input parameters of js layer. Here is an example
   napi_get_cb_info(env, info, &argc, args, NULL, NULL);
   int len;
   // js passes in a number. v8 is converted into an object. Here, the input parameter is converted into int again
   napi_get_value_int32(env, args[0], &len);
   napi_value ret;
   // Create an array
   napi_create_array(env, &ret);
   // Set the initial value of the array according to the js input parameter
   for (int i = 0; i < len; i++) {
     napi_value num;
     napi_create_int32(env, i, &num);
     napi_set_element(env, ret, i, num);
   }

  return ret;
}

3. Use c++ addon

const { getArray } = require('./build/Release/test.node');
console.log(getArray(20)); 

Execute the above code and finally output

[
   0,  1,  2,  3,  4,  5,  6,
   7,  8,  9, 10, 11, 12, 13,
  14, 15, 16, 17, 18, 19
]

4 Analysis

The above code is not complicated. This article mainly analyzes the api provided by napi to see what napi has done. The principles of many APIs are similar. Here we only take the api of array as an example. Because the parameters used in the v8 api are basically the objects provided by v8. What napi does is actually help us deal with the transformation of these objects. Let's first look at napi_ create_ Implementation of array.

// Create an array corresponding to js
napi_status napi_create_array(napi_env env, napi_value* result) {
  // Call the v8 interface v8::Array::New to create an array object, then convert it to the type of napi, and set the return value
  *result = v8impl::JsValueFromV8LocalValue(
      v8::Array::New(env->isolate));
  return napi_clear_last_error(env);
}

We see napi_ create_ The implementation of array is very simple, which is to encapsulate the v8 interface, then convert it to the type of napi, and finally clear the error information. This is the typical api usage of napi. It mainly includes the following
1. The input parameter needs to pass in the env object and a secondary pointer napi_ The interface is used to save the returned value *. The return value of napi is not returned through the return of the function body. Return returns the execution status of the api (success or failure).
2 api for handling v8
3 clear or return error message
Each time the api provided by napi is executed, if there is an error in the execution, it will be executed through napi_set_last_error is set to env and the error code is returned. If not, it is through napi_clear_last_error clears the error message and returns napi_ok. Let's look at the implementation

// Sets the error message of the current function call
static inline napi_status napi_set_last_error(napi_env env, napi_status error_code,
                                uint32_t engine_error_code = 0,
                                void* engine_reserved = nullptr) {
  env->last_error.error_code = error_code;
  env->last_error.engine_error_code = engine_error_code;
  env->last_error.engine_reserved = engine_reserved;
  return error_code;
}

// Clear the error message of the last call
static inline napi_status napi_clear_last_error(napi_env env) {
  env->last_error.error_code = napi_ok;

  // TODO(boingoing): Should this be a callback?
  env->last_error.engine_error_code = 0;
  env->last_error.engine_reserved = nullptr;
  return napi_ok;
}

If the caller generates an error after calling the api, it can use NaPi_ get_ last_ error_ The info interface gets the error information of executing the api.

// Get the error information of the last calling function
napi_status napi_get_last_error_info(napi_env env,
                                     const napi_extended_error_info** result) {
  // Initialize to illegal value
  const int last_status = napi_detachable_arraybuffer_expected;
  // Set the error description information according to the error code (every time the api is called, the result is saved to env).
  env->last_error.error_message =
      error_messages[env->last_error.error_code];

  *result = &(env->last_error);
  return napi_ok;
}

To get back to business, call NaPi_ create_ After array, we get a return value, such as ret below.

napi_value ret;
napi_create_array(env, &ret);

napi was analyzed before_ Value is essentially a first-order pointer. Next, let's look at how to use the array from napi. We can use napi_set_element sets the contents of the array.

// ret is an array, i is an index, and num is a NaPi_ The value variable is essentially a v8 object, that is, the value corresponding to the index
napi_set_element(env, ret, i, num);

Let's look at NaPi_ set_ Implementation of element.

// Set the value corresponding to key. Key is a number
napi_status napi_set_element(napi_env env,
                             napi_value object,
                             uint32_t index,
                             napi_value value) {
  v8::Local<v8::Context> context = env->context();
  v8::Local<v8::Object> obj;
  // napi_value object is converted to v8 Object, and the array inherits Object
  CHECK_TO_OBJECT(env, context, obj, object);
  // Put the value NaPi_ Convert value to v8 object
  v8::Local<v8::Value> val = v8impl::V8LocalValueFromJsValue(value);
  // Call the Set method of v8 Object object to Set the properties of the object, that is, the elements of the array
  auto set_maybe = obj->Set(context, index, val);
  // Execution result processing
  RETURN_STATUS_IF_FALSE(env, set_maybe.FromMaybe(false), napi_generic_failure);

  return GET_RETURN_STATUS(env);
}

From the above analysis, we can roughly see some laws in the implementation of napi. The logic of get api is to call v8 interface to get v8 type objects, and then turn them into napi_ The value type is returned to the caller, and the api of set is the incoming napi_value type, and then converted to v8 type object.

napi is almost always implemented in JS_ native_ api_ v8. In CC, interested students can have a look. The implementation of most APIs is not complex. They can understand JS_ native_ api_ v8. The implementation of CC not only makes us better use napi, but also makes us better understand the use and principle of V8.

Topics: node.js