1# Secure and Efficient N-API Development 2 3## Introduction 4 5N-API, short for Node.js Addon Programming Interface, is a set of C++ APIs provided by Node.js. Shipped with the capabilities of the [V8 engine](https://v8.dev/docs), it is used to build Node.js native addons. By drawing on N-API, you can write high-performance Node.js modules in C++ while maintaining compatibility with Node.js. 6 7N-API provided in [ArkCompiler ArkTS runtime](https://gitee.com/openharmony/arkcompiler_ets_runtime), shipped with the capabilities of the Ark engine, provides the same basic functionality as described in the [Node.js documentation](https://nodejs.org/api/n-api.html). 8 9This document is intended to provide guidance on secure and efficient N-API development. Designed with carefully curated use cases, it is structured into four parts: object lifetime management, cross-language invocation overhead, asynchronous operation, and thread security. 10 11## Object Lifetime Management 12 13As N-API calls are made, handles to objects in the VM heap may be returned as **napi_value**s. These handles control the lifetime of the objects. Object handles are associated with a scope. The default lifespan of the scope is tied to the lifespan of the native method. In some real-world cases, however, an object may require a scope with a shorter or longer lifespan than that of the native method. This section describes how to use N-API functions to change the object lifespan. 14 15### Shortening Object Lifespan 16 17You can use **napi_open_handle_scope** and **napi_close_handle_scope** to manage the lifetime of **napi_value**, so as to minimize the object lifespan and avoid memory leak. 18 19For example, consider a method that has a **for** loop, which iterates through the elements in a large array: 20```cpp 21for (int i = 0; i < 1000000; i++) { 22 napi_value result; 23 napi_status status = napi_get_element(env, object, i, &result); 24 if (status != napi_ok) { 25 break; 26 } 27 // do something with element 28} 29``` 30 31In this case, a large number of handles may be created, consuming substantial resources. To handle this case, N-API provides the function to establish a new scope to which newly created handles will be associated. Once those handles are no longer needed, the scope can be closed and any handles associated with the scope are invalidated. 32 33* The methods available to open and close scopes are **napi_open_handle_scope** and **napi_close_handle_scope**, respectively. 34* N-API only supports a single nested hierarchy of scopes. There is only one active scope at any time, and all new handles will be associated with that scope while it is active. 35* Scopes must be closed in the reverse order from which they are opened. In addition, all scopes created within a native method must be closed before returning from that method. 36 37The following is an optimization based on the earlier example. In this example, at most a single handle is valid throughout the execution of the loop. 38```cpp 39// When N-API is frequently called to create JS objects in the for loop, add handle_scope to release in a timely manner resources that are no longer used. 40// In the following code, the lifespan of the local variable res ends at the end of each loop. Therefore, a scope is added to release the JS object in time to prevent memory leak. 41for (int i = 0; i < 1000000; i++) { 42 napi_handle_scope scope; 43 napi_status status = napi_open_handle_scope(env, &scope); 44 if (status != napi_ok) { 45 break; 46 } 47 napi_value result; 48 status = napi_get_element(env, object, i, &result); 49 if (status != napi_ok) { 50 break; 51 } 52 // do something with element 53 status = napi_close_handle_scope(env, scope); 54 if (status != napi_ok) { 55 break; 56 } 57} 58``` 59There are cases where a handle from a scope needs to outlive the scope, for example, in nested loop scenarios. For this purpose, you can use **napi_open_escapable_handle_scope** and **napi_close_escapable_handle_scope** to open and close an escapable scope, where the lifespan of the defined handle is the same as that of the outer scope. 60 61### Extending Object Lifespan 62 63You can extend the lifespan of an **napi_value** object, by creating an **napi_ref** reference to a **napi_value** object. All **napi_ref** references, created by calling **napi_create_reference**, must be deleted manually by calling **napi_delete_reference**. Failure to delete a reference may results in memory leak. 64 65#### Use Case 1: Saving napi_value 66 67Use **napi_define_class** to create a constructor and save it. You can use the saved constructor to call **napi_new_instance** to create an instance. However, if the constructor is saved as a **napi_value**, it will be destructed once it exceeds the scope of the native method. In this case, continuing to use the constructor will result in a wild pointer. To avoid this issue, it is recommended that you: 68* Save the constructor as an **napi_ref** reference. 69* Manage the lifespan of the constructor object on your own so that it is not restricted by the scope of the native method. 70```cpp 71// 1. Save the constructor as an napi_ref reference. 72static napi_value TestDefineClass(napi_env env, 73 napi_callback_info info) { 74 napi_status status; 75 napi_value result, return_value; 76 77 napi_property_descriptor property_descriptor = { 78 "TestDefineClass", 79 NULL, 80 TestDefineClass, 81 NULL, 82 NULL, 83 NULL, 84 napi_enumerable | napi_static, 85 NULL}; 86 87 NODE_API_CALL(env, napi_create_object(env, &return_value)); 88 89 status = napi_define_class(NULL, 90 "TrackedFunction", 91 NAPI_AUTO_LENGTH, 92 TestDefineClass, 93 NULL, 94 1, 95 &property_descriptor, 96 &result); 97 SaveConstructor(env, result); 98 ... 99} 100``` 101```cpp 102// 2. Manage the lifespan of the constructor object on your own. 103napi_status SaveConstructor(napi_env env, napi_value constructor) { 104 return napi_create_reference(env, constructor, 1, &g_constructor); 105}; 106 107napi_status GetConstructor(napi_env env) { 108 napi_value constructor; 109 return napi_get_reference_value(env, g_constructor, &constructor); 110}; 111``` 112 113#### Use Case 2: napi_wrap 114 115You can use **napi_wrap** to wrap a native instance in a JavaScript object. When the JavaScript object is garbage-collected, use a callback to free the native instance. In effect, the **napi_wrap** API creates an **napi_ref** reference. You can designate the system to manage the created **napi_ref** reference. Alternatively, you can release the reference manually. 116```cpp 117// Usage 1: napi_wrap does not need to receive the created napi_ref reference. The last parameter is nullptr. The created napi_ref reference is managed by the system and does not need to be manually released. 118napi_wrap(env, jsobject, nativeObject, cb, nullptr, nullptr); 119 120// Usage 2: napi_wrap needs to receive the created napi_ref reference. The last parameter is not nullptr. The created napi_ref reference needs to be manually released. Otherwise, memory leak occurs. 121napi_ref result; 122napi_wrap(env, jsobject, nativeObject, cb, nullptr, &result); 123// When the JavaScript object and result are no longer used, call napi_remove_wrap to release the result in a timely manner. 124napi_value result1; 125napi_remove_wrap(env, jsobject, result1) 126``` 127 128## Cross-Language Invocation Overhead 129 130### API Call 131 132Cross-language invocation is the process where code written in different programming languages calls and interacts with each other in an application. For example, ArkTS invoking C++ is one of the cross-language invocation modes. Using N-API to call functions causes certain overhead, because context switching, parameter transfer, function invoking, and return value processing are required. Currently, there are three scenarios where C++ code is called from ArkTS through N-API: call C++ APIs, listen for C++ APIs , and receive C++ callbacks. Frequent cross-language API calls may detract from service performance. Therefore, properly design the API call frequency. 133 134### Value Conversion 135 136When using N-API to convert data between ArkTS and C++, take note of the following: 137* Reduce the number of data conversion times: Frequent data conversion may cause performance deterioration. You can process data in batches or use a more efficient data structure to optimize performance. 138* Avoid unnecessary data replication: During data conversion, use N-API to directly access the original data instead of creating a new data copy. 139* Use cache: If some data is used in multiple conversions, you can store it in the cache to avoid repeated data conversions. In this way, unnecessary calculations can be reduced, leading to better performance. 140 141## Asynchronous Operations 142I/O- and CPU-intensive tasks must be processed asynchronously. Otherwise, the main thread will be blocked. N-API provides the asynchronous capability, allowing an application to continue executing other tasks instead of being blocked when executing a time-consuming task. When an asynchronous operation is complete, the application receives a notification and can process the operation result. 143 144### Example of Asynchronous Processing 145 146You can refer to the following example to implement time-consuming tasks in asynchronous mode. The logic includes the following steps: 147* Call **napi_create_promise** to create a promise. When a promise is created, a "deferred" object is created and returned alongside the promise. 148* Execute the time-consuming task and pass the execution result to the promise. 149* Call **napi_resolve_deferred** or **napi_reject_deferred** to resolve or reject the created promise and release the deferred object. 150 151```cpp 152// Transfer data between executeCB and completeCB. 153struct AddonData { 154 napi_async_work asyncWork = nullptr; 155 napi_deferred deferred = nullptr; 156 napi_ref callback = nullptr; 157 158 double args[2] = {0}; 159 double result = 0; 160}; 161 162// 2. Execute the time-consuming task and pass the execution result to the promise. 163static void addExecuteCB(napi_env env, void *data) { 164 AddonData *addonData = (AddonData *)data; 165 addonData->result = addonData->args[0] + addonData->args[1]; 166}; 167 168// 3. Call napi_resolve_deferred or napi_reject_deferred to resolve or reject the created promise and release the deferred object. 169static void addPromiseCompleteCB(napi_env env, napi_status status, void *data) { 170 AddonData *addonData = (AddonData *)data; 171 napi_value result = nullptr; 172 napi_create_double(env, addonData->result, &result); 173 napi_resolve_deferred(env, addonData->deferred, result); 174 175 if (addonData->callback != nullptr) { 176 napi_delete_reference(env, addonData->callback); 177 } 178 179 // Delete the asynchronous work. 180 napi_delete_async_work(env, addonData->asyncWork); 181 delete addonData; 182 addonData = nullptr; 183}; 184 185// 1. Call napi_create_promise to create a promise. When a promise is created, 186// a "deferred" object is created and returned alongside the promise. 187static napi_value addPromise(napi_env env, napi_callback_info info) { 188 size_t argc = 2; 189 napi_value args[2]; 190 napi_value thisArg = nullptr; 191 napi_get_cb_info(env, info, &argc, args, &thisArg, nullptr); 192 193 napi_valuetype valuetype0; 194 napi_typeof(env, args[0], &valuetype0); 195 napi_valuetype valuetype1; 196 napi_typeof(env, args[1], &valuetype1); 197 if (valuetype0 != napi_number || valuetype1 != napi_number) { 198 napi_throw_type_error(env, nullptr, "Wrong arguments. 2 numbers expected."); 199 return NULL; 200 } 201 202 napi_value promise = nullptr; 203 napi_deferred deferred = nullptr; 204 napi_create_promise(env, &deferred, &promise); 205 206 // Asynchronous work context user data, which is transferred between execute and complete callbacks of the asynchronous work. 207 auto addonData = new AddonData{ 208 .asyncWork = nullptr, 209 .deferred = deferred, 210 }; 211 212 napi_get_value_double(env, args[0], &addonData->args[0]); 213 napi_get_value_double(env, args[1], &addonData->args[1]); 214 215 // Create an async work. When the creation is successful, the handle to the async work is returned through the last parameter (addonData->asyncWork). 216 napi_value resourceName = nullptr; 217 napi_create_string_utf8(env, "addAsyncCallback", NAPI_AUTO_LENGTH, &resourceName); 218 napi_create_async_work(env, nullptr, resourceName, addExecuteCB, addPromiseCompleteCB, (void *)addonData, 219 &addonData->asyncWork); 220 221 // Add the newly created async work to the queue for the underlying layer to schedule and execute. 222 napi_queue_async_work(env, addonData->asyncWork); 223 224 return promise; 225} 226``` 227 228After the asynchronous operation is complete, the callback is invoked and the result is passed to the **Promise** object. In JavaScript, you can use the **then()** method of the **Promise** object to process the result of an asynchronous operation. 229 230```js 231import hilog from '@ohos.hilog'; 232import testNapi from 'libentry.so' 233 234@Entry 235@Component 236struct TestAdd { 237 build() { 238 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 239 Text("hello world") 240 .onClick(() => { 241 let num1 = 2; 242 let num2 = 3; 243 testNapi.addPromise(num1, num2).then((result) => { 244 hilog.info(0x0000, 'testTag', '%{public}d', result); 245 }) 246 }) 247 } 248 .width('100%') 249 .height('100%') 250 } 251} 252``` 253 254### Prioritizing Asynchronous Tasks 255 256For applications developed using [Function Flow](https://gitee.com/openharmony/resourceschedule_ffrt/blob/master/docs/user_guide.md)), a task-based and data-driven concurrent programming model, you can schedule tasks with the help of the Function Flow Runtime (FFRT). Ark ArkTS runtime provides an extension API, which you can call to invoke FFRT with the QoS information of tasks passed in. The tasks are then scheduled based on their QoS level and system resource usage for a balance between power consumption and performance. 257 258* API example: napi_status napi_queue_async_work_with_qos(napi_env env, napi_async_work work, napi_qos_t qos) () 259 * **env**: environment for invoking the API. 260 * **napi_async_work**: asynchronous task. 261 * **napi_qos_t**: QoS level. 262 263* QoS levels: 264```cpp 265typedef enum { 266 napi_qos_background = 0, 267 napi_qos_utility = 1, 268 napi_qos_default = 2, 269 napi_qos_user_initiated = 3, 270} napi_qos_t; 271``` 272 273* The N-API layer encapsulates an external API to connect to the **uv_queue_work_with_qos(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb, uv_qos_t qos)** function at the libuv layer. 274 275* Compared with the existing API **napi_queue_async_work**, this API adds the QoS level to specify the priority for task scheduling. Example: 276```cpp 277static void PromiseOnExec(napi_env env, void *data) { 278 OH_LOG_INFO(LOG_APP, "PromiseOnExec"); 279} 280 281static void PromiseOnComplete(napi_env env, napi_status status, void *data) { 282 int number = *((int *)data); 283 OH_LOG_INFO(LOG_APP, "PromiseOnComplete number = %{public}d", number); 284} 285 286static napi_value Test(napi_env env, napi_callback_info info) { 287 napi_value resourceName = nullptr; 288 napi_create_string_utf8(env, "TestExample", NAPI_AUTO_LENGTH, &resourceName); 289 napi_async_work async_work; 290 int *data = new int(10); 291 napi_create_async_work(env, nullptr, resourceName, PromiseOnExec, PromiseOnComplete, data, &async_work); 292 napi_queue_async_work_with_qos(env, async_work, napi_qos_default); 293 return nullptr; 294} 295``` 296 297## Thread Security 298 299If an application needs to perform a large number of computing or I/O operations, the concurrency mechanism is handy, in that it can make full use of the multi-core CPU to improve the processing efficiency of the application. For example, applications specialized in image processing, video coding, and data analysis may use a concurrency mechanism to improve processing speeds. 300 301Although N-API itself does not support concurrent operations with multithreading, it does allow for some data interactions in a multi-thread environment, where thread security would be a concern. In a multi-thread environment, you can use **napi_create_threadsafe_function** to create a thread-safe function and then call the function in any thread. 302 303*Application scenario*: When there are other threads on the native side and JavaScript functions need to be called based on the completion results of these threads, these threads must communicate with the main thread on the native side so that JavaScript functions can be called in the main thread. Thread-safe functions provide a simplified way to avoid inter-thread communication and to return to the main thread to call JavaScript functions. 304 305### How to Use 306 307#### Passing Callback from the ArkTS Side 308```JS 309struct Index { 310 @State message: string = 'Hello World' 311 312 build() { 313 Row() { 314 Column() { 315 Text(this.message) 316 .fontSize(50) 317 .fontWeight(FontWeight.Bold) 318 .onClick(() => { 319 testNapi.threadSafeTest((value) => { 320 hilog.info(0x0000, 'testTag', 'js callback value = ' + value); 321 }) 322 }) 323 } 324 .width('100%') 325 } 326 .height('100%') 327 } 328} 329``` 330 331#### Creating Thread-Safe Function in Main Thread on Native Side 332```cpp 333static void CallJs(napi_env env, napi_value js_cb, void *context, void *data) { 334 335 std::thread::id this_id = std::this_thread::get_id(); 336 OH_LOG_INFO(LOG_APP, "thread CallJs %{public}d.\n", this_id); 337 napi_status status; 338 339 status = napi_get_reference_value(env, cbObj, &js_cb); 340 341 napi_valuetype valueType = napi_undefined; 342 napi_typeof(env, js_cb, &valueType); 343 OH_LOG_INFO(LOG_APP, "CallJs js_cb is napi_function: %{public}d", valueType == napi_function); 344 345 OH_LOG_INFO(LOG_APP, "CallJs 0"); 346 if (env != NULL) { 347 napi_value undefined, js_the_prime; 348 status = napi_create_int32(env, 666, &js_the_prime); 349 OH_LOG_INFO(LOG_APP, "CallJs 1: %{public}d", status == napi_ok); 350 status = napi_get_undefined(env, &undefined); 351 OH_LOG_INFO(LOG_APP, "CallJs 2: %{public}d", status == napi_ok); 352 353 napi_value ret; 354 355 status = napi_call_function(env, undefined, js_cb, 1, &js_the_prime, &ret); 356 OH_LOG_INFO(LOG_APP, "CallJs 3: %{public}d", status == napi_ok); 357 } 358} 359 360napi_threadsafe_function tsfn; 361 362static napi_value ThreadSafeTest(napi_env env, napi_callback_info info) { 363 size_t argc = 1; 364 napi_value js_cb, work_name; 365 napi_status status; 366 367 status = napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL); 368 OH_LOG_INFO(LOG_APP, "ThreadSafeTest 0: %{public}d", status == napi_ok); 369 370 status = napi_create_reference(env, js_cb, 1, &cbObj); 371 OH_LOG_INFO(LOG_APP, "napi_create_reference of js_cb to cbObj: %{public}d", status == napi_ok); 372 373 status = 374 napi_create_string_utf8(env, "Node-API Thread-safe Call from Async Work Item", NAPI_AUTO_LENGTH, &work_name); 375 OH_LOG_INFO(LOG_APP, "ThreadSafeTest 1: %{public}d", status == napi_ok); 376 377 std::thread::id this_id = std::this_thread::get_id(); 378 OH_LOG_INFO(LOG_APP, "thread ThreadSafeTest %{public}d.\n", this_id); 379 380 napi_valuetype valueType = napi_undefined; 381 napi_typeof(env, js_cb, &valueType); 382 OH_LOG_INFO(LOG_APP, "ThreadSafeTest js_cb is napi_function: %{public}d", valueType == napi_function); 383 384 status = napi_create_threadsafe_function(env, js_cb, NULL, work_name, 0, 1, NULL, NULL, NULL, CallJs, &tsfn); 385 OH_LOG_INFO(LOG_APP, "ThreadSafeTest 2: %{public}d", status == napi_ok); 386} 387``` 388 389#### Calling Thread-Safe Functions in Other Threads 390```cpp 391std::thread t([]() { 392 std::thread::id this_id = std::this_thread::get_id(); 393 OH_LOG_INFO(LOG_APP, "thread0 %{public}d.\n", this_id); 394 napi_status status; 395 status = napi_acquire_threadsafe_function(tsfn); 396 OH_LOG_INFO(LOG_APP, "thread1 : %{public}d", status == napi_ok); 397 status = napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking); 398 OH_LOG_INFO(LOG_APP, "thread2 : %{public}d", status == napi_ok); 399}); 400t.detach(); 401``` 402 403### Precautions for Using Thread Functions 404In a multi-thread environment, avoid using shared data structures and global variables to avoid competition and conflicts. Ensure that threads are synchronized and mutually exclusive to avoid data inconsistency. In addition, pay attention to the following: 405* The calls to thread-safe functions are asynchronous, and the calls to the JavaScript callbacks are placed in the task queue. 406* When **napi_threadsafe_function** is called to create a thread-safe function, the **napi_finalize** callback can be provided. When the thread-safe function is about to be destroyed, the **napi_finalize** callback is invoked on the main thread. 407* The context is given when **napi_create_threadsafe_function** is called and can be obtained from any thread that calls **napi_get_threadsafe_function_context**. 408