This content originally appeared on DEV Community and was authored by HarmonyOS
Introduction
Welcome to our Node-API series! Ever hit a performance wall in your ArkTS app, especially with complex calculations or games? That’s where Node-API steps in. It’s how HarmonyOSNext lets your ArkTS/JS code talk directly to super-efficient C/C++ modules. Let’s Start!
🎯 Why Combine Native C++ with ArkTS?
ArkTS is powerful and easy for HarmonyOSNext app development. But for compute-heavy tasks — like games or physics simulations — pure ArkTS might not cut it. That’s where your optimized C++ code comes in handy. Wrapping it for ArkTS lets you speed up your app without rewriting everything.
🔍 What is Node-API?
HarmonyOSNext Node-API bridges ArkTS/JS and native C/C++ modules with a stable, cross-platform API. Developed based on Node.js 12.x LTS Node-API, it provides a consistent set of APIs that can be used across different operating systems.
NOTE: For details about the differences between HarmonyOSNext Node-API and Node.js 12.x LTS Node-API, refer to the official Node-API documentation.
Node-API offers several key benefits:
- Seamless Native Module Integration: Easily import native modules into ArkTS.
- Efficient Resource Management: Cleanly manage memory and object lifecycles.
- High-Performance Calls: Call native C/C++ methods from ArkTS with great performance.
- Access to Core Capabilities: Encapsulate I/O, CPU-intensive tasks, and OS underlying capabilities, exposing them as ArkTS/JS interfaces.
- Framework Layer Extension: Enables the system to expose rich module functions from the framework layer to upper-layer applications through ArkTS/JS APIs.
- Improved Application Efficiency: Encapsulate core logic in C/C++ and use it with ArkTS/JS APIs to boost your application’s execution efficiency.
📊 Node-API Architecture
Understanding the layers and interactions of Node-API is crucial for effective integration.
- Native module: A module developed using Node-API and imported into ArkTS.
- Node-API: Implements the core logic for interaction between ArkTS and C/C++ code.
- ModuleManager: Manages native modules, including loading and locating them.
- ScopeManager: Manages the lifecycle of napi_value (ArkTS/JS values).
- ReferenceManager: Manages the lifecycle of napi_ref (persistent references to ArkTS/JS objects).
- Native engine: An abstraction layer for the ArkTS engine, standardizing - the API behavior at the Node-API level.
- ArkCompiler ArkTS Runtime: The runtime environment where ArkTS code executes. 🔄 Key Interaction Process of Node-API The interaction between ArkTS and C++ primarily consists of two steps:
Initialization:
- When a native module is imported into ArkTS, the ArkTS engine calls the ModuleManager.
- The ModuleManager then loads the module's .so file (shared library) and its dependencies.
- Upon the module’s first load, module registration is triggered.
- Finally, the method properties defined by the module are embedded into an exports object, and this object is returned to ArkTS.
Invocation:
- When an ArkTS method is called using the exports object, the ArkTS engine locates and invokes the corresponding embedded C/C++ method.
🛠️ How to Wrap Native Objects as ArkTS Objects (Wrap/Unwrap)
To use a native C++ class in ArkTS, you need to wrap it. This links the native C++ object to an ArkTS object, letting ArkTS manage its lifecycle. Here’s where napi_wrap and napi_unwrap come into play.
Let's experience Step By Step with DevEcoStudio :
1- Create a project (File — New — Create Project — Native C++)
2- Add abiFilters config at entry-level build-profile.json5
{
"apiType": "stageMode",
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": [
"arm64-v8a",
"x86_64"
]
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
},
"nativeLib": {
"debugSymbol": {
"strip": true,
"exclude": []
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}
build-profile.json5
3- Set the interface class (Index.d.ts)
export class MyObject {
constructor(arg: number);
plusOne: () => number;
public get value();
public set value(newVal: number);
}
index.d.ts
4- Set napi_init.cpp class
// napi_init.cpp
#include "napi/native_api.h"
#include "hilog/log.h"
// A native C++ class that will be wrapped into an ArkTS (JavaScript) object
class MyObject {
public:
// Initializes the class and exports it to ArkTS
static napi_value Init(napi_env env, napi_value exports);
// Destructor called when the native object is garbage collected
static void Destructor(napi_env env, void* nativeObject, void* finalize_hint);
private:
// Constructor and Destructor
explicit MyObject(double value_ = 0);
~MyObject();
// Class methods accessible from ArkTS
static napi_value New(napi_env env, napi_callback_info info);
static napi_value GetValue(napi_env env, napi_callback_info info);
static napi_value SetValue(napi_env env, napi_callback_info info);
static napi_value PlusOne(napi_env env, napi_callback_info info);
// Member variables
double value_; // internal numeric value
napi_env env_; // N-API environment pointer
napi_ref wrapper_; // Reference to the wrapped JS object
};
// Global reference to the constructor function, used for instance creation
static thread_local napi_ref g_ref = nullptr;
// Constructor implementation
MyObject::MyObject(double value)
: value_(value), env_(nullptr), wrapper_(nullptr) {}
// Destructor implementation
MyObject::~MyObject()
{
napi_delete_reference(env_, wrapper_);
}
// This function is called when the JS object is garbage collected
void MyObject::Destructor(napi_env env, void* nativeObject, [[maybe_unused]] void* finalize_hint)
{
OH_LOG_INFO(LOG_APP, "MyObject::Destructor called");
delete reinterpret_cast<MyObject*>(nativeObject);
}
// Initializes and exports the MyObject class to the ArkTS runtime
napi_value MyObject::Init(napi_env env, napi_value exports)
{
// Define class methods and properties exposed to ArkTS
napi_property_descriptor properties[] = {
{ "value", 0, 0, GetValue, SetValue, 0, napi_default, 0 },
{ "plusOne", nullptr, PlusOne, nullptr, nullptr, nullptr, napi_default, nullptr }
};
napi_value cons;
napi_define_class(env, "MyObject", NAPI_AUTO_LENGTH, New, nullptr, 2, properties, &cons);
// Store a reference to the constructor for later use
napi_create_reference(env, cons, 1, &g_ref);
// Export the class as a property of the module
napi_set_named_property(env, exports, "MyObject", cons);
return exports;
}
// Export function that will be called when the module is loaded
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
MyObject::Init(env, exports);
return exports;
}
EXTERN_C_END
// Native module definition
static napi_module nativeModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = nullptr,
.reserved = { 0 },
};
// This function is automatically called when the native module is loaded
extern "C" __attribute__((constructor)) void RegisterObjectWrapModule()
{
napi_module_register(&nativeModule);
}
// Class constructor callable from ArkTS
napi_value MyObject::New(napi_env env, napi_callback_info info)
{
OH_LOG_INFO(LOG_APP, "MyObject::New called");
napi_value newTarget;
napi_get_new_target(env, info, &newTarget);
if (newTarget != nullptr) {
// Called using `new MyObject(...)`
size_t argc = 1;
napi_value args[1];
napi_value jsThis;
napi_get_cb_info(env, info, &argc, args, &jsThis, nullptr);
double value = 0.0;
napi_valuetype valuetype;
napi_typeof(env, args[0], &valuetype);
if (valuetype != napi_undefined) {
napi_get_value_double(env, args[0], &value);
}
// Create the native object
MyObject* obj = new MyObject(value);
obj->env_ = env;
// Wrap native C++ object with JS object
napi_status status = napi_wrap(env,
jsThis,
reinterpret_cast<void*>(obj),
MyObject::Destructor,
nullptr,
&obj->wrapper_);
// Check for wrap failure
if (status != napi_ok) {
OH_LOG_INFO(LOG_APP, "Failed to bind native object to js object, return code: %{public}d", status);
delete obj;
return jsThis;
}
// Convert reference to weak to avoid memory leaks
uint32_t refCount = 0;
napi_reference_unref(env, obj->wrapper_, &refCount);
return jsThis;
} else {
// Called as a function: `MyObject(...)` instead of `new MyObject(...)`
size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// Create a new instance using the constructor reference
napi_value cons;
napi_get_reference_value(env, g_ref, &cons);
napi_value instance;
napi_new_instance(env, cons, argc, args, &instance);
return instance;
}
}
// Getter: Returns the value_ field to ArkTS
napi_value MyObject::GetValue(napi_env env, napi_callback_info info)
{
OH_LOG_INFO(LOG_APP, "MyObject::GetValue called");
napi_value jsThis;
napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr);
// Unwrap native object from JS
MyObject* obj;
napi_unwrap(env, jsThis, reinterpret_cast<void**>(&obj));
// Create JS number from native value
napi_value num;
napi_create_double(env, obj->value_, &num);
return num;
}
// Setter: Updates the native value_ field from ArkTS
napi_value MyObject::SetValue(napi_env env, napi_callback_info info)
{
OH_LOG_INFO(LOG_APP, "MyObject::SetValue called");
size_t argc = 1;
napi_value value;
napi_value jsThis;
napi_get_cb_info(env, info, &argc, &value, &jsThis, nullptr);
// Unwrap native object from JS
MyObject* obj;
napi_unwrap(env, jsThis, reinterpret_cast<void**>(&obj));
// Set internal value_
napi_get_value_double(env, value, &obj->value_);
return nullptr;
}
// Adds 1 to the internal value_ and returns it
napi_value MyObject::PlusOne(napi_env env, napi_callback_info info)
{
OH_LOG_INFO(LOG_APP, "MyObject::PlusOne called");
napi_value jsThis;
napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr);
// Unwrap native object from JS
MyObject* obj;
napi_unwrap(env, jsThis, reinterpret_cast<void**>(&obj));
// Increment value_ and return
obj->value_ += 1;
napi_value num;
napi_create_double(env, obj->value_, &num);
return num;
}
napi_init.cpp
- napi_wrap: Binds the ArkTS object to the native C++ instance. This function establishes the connection, allowing the ArkTS runtime to manage the native object.
- napi_unwrap: Retrieves the native C++ object from its wrapped ArkTS counterpart. This is essential when you need to access or manipulate the native C++ instance from within your native methods.
Don't forget to set the MakeList.txt config file
# 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_definitions("-DLOG_DOMAIN=0x0000")
add_definitions("-DLOG_TAG=\"testTag\"")
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so)
Main Config File
💻 It is time to use the Wrapped Object in ArkTS
Once wrapped, your native C++ object can be used directly within ArkTS code, as if it were a native ArkTS object:
import hilog from '@ohos.hilog';
import { MyObject } from 'libentry.so';
let object: MyObject = new MyObject(0);
object.value = 1023;
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
hilog.info(0x0000, 'testTag', 'MyObject value after set: %{public}d', object.value);
hilog.info(0x0000, 'testTag', 'MyObject plusOne: %{public}d', object.plusOne());
this.message = object.value.toString() + " " + object.plusOne()
})
}
.width('100%')
}
.height('100%')
}
}
index.ets (Only ArkTS Class on project — Caller Script)
📝 Summary
- Node-API is a powerful bridge between ArkTS and C++, allowing high-performance native module integration.
- Use napi_wrap and napi_unwrap to bind native objects with ArkTS objects and retrieve them.
- The Destructor function is crucial for memory safety, ensuring native resources are released when the ArkTS wrapper is garbage collected.
- This approach helps you integrate high-performance native modules into your ArkTS apps effortlessly.
🎁 Conclusion
Wrapping native C++ modules with Node-API unlocks a world of performance and flexibility for your HarmonyOSNext projects. I hope this comprehensive guide helps you get started quickly.
References:
https://forums.developer.huawei.com/forumPortal/en/topic/0203190036169712060
Written by BunyaminEymenAlagoz
This content originally appeared on DEV Community and was authored by HarmonyOS

HarmonyOS | Sciencx (2025-08-27T12:46:25+00:00) Node-API Part-1: Introduction to Node-API & Wrapping Native C++ Objects in ArkTS on HarmonyOSNext. Retrieved from https://www.scien.cx/2025/08/27/node-api-part-1-introduction-to-node-api-wrapping-native-c-objects-in-arkts-on-harmonyosnext/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.