Exporting C++ Class Objects via Node-API and Managing Their Lifecycle

Sample Project Source Code Analysis

The sample project uses a Native C++ template with the Stage model. This analysis focuses on key implementation files for class object export and lifecycle management.

Class Definition and Method Implementation

NapiTest.h Header File

#ifndef __NAPI_TEST_H__
#define __NAPI_TEST_H__

#include "napi/native_api.h"
#include <js_native_api_types.h>
#include <iostream>

#define NAPI_CLASS_NAME "NapiTestClass"

class NapiTest {
public:
    NapiTest() : mEnv(nullptr), mRef(nullptr) {
    }
    
    NapiTest(napi_env env) : mEnv(env), mRef(nullptr) {
    }
    
    ~NapiTest();
    
    // Factory method to create instances for JavaScript
    static napi_value Create(napi_env env, napi_callback_info info);
    
    // Initialize JS class and set properties for export
    static napi_value Init(napi_env env, napi_value exports);


private:
    // Setter method exposed to JavaScript
    static napi_value SetMsg(napi_env env, napi_callback_info info);
    
    // Getter method exposed to JavaScript
    static napi_value GetMsg(napi_env env, napi_callback_info info);
    
    // JavaScript constructor callback
    static napi_value Constructor(napi_env env, napi_callback_info info);
    
    // Finalizer callback (similar to destructor)
    static void Destructor(napi_env env, void *nativeObject, void *finalize);

    // Class-level reference for constructor
    static napi_ref sConstructor_;
    
    // Internal data storage
    static std::string _msg;
    
    // Environment storage
    napi_env mEnv = nullptr;
    
    // Instance-level reference
    napi_ref mRef = nullptr;
};

#endif  /* __NAPI_TEST_H__ */

Core NAPI Types

napi_value

The Node-API defines napi_value as an opaque pointer representing JavaScript values. This type encapsulates all ECMAScript primitive types (Boolean, Null, Undefined, Number, BigInt, String, Symbol, Object) and Functon types into a unified interface for data exchange between native code and ArkUI.

napi_ref

This abstract type manages references to napi_value instances, enabling explicit control over JavaScript value lifetimes by defining minimum retention periods.

napi_env

The environment handle (napi_env) carries context information that Node-API implementations use to persist VM-specific state across API calls.

Defining JavaScript Class Methods

Before defining the JS class, specify the methods to export:

napi_property_descriptor desc[] = {
    { "getMsg", nullptr, NapiTest::GetMsg, nullptr, nullptr, nullptr, napi_default, nullptr },
    { "setMsg", nullptr, NapiTest::SetMsg, nullptr, nullptr, nullptr, napi_default, nullptr },
    { "create", nullptr, NapiTest::Create, nullptr, nullptr, nullptr, napi_default, nullptr }
};

napi_property_descriptor Structure

typedef struct {
    const char* utf8name;  // UTF-8 encoded property name
    napi_value name;       // Alternative: napi_value key
    napi_callback method;  // Function implementation
    napi_callback getter;  // Get accessor
    napi_callback setter;  // Set accessor
    napi_value value;      // Data value
    napi_property_attributes attributes;
    void* data;            // Callback data
} napi_property_descriptor;

Key parameters:

  • utf8name: UTF-8 encoded method name (mutually exclusive with name)
  • method: Function assigned to the property's value attribute
  • attributes: Property characteristics (readable, writable, configurable, etc.)
  • data: User data passed to method/getter/setter callbacks

Creating the JavaScript Class

napi_value constructor = nullptr;

if (napi_define_class(env, NAPI_CLASS_NAME, NAPI_AUTO_LENGTH, Constructor, nullptr,
                      sizeof(desc) / sizeof(desc[0]), desc, &constructor) != napi_ok) {
    return nullptr;
}

napi_define_class Function Signature

napi_status napi_define_class(napi_env env,
                              const char* utf8name,
                              size_t length,
                              napi_callback constructor,
                              void* data,
                              size_t property_count,
                              const napi_property_descriptor* properties,
                              napi_value* result);

Parameters:

  • utf8name: C++ class name exposed to JavaScript
  • length: Name length, use NAPI_AUTO_LENGTH for null-terminated strings
  • constructor: Callback handling C++ instance creation (must have napi_callback signature)
  • data: Optional data passed to constructor callback
  • property_count: Number of proeprties in the descriptor array
  • result: Output napi_value bound to the class constructor

Note: The JavaScript constructor is a function invoked with new. C++ callbacks are functions that third-party APIs call back into your code.

Constructor Implementation

When ArkTS invokes the class with new, the constructor callback executes:

napi_value NapiTest::Constructor(napi_env env, napi_callback_info info)
{
    napi_value undefined = nullptr, thisVar = nullptr;
    napi_get_undefined(env, &undefined);

    if (napi_get_cb_info(env, info, nullptr, nullptr, &thisVar, nullptr) == napi_ok && thisVar != nullptr) {
        NapiTest *instance = new NapiTest(env);

        if (napi_wrap(env, thisVar, reinterpret_cast<void *>(instance), NapiTest::Destructor,
                      nullptr, &(instance->mRef)) == napi_ok) {
            return thisVar;
        }
        return thisVar;
    }
    
    return undefined;
}

