DEV Community

HarmonyOS
HarmonyOS

Posted on

Node-API Part-2: Safe Native-to-ArkTS Communication in HarmonyOS Next Using napi_create_threadsafe_function

Read the original article:Node-API Part-2: Safe Native-to-ArkTS Communication in HarmonyOS Next Using napi_create_threadsafe_function

👋 Introduction: Why Do We Need Thread-Safe JS Invocation from Native Code?

When building NDK-based modules in HarmonyOS, developers often execute long-running or performance-critical tasks in native C++ code. However, these native threads may eventually need to send results or trigger updates in the (ArkTS) layer.

Here lies the challenge:

Native threads cannot directly invoke ArkTS functions.

ArkTS is single-threaded and not thread-safe by design. This is where the Node-API’s napi_create_threadsafe_function interface becomes crucial.

🔐 What Is napi_create_threadsafe_function?

napi_create_threadsafe_function is a Node-API interface that enables asynchronous and thread-safe invocation of JavaScript functions from multiple native threads. It provides:

  • Thread safety — No race conditions or deadlocks.
  • Non-blocking communication — Native tasks do not block the main thread.
  • Promise-based interaction — Native threads can await JS results via std::promise.

🧩 Use Case: Multithreading in Native Code with ArkTS Integration

Let’s break down a practical implementation of calling an asynchronous JavaScript callback from a native thread in HarmonyOS.

⚙️ Step-by-Step Implementation

1. 📁 API Declaration in index.d.ts

//API Declaration

export const startThread: (callback: () => Promise<string>) => void;

Enter fullscreen mode Exit fullscreen mode

API Interface Declaration

2. 📁 Main Logics in napi_init.cpp

#include "napi/native_api.h"
#include <future>
#include <hilog/log.h>

// Callback definition
struct CallbackData {
    napi_threadsafe_function tsfn;
    napi_async_work work;
};

// Executes the async work in a background thread
static void ExecuteWork(napi_env env, void *data)
{
    CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);
    std::promise<std::string> promise;
    auto future = promise.get_future();
    napi_call_threadsafe_function(callbackData->tsfn, &promise, napi_tsfn_nonblocking);
    try {
        auto result = future.get();
        OH_LOG_INFO(LOG_APP, "XXX YYY ZZZ, Result from JS %{public}s", result.c_str());
    } catch (const std::exception &e) {
        OH_LOG_INFO(LOG_APP, "XXX YYY ZZZ, Result from JS %{public}s", e.what());
    }
}

// Called when the JS promise resolves successfully
static napi_value ResolvedCallback(napi_env env, napi_callback_info info)
{
    void *data = nullptr;
    size_t argc = 1;
    napi_value argv[1];
    if (napi_get_cb_info(env, info, &argc, argv, nullptr, &data) != napi_ok) {
        return nullptr;
    }
    size_t result = 0;
    char buf[32] = {0};
    napi_get_value_string_utf8(env, argv[0], buf, 32, &result);
    reinterpret_cast<std::promise<std::string> *>(data)->set_value(std::string(buf));
    return nullptr;
}

// Called when the JS promise is rejected
static napi_value RejectedCallback(napi_env env, napi_callback_info info)
{
    void *data = nullptr;
    if (napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data) != napi_ok) {
        return nullptr;
    }
    reinterpret_cast<std::promise<std::string> *>(data)->set_exception(
        std::make_exception_ptr(std::runtime_error("Error in jsCallback")));
    return nullptr;
}

// Calls the JavaScript callback and attaches .then and .catch
static void CallJs(napi_env env, napi_value jsCb, void *context, void *data)
{
    if (env == nullptr) {
        return;
    }
    napi_value undefined = nullptr;
    napi_value promise = nullptr;
    napi_get_undefined(env, &undefined);
    napi_call_function(env, undefined, jsCb, 0, nullptr, &promise);
    napi_value thenFunc = nullptr;
    if (napi_get_named_property(env, promise, "then", &thenFunc) != napi_ok) {
        return;
    }
    napi_value resolvedCallback;
    napi_value rejectedCallback;
    napi_create_function(env, "resolvedCallback", NAPI_AUTO_LENGTH, ResolvedCallback, data,
                         &resolvedCallback);
    napi_create_function(env, "rejectedCallback", NAPI_AUTO_LENGTH, RejectedCallback, data,
                         &rejectedCallback);
    napi_value argv[2] = {resolvedCallback, rejectedCallback};
    napi_call_function(env, promise, thenFunc, 2, argv, nullptr);
}

