DEV Community

HarmonyOS
HarmonyOS

Posted on

Node-API Part-9: Running or Stopping an Event Loop in Asynchronous Threads on HarmonyOSNext

Read the original article:Node-API Part-9: Running or Stopping an Event Loop in Asynchronous Threads on HarmonyOSNext

🚀 Node-API Part-9: Running or Stopping an Event Loop in Asynchronous Threads on HarmonyOSNext

🧩 Introduction

In hybrid HarmonyOS development, it’s not uncommon to encounter scenarios where native C++ components must call asynchronous ArkTS methods — especially in performance-critical or multithreaded contexts. But how do we manage ArkTS promises or timers running inside native threads?

That’s where napi_run_event_loop and napi_stop_event_loop come into play. These Node-API extension methods allow native threads to run and control event loops, enabling seamless cross-thread async execution between ArkTS and C++.

In this article, we’ll explore the mechanism, modes, and a complete working example of running an ArkTS-based promise from a native thread, and gracefully shutting it down.

📜 Context

Let’s consider this common pattern:

  • You create a native C++ thread in HarmonyOS.
  • Inside this thread, you want to load an ArkTS module (like ObjectUtils.ets) and call a promise-based function (e.g., SetTimeout()).
  • This function uses setTimeout() and resolves after a delay.
  • You want to wait for the function to finish before shutting down the thread.

How can you achieve this safely?

The answer lies in managing the event loop manually using napi_run_event_loop and napi_stop_event_loop.

🛠 Description

Node-API provides two key functions for handling event loops inside native threads:

  • napi_run_event_loop(env, mode)
  • napi_stop_event_loop(env)

These methods help native threads process asynchronous tasks initiated by ArkTS code (like promises or timers).

There are two modes available:

napi_event_mode_nowait

  • The thread does not block.
  • Tasks in the event queue are processed once.
  • If no task is found, the event loop exits immediately.

napi_event_mode_default

  • The thread is blocked and continuously polls the queue.
  • Best suited when waiting for an async callback to finish (e.g., Promise.then()).

Once a task completes, the callback function should invoke napi_stop_event_loop(env) to terminate the loop gracefully.

🔧 Solution / Approach

Here’s the full strategy:

  1. Start a native thread in C++ using pthread_create.
  2. Create a new ArkTS runtime within that thread using napi_create_ark_runtime.
  3. Dynamically load an .ets module with napi_load_module_with_info.
  4. Call a promise-returning function (SetTimeout) and attach .then() with native callbacks.
  5. Inside the callback, stop the event loop with napi_stop_event_loop.
  6. Join the thread and clean up.

⚙️ Implementation in Sample Project

🧩 API Declaration in index.d.ts

export const runEventLoop: (isDefault: boolean) => object;
Enter fullscreen mode Exit fullscreen mode

index.d.ts

🏗️napi_init.cpp

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

static napi_value ResolvedCallback(napi_env env, napi_callback_info info) {
    // Stop the event loop when the promise is resolved.
    OH_LOG_INFO(LOG_APP, "XXX Promise resolved, stopping event loop.");
    napi_stop_event_loop(env);
    return nullptr;
}

static napi_value RejectedCallback(napi_env env, napi_callback_info info) {
    // Stop the event loop when the promise is rejected.
    OH_LOG_INFO(LOG_APP, "XXX Promise rejected, stopping event loop.");
    napi_stop_event_loop(env);
    return nullptr;
}

static void *RunEventLoopFunc(void *arg) {
    napi_env env;
    // 1. Create a new ArkTS runtime instance.
    napi_status ret = napi_create_ark_runtime(&env);
    if (ret != napi_ok) {
        OH_LOG_INFO(LOG_APP, "XXX Failed to create ArkTS runtime.");
        return nullptr;
    }
    OH_LOG_INFO(LOG_APP, "XXX ArkTS runtime created successfully.");

    // 2. Load the custom ArkTS module.
    napi_value objectUtils;
    ret = napi_load_module_with_info(env, "entry/src/main/ets/pages/ObjectUtils", "com.huawei.myapplication/entry",
                                     &objectUtils);
    if (ret != napi_ok) {
        OH_LOG_INFO(LOG_APP, "XXX Failed to load module: ObjectUtils.");
        return nullptr;
    }
    OH_LOG_INFO(LOG_APP, "XXX Module ObjectUtils loaded successfully.");

    // 3. Call the asynchronous SetTimeout API.
    napi_value setTimeout = nullptr;
    napi_value promise = nullptr;

    napi_get_named_property(env, objectUtils, "SetTimeout", &setTimeout);
    OH_LOG_INFO(LOG_APP, "XXX SetTimeout function obtained.");

    napi_call_function(env, objectUtils, setTimeout, 0, nullptr, &promise);
    OH_LOG_INFO(LOG_APP, "XXX SetTimeout function called, promise returned.");

    // Get the 'then' function of the returned promise.
    napi_value theFunc = nullptr;
    if (napi_get_named_property(env, promise, "then", &theFunc) != napi_ok) {
        OH_LOG_INFO(LOG_APP, "XXX Failed to get 'then' function from promise.");
        return nullptr;
    }

    // Create the resolve and reject callback functions.
    napi_value resolvedCallback = nullptr;
    napi_value rejectedCallback = nullptr;
    napi_create_function(env, "resolvedCallback", NAPI_AUTO_LENGTH, ResolvedCallback, nullptr, &resolvedCallback);
    napi_create_function(env, "rejectedCallback", NAPI_AUTO_LENGTH, RejectedCallback, nullptr, &rejectedCallback);
    napi_value argv[2] = {resolvedCallback, rejectedCallback};

    // Call the 'then' method with the resolve and reject callbacks.
    napi_call_function(env, promise, theFunc, 2, argv, nullptr);
    OH_LOG_INFO(LOG_APP, "XXX Then function called with resolve and reject callbacks.");

    // Decide which event loop mode to use.
    auto flag = reinterpret_cast<bool *>(arg);
    if (*flag == true) {
        OH_LOG_INFO(LOG_APP, "XXX Running event loop in default (blocking) mode.");
        napi_run_event_loop(env, napi_event_mode_default);
    } else {
        OH_LOG_INFO(LOG_APP, "XXX Running event loop in nowait (non-blocking) mode.");
        napi_run_event_loop(env, napi_event_mode_nowait);
    }
    return nullptr;
}