void NapiTest::Destructor(napi_env env, void *nativeObject, void *finalize)
{
    NapiTest *instance = reinterpret_cast<NapiTest*>(nativeObject);
    instance->~NapiTest();
}

napi_wrap Function

napi_status napi_wrap(napi_env env,
                      napi_value js_object,
                      void* native_object,
                      napi_finalize finalize_cb,
                      void* finalize_hint,
                      napi_ref* result);

This binds a C++ instance to a JavaScript object and associates lifecycle management:

  • js_object: Target JavaScript object
  • native_object: C++ instance to bind
  • finalize_cb: Callback invoked when the JS object is garbage collected
  • result: Reference handle for the bound object

napi_get_cb_info Function

napi_status napi_get_cb_info(napi_env env,
                             napi_callback_info cbinfo,
                             size_t* argc,
                             napi_value* argv,
                             napi_value* this_arg,
                             void** data);

Retrieves callback information including:

  • argc: Input/output parameter specifying buffer size and actual argument count
  • argv: Output buffer for argument values
  • this_arg: Output receiver for the JavaScript this value
  • data: Optional context data from the callback registration

Exporting the JavaScript Class

Creating the Class Reference

if (napi_create_reference(env, constructor, 1, &sConstructor_) != napi_ok) {
    return nullptr;
}

napi_create_reference Function

napi_status napi_create_reference(napi_env env,
                                   napi_value value,
                                   uint32_t initial_refcount,
                                   napi_ref* result);

Creates a strong reference extending the target object's lifetime. The caller must manage the reference lifecycle explicitly.

Binding Properties to Exports

if (napi_set_named_property(env, exports, NAPI_CLASS_NAME, constructor) != napi_ok) {
    return nullptr;
}

napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);

napi_set_named_property Function

napi_status napi_set_named_property(napi_env env,
                                    napi_value object,
                                    const char* utf8Name,
                                    napi_value value);

Sets a named property on the target object using a UTF-8 encoded string key.

napi_define_properties Function

napi_status napi_define_properties(napi_env env,
                                   napi_value object,
                                   size_t property_count,
                                   const napi_property_descriptor* properties);

Batch-defines multiple properties on a JavaScript object in a single call.

Creating Instance Objects via Factory Method

ArkTS can obtain instances through the Create factory method instead of new:

napi_value NapiTest::Create(napi_env env, napi_callback_info info) {
    napi_status status;
    napi_value constructor = nullptr, result = nullptr;
    
    status = napi_get_reference_value(env, sConstructor_, &constructor);

    status = napi_new_instance(env, constructor, 0, nullptr, &result);
    
    auto instance = new NapiTest();
    if (napi_wrap(env, result, reinterpret_cast<void *>(instance), Destructor,
                  nullptr, &(instance->mRef)) == napi_ok) {
        return result;
    }
    
    return nullptr;
}

napi_get_reference_value Function

napi_status napi_get_reference_value(napi_env env,
                                     napi_ref ref,
                                     napi_value* result);

Retrieves the JavaScript object associated with a reference handle.

napi_new_instance Function

napi_status napi_new_instance(napi_env env,
                               napi_value cons,
                               size_t argc,
                               napi_value* argv,
                               napi_value* result)

Constructs a new JavaScript object using the specified constructor function.

TypeScript Declaration File

export const create: () => NapiTest;

export class NapiTest {
    setMsg(msg: string): void;
    getMsg(): string;
}

Alternative structure with static factory method:

export class NapiTest {
    create();
    setMsg(msg: string): void;
    getMsg(): string;
}

The declaration file can be auto-generated from headers using NAPI tooling.

Build Configuration

cmake_minimum_required(VERSION 3.4.1)
project(ObjectWrapTest)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

add_library(entry SHARED hello.cpp NapiTest.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so)

ArkTS Consumer Code

import testNapi from "libentry.so";

@Entry
@Component
struct Index {
    @State message: string = "Exported Object"
    @State nativePointer: number = 0
    
    private testInstance = testNapi.create();

    build() {
        Row() {
            Column() {
                Text(this.message)
                    .fontSize(50)
                    .fontWeight(FontWeight.Bold)
                    .onClick(() => {
                        try {
                            if (this.nativePointer == 0) {
                                console.info("[NapiTest] Computing 2 + 3");
                                this.nativePointer = testNapi.add(2, 3);
                                this.testInstance.setMsg("2+3");
                            } else {
                                console.info("[NapiTest] Computing 4 + 5");
                                this.nativePointer = testNapi.add(4, 5);
                                this.testInstance.setMsg("4+5");
                            }
                        } catch(e) {
                            console.info("[NapiTest] Error: " + JSON.stringify(e));
                        }
                        console.info("[NapiTest] Result: " + this.testInstance.getMsg() + " = " + this.nativePointer);
                    })
            }
            .width("100%")
        }
        .height("100%")
    }
}

Tags: OpenHarmony Node-API NAPI HarmonyOS ArkTS

Posted on Thu, 11 Jun 2026 18:28:49 +0000 by itworks