How to Bridge Native Code and ArkTS on HarmonyOS Using Node‑API Thread‑Safe Functions

This article explains how HarmonyOS developers can reuse native C/C++ file upload code across iOS, Android, and HarmonyOS by employing Node‑API thread‑safe functions to asynchronously report upload progress, errors, and completion from native threads back to the ArkTS UI thread.

Sohu Smart Platform Tech Team
Sohu Smart Platform Tech Team
Sohu Smart Platform Tech Team
How to Bridge Native Code and ArkTS on HarmonyOS Using Node‑API Thread‑Safe Functions

Introduction

HarmonyOS is an emerging smart operating system, and more applications are being adapted to it, which creates a three‑platform compatibility challenge (iOS, Android, HarmonyOS). For file upload/download scenarios, a common solution is to move the functionality down to C so that native code can be reused, but HarmonyOS raises a specific problem: how to asynchronously callback native file upload/download progress from a native thread to the ArkTS (JS) thread.

Answer: Use the thread‑safe functions provided by Node‑API to pass progress from the native thread to the ArkTS thread.

Native、ArkTs线程异步回调
Native、ArkTs线程异步回调

What is Node‑API?

Node‑API (formerly N‑API) is an API for building native functionality that is independent of the underlying JavaScript engine (e.g., V8) and is maintained as part of Node.js. HarmonyOS Native API wraps and rewrites parts of Node‑API, exposing a subset to enable interaction between ArkTS/JS and C/C++ modules.

Node‑API must be called on the same thread as the JS call; the napi_env cannot be used across threads, and using it from a native sub‑thread will cause a crash.

Node-API结构层次图
Node-API结构层次图

Thread‑Safe Functions (TSF)

Thread‑safe functions (TSF) solve cross‑thread function‑call problems by wrapping a normal JS function so it can be passed between threads. The definition of napi_create_threadsafe_function is shown below.

NAPI_EXTERN napi_status
napi_create_threadsafe_function(napi_env env,
                                 napi_value func,
                                 napi_value async_resource,
                                 napi_value async_resource_name,
                                 size_t max_queue_size,
                                 size_t initial_thread_count,
                                 void* thread_finalize_data,
                                 napi_finalize thread_finalize_cb,
                                 void* context,
                                 napi_threadsafe_function_call_js call_js_cb,
                                 napi_threadsafe_function* result);

The table below explains each parameter.

Parameter

Explanation

env

Calling environment

func

JS callback function

async_resource

Optional object associated with async work for async_hooks

async_resource_name

JS string identifier for async_hooks resource type

max_queue_size

Maximum size of the event queue (0 = unlimited)

initial_thread_count

Initial thread count, including the main thread

thread_finalize_data

Optional data passed to the finalize callback

thread_finalize_cb

Optional function called when the TSF is destroyed

context

TSF context

call_js_cb

Optional callback that invokes the JS function on the main thread

线程安全函数native交互图
线程安全函数native交互图

Using TSF for Cross‑Thread Communication

The typical workflow consists of three steps:

Native side creates a TSF, binding the ArkTS API callback and the native thread‑safe callback function.

Native sub‑thread performs the asynchronous task and calls napi_call_threadsafe_function, which schedules the JS callback on the event loop.

In the JS callback, napi_call_function invokes the original ArkTS function, allowing UI updates such as progress bars.

Event Loop (libuv)

Libuv is a C‑based asynchronous I/O library that drives the event loop. A simple libuv example is shown below.

#include "stdio.h"
#include "uv.h"
int main() {
  uv_loop_t *loop = uv_default_loop();
  printf("hello libuv");
  uv_run(loop, UV_RUN_DEFAULT);
}

Libuv repeatedly extracts events from the watcher queue, uses epoll_wait to wait for I/O events, and dispatches the corresponding callbacks, thereby enabling communication between threads.

node事件循环.drawio.png
node事件循环.drawio.png

Practical Example: HarmonyOS Sohu News File Upload

The article demonstrates a real‑world scenario where video and image files are uploaded from a native sub‑thread, and upload progress, errors, and completion are reported back to the ArkTS UI using TSF.

Upload scenario
Upload scenario

The implementation includes:

Creating the TSF on the ArkTS side and binding onProgressFun, onCompleteFun, and onErrorFun callbacks.

Defining a TypeScript declaration for the upload API:

/**
 * File upload
 * @param uploadUrl  Upload URL
 * @param filePath   Local file path
 * @param onProgressFun  Progress callback (0‑100)
 * @param onCompleteFun  Completion callback
 * @param onErrorFun     Error callback (code & description)
 */
export const uploadFile: (uploadUrl: string, filePath: string,
  onProgressFun: (progress: number) => void,
  onCompleteFun: () => void,
  onErrorFun: (errorCode: number, errorDesc: string) => void) => void;

Native C++ functions wrap libcurl for the actual upload, invoke the TSF via napi_call_threadsafe_function, and handle progress calculation.

static napi_value NAPI_Global_uploadFile(napi_env env, napi_callback_info info) {
    size_t argc = 5;
    napi_value args[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};
    // ... argument parsing omitted ...
    MultiFileManager::getInstance()->uploadFileWrapper(uploadUrl, filePath, env, args[2], args[3], args[4]);
    return nullptr;
}

void MultiFileManager::uploadFileWrapper(std::string uploadUrl, std::string filePath,
                                         napi_env env, napi_value onProgressFun,
                                         napi_value onCompleteFun, napi_value onErrorFun) {
    UploadBridge::createThreadUploadCallBack(env, onProgressFun, onCompleteFun, onErrorFun);
    pool->enqueue([this](std::string uploadUrl, std::string filePath) { uploadFile(uploadUrl, filePath); },
                  uploadUrl, filePath);
}

When the native thread reports progress, it calls napi_call_threadsafe_function, which ultimately triggers the ArkTS onProgressFun to update the UI.

void UploadBridge::createThreadUploadCallBack(napi_env env, napi_value onProgressFun,
                                            napi_value onCompleteFun, napi_value onErrorFun) {
    napi_value workName;
    napi_create_string_utf8(env, "workItem", NAPI_AUTO_LENGTH, &workName);
    napi_create_threadsafe_function(env, onProgressFun, NULL, workName, 0, 1,
                                    NULL, NULL, NULL, callJsUploadOnProgress, &onProgressTsFn);
    // ... other TSFs for complete and error omitted ...
}

The JS side callback implementation looks like this:

void UploadBridge::callJsUploadOnProgress(napi_env env, napi_value jsCallBack, void *context, void *data) {
    napi_value argv;
    napi_create_int32(env, uploadProgress, &argv);
    napi_value result = nullptr;
    napi_call_function(env, nullptr, jsCallBack, 1, &argv, &result);
    // ...
}

Finally, the article notes that libuv can also be used directly for cross‑thread communication, but it requires compiling libuv from source.

libuv结构图
libuv结构图
nativeHarmonyOSFile UploadArkTSNode-APIThread-safe function
Sohu Smart Platform Tech Team
Written by

Sohu Smart Platform Tech Team

The Sohu News app's technical sharing hub, offering deep tech analyses, the latest industry news, and fun developer anecdotes. Follow us to discover the team's daily joys.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.