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.
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.
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.
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
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.
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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
