This content originally appeared on DEV Community and was authored by HarmonyOS
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;
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);
}
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%')
}
}
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",
}
]
}
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
This content originally appeared on DEV Community and was authored by HarmonyOS

HarmonyOS | Sciencx (2025-09-02T02:30:55+00:00) Node-API Part-11 : Passing Prioritised Work from Native C++ to ArkTS — a Step‑by‑Step Guide. Retrieved from https://www.scien.cx/2025/09/02/node-api-part-11-passing-prioritised-work-from-native-c-to-arkts-a-step%e2%80%91by%e2%80%91step-guide/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.