DEV Community

HarmonyOS
HarmonyOS

Posted on

Node-API Part-11 : Passing Prioritised Work from Native C++ to ArkTS — a Step‑by‑Step Guide

Read the original article:Node-API Part-11 : Passing Prioritised Work from Native C++ to ArkTS — a Step‑by‑Step Guide

Introduction

When your HarmonyOS NEXT application needs to push work from a C/C++ worker thread back to the ArkTS event loop, the Node-API extension napi_call_threadsafe_function_with_priority() is the most direct – and safest – tool you have. It builds on the regular thread-safe-function (TSFN) mechanism, but adds two extra dials: task priority and queue position (head or tail). With those, you can fine-tune both the importance of each job and how it is blended into the ArkTS message queue, all without worrying about mutexes or race conditions.

Why a“priority TSFN” ?

  • Smoother UX. A media player might treat audio-callback tasks as immediate, analytics pings as low, and thumbnail generation as idle so that the UI never stutters.
  • Deterministic ordering. Sometimes you want a burst of urgent work to jump the line (enqueue at the head), while routine updates can wait their turn (enqueue at the tail).
  • Zero-copy context hand-off. TSFNs let you pass an opaque void* pointer straight through to the ArkTS layer, avoiding serialization overhead.

How the flow feels in practice

1.ArkTS thread (setup)
◦ Create a TSFN linked to a JavaScript/ArkTS callback.

◦ Spin up an async-work object that will run off-thread.

2. Worker thread (execution)
◦ Inside ExecuteWork, call the priority API as many times as you like, mixing priorities and queue positions.

◦ ArkTS silently marshals those requests into its own message loop.

3. ArkTS thread (delivery)
◦ The runtime drains its queue, invoking your JS callback in the order implied by priority + head/tail.

◦ Any return value can be pulled out with napi_get_value_* if you need it back in native code.

4. Cleanup
◦ Release the TSFN (napi_release_threadsafe_function) and delete the async work once everything is done.

🧩 Step-by-Step: Code Integration in Sample Project

Let’s go through how to build this into your HarmonyOSNext project.

🧩 API Declaration in index.d.ts


export const callThreadSafeWithPriority: (cb: (a: number, b: number) => number) => void;

Enter fullscreen mode Exit fullscreen mode

index.d.ts

🏗️napi_init.cpp

// napi_init.cpp
// ------------------------------------------------------------
// Example add-on that queues tasks back to the ArkTS main
// thread with *different* priorities using
// napi_call_threadsafe_function_with_priority.
// ------------------------------------------------------------

#include "napi/native_api.h"   // Node-API declarations
#include <string.h>            
#include <stdlib.h>            

// -------- State container passed across callbacks ------------
struct CallbackData {
    napi_threadsafe_function tsfn;  // Handle to the thread-safe function
    napi_async_work          work;  // Handle to the worker-thread job
};

// ------------------------------------------------------------
// JS-side callback runner  (always executes on ArkTS main thread)
// ------------------------------------------------------------
static void CallJs(napi_env env,
                   napi_value jsCb,
                   void *context,
                   void *data)
{
    if (env == nullptr) {           // Runtime is shutting down → do nothing
        return;
    }

    // Build arguments (12, 15) to pass into the original JS callback
    napi_value undefined  = nullptr;
    napi_get_undefined(env, &undefined);

    napi_value number1 = nullptr;
    napi_create_int32(env, 12, &number1);

    napi_value number2 = nullptr;
    napi_create_int32(env, 15, &number2);

    napi_value argv[2] = { number1, number2 };

    // Invoke the callback:  result = jsCb(12, 15)
    napi_value resultNumber = nullptr;
    napi_call_function(env, undefined, jsCb, 2, argv, &resultNumber);

    // Optional: read the numeric return value for logging
    int32_t res = 0;
    napi_get_value_int32(env, resultNumber, &res);
}

// ------------------------------------------------------------
// Worker-thread routine — posts five tasks with varying priorities
// ------------------------------------------------------------
static void ExecuteWork(napi_env env, void *data)
{
    CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);

    // 1) IDLE priority, enqueue at tail
    napi_call_threadsafe_function_with_priority(
        callbackData->tsfn, nullptr, napi_priority_idle, true);

    // 2) LOW priority, enqueue at tail
    napi_call_threadsafe_function_with_priority(
        callbackData->tsfn, nullptr, napi_priority_low, true);

    // 3) HIGH priority, enqueue at tail
    napi_call_threadsafe_function_with_priority(
        callbackData->tsfn, nullptr, napi_priority_high, true);

    // 4) IMMEDIATE priority (highest), enqueue at tail
    napi_call_threadsafe_function_with_priority(
        callbackData->tsfn, nullptr, napi_priority_immediate, true);

    // 5) HIGH priority, enqueue at *head*  → executes before #3
    napi_call_threadsafe_function_with_priority(
        callbackData->tsfn, nullptr, napi_priority_high, false);
}