static napi_value RunEventLoop(napi_env env, napi_callback_info info) {
    pthread_t tid;
    size_t argc = 1;
    napi_value argv[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);

    // Get the boolean flag from the callback argument.
    bool flag = false;
    napi_get_value_bool(env, argv[0], &flag);
    OH_LOG_INFO(LOG_APP, "XXX RunEventLoop called with flag: %s", flag ? "true" : "false");

    // Create and run a new thread.
    pthread_create(&tid, nullptr, RunEventLoopFunc, &flag);
    pthread_join(tid, nullptr);
    OH_LOG_INFO(LOG_APP, "XXX Thread joined after event loop run.");

    return nullptr;
}

// Register the native module.
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"runEventLoop", nullptr, RunEventLoop, nullptr, nullptr, nullptr, napi_default, nullptr}};
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    OH_LOG_INFO(LOG_APP, "XXX Native module initialized.");
    return exports;
}
EXTERN_C_END

// Module definition structure.
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},
};

// Register the module when the library is loaded.
extern "C" __attribute__((constructor)) void RegisterEntryModule() {
    OH_LOG_INFO(LOG_APP, "XXX Registering native module.");
    napi_module_register(&nativeModule);
}

Enter fullscreen mode Exit fullscreen mode

napi_init.cpp

📲ObjectUtils.ets

export function SetTimeout(): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.info('set timer delay 1s');
      resolve();
    }, 1000)
  })
}
Enter fullscreen mode Exit fullscreen mode

ObjectUtil.ets

▶️Index.ets

import { hilog } from '@kit.PerformanceAnalysisKit';
import testNapi from 'libentry.so';

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

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            testNapi.runEventLoop(true);
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Enter fullscreen mode Exit fullscreen mode

Index.ets

🛠 CMake & Build Config

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()

                    find_library(
                        # Sets the name of the path variable.
                        hilog-lib
                        # Specifies the name of the NDK library that
                        # you want CMake to locate.
                        hilog_ndk.z
                    )

                    add_definitions("-DLOG_DOMAIN=0x0000")
                    add_definitions("-DLOG_TAG=\"testTag\"")

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

add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so)
Enter fullscreen mode Exit fullscreen mode

CMakeLists.txt

🧮build-profile.json5 (Entry Level)

Set runtimeOnly config in arkOptions-buildOption please.

{
  "apiType": "stageMode",
  "buildOption": {

    "arkOptions" : {
      "runtimeOnly" : {
        "sources": [
          "./src/main/ets/pages/ObjectUtils.ets"
        ]
      }
    },
    "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",
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

build-profile.json5

🧪 Key Takeaways

  • Thread-local ArkTS runtimes are fully capable of loading .ets files and executing ArkTS logic.
  • You can process async ArkTS logic from native threads using event loops.
  • napi_event_mode_default is ideal for blocking until tasks finish.
  • napi_stop_event_loop() allows ArkTS-side callbacks to terminate native event loops safely.
  • Proper synchronization with pthread_join ensures safe multithreading.

🧱 Example Scenario Recap

You have an ArkTS module that delays for 1 second. You want to:

  • Load and invoke it inside a native thread.
  • Block the thread until it finishes.
  • Shut down the event loop when the Promise resolves.

By combining napi_load_module_with_info, napi_call_function, and napi_run_event_loop, you can do all this with just a few lines of native and ArkTS code.

🧠 Conclusion

Managing asynchronous ArkTS operations within native C++ threads is not only possible on HarmonyOS — it’s efficient and well-supported thanks to Node-API’s event loop extensions. By combining napi_run_event_loop and napi_stop_event_loop, developers can safely bridge ArkTS promises, timers, or I/O callbacks inside native contexts.

This capability is especially useful in complex apps that require performance, modularity, and cross-layer interaction. Whether you’re building IoT runtimes, modular plugin systems, or just want more control over async flow in native modules, mastering this pattern unlocks a powerful layer of interoperability in HarmonyOS.

With just a few lines of setup and the right runtime configuration, your native thread can speak TypeScript fluently — on your terms.

📚 Additional Resources

Top comments (0)