Read the original article:Node-API Part-5 : Asynchronous Task Development Using Node-API
🚀Node-API Part-5 : Asynchronous Task Development Using Node-API
✨ “What happens when the power of native C++ meets the async elegance of ArkTS?” Let’s find out, and make them handshake over a Promise. 🤝
🧭 Introduction
When developing native modules using Node-API for HarmonyOS, maintaining application responsiveness is essential — especially for time-consuming operations like file I/O, image processing, or network communication. Blocking the main thread can lead to performance bottlenecks and poor user experience. Fortunately, napi_create_async_work provides an elegant way to offload heavy tasks to a worker thread without impacting the UI or logic layers of your application.
This article walks you through using asynchronous tasks in Node-API with both Promise-based and callback-based implementations. By the end, you’ll have a practical understanding of when and how to use napi_async_work in your HarmonyOS native modules.
🔍 Context: Why Call ArkTS Promises from Native Code?
You might want this if:
- Your native C++ module needs to execute ArkTS logic.
- That ArkTS logic involves asynchronous operations (e.g. setTimeout, network requests).
- You want to handle .then() and .catch() callbacks from within native C++.
🧠 Think: native modules reacting to async ArkTS behaviors — without breaking flow.
📌 When to Use napi_create_async_work
Use napi_create_async_work when your native code performs tasks that are:
- File-heavy (e.g., reading large files)
- Network-bound (e.g., making API requests)
- Database-intensive (e.g., complex queries or inserts)
- Image-processing related (e.g., resizing, filtering)
These operations should not run on the main thread, as they may freeze the UI or delay logic execution. Instead, you can offload them using asynchronous work queues provided by Node-API.
⚙️ Implementation
📁 API Declaration in index.d.ts
export const asyncWorkPromiseSample: (data: number) => Promise<number>;
export const asyncWorkCallbacksSample: (arg1: number, arg2: number, callback: (result: number) => void) => void;
🔧 Promise Structs
promise_sample.h
#ifndef NDKBASICS_PROMISE_SAMPLE_H
#define NDKBASICS_PROMISE_SAMPLE_H
#include "napi/native_api.h"
class promise_sample {
};
napi_value asyncWorkPromiseSample(napi_env env, napi_callback_info info);
#endif
promise_sample.cpp
#include "promise_sample.h"
// Structure to hold callback-related data for async work
struct CallbackData {
napi_async_work asyncWork = nullptr; // Handle to async work
napi_deferred deferred = nullptr; // Deferred object for promise
napi_ref callback = nullptr; // (Unused here) Reference to a JS callback function
double args = 0; // Input argument
double result = 0; // Result to return
};
// This function runs in a separate thread and performs the actual computation
static void ExecuteCB(napi_env env, void *data)
{
auto *callbackData = reinterpret_cast<CallbackData *>(data);
// In this example, we just pass the input value as the result
callbackData->result = callbackData->args;
}
// This function runs in the main thread once the async work is complete
static void CompleteCB(napi_env env, napi_status status, void *data)
{
auto *callbackData = reinterpret_cast<CallbackData *>(data);
napi_value result = nullptr;
napi_create_double(env, callbackData->result, &result);
// Resolve or reject the promise based on the result value
if (callbackData->result > 0) {
napi_resolve_deferred(env, callbackData->deferred, result);
} else {
napi_reject_deferred(env, callbackData->deferred, result);
}
// Clean up the async work and free allocated memory
napi_delete_async_work(env, callbackData->asyncWork);
delete callbackData;
}
// This is the native function exposed to JavaScript
// It starts an asynchronous operation and returns a Promise
napi_value asyncWorkPromiseSample(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value args[1];
// Get the arguments passed from JavaScript
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_value promise = nullptr;
napi_deferred deferred = nullptr;
// Create a new promise and get its deferred handle
napi_create_promise(env, &deferred, &promise);
// Allocate and initialize callback data
auto *callbackData = new CallbackData();
callbackData->deferred = deferred;
napi_get_value_double(env, args[0], &callbackData->args);
// Create a string to name the async work
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "AsyncCallback", NAPI_AUTO_LENGTH, &resourceName);
// Create the async work item
napi_create_async_work(env, nullptr, resourceName, ExecuteCB, CompleteCB, callbackData, &callbackData->asyncWork);
// Queue the async work to be executed
napi_queue_async_work(env, callbackData->asyncWork);
// Return the promise to JavaScript
return promise;
}
🔧 Callback Structs
callbacks_sample.h
#ifndef NDKBASICS_CALLBACKS_SAMPLE_H
#define NDKBASICS_CALLBACKS_SAMPLE_H
#include "napi/native_api.h"
class callbacks_sample {
};
napi_value asyncWorkCallbacksSample(napi_env env, napi_callback_info info);
#endif
callbacks_sample.h
#ifndef NDKBASICS_CALLBACKS_SAMPLE_H
#define NDKBASICS_CALLBACKS_SAMPLE_H
#include "napi/native_api.h"
class callbacks_sample {
};
napi_value asyncWorkCallbacksSample(napi_env env, napi_callback_info info);
#endif
callbacks_sample.cpp
#include "callbacks_sample.h"
#include <js_native_api.h>
#include <node_api.h>
#include <node_api_types.h>
// Struct to hold data required across async work lifecycle
struct CallbackData {
napi_async_work asyncWork = nullptr; // Handle for the async work
napi_ref callbackRef = nullptr; // Reference to the JS callback function
double args[2] = {0}; // Input arguments
double result = 0; // Computed result (sum of args)
};
// Function executed on a worker thread (non-blocking)
// Performs the actual computation (sum of two numbers)
static void ExecuteCB(napi_env env, void *data)
{
CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);
callbackData->result = callbackData->args[0] + callbackData->args[1];
}
// Function executed on the main thread after async work completes
// Calls the stored JS callback with the result
static void CompleteCB(napi_env env, napi_status status, void *data)
{
CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);
// Create a JavaScript number to hold the result
napi_value callbackArg[1] = {nullptr};
napi_create_double(env, callbackData->result, &callbackArg[0]);
// Retrieve the original JS callback function from reference
napi_value callback = nullptr;
napi_get_reference_value(env, callbackData->callbackRef, &callback);
// Call the callback with the result
napi_value result;
napi_value undefined;
napi_get_undefined(env, &undefined);
napi_call_function(env, undefined, callback, 1, callbackArg, &result);
// Clean up resources: reference and async work
napi_delete_reference(env, callbackData->callbackRef);
napi_delete_async_work(env, callbackData->asyncWork);
delete callbackData;
}
// This function is exposed to JavaScript
// It performs asynchronous computation and calls a callback upon completion
napi_value asyncWorkCallbacksSample(napi_env env, napi_callback_info info)
{
size_t argc = 3;
napi_value args[3];
// Retrieve arguments passed from JavaScript
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// Create and initialize callback data to store across async lifecycle
auto asyncContext = new CallbackData();
napi_get_value_double(env, args[0], &asyncContext->args[0]); // First number
napi_get_value_double(env, args[1], &asyncContext->args[1]); // Second number
// Convert the JS callback function to a persistent reference
napi_create_reference(env, args[2], 1, &asyncContext->callbackRef);
// Create a string resource name for async work (for debugging/profiling)
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "asyncWorkCallback", NAPI_AUTO_LENGTH, &resourceName);
// Create the async work object with execution and completion callbacks
napi_create_async_work(env, nullptr, resourceName, ExecuteCB, CompleteCB,
asyncContext, &asyncContext->asyncWork);
// Queue the async work for execution
napi_queue_async_work(env, asyncContext->asyncWork);
// Return undefined (no immediate return value, result will be sent via callback)
return nullptr;
}
napi_init.cpp
#include "callbacks/callbacks_sample.h"
#include "napi/native_api.h"
#include "promise/promise_sample.h"
EXTERN_C_START // Ensures C-style linkage for compatibility with Node.js
// This function is called when the module is initialized
static napi_value Init(napi_env env, napi_value exports) {
// Define properties (functions) to be exported to JavaScript
napi_property_descriptor desc[] = {
{
"asyncWorkPromiseSample", // JavaScript function name
nullptr,
asyncWorkPromiseSample, // Native function for Promise-based async work
nullptr, nullptr, nullptr,
napi_default,
nullptr
},
{
"asyncWorkCallbacksSample", // JavaScript function name
nullptr,
asyncWorkCallbacksSample, // Native function for callback-based async work
nullptr, nullptr, nullptr,
napi_default,
nullptr
}
};
// Bind the functions to the exports object
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
// Return the updated exports object
return exports;
}
EXTERN_C_END // End of C-style linkage block
// Define the module descriptor for registration
static napi_module demoModule = {
.nm_version = 1, // N-API version
.nm_flags = 0, // Reserved (set to 0)
.nm_filename = nullptr, // Optional: filename (unused here)
.nm_register_func = Init, // Module initialization function
.nm_modname = "entry", // Module name used in JS: require('entry')
.nm_priv = ((void *)0), // Private data (not used)
.reserved = {0}, // Reserved fields
};
// Register the native module when the shared object is loaded
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
Modular Initialization with Init()
The Init function serves as the entry point when the native module is loaded. It binds two native functions—asyncWorkPromiseSample and asyncWorkCallbacksSample—to JavaScript-accessible properties using napi_property_descriptor. This allows JavaScript code to call native functions using familiar names.
Promise & Callback-Based Async APIs
Two different asynchronous patterns are supported:
- asyncWorkPromiseSample returns a Promise for async control in modern JavaScript.
- asyncWorkCallbacksSample supports traditional callback-style async handling.
This makes the native module flexible for different usage patterns in ArkTS or Node.js-style environments.
C Compatibility with EXTERN_C_START and EXTERN_C_END
hese macros wrap the native functions to ensure C-style linkage, which prevents name mangling and ensures compatibility with Node-API expectations, especially when the code is compiled as C++.
Self-Registering Module with napi_module_register
The module defines a static napi_module structure, specifying metadata such as:
- The module name (entry)
- The init function (Init)
- API version and flags
Then, using the special GCC constructor attribute, the RegisterEntryModule() function is automatically called when the shared library is loaded—no manual registration needed.
Exports Ready for JavaScript Integration
Once compiled and linked, the resulting .so (shared object) file exposes two functions directly usable from ArkTS or JavaScript:
import { asyncWorkPromiseSample, asyncWorkCallbacksSample } from 'libentry.so';
Configuration
CMakeLists.txt
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(NDKBasics)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
add_library(entry SHARED napi_init.cpp promise/promise_sample.cpp callbacks/callbacks_sample.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so)
🧩 Define ArkTS Script (Index.ets)
📈 Visual Architecture
ArkTS Promise (async logic)
|
v
C++ Calls ArkTS (via Node-API)
|
v
Handles .then()
using:
├── ✅ ResolvedCallback
└── ❌ RejectedCallback
🖱️ Click → ArkTS Method → Native C++ Callbacks → Logs 🪵
🧠 Key Takeaways
✅ Use napi_call_function() to invoke ArkTS methods.
✅ Always attach both .then() and .catch() for full Promise handling.
✅ Convert JS values using napi_get_value_* before logging or processing.
✅ Use hilog for safe, structured native logs.
👋 Conclusion
Bridging native and ArkTS async logic opens powerful integration paths. Whether you’re offloading work to C++ or responding to UI triggers in ArkTS, managing Promises from native land is a game-changer.
And now, you’re ready to build that bridge — brick by brick, API by API
🧱🌉. Happy coding on HarmonyOSNext! 💙
📚 Additional Resources
- 📘 Node-API for HarmonyOS
- 📙 ArkTS Async Programming Guide1
- 📘 ArkTS Async Programming Guide2
- 📙 ArkTS Async Programming Guide3
- 📄 CMake Best Practices in HarmonyOS
The OpenCms demo, brought to you by Alkacon Software.developer.huawei.com
Top comments (0)