1e41f4b71Sopenharmony_ci# Node-API常见问题
2e41f4b71Sopenharmony_ci
3e41f4b71Sopenharmony_ci## ArkTS/JS侧import xxx from libxxx.so后,使用xxx报错显示undefined/not callable或明确的Error message
4e41f4b71Sopenharmony_ci
5e41f4b71Sopenharmony_ci1. 排查.cpp文件在注册模块时的模块名称与so的名称匹配一致。
6e41f4b71Sopenharmony_ci   如模块名为entry,则so的名字为libentry.so,napi_module中nm_modname字段应为entry,大小写与模块名保持一致。
7e41f4b71Sopenharmony_ci
8e41f4b71Sopenharmony_ci2. 排查so是否加载成功。
9e41f4b71Sopenharmony_ci   应用启动时过滤模块加载相关日志,重点搜索"dlopen"关键字,确认是否有相关报错信息;常见加载失败原因有权限不足、so文件不存在以及so已拉入黑名单等,可根据以下关键错误日志确认问题。其中,多线程场景(worker、taskpool等)下优先检查模块实现中nm_modname是否与模块名一致,区分大小写。
10e41f4b71Sopenharmony_ci
11e41f4b71Sopenharmony_ci3. 排查依赖的so是否加载成功。
12e41f4b71Sopenharmony_ci   确定所依赖的其它so是否打包到应用中以及是否有权限打开。常见加载失败原因有权限不足、so文件不存在等,可根据以下关键错误日志确认问题。
13e41f4b71Sopenharmony_ci
14e41f4b71Sopenharmony_ci4. 排查模块导入方式与so路径是否对应。
15e41f4b71Sopenharmony_ci   若JS侧导入模块的形式为: import xxx from '\@ohos.yyy.zzz',则该so将在/system/lib/module/yyy中找libzzz.z.solibzzz_napi.z.so,若so不存在或名称无法对应,则报错日志中会出现dlopen相关日志。
16e41f4b71Sopenharmony_ci
17e41f4b71Sopenharmony_ci   注意,32位系统路径为/system/lib,64位系统路径为/system/lib6418e41f4b71Sopenharmony_ci
19e41f4b71Sopenharmony_ci| **已知关键错误日志** | **修改建议** |
20e41f4b71Sopenharmony_ci| -------- | -------- |
21e41f4b71Sopenharmony_ci| module $SO is not allowed to load in restricted runtime | $SO表示模块名。该模块不在受限worker线程的so加载白名单,不允许加载,建议用户删除该模块。 |
22e41f4b71Sopenharmony_ci| module $SO is in blocklist, loading prohibited | $SO表示模块名。受卡片或者Extension管控,该模块在黑名单内,不允许加载,建议用户删除该模块。 |
23e41f4b71Sopenharmony_ci| load module failed. $ERRMSG | 动态库加载失败。$ERRMSG表示加载失败原因,一般常见原因是so文件不存在、依赖的so文件不存在或者符号未定义,需根据加载失败原因具体分析。 |
24e41f4b71Sopenharmony_ci| try to load abc file from $FILEPATH failed. | 通常加载动态库和abc文件为二选一:如果是要加载动态库并且加载失败,该告警可以忽略;如果是要加载abc文件,则该错误打印的原因是abc文件不存在,$FILEPATH表示模块路径。 |
25e41f4b71Sopenharmony_ci
26e41f4b71Sopenharmony_ci5. 如果有明确的Error message,可以通过Error message判断当前问题。
27e41f4b71Sopenharmony_ci
28e41f4b71Sopenharmony_ci| **Error message** | **修改建议** |
29e41f4b71Sopenharmony_ci| -------- | -------- |
30e41f4b71Sopenharmony_ci| First attempt: $ERRMSG | 首先加载后缀不拼接'_napi'的模块名为'xxx'的so,如果加载失败会有该错误信息,$ERRMSG表示具体加载时的错误信息。 |
31e41f4b71Sopenharmony_ci| Second attempt: $ERRMSG | 第二次加载后缀拼接'_napi'的模块名为'xxx_napi'的so,如果加载失败会有该错误信息,$ERRMSG表示具体加载时的错误信息。 |
32e41f4b71Sopenharmony_ci| try to load abc file from xxx failed | 第三次加载名字为'xxx'的abc文件,如果加载失败会有该错误信息。 |
33e41f4b71Sopenharmony_ci| module xxx is not allowed to load in restricted runtime. | 该模块不允许在受限运行时中使用,xxx表示模块名,建议用户删除该模块。 |
34e41f4b71Sopenharmony_ci| module xxx is in blocklist, loading prohibited. | 该模块不允许在当前extension下使用,xxx表示模块名,建议用户删除该模块。 |
35e41f4b71Sopenharmony_ci
36e41f4b71Sopenharmony_ci## 接口执行结果非预期,日志显示occur exception need return
37e41f4b71Sopenharmony_ci
38e41f4b71Sopenharmony_ci部分Node-API接口在调用结束前会进行检查,检查虚拟机中是否存在JS异常。如果存在异常,则会打印出occur exception need return日志,并打印出检查点所在的行号,以及对应的Node-API接口名称。
39e41f4b71Sopenharmony_ci
40e41f4b71Sopenharmony_ci解决此类问题有以下两种思路:
41e41f4b71Sopenharmony_ci
42e41f4b71Sopenharmony_ci- 若该异常开发者不关心,可以选择直接清除。
43e41f4b71Sopenharmony_ci  可直接使用napi接口napi_get_and_clear_last_exception,清理异常。调用时机:在打印occur exception need return日志的接口之前调用。
44e41f4b71Sopenharmony_ci
45e41f4b71Sopenharmony_ci- 将该异常继续向上抛到ArkTS层,在ArkTS层进行捕获。
46e41f4b71Sopenharmony_ci  发生异常时,可以选择走异常分支, 确保不再走多余的Native逻辑 ,直接返回到ArkTS层。
47e41f4b71Sopenharmony_ci
48e41f4b71Sopenharmony_ci## napi_value和napi_ref的生命周期有何区别
49e41f4b71Sopenharmony_ci
50e41f4b71Sopenharmony_ci- native_value由HandleScope管理,一般开发者不需要自己加HandleScope(uv_queue_work的complete callback除外)。
51e41f4b71Sopenharmony_ci
52e41f4b71Sopenharmony_ci- napi_ref由开发者自己管理,需要手动delete。
53e41f4b71Sopenharmony_ci
54e41f4b71Sopenharmony_ci## Node-API接口返回值不是napi_ok时,如何排查定位
55e41f4b71Sopenharmony_ci
56e41f4b71Sopenharmony_ciNode-API接口正常执行后,会返回一个napi_ok的状态枚举值,若napi接口返回值不为napi_ok,可从以下几个方面进行排查。
57e41f4b71Sopenharmony_ci
58e41f4b71Sopenharmony_ci- Node-API接口执行前一般会进行入参校验,首先进行的是判空校验。在代码中体现为:
59e41f4b71Sopenharmony_ci
60e41f4b71Sopenharmony_ci  ```cpp
61e41f4b71Sopenharmony_ci  CHECK_ENV: env判空校验
62e41f4b71Sopenharmony_ci  CHECK_ARG:其它入参判空校验
63e41f4b71Sopenharmony_ci  ```
64e41f4b71Sopenharmony_ci
65e41f4b71Sopenharmony_ci- 某些Node-API接口还有入参类型校验。比如napi_get_value_double接口是获取JS number对应的C double值,首先就要保证的是:JS value类型为number,因此可以看到相关校验。
66e41f4b71Sopenharmony_ci
67e41f4b71Sopenharmony_ci  ```cpp
68e41f4b71Sopenharmony_ci  RETURN_STATUS_IF_FALSE(env, nativeValue->TypeOf() == NATIVE_NUMBER, napi_number_expected);
69e41f4b71Sopenharmony_ci  ```
70e41f4b71Sopenharmony_ci
71e41f4b71Sopenharmony_ci- 还有一些接口会对其执行结果进行校验。比如napi_call_function这个接口,其功能是执行一个JS function,当JS function中出现异常时,Node-API将会返回napi_pending_exception的状态值。
72e41f4b71Sopenharmony_ci
73e41f4b71Sopenharmony_ci  ```cpp
74e41f4b71Sopenharmony_ci  auto resultValue = engine->CallFunction(nativeRecv, nativeFunc, nativeArgv, argc);
75e41f4b71Sopenharmony_ci  RETURN_STATUS_IF_FALSE(env, resultValue != nullptr, napi_pending_exception)
76e41f4b71Sopenharmony_ci  ```
77e41f4b71Sopenharmony_ci
78e41f4b71Sopenharmony_ci- 还有一些状态值需要根据相应Node-API接口具体分析:确认具体的状态值,分析这个状态值在什么情况下会返回,再排查具体出错原因。
79e41f4b71Sopenharmony_ci
80e41f4b71Sopenharmony_ci## napi_threadsafe_function内存泄漏,应该如何处理
81e41f4b71Sopenharmony_ci
82e41f4b71Sopenharmony_ci`napi_threadsafe_function`(下文简称tsfn)在使用时,常常会调用 `napi_acquire_threadsafe_function` 来更改tsfn的引用计数,确保tsfn不会意外被释放。但在使用完成后,应该及时使用 `napi_tsfn_release` 模式调用 `napi_release_threadsafe_function` 方法,以确保在所有调用回调都执行完成后,其引用计数能回归到调用 `napi_acquire_threadsafe_function` 方法之前的水平。当其引用计数归位0时,tsfn才能正确的被释放。
83e41f4b71Sopenharmony_ci
84e41f4b71Sopenharmony_ci当在env即将退出,但tsfn的引用计数未被归零时,应该使用 `napi_tsfn_abort` 模式调用 `napi_release_threadsafe_function` 方法,确保在env释放后不再对tsfn进行持有及使用。在env退出后,继续持有tsfn进行使用,是一种未定义的行为,可能会触发崩溃。
85e41f4b71Sopenharmony_ci
86e41f4b71Sopenharmony_ci如下代码将展示通过注册 `env_cleanup` 钩子函数的方式,以确保在env退出后不再继续持有tsfn。
87e41f4b71Sopenharmony_ci
88e41f4b71Sopenharmony_ci```cpp
89e41f4b71Sopenharmony_ci#include <hilog/log.h> // hilog, 输出日志, 需链接 libhilog_ndk.z.so
90e41f4b71Sopenharmony_ci#include <thread> // 创建线程
91e41f4b71Sopenharmony_ci#include <unistd.h> // 线程休眠
92e41f4b71Sopenharmony_ci
93e41f4b71Sopenharmony_ci// 定义输出日志的标签和域
94e41f4b71Sopenharmony_ci#undef LOG_DOMAIN
95e41f4b71Sopenharmony_ci#undef LOG_TAG
96e41f4b71Sopenharmony_ci#define LOG_DOMAIN 0x2342
97e41f4b71Sopenharmony_ci#define LOG_TAG "MY_TSFN_DEMO"
98e41f4b71Sopenharmony_ci
99e41f4b71Sopenharmony_ci/*
100e41f4b71Sopenharmony_ci  为构造一个env生命周期小于native生命周期的场景,
101e41f4b71Sopenharmony_ci  本文需要使用worker, taskpool 或 napi_create_ark_runtime 等方法,
102e41f4b71Sopenharmony_ci  创建非主线程的ArkTS运行环境,并人为的提前结束掉该线程
103e41f4b71Sopenharmony_ci*/
104e41f4b71Sopenharmony_ci
105e41f4b71Sopenharmony_ci
106e41f4b71Sopenharmony_ci// 定义一个数据结构,模拟存储tsfn的场景
107e41f4b71Sopenharmony_ciclass MyTsfnContext {
108e41f4b71Sopenharmony_cipublic:
109e41f4b71Sopenharmony_ci// 因使用了napi方法, MyTsfnContext 应当只在js线程被构造
110e41f4b71Sopenharmony_ciMyTsfnContext(napi_env env, napi_value workName) {
111e41f4b71Sopenharmony_ci    // 注册env销毁钩子函数
112e41f4b71Sopenharmony_ci    napi_add_env_cleanup_hook(env, Cleanup, this);
113e41f4b71Sopenharmony_ci    // 创建线程安全函数
114e41f4b71Sopenharmony_ci    if (napi_create_threadsafe_function(env, nullptr, nullptr, workName, 1, 1, this,
115e41f4b71Sopenharmony_ci            TsfnFinalize, this, TsfnCallJs, &tsfn_) != napi_ok) {
116e41f4b71Sopenharmony_ci        OH_LOG_INFO(LOG_APP, "tsfn is created faild");
117e41f4b71Sopenharmony_ci        return;
118e41f4b71Sopenharmony_ci    };
119e41f4b71Sopenharmony_ci};
120e41f4b71Sopenharmony_ci
121e41f4b71Sopenharmony_ci~MyTsfnContext() { OH_LOG_INFO(LOG_APP, "MyTsfnContext is deconstructed"); };
122e41f4b71Sopenharmony_ci
123e41f4b71Sopenharmony_cinapi_threadsafe_function GetTsfn() {
124e41f4b71Sopenharmony_ci    std::unique_lock<std::mutex> lock(mutex_);
125e41f4b71Sopenharmony_ci    return tsfn_;
126e41f4b71Sopenharmony_ci}
127e41f4b71Sopenharmony_ci
128e41f4b71Sopenharmony_cibool Acquire() {
129e41f4b71Sopenharmony_ci    if (GetTsfn() == nullptr) {
130e41f4b71Sopenharmony_ci        return false;
131e41f4b71Sopenharmony_ci    };
132e41f4b71Sopenharmony_ci    return (napi_acquire_threadsafe_function(GetTsfn()) == napi_ok);
133e41f4b71Sopenharmony_ci};
134e41f4b71Sopenharmony_ci
135e41f4b71Sopenharmony_cibool Release() {
136e41f4b71Sopenharmony_ci    if (GetTsfn() == nullptr) {
137e41f4b71Sopenharmony_ci        return false;
138e41f4b71Sopenharmony_ci    };
139e41f4b71Sopenharmony_ci    return (napi_release_threadsafe_function(GetTsfn(), napi_tsfn_release) == napi_ok);
140e41f4b71Sopenharmony_ci};
141e41f4b71Sopenharmony_ci
142e41f4b71Sopenharmony_cibool Call(void *data) {
143e41f4b71Sopenharmony_ci    if (GetTsfn() == nullptr) {
144e41f4b71Sopenharmony_ci        return false;
145e41f4b71Sopenharmony_ci    };
146e41f4b71Sopenharmony_ci    return (napi_call_threadsafe_function(GetTsfn(), data, napi_tsfn_blocking) == napi_ok);
147e41f4b71Sopenharmony_ci};
148e41f4b71Sopenharmony_ci
149e41f4b71Sopenharmony_ciprivate:
150e41f4b71Sopenharmony_ci// 保护多线程读写tsfn的准确性
151e41f4b71Sopenharmony_cistd::mutex mutex_;
152e41f4b71Sopenharmony_cinapi_threadsafe_function tsfn_ = nullptr;
153e41f4b71Sopenharmony_ci
154e41f4b71Sopenharmony_ci// napi_add_env_cleanup_hook 回调
155e41f4b71Sopenharmony_cistatic void Cleanup(void *data) {
156e41f4b71Sopenharmony_ci    MyTsfnContext *that = reinterpret_cast<MyTsfnContext *>(data);
157e41f4b71Sopenharmony_ci    napi_threadsafe_function tsfn = that->GetTsfn();
158e41f4b71Sopenharmony_ci    std::unique_lock<std::mutex> lock(that->mutex_);
159e41f4b71Sopenharmony_ci    that->tsfn_ = nullptr;
160e41f4b71Sopenharmony_ci    lock.unlock();
161e41f4b71Sopenharmony_ci    OH_LOG_WARN(LOG_APP, "cleanup is called");
162e41f4b71Sopenharmony_ci    napi_release_threadsafe_function(tsfn, napi_tsfn_abort);
163e41f4b71Sopenharmony_ci};
164e41f4b71Sopenharmony_ci
165e41f4b71Sopenharmony_ci// tsfn 释放时的回调
166e41f4b71Sopenharmony_cistatic void TsfnFinalize(napi_env env, void *data, void *hint) {
167e41f4b71Sopenharmony_ci    MyTsfnContext *ctx = reinterpret_cast<MyTsfnContext *>(data);
168e41f4b71Sopenharmony_ci    OH_LOG_INFO(LOG_APP, "tsfn is released");
169e41f4b71Sopenharmony_ci    napi_remove_env_cleanup_hook(env, MyTsfnContext::Cleanup, ctx);
170e41f4b71Sopenharmony_ci    // cleanup 提前释放线程安全函数, 为避免UAF, 将释放工作交给调用方
171e41f4b71Sopenharmony_ci    if (ctx->GetTsfn() != nullptr) {
172e41f4b71Sopenharmony_ci        OH_LOG_INFO(LOG_APP, "ctx is released");
173e41f4b71Sopenharmony_ci        delete ctx;
174e41f4b71Sopenharmony_ci    }
175e41f4b71Sopenharmony_ci};
176e41f4b71Sopenharmony_ci
177e41f4b71Sopenharmony_ci// tsfn 发送到 js 线程执行的回调
178e41f4b71Sopenharmony_cistatic void TsfnCallJs(napi_env env, napi_value func, void *context, void *data) {
179e41f4b71Sopenharmony_ci    MyTsfnContext *ctx = reinterpret_cast<MyTsfnContext *>(context);
180e41f4b71Sopenharmony_ci    char *str = reinterpret_cast<char *>(data);
181e41f4b71Sopenharmony_ci    OH_LOG_INFO(LOG_APP, "tsfn is called, data is: \"%{public}s\"", str);
182e41f4b71Sopenharmony_ci    // 业务逻辑已省略
183e41f4b71Sopenharmony_ci};
184e41f4b71Sopenharmony_ci};
185e41f4b71Sopenharmony_ci
186e41f4b71Sopenharmony_ci// 该方法需注册到模块, 注册名为 myTsfnDemo, 接口描述如下
187e41f4b71Sopenharmony_ci// export const myTsfnDemo: () => void;
188e41f4b71Sopenharmony_cinapi_value MyTsfnDemo(napi_env env, napi_callback_info info) {
189e41f4b71Sopenharmony_ci    OH_LOG_ERROR(LOG_APP, "MyTsfnDemo is called");
190e41f4b71Sopenharmony_ci    napi_value workName = nullptr;
191e41f4b71Sopenharmony_ci    napi_create_string_utf8(env, "MyTsfnWork", NAPI_AUTO_LENGTH, &workName);
192e41f4b71Sopenharmony_ci    MyTsfnContext *myContext = new MyTsfnContext(env, workName);
193e41f4b71Sopenharmony_ci    if (myContext->GetTsfn() == nullptr) {
194e41f4b71Sopenharmony_ci        OH_LOG_ERROR(LOG_APP, "faild to create tsfn");
195e41f4b71Sopenharmony_ci        delete myContext;
196e41f4b71Sopenharmony_ci        return nullptr;
197e41f4b71Sopenharmony_ci    };
198e41f4b71Sopenharmony_ci    char *data0 = new char[]{"Im call in ArkTS Thread"};
199e41f4b71Sopenharmony_ci    if (!myContext->Call(data0)) {
200e41f4b71Sopenharmony_ci        OH_LOG_INFO(LOG_APP, "call tsfn failed");
201e41f4b71Sopenharmony_ci    };
202e41f4b71Sopenharmony_ci
203e41f4b71Sopenharmony_ci    // 创建一个线程,模拟异步场景
204e41f4b71Sopenharmony_ci    std::thread(
205e41f4b71Sopenharmony_ci        [](MyTsfnContext *myCtx) {
206e41f4b71Sopenharmony_ci            if (!myCtx->Acquire()) {
207e41f4b71Sopenharmony_ci                OH_LOG_ERROR(LOG_APP, "acquire tsfn faild");
208e41f4b71Sopenharmony_ci                return;
209e41f4b71Sopenharmony_ci            };
210e41f4b71Sopenharmony_ci            char *data1 = new char[]{"Im call in std::thread"};
211e41f4b71Sopenharmony_ci            // 非必要操作, 仅用于异步流程tsfn仍有效
212e41f4b71Sopenharmony_ci            if (!myCtx->Call(data1)) {
213e41f4b71Sopenharmony_ci                OH_LOG_ERROR(LOG_APP, "call tsfn failed");
214e41f4b71Sopenharmony_ci            };
215e41f4b71Sopenharmony_ci            // 休眠 5s, 模拟耗时场景, env退出后, 异步任务仍未执行完成
216e41f4b71Sopenharmony_ci            sleep(5);
217e41f4b71Sopenharmony_ci            // 此时异步任务已执行完成, 但tsfn已被释放并置为 nullptr
218e41f4b71Sopenharmony_ci            char *data2 = new char[]{"Im call after work"};
219e41f4b71Sopenharmony_ci            if (!myCtx->Call(data2) && !myCtx->Release()) {
220e41f4b71Sopenharmony_ci                OH_LOG_ERROR(LOG_APP, "call and release tsfn failed");
221e41f4b71Sopenharmony_ci                delete myCtx;
222e41f4b71Sopenharmony_ci            }
223e41f4b71Sopenharmony_ci        },
224e41f4b71Sopenharmony_ci        myContext)
225e41f4b71Sopenharmony_ci        .detach();
226e41f4b71Sopenharmony_ci    return nullptr;
227e41f4b71Sopenharmony_ci};
228e41f4b71Sopenharmony_ci```
229e41f4b71Sopenharmony_ci
230e41f4b71Sopenharmony_ci以下内容为主线程逻辑,主要用作创建worker线程和通知worker执行任务
231e41f4b71Sopenharmony_ci
232e41f4b71Sopenharmony_ci```ts
233e41f4b71Sopenharmony_ci// 主线程
234e41f4b71Sopenharmony_ciimport worker, { MessageEvents } from '@ohos.worker';
235e41f4b71Sopenharmony_ci
236e41f4b71Sopenharmony_ciconst mWorker = new worker.ThreadWorker('../workers/Worker');
237e41f4b71Sopenharmony_cimWorker.onmessage = (e: MessageEvents) => {
238e41f4b71Sopenharmony_ci    const action: string | undefined = e.data?.action;
239e41f4b71Sopenharmony_ci    if (action === 'kill') {
240e41f4b71Sopenharmony_ci        mWorker.terminate();
241e41f4b71Sopenharmony_ci    }
242e41f4b71Sopenharmony_ci}
243e41f4b71Sopenharmony_ci
244e41f4b71Sopenharmony_ci// 触发方式的注册已省略
245e41f4b71Sopenharmony_cimWorker.postMessage({action: 'tsfn-demo'})
246e41f4b71Sopenharmony_ci
247e41f4b71Sopenharmony_ci```
248e41f4b71Sopenharmony_ci
249e41f4b71Sopenharmony_ci以下内容为Worker线程逻辑,主要用以触发Native任务
250e41f4b71Sopenharmony_ci
251e41f4b71Sopenharmony_ci```ts
252e41f4b71Sopenharmony_ci// worker.ets
253e41f4b71Sopenharmony_ciimport worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker';
254e41f4b71Sopenharmony_ciimport napiModule from 'libentry.so'; // libentry.so: napi 库的模块名称
255e41f4b71Sopenharmony_ci
256e41f4b71Sopenharmony_ciconst workerPort: ThreadWorkerGlobalScope = worker.workerPort;
257e41f4b71Sopenharmony_ci
258e41f4b71Sopenharmony_ciworkerPort.onmessage = (e: MessageEvents) => {
259e41f4b71Sopenharmony_ci    const action: string | undefined = e.data?.action;
260e41f4b71Sopenharmony_ci    if (action === 'tsfn-demo') {
261e41f4b71Sopenharmony_ci        // 触发 c++ 层的 tsfn demo
262e41f4b71Sopenharmony_ci        napiModule.myTsfnDemo();
263e41f4b71Sopenharmony_ci        // 通知主线程结束 worker
264e41f4b71Sopenharmony_ci        workerPort.postMessage({action: 'kill'});
265e41f4b71Sopenharmony_ci    };
266e41f4b71Sopenharmony_ci}
267e41f4b71Sopenharmony_ci```
268