// Called when the async work is complete; cleans up resources
static void WorkComplete(napi_env env, napi_status status, void *data)
{
    CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);
    napi_release_threadsafe_function(callbackData->tsfn, napi_tsfn_release);
    napi_delete_async_work(env, callbackData->work);
    callbackData->tsfn = nullptr;
    callbackData->work = nullptr;
}

// Entry point for JS to start the background thread
static napi_value StartThread(napi_env env, napi_callback_info info)
{
    size_t argc = 1;
    napi_value jsCb = nullptr;
    CallbackData *callbackData = nullptr;
    napi_get_cb_info(env, info, &argc, &jsCb, nullptr, reinterpret_cast<void **>(&callbackData));

    // Create a thread-safe function
    napi_value resourceName = nullptr;
    napi_create_string_utf8(env, "Thread-safe Function Demo", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_threadsafe_function(env, jsCb, nullptr, resourceName, 0, 1, callbackData, nullptr,
        callbackData, CallJs, &callbackData->tsfn);

    // Create async work object to run on background thread
    napi_create_async_work(env, nullptr, resourceName, ExecuteWork, WorkComplete, callbackData,
        &callbackData->work);

    // Queue the async work
    napi_queue_async_work(env, callbackData->work);
    return nullptr;
}

EXTERN_C_START

// Initializes the native module and exports the startThread method
static napi_value Init(napi_env env, napi_value exports) {
    CallbackData *callbackData = new CallbackData(); // Released in WorkComplete
    napi_property_descriptor desc[] = {
        {"startThread", nullptr, StartThread, nullptr, nullptr, nullptr, napi_default, callbackData},
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

EXTERN_C_END

// Registers the native module with Node-API
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    napi_module_register(&demoModule);
}

// Module definition
static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

Enter fullscreen mode Exit fullscreen mode

napi_init.cpp

3. Execute the Callback in ArkTS

import nativeModule from 'libentry.so';

let callback = (): Promise<string> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("YYY ZZZ string from promise");
    }, 2000);
  });
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            nativeModule.startThread(callback)
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}


Enter fullscreen mode Exit fullscreen mode

index.ets (Only ArkTS Class on project — Caller Script)

🎯 Key Benefits

  • ✅ Safe communication from native threads to ArkTS
  • 🚀 Efficient execution of long-running native operations
  • 🔄 Two-way Promise-based bridging using std::promise and ArkTS Promises
  • 💡 Clean separation between native computation and UI logic

💡 Practical Scenarios

  • Fetching large datasets in native threads and updating the UI
  • Performing heavy computations (e.g., AI/ML) in C++ and pushing results to ArkTS
  • Executing background device I/O or sensor reads in C++ and notifying the ArkTS layer

⚠️ Developer Notes

  • ❌ Never invoke ArkTS functions directly from native threads.
  • ✅ Always use napi_call_threadsafe_function.
  • 🔁 Avoid tight loops or flooding ArkTS with native requests.
  • 🧼 Clean up CallbackData and release resources properly.

💬 Conclusion

napi_create_threadsafe_function is a vital tool for any HarmonyOSNext NDK developer who wants to safely interact with the ArkTS layer from native code. It offers flexibility, safety, and scalability for hybrid app architectures involving both C++ and ArkTS logic.

📚 Resources

Document
The OpenCms demo, brought to you by Alkacon Software.developer.huawei.com
Document
The OpenCms demo, brought to you by Alkacon Software.developer.huawei.com

Written by Bunyamin Eymen Alagoz

Top comments (0)