// ------------------------------------------------------------
// Completion callback — runs on main thread after ExecuteWork returns
// ------------------------------------------------------------
static void WorkComplete(napi_env env, napi_status status, void *data)
{
    CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);

    // Release TSFN so runtime can clean it up when refcount hits zero
    napi_release_threadsafe_function(callbackData->tsfn, napi_tsfn_release);

    // Destroy the async-work handle
    napi_delete_async_work(env, callbackData->work);

    // Avoid dangling pointers
    callbackData->work = nullptr;
    callbackData->tsfn = nullptr;
}

// ------------------------------------------------------------
// Native method exposed to ArkTS: creates TSFN + async work
// ------------------------------------------------------------
static napi_value CallThreadSafeWithPriority(napi_env env,
                                             napi_callback_info info)
{
    // ---- Parse JS arguments ----
    size_t argc = 1;
    napi_value jsCb = nullptr;          // First arg: user's JS callback
    CallbackData *callbackData = nullptr;
    napi_get_cb_info(env, info, &argc, &jsCb, nullptr,
                     reinterpret_cast<void **>(&callbackData));

    // ---- Create a resource name (helps debugging tools) ----
    napi_value resourceName = nullptr;
    napi_create_string_utf8(env,
                            "Thread-safe Function Demo",
                            NAPI_AUTO_LENGTH,
                            &resourceName);

    // ---- Create the thread-safe function ----
    napi_create_threadsafe_function(
        env,
        jsCb,                 // JS callback to invoke
        nullptr,              // async_resource
        resourceName,         // async_resource_name
        0,                    // unlimited queue
        1,                    // initial refcount
        callbackData,         // finalize_data (not used)
        nullptr,              // finalize_cb
        callbackData,         // context passed to CallJs
        CallJs,               // call_js_cb
        &callbackData->tsfn); // out parameter

    // ---- Create async work item that will run ExecuteWork ----
    napi_create_async_work(
        env,
        nullptr,              // async_resource
        resourceName,
        ExecuteWork,          // execute (worker thread)
        WorkComplete,         // complete (main thread)
        callbackData,         // data passed to both
        &callbackData->work); // out parameter

    // ---- Dispatch the work item ----
    napi_queue_async_work(env, callbackData->work);

    // No synchronous result
    return nullptr;
}

// ------------------------------------------------------------
// Module registration boilerplate
// ------------------------------------------------------------
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    // One CallbackData instance per module instance
    CallbackData *callbackData = new CallbackData();

    // Define JS property: exports.callThreadSafeWithPriority = native fn
    napi_property_descriptor desc[] = {
        { "callThreadSafeWithPriority",
          nullptr,
          CallThreadSafeWithPriority,
          nullptr, nullptr, nullptr,
          napi_default,
          callbackData }
    };
    napi_define_properties(env, exports,
                           sizeof(desc) / sizeof(desc[0]),
                           desc);

    return exports;
}
EXTERN_C_END

// Describe the native module for the loader
static napi_module nativeModule = {
    .nm_version       = 1,
    .nm_flags         = 0,
    .nm_filename      = nullptr,
    .nm_register_func = Init,
    .nm_modname       = "entry",  // import name on ArkTS side
    .nm_priv          = nullptr,
    .reserved         = { 0 },
};

// Automatic registration when the shared object is loaded
extern "C" __attribute__((constructor))
void RegisterEntryModule()
{
    napi_module_register(&nativeModule);
}

Enter fullscreen mode Exit fullscreen mode

napi_init.cpp

▶️Index.ets

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

            let callback = (a: number, b: number): number => {
              console.info('Callback Log, Sum = ' + (a + b))
              return a + b;
            }

            testNapi.callThreadSafeWithPriority(callback);

          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Enter fullscreen mode Exit fullscreen mode

Index.ets

🧮build-profile.json5 (Entry Level)

Set runtimeOnly config in arkOptions-buildOption please.

{
  "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",
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

build-profile.json5

Practical tips & pitfalls

  • Tip — batch wisely. Group related low-priority jobs into one worker; don’t spam multiple threads with idle-priority messages.
  • Tip — keep callbacks lean. The ArkTS side should finish fast; long-running JS will still block the event loop regardless of priority.
  • Pitfall — forgotten release. Leaking a TSFN keeps its env alive and can crash the VM on hot-reload. Always free it in the completion callback.
  • Pitfall — cross-env misuse. Never pass a TSFN from one napi_env instance to another. The runtime’s multi-thread checker will abort.

When should you not use it?

If every task is trivially short or naturally ordered, the plain napi_call_threadsafe_function() is simpler and has less cognitive overhead. Likewise, heavy data shuffling may be better served by a shared ring-buffer or an IPC channel outside Node-API altogether.

Conclusion

napi_call_threadsafe_function_with_priority() is a small but powerful extension that gives your native add-on the finesse of a real-time scheduler without the usual concurrency headaches. By assigning a priority level and deciding whether to queue at the head or tail, you can keep high-value work snappy and let background tasks wait politely – all while staying 100 % thread-safe on HarmonyOS NEXT’s ArkTS runtime.

📚 Additional 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)