diff --git a/zh-cn/application-dev/napi/Readme-CN.md b/zh-cn/application-dev/napi/Readme-CN.md index 2b9e11c4919e392e57a463741ba6247c37b848b6..0786a9faf169761d6a24181e6b8de0009cf37710 100644 --- a/zh-cn/application-dev/napi/Readme-CN.md +++ b/zh-cn/application-dev/napi/Readme-CN.md @@ -51,7 +51,11 @@ - [使用Node-API接口创建、切换和销毁上下文环境](use-napi-about-context.md) - [使用Node-API接口产生的异常日志/崩溃分析](use-napi-about-crash.md) - [使用Node-API调用返回值为promise的ArkTS方法](use-napi-method-promise.md) - - [Node-API常见问题](use-napi-faqs.md) + - Node-API常见问题汇总 + - [Node-API常见问题](use-napi-faqs.md) + - [稳定性相关问题汇总](napi-faq-about-stability.md) + - [内存泄漏相关问题汇总](napi-faq-about-memory-leak.md) + - [常见基本功能问题汇总](napi-faq-about-common-basic.md) - 使用JSVM-API实现JS与C/C++语言交互 - [JSVM-API简介](jsvm-introduction.md) - [JSVM-API支持的数据类型和接口](jsvm-data-types-interfaces.md) diff --git a/zh-cn/application-dev/napi/figures/zh_cn_image_20-25-06-40-15-09.png b/zh-cn/application-dev/napi/figures/zh_cn_image_20-25-06-40-15-09.png new file mode 100644 index 0000000000000000000000000000000000000000..22c49abbad4943edf7adc69f5e48c38a9f0df581 Binary files /dev/null and b/zh-cn/application-dev/napi/figures/zh_cn_image_20-25-06-40-15-09.png differ diff --git a/zh-cn/application-dev/napi/napi-faq-about-common-basic.md b/zh-cn/application-dev/napi/napi-faq-about-common-basic.md new file mode 100644 index 0000000000000000000000000000000000000000..1e18f9fa044066def31a2d76e390a853d707ed2d --- /dev/null +++ b/zh-cn/application-dev/napi/napi-faq-about-common-basic.md @@ -0,0 +1,313 @@ +# 常见基本功能问题汇总 + +## 模块加载失败,报错信息显示`Error message: is not callable`. + +- 问题描述: +通过如下模块注册代码提供的libxxx.so,在部分项目中调用动态库的API,出现`Error message: is not callable` +```cpp +static napi_module demoModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, + .nm_modname = "xxx", + .nm_priv = nullptr, + .reserved = { 0 }, +}; + +extern "C" __attribute__((constructor)) void RegisterEntryModule() +{ + napi_module_register(&demoModule); +} +``` + +- 排查建议: +1. 可根据以下文档进行排查: +[ArkTS/JS侧import xxx from libxxx.so后,使用xxx报错显示undefined/not callable或明确的Error message](napi-faq-about-common-basic.md#ArkTS侧报错显示undefined) +[模块注册与模块命名](napi-guidelines.md#模块注册与模块命名) +2. 同时也可参考动态加载能力是否可以满足该场景 +[napi_load_module_with_info支持的场景](use-napi-load-module-with-info.md#napi_load_module_with_info支持的场景) +[napi_load_module支持的场景](use-napi-load-module.md#napi_load_module支持的场景) + +## 在大量需要调用ArkTS方法进行通信的场景如何保证异步任务的有序性 + +- 具体问题:在大量需要通过c++调用ArkTS方法进行通信的场景,如何保证异步任务的有序性? +- 参考方案: +可参考线程安全函数来实现,napi_call_threadsafe_function可保证异步任务执行顺序, 需要注意的是这些异步任务会抛回到ArkTS线程顺序执行,如果是抛回到主线程,异步任务的执行时间过长可能导致应用冻结退出,所以不建议将长耗时的任务通过线程安全函数抛回到主线程执行。 +[使用Node-API接口进行线程安全开发](use-napi-thread-safety.md) + +此外,napi中常见的抛任务方法的差异如下: + +1. napi_async_work系列接口:只能保证execute_cb执行在complete_cb之前,对于不同的async_work,时序无法保证 +[napi_queue_async_work_with_qos](../../application-dev/reference/native-lib/napi.md#napi_queue_async_work_with_qos)是在普通napi_queue_async_work的基础上,支持自定义qos优先级,而这里只是指定libuv调度任务时使用线程的优先级,不是指任务的优先级,所以也无法保证任务的时序。 +2. napi_threadsafe_function系列接口:接口内部维护了一个队列,是保序的。 +napi_call_threadsafe_function:先入先出 +napi_call_threadsafe_function_with_priority:根据自己指定的任务优先级执行 +[使用Node-API接口从异步线程向ArkTS线程投递指定优先级和入队方式的的任务](use-call-threadsafe-function-with-priority.md) + +## 是否存在便捷的回调ArkTS的方式 + +- 具体描述: +在进行多线程开发时,ArkTS函数只能在其创建线程上执行,c++线程不能直接通过napi_call_function的形式直接调用ArkTS回调,是否存在便捷的方法? +可参考文档: +[Native侧子线程与UI主线程通信](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-native-sub-main-comm) +[使用Node-API接口进行异步任务开发](use-napi-asynchronous-task.md) + +## 如何在C++代码中回调ArkTS方法 + +- 参考文档: +[如何在C++调用从ArkTS传递过来的function](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-ndk-26) + +## 由于工作线程 A 长时间持有锁M,而主线程又在等待获取锁M,形成循环等待条件,导致系统进入死锁状态 + +- 参考方案: +可使用napi_threadsafe_function系列接口,具体可参考前文问题二的解决方案 + +## 如何确保数据类型的正确映射与内存管理的安全性 + +- 具体问题:如何在遵循N-API单一返回值约束的前提下,安全、高效地将多个返回值(包括结构化数据和指针信息)传递给ArkTS运行时环境,并确保数据类型的正确映射与内存管理的安全性? +- 参考实现: +尽管napi_value接口仅支持单一返回值,但开发者可通过该返回值封装所需的全部信息。 + +比如通过napi_create_object,创建出一个ArkTS对象,用这个对象来承载返回的所有信息,number和string都可以通过napi_set_property/napi_set_named_property等属性设置的接口设置到这个对象上。native对象也可以通过napi_wrap接口和ArkTS对象进行绑定,后续在通过napi_unwrap取出来。 +此外,亦可使用ArkTS数组作为数据载体,其具有良好的灵活性。 +- 参考文档: +[使用Node-API接口进行object相关开发](use-napi-about-object.md) +[使用Node-API接口进行array相关开发](use-napi-about-array.md) + +## napi_get_uv_event_loop接口错误码说明 + +在OpenHarmony中,对使用非法的napi_env作为`napi_get_uv_event_loop`入参的行为加入了额外的参数校验,这一行为将直接反映到该接口的返回值上。该接口返回值详情如下: + +1. 当env且(或)loop为nullptr时,返回`napi_invalid_arg`。 +2. 当env为有效的napi_env且loop为合法指针,返回`napi_ok`。 +3. 当env不是有效的napi_env(如已释放的env),返回`napi_generic_failure`。 + +- 常见错误场景示例如下: + +```c++ +napi_value NapiInvalidArg(napi_env env, napi_callback_info) +{ + napi_status status = napi_ok; + status = napi_get_uv_event_loop(env, nullptr); // loop为nullptr, 状态码napi_invalid_arg + if (status == napi_ok) { + // do something + } + + uv_loop_s* loop = nullptr; + status = napi_get_uv_event_loop(nullptr, &loop); // env为nullptr, 状态码napi_invalid_arg + if (status == napi_ok) { + // do something + } + + status = napi_get_uv_event_loop(nullptr, nullptr); // env, loop均为nullptr, 状态码napi_invalid_arg + if (status == napi_ok) { + // do something + } + + return nullptr; +} + +napi_value NapiGenericFailure(napi_env env, napi_callback_info) +{ + std::thread([]() { + napi_env env = nullptr; + napi_create_ark_runtime(&env); // 通常情况下,需要对该接口返回值进行判断 + // napi_destroy_ark_runtime 会将指针置空,拷贝一份用以构造问题场景 + napi_env copiedEnv = env; + napi_destroy_ark_runtime(&env); + uv_loop_s* loop = nullptr; + napi_status status = napi_get_uv_event_loop(copiedEnv, &loop); // env无效, 状态码napi_generic_failure + if (status == napi_ok) { + // do something + } + }).detach(); +} +``` + +## Native层调用ts层对象方法必须传入一个function给Native层吗 + +- 具体问题:【NAPI】Native层调用ts层对象方法,必须传入一个function给Native层吗? +- 参考方案: +如果想要在Native层调用ts层对象方法,则Native层需获取该TS Function对象。 +获取的途径也有很多,比如: +1. 通过传递的方式,ts层传给Native层,也就是问题描述的方案 +2. 可以把这个ts function通过属性设置方式绑定到Native层可访问的对象上,这样Native层通过这个对象也能拿到function进行调用 +3. napi层也提供了一个创建ts function的能力,即napi_create_function,可以直接在Native层中创建出来,这样,Native层自然就能拿到这个ts function + +## 是否能调用ets的方法并获取到结果 + +- 具体问题:在c++通过pthread或std::thread创建的线程,是否能调用ets的方法并获取到结果? +问题分析: +如果是直接创建出来的c++线程,这个线程是没有ArkTS运行环境的,也就是该线程上没有对应的napi env,所以无法直接在刚创建出来的c++线程上直接运行ets方法并获取到结果。 + +- 解决方案参考: +1. 使用napi_threadsafe_function系列的napi接口,这系列接口,相当于在c++线程抛任务回到ArkTS线程执行ets方法 +[使用Node-API接口进行线程安全开发](use-napi-thread-safety.md) +2. 在c++线程创建出ArkTS运行环境 +[使用Node-API接口创建ArkTS运行时环境](use-napi-ark-runtime.md) + +## 是否有不拷贝的napi_get_value_string_utf8接口或者能力 + +- 具体问题:当前napi的napi_get_value_string_utf8每次调用的时候都要进行拷贝,是否有不拷贝的napi_get_value_string_utf8接口或者能力? +- 问题解答: +当前版本暂不支持该功能,每次napi_get_value_string_utf8都是需要有一个拷贝过程的。 + +拷贝是必要的,因为会涉及到string生命周期。当触发GC的时候,ArkTS对象可能会在虚拟机里面被搬移,可能搬移到其他地方,也可能直接对象被回收。如果直接返回一个类似char*的地址,对象被移动或回收后,那这个地址的指向就不再是之前的字符串了,此时再用这个地址去解引用很容易崩溃。 + +## 鸿蒙多线程下napi_env的使用注意事项 + +- 具体问题: +鸿蒙多线程下napi_env的使用注意事项是什么?是否存在napi_env切换导致的异常问题?是否必须在主线程? + +- 注意事项: +1. napi_env和ArkTS线程是绑定的,napi_env不能跨线程使用,否则会导致稳定性问题。 +[多线程限制](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/use-napi-process#%E5%A4%9A%E7%BA%BF%E7%A8%8B%E9%99%90%E5%88%B6) +2. 在使用env调用napi接口时,需要注意,大部分的napi接口只能在env所在的ArkTS线程上调用,不然会出现多线程安全问题。 +参考该文档的第四点【multi-thread】 [开发者使用napi接口时,跨线程使用napi_env或napi_value引发多线程安全问题](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) +3. 最好不要缓存napi env,否则容易出现多线程安全问题和use-after-free问题 +参考该文档的第八点【use-after-free】[开发者使用napi接口时,跨线程使用napi_env或napi_value引发多线程安全问题](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) +4. [napi_env禁止缓存的原因是什么](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-ndk-73) + +## napi_call_threadsafe_function执行顺序不符合预期 + +- 问题描述: +napi_call_threadsafe_function执行结果不符合预期 +原本期望的执行顺序是 a -> b -> c +posttask(a); +posttask(b); +posttask(c); +但是实际的执行顺序是 b -> a -> c + +- 排查方向: +1. 是不是用的同一个napi_threadsafe_function,若使用不同实例,则无法保障与调用顺序一致; +注:对于同一个napi_threadsafe_function来说,napi_call_threadsafe_function是保序的,接口内维护了一个队列,先调用就会先执行。 +注:不同实例是先创建,先执行,但是若使用不同实例,得保证对应关系。 +2. 是否能保证实际call的顺序是a -> b -> c; +3. 使用了napi_call_threadsafe_function_with_priority,该接口是向主线程的事件循环中投递任务, 由于主线程存在不同优先级的队列, 不同的优先级的任务是无法保证时序的,同意优先级的任务,由于是在同一事件队列,可以保证时序。 + +参考文档: +[使用Node-API接口从异步线程向ArkTS线程投递指定优先级和入队方式的的任务](use-call-threadsafe-function-with-priority.md) + +## ArkTS侧报错显示undefined +具体问题: +ArkTS/JS侧import xxx from libxxx.so后,使用xxx报错显示undefined/not callable或明确的Error message +1. 排查.cpp文件在注册模块时的模块名称与so的名称匹配一致。 + 如模块名为entry,则so的名字为libentry.so,napi_module中nm_modname字段应为entry,大小写与模块名保持一致。 + +2. 排查so是否加载成功。 + 应用启动时过滤模块加载相关日志,重点搜索"dlopen"关键字,确认是否有相关报错信息;常见加载失败原因有权限不足、so文件不存在以及so已拉入黑名单等,可根据以下关键错误日志确认问题。其中,多线程场景(worker、taskpool等)下优先检查模块实现中nm_modname是否与模块名一致,区分大小写。 + +3. 排查依赖的so是否加载成功。 + 确定所依赖的其它so是否打包到应用中以及是否有权限打开。常见加载失败原因有权限不足、so文件不存在等。 + +4. 排查模块导入方式与so路径是否对应。 + 若JS侧导入模块的形式为: import xxx from '\@ohos.yyy.zzz',则该so将在/system/lib/module/yyy中找libzzz.z.so或libzzz_napi.z.so,若so不存在或名称无法对应,则报错日志中会出现dlopen相关日志。 + + 注意,32位系统路径为/system/lib,64位系统路径为/system/lib64。 + +| **已知关键错误日志** | **修改建议** | +| -------- | -------- | +| module $SO is not allowed to load in restricted runtime. | $SO表示模块名。该模块不在受限worker线程的so加载白名单,不允许加载,建议用户删除该模块。 | +| module $SO is in blocklist, loading prohibited. | $SO表示模块名。受卡片或者Extension管控,该模块在黑名单内,不允许加载,建议用户删除该模块。 | +| load module failed. $ERRMSG. | 动态库加载失败。$ERRMSG表示加载失败原因,一般常见原因是so文件不存在、依赖的so文件不存在或者符号未定义,需根据加载失败原因具体分析。 | +| try to load abc file from $FILEPATH failed. | 通常加载动态库和abc文件为二选一:如果是要加载动态库并且加载失败,该告警可以忽略;如果是要加载abc文件,则该错误打印的原因是abc文件不存在,$FILEPATH表示模块路径。 | + +5. 如果有明确的Error message,可以通过Error message判断当前问题。 + +| **Error message** | **修改建议** | +| -------- | -------- | +| First attempt: $ERRMSG. | 首先加载后缀不拼接'_napi'的模块名为'xxx'的so,如果加载失败会有该错误信息,$ERRMSG表示具体加载时的错误信息。 | +| Second attempt: $ERRMSG. | 第二次加载后缀拼接'_napi'的模块名为'xxx_napi'的so,如果加载失败会有该错误信息,$ERRMSG表示具体加载时的错误信息。 | +| try to load abc file from xxx failed. | 第三次加载名字为'xxx'的abc文件,如果加载失败会有该错误信息。 | +| module xxx is not allowed to load in restricted runtime. | 该模块不允许在受限运行时中使用,xxx表示模块名,建议用户删除该模块。 | +| module xxx is in blocklist, loading prohibited. | 该模块不允许在当前extension下使用,xxx表示模块名,建议用户删除该模块。 | + +## 接口执行结果非预期 + +问题描述:接口执行结果非预期,日志显示occur exception need return。 + +部分Node-API接口在调用结束前会进行检查,检查虚拟机中是否存在ArkTS异常。如果存在异常,则会打印出occur exception need return日志,并打印出检查点所在的行号,以及对应的Node-API接口名称。 + +解决此类问题有以下两种思路: + +- 若该异常开发者不关心,可以选择直接清除。 + 可直接使用napi接口napi_get_and_clear_last_exception,清理异常。调用时机:在打印occur exception need return日志的接口之前调用。 + +- 将该异常继续向上抛到ArkTS层,在ArkTS层进行捕获。 + 发生异常时,可以选择走异常分支, 确保不再走多余的Native逻辑 ,直接返回到ArkTS层。 + +## napi_value和napi_ref的生命周期有何区别 + +- Native_value由HandleScope管理,一般开发者不需要自己加HandleScope(uv_queue_work的complete callback除外)。 + +- napi_ref由开发者自己管理,需要手动delete。 + +## Node-API接口返回值不是napi_ok时如何排查定位 + +Node-API接口正常执行后,会返回一个napi_ok的状态枚举值,若napi接口返回值不为napi_ok,可从以下几个方面进行排查。 + +- Node-API接口执行前一般会进行入参校验,首先进行的是判空校验。在代码中体现为: + + ```cpp + CHECK_ENV:env判空校验 + CHECK_ARG:其它入参判空校验 + ``` + +- 某些Node-API接口还有入参类型校验。比如napi_get_value_double接口是获取ArkTS number对应的C double值,首先就要保证的是:ArkTS value类型为number,因此可以看到相关校验。 + + ```cpp + RETURN_STATUS_IF_FALSE(env, NativeValue->TypeOf() == Native_NUMBER, napi_number_expected); + ``` + +- 还有一些接口会对其执行结果进行校验。比如napi_call_function这个接口,其功能是执行一个ArkTS function,当ArkTS function中出现异常时,Node-API将会返回napi_pending_exception的状态值。 + + ```cpp + // 接口内部实现,经校验可返回状态值 + auto resultValue = engine->CallFunction(NativeRecv, NativeFunc, NativeArgv, argc); + RETURN_STATUS_IF_FALSE(env, resultValue != nullptr, napi_pending_exception) + ``` + +- 还有一些状态值需要根据相应Node-API接口具体分析:确认具体的状态值,分析这个状态值在什么情况下会返回,再排查具体出错原因。 + +## napi_wrap如何保证被wrap的对象按期望顺序析构 +问题:在使用`napi_wrap`把两个 C++ 对象包装成两个 JavaScript 对象的场景中,由于这两个 C++ 对象存在依赖关系,要求其中一个c++对象必须在另一个c++对象之前析构。然而,JavaScript 垃圾回收(GC)的时机不确定,直接在`napi_wrap`的`finalize_cb`回调里销毁 C++ 对象,没办法保证析构顺序符合要求。该如何保证两个c++对象析构的前后顺序? + +参考方案: +先标记可释放状态,当A和B都为可释放状态时同时释放C++对象 +原理:将所有依赖对象的释放逻辑集中在最后一个被销毁的 JS 对象的 finalize_cb 中处理。 +实现步骤: +在 jsObjA 的 finalize_cb 中标记 cppObjA 为待销毁(不立即释放)。 +在 jsObjB 的 finalize_cb 中标记 cppObjB 为待销毁(不立即释放)。 +jsObjA 和 jsObjB 都为待销毁状态时,按顺序销毁A和B。 +示例代码: +```cpp +struct ObjectPair { + CppObjA* objA; + CppObjB* objB; + bool objADestroyedA = false; + bool objADestroyedB = false; +}; + +// jsObjA 的 finalize 回调 +void FinalizeA(napi_env env, void* data, void* hint) { + ObjectPair* pair = static_cast(data); + pair->objADestroyedA = true; + if (pair->objADestroyedA && pair->objADestroyedB) { + delete pair->objA; // 确保先销毁 A + delete pair->objB; // 再销毁 B + delete pair; // 释放包装结构 + } +} + +// jsObjB 的 finalize 回调 +void FinalizeB(napi_env env, void* data, void* hint) { + ObjectPair* pair = static_cast(data); + pair->objADestroyedB = true; + if (pair->objADestroyedA && pair->objADestroyedB) { + delete pair->objA; // 确保先销毁 A + delete pair->objB; // 再销毁 B + delete pair; // 释放包装结构 + } +} +``` diff --git a/zh-cn/application-dev/napi/napi-faq-about-memory-leak.md b/zh-cn/application-dev/napi/napi-faq-about-memory-leak.md new file mode 100644 index 0000000000000000000000000000000000000000..0795403552fdee33c5c1803e540cedc33e3eb563 --- /dev/null +++ b/zh-cn/application-dev/napi/napi-faq-about-memory-leak.md @@ -0,0 +1,218 @@ +# 内存泄漏相关问题汇总 + +## 请问当前是否有机制来检查是否有泄漏的napi_ref + +- 具体问题:napi_create_reference可以创建对js对象的引用,保持js对象不释放,正常来说使用完需要使用napi_delete_reference进行释放,但怕漏delete导致js对象内存泄漏,请问当前是否有机制来检查/测试是否有泄漏的napi_ref? +- 检测方式: +可使用 DevEco Studio(IDE)提供的 Allocation 工具进行检测。 +[基础内存分析:Allocation分析](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-insight-session-allocations) +napi_create_reference这个接口内部实现会new一个c++对象,因此,如果忘记使用napi_delete_reference接口,那这个new出来的c++对象也会泄漏,这时候就可以用Allocation工具来进行检测,这个工具会把未释放的对象的分配栈都打印出来,如果napi_ref泄漏了,可以在分配栈上看出来 + +## napi开发过程中遇见内存泄漏问题要怎么定位解决 + +每次点击按钮内存都会增加一些,并且主动触发gc也不见回收,napi开发过程中的内存泄漏问题要怎么定位解决? + +- 解决建议: +先了解一下napi生命周期相关材料: +[使用Node-API接口进行生命周期相关开发](use-napi-life-cycle.md) +使用napi导致内存泄漏的常见原因: +1. napi_value不在handle_scope管控中,导致native持有的js对象不释放,尤其常见在直接使用uv_queue_work中。解决方案为添加 napi_open_handle_scope 和 napi_close_handle_scope 接口。 + +[易错API的使用规范](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) + +此类泄漏可通过snapshot分析定位原因,泄漏的js对象distance为1,即不知道被谁持有,这种情况下一般就是被native(napi_value是个指针,指向native持有者)持有了,且napi_value不在handle_scope范围内。 +2. 使用了napi_create_reference为js对象创建了强引用(initial_refcount参数大于0),并且一直未delete,导致js对象一直被持有,无法被回收。napi_create_reference接口会new一个c++对象,因此这种泄漏通常表现为ArkTS与C++层的双重泄漏。可以使用Allocation工具抓c++泄漏栈,参考是否有napi_create_reference相关栈帧。 + +[基础内存分析:Allocation分析](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-insight-session-allocations) + +3. 被其他存活js对象持有,这时候snapshot查看泄漏对象被谁持有。 + +## napi_threadsafe_function内存泄漏应该如何处理 + +`napi_threadsafe_function`(下文简称tsfn)在使用时,常常会调用 `napi_acquire_threadsafe_function` 来更改tsfn的引用计数,确保tsfn不会意外被释放。但在使用完成后,应该及时使用 `napi_tsfn_release` 模式调用 `napi_release_threadsafe_function` 方法,以确保在所有调用回调都执行完成后,其引用计数能回归到调用 `napi_acquire_threadsafe_function` 方法之前的水平。当其引用计数归为0时,tsfn才能正确的被释放。 + +当在env即将退出,但tsfn的引用计数未被归零时,应该使用 `napi_tsfn_abort` 模式调用 `napi_release_threadsafe_function` 方法,确保在env释放后不再对tsfn进行持有及使用。在env退出后,继续持有tsfn进行使用,是一种未定义的行为,可能会触发崩溃。 + +如下代码将展示通过注册 `env_cleanup` 钩子函数的方式,以确保在env退出后不再继续持有tsfn。 + +```cpp +//napi_init.cpp +#include // hilog, 输出日志, 需链接 libhilog_ndk.z.so +#include // 创建线程 +#include // 线程休眠 + +// 定义输出日志的标签和域 +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0x2342 +#define LOG_TAG "MY_TSFN_DEMO" + +/* + 为构建一个 env 生命周期短于 native 生命周期的场景, + 本示例需要使用worker, taskpool 或 napi_create_ark_runtime 等方法, + 创建非主线程的ArkTS运行环境,并人为的提前结束掉该线程 +*/ + + +// 定义一个数据结构,模拟存储tsfn的场景 +class MyTsfnContext { +public: +// 因使用了napi方法, MyTsfnContext 应当只在js线程被构造 +MyTsfnContext(napi_env env, napi_value workName) { + // 注册env销毁钩子函数 + napi_add_env_cleanup_hook(env, Cleanup, this); + // 创建线程安全函数 + if (napi_create_threadsafe_function(env, nullptr, nullptr, workName, 1, 1, this, + TsfnFinalize, this, TsfnCallJs, &tsfn_) != napi_ok) { + OH_LOG_INFO(LOG_APP, "tsfn is created failed"); + return; + }; +}; + +~MyTsfnContext() { OH_LOG_INFO(LOG_APP, "MyTsfnContext is deconstructed"); }; + +napi_threadsafe_function GetTsfn() { + std::unique_lock lock(mutex_); + return tsfn_; +} + +bool Acquire() { + if (GetTsfn() == nullptr) { + return false; + }; + return (napi_acquire_threadsafe_function(GetTsfn()) == napi_ok); +}; + +bool Release() { + if (GetTsfn() == nullptr) { + return false; + }; + return (napi_release_threadsafe_function(GetTsfn(), napi_tsfn_release) == napi_ok); +}; + +bool Call(void *data) { + if (GetTsfn() == nullptr) { + return false; + }; + return (napi_call_threadsafe_function(GetTsfn(), data, napi_tsfn_blocking) == napi_ok); +}; + +private: +// 保护多线程读写tsfn的准确性 +std::mutex mutex_; +napi_threadsafe_function tsfn_ = nullptr; + +// napi_add_env_cleanup_hook 回调 +static void Cleanup(void *data) { + MyTsfnContext *that = reinterpret_cast(data); + napi_threadsafe_function tsfn = that->GetTsfn(); + std::unique_lock lock(that->mutex_); + that->tsfn_ = nullptr; + lock.unlock(); + OH_LOG_WARN(LOG_APP, "cleanup is called"); + napi_release_threadsafe_function(tsfn, napi_tsfn_abort); +}; + +// tsfn 释放时的回调 +static void TsfnFinalize(napi_env env, void *data, void *hint) { + MyTsfnContext *ctx = reinterpret_cast(data); + OH_LOG_INFO(LOG_APP, "tsfn is released"); + napi_remove_env_cleanup_hook(env, MyTsfnContext::Cleanup, ctx); + // cleanup 提前释放线程安全函数, 为避免UAF, 将释放工作交给调用方 + if (ctx->GetTsfn() != nullptr) { + OH_LOG_INFO(LOG_APP, "ctx is released"); + delete ctx; + } +}; + +// tsfn 发送到 ArkTS 线程执行的回调 +static void TsfnCallJs(napi_env env, napi_value func, void *context, void *data) { + MyTsfnContext *ctx = reinterpret_cast(context); + char *str = reinterpret_cast(data); + OH_LOG_INFO(LOG_APP, "tsfn is called, data is: \"%{public}s\"", str); + // 业务逻辑已省略 +}; +}; + +// 该方法需注册到模块Index.d.ts, 注册名为 myTsfnDemo, 接口描述如下 +// export const myTsfnDemo: () => void; +napi_value MyTsfnDemo(napi_env env, napi_callback_info info) { + OH_LOG_ERROR(LOG_APP, "MyTsfnDemo is called"); + napi_value workName = nullptr; + napi_create_string_utf8(env, "MyTsfnWork", NAPI_AUTO_LENGTH, &workName); + MyTsfnContext *myContext = new MyTsfnContext(env, workName); + if (myContext->GetTsfn() == nullptr) { + OH_LOG_ERROR(LOG_APP, "failed to create tsfn"); + delete myContext; + return nullptr; + }; + char *data0 = new char[]{"Im call in ArkTS Thread"}; + if (!myContext->Call(data0)) { + OH_LOG_INFO(LOG_APP, "call tsfn failed"); + }; + + // 创建一个线程,模拟异步场景 + std::thread( + [](MyTsfnContext *myCtx) { + if (!myCtx->Acquire()) { + OH_LOG_ERROR(LOG_APP, "acquire tsfn failed"); + return; + }; + char *data1 = new char[]{"Im call in std::thread"}; + // 非必要操作, 仅用于异步流程tsfn仍有效 + if (!myCtx->Call(data1)) { + OH_LOG_ERROR(LOG_APP, "call tsfn failed"); + }; + // 休眠 5s, 模拟耗时场景, env退出后, 异步任务仍未执行完成 + sleep(5); + // 此时异步任务已执行完成, 但tsfn已被释放并置为 nullptr + char *data2 = new char[]{"Im call after work"}; + if (!myCtx->Call(data2) && !myCtx->Release()) { + OH_LOG_ERROR(LOG_APP, "call and release tsfn failed"); + delete myCtx; + } + }, + myContext) + .detach(); + return nullptr; +}; +``` + +以下内容为主线程逻辑,主要用作创建worker线程和通知worker执行任务 + +```ts +// 主线程 Index.ets +import worker, { MessageEvents } from '@ohos.worker'; + +const mWorker = new worker.ThreadWorker('../workers/Worker'); +mWorker.onmessage = (e: MessageEvents) => { + const action: string | undefined = e.data?.action; + if (action === 'kill') { + mWorker.terminate(); + } +} + +// 触发方式的注册已省略 +mWorker.postMessage({action: 'tsfn-demo'}); + +``` + +以下内容为Worker线程逻辑,主要用以触发Native任务 + +```ts +// worker.ets +import worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker'; +import napiModule from 'libentry.so'; // libentry.so: napi 库的模块名称 + +const workerPort: ThreadWorkerGlobalScope = worker.workerPort; + +workerPort.onmessage = (e: MessageEvents) => { + const action: string | undefined = e.data?.action; + if (action === 'tsfn-demo') { + // 触发 c++ 层的 tsfn demo + napiModule.myTsfnDemo(); + // 通知主线程结束 worker + workerPort.postMessage({action: 'kill'}); + }; +} +``` \ No newline at end of file diff --git a/zh-cn/application-dev/napi/napi-faq-about-stability.md b/zh-cn/application-dev/napi/napi-faq-about-stability.md new file mode 100644 index 0000000000000000000000000000000000000000..7f41aaf97854d7905e3212e20fe2388fd789de5c --- /dev/null +++ b/zh-cn/application-dev/napi/napi-faq-about-stability.md @@ -0,0 +1,129 @@ +# 稳定性相关问题汇总 + +## 应用运行过程中出现高概率闪退怎么进行定位解决 + +- 具体问题:在使用鸿蒙Node-Api(napi)开发过程中,应用运行过程中出现高概率闪退,出现cppcrash栈,栈顶为系统库libark_jsruntime.so,崩溃栈前几帧也有libace_napi.z.so,怎么进行定位解决? +复现概率高,每次崩溃栈略有区别,但是共性都是:崩溃栈顶是系统库的libark_jsruntime.so或者libace_napi.z.so +- 崩溃信息如下: +```sh +Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00000136 probably caus +Fault thread info: +Tid:15894, Name:e.myapplication +#00 pc 002b8dd4 /system/lib/platformsdk/libark_jsruntime.so +#01 pc 0024d3e1 /system/lib/platformsdk/libark_jsruntime.so +#02 pc 0024d0d9 /system/lib/platformsdk/libark_jsruntime.so +#03 pc 002eac5d /system/lib/platformsdk/libark_jsruntime.so +#04 pc 00428d0f /system/lib/platformsdk/libark_jsruntime.so +``` + +- 定位问题: +使用napi时如果出现高概率闪退,崩溃栈顶在系统库libark_jsruntime.so,一般是开发者napi接口使用不当导致。 +- 以下定位问题的思路,可作为参考: +1. 排查是否存在多线程安全问题(概率较大) +IDE中提供了相关开关,开启开关后,重新编译打包并运行,看看崩溃栈是不是符合下面这个文档的描述,如果是,那就是在使用napi时,存在多线程安全问题。 +[常见多线程安全问题](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-ark-runtime-detection#section19357830121120) +IDE开关: +![IDE多线程开关](figures/zh_cn_image_20-25-06-40-15-09.png) +2. 使用napi接口时入参非法导致。 +- 这种情况一般是崩溃栈上的so会很浅,so调用了某个具体的napi接口,比如调用了napi_call_function之类的接口,然后napi又调到了libark_jsruntime的so,然后直接崩溃在libark_jsruntime里面。 +示例栈结构如下。 +```sh +#01 /system/lib/platformsdk/libark_jsruntime.so +#02 /system/lib/platformsdk/libark_jsruntime.so +#03 /system/lib/platformsdk/libace_napi.z.so(napi_set_named_property+170) -- napi的so,该位置显示具体调用报错的接口 +#04 /data/storage/el1/bundle/libs/arm/libentry.so -- 你的so +``` +- 如果是入参问题,一般so在崩溃栈上的位置比较浅(不会跑到#10这种离栈顶很远的位置),不过也可以按照这个思路进行排查一下。 +- 排查思路参考: +a. 排查有没有napi_value未初始化,还没赋值成功,直接作为非法入参传递给接口了 +b. 排查有没有在这个易错API列表里面找到相应的篇章--[方舟运行时API](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) + +## 线程池中并发调用ArkTS方法如何处理线程安全问题 + +- 现有个场景,ArkTS中有个类方法,对这个方法创建了napi_ref引用,现想在c++线程池中并发的调用ArkTS方法,请问 +1. 可以在c++创建的线程池中调用napi_ref缓存的ArkTS类方法吗? +只能在c++线程中将ArkTS任务抛回ts线程,这时候并不是同步调用,而是一个抛任务的动作。 +需要注意的是,这个ArkTS方法真正的执行动作只能在ts线程中完成,即,只能方法运行在创建的ts线程上。 +2. 回调到ArkTS要怎么确保线程安全? +上面提到,c++线程都是抛任务到ts线程,进而执行ts方法。关于线程安全,可参考napi线程安全文档,相关能力是有的。 +[使用Node-API接口进行线程安全开发](use-napi-thread-safety.md) +另外,开发过程中也可以打开多线程安全检查开关,这个开关可以拦截多线程安全问题。 +[方舟多线程检测](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-ark-runtime-detection#section75786272088) + +## napi_value内容产生变化 + +- 具体描述:在一个程序初始化的时候,保存了env和一个method(napi_value),这个method在刚刚创建的时候有进行check,napi_typeof的结果是napi_function,符合预期。程序运行一段时间后,使用保存的env和method再去调用,发现method check不过了,此时不是一个napi_function了,保存与使用时均处于同一主线程,要如何解决? + +排查建议: +1. 确认一下,是否napi_value出了scope还在使用,导致use-after-scope问题。 +可参考文档: +[方舟运行时API](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) +2. 保存时建议使用napi_ref进行保存,而不是直接保存napi_value。 + +## 是否存在获取最新napi_env的方法 + +- 具体描述:有个场景,Native 层在较深的调用层级中需调用 ArkTS 方法,无法逐层传递 napi_env,直接缓存的话会有crash,崩溃栈如下: +```sh +#00 /system/lib/platformsdk/libark_jsruntime.so(panda::JSValueRef::IsFunction) +#01 /system/lib/platformsdk/libace_napi.z.so(napi_call_function) +#02 /data/storage/el1/bundle/libs/arm/libentry.so +... +``` +- 参考方案: +1. 关于保存napi_env: +napi没有提供直接获取napi_env的能力,只能通过传递一层一层传下来。一般不推荐保存napi_env,有两个原因: +其一,napi_env退出时候如果没有被使用方感知到,很容易出现use-after-free问题; +其二,napi_env和js线程时强绑定的,如果napi_env放在其他js线程使用,就会有多线程安全问题。 +可参考文档: +[napi_env禁止缓存的原因是什么](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-ndk-73) + + +2. 该问题重点在于: +如果要强行保存env,就一定要感知env是否退出,用napi_add_env_cleanup_hook的回调去感知。同时在开发过程中把多线程检测开关打开,避免出现多线程安全问题。 +可参考多线程检测文档: +[常见多线程安全问题](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-ark-runtime-detection#section19357830121120) + +3. 崩溃本身,该崩溃可能发生在调用napi_call_function时,入参 func 有问题,即非法入参,开发者可排查napi_value是否被缓存。这种情况可能是napi_value被缓存之后,napi_value不在作用域导致失效。 +如果有类似逻辑,需使用napi_ref进行存储,napi_ref可以延长生命周期。 + +- 可参考文档: +[napi_create_reference、napi_delete_reference](use-napi-life-cycle.md) + +[方舟运行时API](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) + +## napi_add_env_cleanup_hook调用报错该如何处理 + +-具体问题:napi_add_env_cleanup_hook/napi_remove_env_cleanup_hook调用报错,该如何处理? +`napi_add_env_cleanup_hook`/`napi_remove_env_cleanup_hook`调用报错,有以下几个常见原因和对应的特征日志,均为接口使用不当导致。 +1. 在`env`所在的js线程外使用上述两个接口,导致多线程安全问题。特征报错日志`current napi interface cannot run in multi-thread`。 +2. 调用`napi_add_env_cleanup_hook`时,重复使用同一个`args`注册不同的回调函数,导致后续注册失败问题。该接口第三个入参`args`是作为接口内部`map`的`key`值,当重复注册同一个`args`的回调时,后续注册动作将会失败,仅第一次注册才会成功。注册失败可能导致后续业务功能异常或崩溃。特征报错日志`AddCleanupHook Failed`。 +3. 调用`napi_remove_env_cleanup_hook`时,尝试通过一个不存在(或已被删除)的`args`删除回调函数,该接口调用失败,出现特征报错日志`RemoveCleanupHook Failed`。 + +常见错误场景示例如下: + +```c++ +void AddEnvCleanupHook(napi_env env) +{ + napi_add_env_cleanup_hook(env, [](void* args) -> void { + // cleanup function回调 + }, env); // env是个通用的数据,即使此处没有重复注册,可能会被其他地方所提前注册,导致该处注册失败。 +} + +static napi_value Test(napi_env env, napi_callback_info info) +{ + //第一次注册 + AddEnvCleanupHook(env); + //第二次重复注册 + AddEnvCleanupHook(env); + return nullptr; +} +``` + +- 修复建议: +1. 对于多线程安全问题,需确保调用接口的线程在`env`所在的js线程上。 +2. 对于注册失败的问题,需由使用者明确待注册的函数。需要保证`key`值(也就是`napi_add_env_cleanup_hook`的第三个入参)是唯一的即可。 +3. 对于删除失败的问题,需要使用者确保`args`之前被注册过且未被删除。 + +相关参考资料链接: +[使用Node-API接口注册和使用环境清理钩子](use-napi-about-cleanuphook.md) +[方舟运行时API](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) diff --git a/zh-cn/application-dev/napi/use-napi-faqs.md b/zh-cn/application-dev/napi/use-napi-faqs.md index e95b141a781d4e15eb89ba2d1724d09855e6c5f9..de6601ac4650142e3ae94ba50fbca716b3fcc264 100644 --- a/zh-cn/application-dev/napi/use-napi-faqs.md +++ b/zh-cn/application-dev/napi/use-napi-faqs.md @@ -1,395 +1,33 @@ # Node-API常见问题 -## ArkTS/JS侧import xxx from libxxx.so后,使用xxx报错显示undefined/not callable或明确的Error message +## 一.稳定性 +1. [应用运行过程中出现高概率闪退,出现cppcrash栈,栈顶为系统库libark_jsruntime.so,崩溃栈前几帧也有libace_napi.z.so,怎么进行定位解决](napi-faq-about-stability.md#应用运行过程中出现高概率闪退怎么进行定位解决) +2. [c++线程池中并发调用ArkTS方法(c++多线程调用ArkTS方法),如何处理线程安全问题?](napi-faq-about-stability.md#线程池中并发调用ArkTS方法如何处理线程安全问题) +3. [【napi_value非预期】napi_value内容产生变化,napi_value创建时类型是napi_function,保存一段时间后napi_value类型发生变化](napi-faq-about-stability.md#napi_value内容产生变化) +4. [是否存在获取最新napi_env的方法?](napi-faq-about-stability.md#是否存在获取最新napi_env的方法) +5. [napi_add_env_cleanup_hook/napi_remove_env_cleanup_hook调用报错,该如何处理?](napi-faq-about-stability.md#napi_add_env_cleanup_hook调用报错该如何处理) +## 二.内存泄漏 +1. [napi_create_reference可以创建对js对象的引用,保持js对象不释放,正常来说使用完需要使用napi_delete_reference进行释放,但怕漏delete导致js对象内存泄漏,请问当前是否有机制来检查/测试是否有泄漏的napi_reference?](napi-faq-about-memory-leak.md#请问当前是否有机制来检查是否有泄漏的napi_ref) +2. [napi开发过程中,遇见内存泄漏问题,要怎么定位解决?](napi-faq-about-memory-leak.md#napi开发过程中遇见内存泄漏问题要怎么定位解决) +3. [参数泄漏问题参考napi_open_handle_scope、napi_close_handle_scope](use-napi-life-cycle.md) +4. [napi_threadsafe_function内存泄漏,应该如何处理?](napi-faq-about-memory-leak.md#napi_threadsafe_function内存泄漏应该如何处理) +## 三.常见基本功能问题 +1. [模块加载失败,Error message: is not callable NativeModule调用报错?](napi-faq-about-common-basic.md) +2. [是否有保序的线程通信推荐写法?](napi-faq-about-common-basic.md#在大量需要调用ArkTS方法进行通信的场景如何保证异步任务的有序性) +3. [是否存在便捷的NAPI回调arkts的方式?](napi-faq-about-common-basic.md#是否存在便捷的回调ArkTS的方式) +4. [如何在C++调用从ArkTS传递过来的function?](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-ndk-26) +5. [如何在遵循 N-API 单一返回值约束的前提下,安全、高效地将多个返回值(包括结构化数据和指针信息)传递给 ArkTS 运行时环境,并确保数据类型的正确映射与内存管理的安全性?](napi-faq-about-common-basic.md#如何确保数据类型的正确映射与内存管理的安全性) +6. [napi调用三方so](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-dynamic-link-library) +7. [napi_get_uv_event_loop接口错误码说明](napi-faq-about-common-basic.md#napi_get_uv_event_loop接口错误码说明) +8. [【NAPI】native层调用ts层对象方法,必须传入一个function给native层吗?](napi-faq-about-common-basic.md#Native层调用ts层对象方法必须传入一个function给Native层吗) +9. [在c++通过pthread或std::thread创建的线程,是否能调用ets的方法并获取到结果?](napi-faq-about-common-basic.md#是否能调用ets的方法并获取到结果) +10. [当前napi的napi_get_value_string_utf8每次调用的时候都要进行拷贝,是否有nocopy、不拷贝的napi_get_value_string_utf8接口或者能力? ](napi-faq-about-common-basic.md#是否有不拷贝的napi_get_value_string_utf8接口或者能力) +11. [鸿蒙多线程下napi env的使用注意事项是什么?是否存在napi_env切换导致的异常问题?是否必须在主线程?](napi-faq-about-common-basic.md#鸿蒙多线程下napi_env的使用注意事项) +12. [请问napi_call_threadsafe_function执行顺序是怎样的?](napi-faq-about-common-basic.md#napi_call_threadsafe_function执行顺序不符合预期) +13. [ArkTS/JS侧import xxx from libxxx.so后,使用xxx报错显示undefined/not callable或明确的Error message](napi-faq-about-common-basic.md#ArkTS侧报错显示undefined) +14. [接口执行结果非预期,日志显示occur exception need return](napi-faq-about-common-basic.md#接口执行结果非预期) +15. [napi_value和napi_ref的生命周期有何区别](napi-faq-about-common-basic.md#napi_value和napi_ref的生命周期有何区别) +16. [Node-API接口返回值不是napi_ok时,如何排查定位](napi-faq-about-common-basic.md#Node-API接口返回值不是napi_ok时如何排查定位) +17. [napi_wrap如何保证被wrap的对象按期望顺序析构?](napi-faq-about-common-basic.md#napi_wrap如何保证被wrap的对象按期望顺序析构) +18. -1. 排查.cpp文件在注册模块时的模块名称与so的名称匹配一致。 - 如模块名为entry,则so的名字为libentry.so,napi_module中nm_modname字段应为entry,大小写与模块名保持一致。 - -2. 排查so是否加载成功。 - 应用启动时过滤模块加载相关日志,重点搜索"dlopen"关键字,确认是否有相关报错信息;常见加载失败原因有权限不足、so文件不存在以及so已拉入黑名单等,可根据以下关键错误日志确认问题。其中,多线程场景(worker、taskpool等)下优先检查模块实现中nm_modname是否与模块名一致,区分大小写。 - -3. 排查依赖的so是否加载成功。 - 确定所依赖的其它so是否打包到应用中以及是否有权限打开。常见加载失败原因有权限不足、so文件不存在等,可根据以下关键错误日志确认问题。 - -4. 排查模块导入方式与so路径是否对应。 - 若JS侧导入模块的形式为: import xxx from '\@ohos.yyy.zzz',则该so将在/system/lib/module/yyy中找libzzz.z.so或libzzz_napi.z.so,若so不存在或名称无法对应,则报错日志中会出现dlopen相关日志。 - - 注意,32位系统路径为/system/lib,64位系统路径为/system/lib64。 - -| **已知关键错误日志** | **修改建议** | -| -------- | -------- | -| module $SO is not allowed to load in restricted runtime. | $SO表示模块名。该模块不在受限worker线程的so加载白名单,不允许加载,建议用户删除该模块。 | -| module $SO is in blocklist, loading prohibited. | $SO表示模块名。受卡片或者Extension管控,该模块在黑名单内,不允许加载,建议用户删除该模块。 | -| load module failed. $ERRMSG. | 动态库加载失败。$ERRMSG表示加载失败原因,一般常见原因是so文件不存在、依赖的so文件不存在或者符号未定义,需根据加载失败原因具体分析。 | -| try to load abc file from $FILEPATH failed. | 通常加载动态库和abc文件为二选一:如果是要加载动态库并且加载失败,该告警可以忽略;如果是要加载abc文件,则该错误打印的原因是abc文件不存在,$FILEPATH表示模块路径。 | - -5. 如果有明确的Error message,可以通过Error message判断当前问题。 - -| **Error message** | **修改建议** | -| -------- | -------- | -| First attempt: $ERRMSG. | 首先加载后缀不拼接'_napi'的模块名为'xxx'的so,如果加载失败会有该错误信息,$ERRMSG表示具体加载时的错误信息。 | -| Second attempt: $ERRMSG. | 第二次加载后缀拼接'_napi'的模块名为'xxx_napi'的so,如果加载失败会有该错误信息,$ERRMSG表示具体加载时的错误信息。 | -| try to load abc file from xxx failed. | 第三次加载名字为'xxx'的abc文件,如果加载失败会有该错误信息。 | -| module xxx is not allowed to load in restricted runtime. | 该模块不允许在受限运行时中使用,xxx表示模块名,建议用户删除该模块。 | -| module xxx is in blocklist, loading prohibited. | 该模块不允许在当前extension下使用,xxx表示模块名,建议用户删除该模块。 | - -## 接口执行结果非预期,日志显示occur exception need return - -部分Node-API接口在调用结束前会进行检查,检查虚拟机中是否存在JS异常。如果存在异常,则会打印出occur exception need return日志,并打印出检查点所在的行号,以及对应的Node-API接口名称。 - -解决此类问题有以下两种思路: - -- 若该异常开发者不关心,可以选择直接清除。 - 可直接使用napi接口napi_get_and_clear_last_exception,清理异常。调用时机:在打印occur exception need return日志的接口之前调用。 - -- 将该异常继续向上抛到ArkTS层,在ArkTS层进行捕获。 - 发生异常时,可以选择走异常分支, 确保不再走多余的Native逻辑 ,直接返回到ArkTS层。 - -## napi_value和napi_ref的生命周期有何区别 - -- native_value由HandleScope管理,一般开发者不需要自己加HandleScope(uv_queue_work的complete callback除外)。 - -- napi_ref由开发者自己管理,需要手动delete。 - -## Node-API接口返回值不是napi_ok时,如何排查定位 - -Node-API接口正常执行后,会返回一个napi_ok的状态枚举值,若napi接口返回值不为napi_ok,可从以下几个方面进行排查。 - -- Node-API接口执行前一般会进行入参校验,首先进行的是判空校验。在代码中体现为: - - ```cpp - CHECK_ENV: env判空校验 - CHECK_ARG:其它入参判空校验 - ``` - -- 某些Node-API接口还有入参类型校验。比如napi_get_value_double接口是获取JS number对应的C double值,首先就要保证的是:JS value类型为number,因此可以看到相关校验。 - - ```cpp - RETURN_STATUS_IF_FALSE(env, nativeValue->TypeOf() == NATIVE_NUMBER, napi_number_expected); - ``` - -- 还有一些接口会对其执行结果进行校验。比如napi_call_function这个接口,其功能是执行一个JS function,当JS function中出现异常时,Node-API将会返回napi_pending_exception的状态值。 - - ```cpp - auto resultValue = engine->CallFunction(nativeRecv, nativeFunc, nativeArgv, argc); - RETURN_STATUS_IF_FALSE(env, resultValue != nullptr, napi_pending_exception) - ``` - -- 还有一些状态值需要根据相应Node-API接口具体分析:确认具体的状态值,分析这个状态值在什么情况下会返回,再排查具体出错原因。 - -## napi_threadsafe_function内存泄漏,应该如何处理 - -`napi_threadsafe_function`(下文简称tsfn)在使用时,常常会调用 `napi_acquire_threadsafe_function` 来更改tsfn的引用计数,确保tsfn不会意外被释放。但在使用完成后,应该及时使用 `napi_tsfn_release` 模式调用 `napi_release_threadsafe_function` 方法,以确保在所有调用回调都执行完成后,其引用计数能回归到调用 `napi_acquire_threadsafe_function` 方法之前的水平。当其引用计数归为0时,tsfn才能正确的被释放。 - -当在env即将退出,但tsfn的引用计数未被归零时,应该使用 `napi_tsfn_abort` 模式调用 `napi_release_threadsafe_function` 方法,确保在env释放后不再对tsfn进行持有及使用。在env退出后,继续持有tsfn进行使用,是一种未定义的行为,可能会触发崩溃。 - -如下代码将展示通过注册 `env_cleanup` 钩子函数的方式,以确保在env退出后不再继续持有tsfn。 - -```cpp -//napi_init.cpp -#include // hilog, 输出日志, 需链接 libhilog_ndk.z.so -#include // 创建线程 -#include // 线程休眠 - -// 定义输出日志的标签和域 -#undef LOG_DOMAIN -#undef LOG_TAG -#define LOG_DOMAIN 0x2342 -#define LOG_TAG "MY_TSFN_DEMO" - -/* - 为构造一个env生命周期小于native生命周期的场景, - 本文需要使用worker, taskpool 或 napi_create_ark_runtime 等方法, - 创建非主线程的ArkTS运行环境,并人为的提前结束掉该线程 -*/ - - -// 定义一个数据结构,模拟存储tsfn的场景 -class MyTsfnContext { -public: -// 因使用了napi方法, MyTsfnContext 应当只在js线程被构造 -MyTsfnContext(napi_env env, napi_value workName) { - // 注册env销毁钩子函数 - napi_add_env_cleanup_hook(env, Cleanup, this); - // 创建线程安全函数 - if (napi_create_threadsafe_function(env, nullptr, nullptr, workName, 1, 1, this, - TsfnFinalize, this, TsfnCallJs, &tsfn_) != napi_ok) { - OH_LOG_INFO(LOG_APP, "tsfn is created failed"); - return; - }; -}; - -~MyTsfnContext() { OH_LOG_INFO(LOG_APP, "MyTsfnContext is deconstructed"); }; - -napi_threadsafe_function GetTsfn() { - std::unique_lock lock(mutex_); - return tsfn_; -} - -bool Acquire() { - if (GetTsfn() == nullptr) { - return false; - }; - return (napi_acquire_threadsafe_function(GetTsfn()) == napi_ok); -}; - -bool Release() { - if (GetTsfn() == nullptr) { - return false; - }; - return (napi_release_threadsafe_function(GetTsfn(), napi_tsfn_release) == napi_ok); -}; - -bool Call(void *data) { - if (GetTsfn() == nullptr) { - return false; - }; - return (napi_call_threadsafe_function(GetTsfn(), data, napi_tsfn_blocking) == napi_ok); -}; - -private: -// 保护多线程读写tsfn的准确性 -std::mutex mutex_; -napi_threadsafe_function tsfn_ = nullptr; - -// napi_add_env_cleanup_hook 回调 -static void Cleanup(void *data) { - MyTsfnContext *that = reinterpret_cast(data); - napi_threadsafe_function tsfn = that->GetTsfn(); - std::unique_lock lock(that->mutex_); - that->tsfn_ = nullptr; - lock.unlock(); - OH_LOG_WARN(LOG_APP, "cleanup is called"); - napi_release_threadsafe_function(tsfn, napi_tsfn_abort); -}; - -// tsfn 释放时的回调 -static void TsfnFinalize(napi_env env, void *data, void *hint) { - MyTsfnContext *ctx = reinterpret_cast(data); - OH_LOG_INFO(LOG_APP, "tsfn is released"); - napi_remove_env_cleanup_hook(env, MyTsfnContext::Cleanup, ctx); - // cleanup 提前释放线程安全函数, 为避免UAF, 将释放工作交给调用方 - if (ctx->GetTsfn() != nullptr) { - OH_LOG_INFO(LOG_APP, "ctx is released"); - delete ctx; - } -}; - -// tsfn 发送到 js 线程执行的回调 -static void TsfnCallJs(napi_env env, napi_value func, void *context, void *data) { - MyTsfnContext *ctx = reinterpret_cast(context); - char *str = reinterpret_cast(data); - OH_LOG_INFO(LOG_APP, "tsfn is called, data is: \"%{public}s\"", str); - // 业务逻辑已省略 -}; -}; - -// 该方法需注册到模块Index.d.ts, 注册名为 myTsfnDemo, 接口描述如下 -// export const myTsfnDemo: () => void; -napi_value MyTsfnDemo(napi_env env, napi_callback_info info) { - OH_LOG_ERROR(LOG_APP, "MyTsfnDemo is called"); - napi_value workName = nullptr; - napi_create_string_utf8(env, "MyTsfnWork", NAPI_AUTO_LENGTH, &workName); - MyTsfnContext *myContext = new MyTsfnContext(env, workName); - if (myContext->GetTsfn() == nullptr) { - OH_LOG_ERROR(LOG_APP, "failed to create tsfn"); - delete myContext; - return nullptr; - }; - char *data0 = new char[]{"Im call in ArkTS Thread"}; - if (!myContext->Call(data0)) { - OH_LOG_INFO(LOG_APP, "call tsfn failed"); - }; - - // 创建一个线程,模拟异步场景 - std::thread( - [](MyTsfnContext *myCtx) { - if (!myCtx->Acquire()) { - OH_LOG_ERROR(LOG_APP, "acquire tsfn failed"); - return; - }; - char *data1 = new char[]{"Im call in std::thread"}; - // 非必要操作, 仅用于异步流程tsfn仍有效 - if (!myCtx->Call(data1)) { - OH_LOG_ERROR(LOG_APP, "call tsfn failed"); - }; - // 休眠 5s, 模拟耗时场景, env退出后, 异步任务仍未执行完成 - sleep(5); - // 此时异步任务已执行完成, 但tsfn已被释放并置为 nullptr - char *data2 = new char[]{"Im call after work"}; - if (!myCtx->Call(data2) && !myCtx->Release()) { - OH_LOG_ERROR(LOG_APP, "call and release tsfn failed"); - delete myCtx; - } - }, - myContext) - .detach(); - return nullptr; -}; -``` - -以下内容为主线程逻辑,主要用作创建worker线程和通知worker执行任务 - -```ts -// 主线程 Index.ets -import worker, { MessageEvents } from '@ohos.worker'; - -const mWorker = new worker.ThreadWorker('../workers/Worker'); -mWorker.onmessage = (e: MessageEvents) => { - const action: string | undefined = e.data?.action; - if (action === 'kill') { - mWorker.terminate(); - } -} - -// 触发方式的注册已省略 -mWorker.postMessage({action: 'tsfn-demo'}); - -``` - -以下内容为Worker线程逻辑,主要用以触发Native任务 - -```ts -// worker.ets -import worker, { ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@ohos.worker'; -import napiModule from 'libentry.so'; // libentry.so: napi 库的模块名称 - -const workerPort: ThreadWorkerGlobalScope = worker.workerPort; - -workerPort.onmessage = (e: MessageEvents) => { - const action: string | undefined = e.data?.action; - if (action === 'tsfn-demo') { - // 触发 c++ 层的 tsfn demo - napiModule.myTsfnDemo(); - // 通知主线程结束 worker - workerPort.postMessage({action: 'kill'}); - }; -} -``` - -## napi_get_uv_event_loop接口错误码说明 - -在OpenHarmony中,对使用非法的napi_env作为`napi_get_uv_event_loop`入参的行为加入了额外的参数校验,这一行为将直接反映到该接口的返回值上。该接口返回值详情如下: - -1. 当env且(或)loop为nullptr时,返回`napi_invalid_arg`。 -2. 当env为有效的napi_env且loop为合法指针,返回`napi_ok`。 -3. 当env不是有效的napi_env(如已释放的env),返回`napi_generic_failure`。 - -常见错误场景示例如下: - -```c++ -napi_value NapiInvalidArg(napi_env env, napi_callback_info) -{ - napi_status status = napi_ok; - status = napi_get_uv_event_loop(env, nullptr); // loop为nullptr, napi_invalid_arg - if (status == napi_ok) { - // do something - } - - uv_loop_s* loop = nullptr; - status = napi_get_uv_event_loop(nullptr, &loop); // env为nullptr, napi_invalid_arg - if (status == napi_ok) { - // do something - } - - status = napi_get_uv_event_loop(nullptr, nullptr); // env, loop均为nullptr, napi_invalid_arg - if (status == napi_ok) { - // do something - } - - return nullptr; -} - -napi_value NapiGenericFailure(napi_env env, napi_callback_info) -{ - std::thread([]() { - napi_env env = nullptr; - napi_create_ark_runtime(&env); // 通常情况下,需要对该接口返回值进行判断 - // napi_destroy_ark_runtime 会将指针置空,拷贝一份用以构造问题场景 - napi_env copiedEnv = env; - napi_destroy_ark_runtime(&env); - uv_loop_s* loop = nullptr; - napi_status status = napi_get_uv_event_loop(copiedEnv, &loop); // env无效, napi_generic_failure - if (status == napi_ok) { - // do something - } - }).detach();; -} -``` - -## napi_add_env_cleanup_hook/napi_remove_env_cleanup_hook调用报错,该如何处理 -`napi_add_env_cleanup_hook`/`napi_remove_env_cleanup_hook`调用报错,有以下几个常见原因和对应的特征日志,均为接口使用不当导致。 -1. 在`env`所在的js线程外使用上述两个接口,导致多线程安全问题。特征报错日志`ecma_vm cannot run in multi-thread`。 -2. 调用`napi_add_env_cleanup_hook`时,重复使用同一个`args`注册不同的回调函数,导致后续注册失败问题。该接口第三个入参`args`是作为接口内部`map`的`key`值,当重复注册同一个`args`的回调时,后续注册动作将会失败,仅第一次注册才会成功。注册失败可能会引起后续业务上的功能/崩溃问题。特征报错日志`AddCleanupHook Failed`。 -3. 调用`napi_remove_env_cleanup_hook`时,尝试通过一个不存在(或已被删除)的`args`删除回调函数,该接口调用失败,出现特征报错日志`RemoveCleanupHook Failed`。 - -常见错误场景示例如下: - -```c++ -void AddEnvCleanupHook(napi_env env) -{ - napi_add_env_cleanup_hook(env, [](void* args) -> void { - // cleanup function回调 - }, env); // env是个通用的数据,即使此处没有重复注册,可能会被其他地方所提前注册,导致该处注册失败。 -} - -static napi_value Test(napi_env env, napi_callback_info info) -{ - //第一次注册 - AddEnvCleanupHook(env); - //第二次重复注册 - AddEnvCleanupHook(env); - return nullptr; -} -``` - -修复建议: -1. 对于多线程安全问题,需确保调用接口的线程在`env`所在的js线程上。 -2. 对于注册失败的问题,需要使用者评估想注册的函数到底是哪一个。需要保证`key`值(也就是`napi_add_env_cleanup_hook`的第三个入参)是唯一的即可。 -3. 对于删除失败的问题,需要使用者确保`args`之前被注册过且未被删除。 - -相关参考资料链接: -[使用Node-API接口注册和使用环境清理钩子](use-napi-about-cleanuphook.md) -[方舟运行时的NApi](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-stability-coding-standard-api#section1219614634615) - -## `napi_wrap`如何保证被wrap的对象按期望顺序析构 -问题:在使用`napi_wrap`把两个 C++ 对象包装成两个 JavaScript 对象的场景中,由于这两个 C++ 对象存在依赖关系,要求其中一个c++对象必须在另一个c++对象之前析构。然而,JavaScript 垃圾回收(GC)的时机不确定,直接在`napi_wrap`的`finalize_cb`回调里销毁 C++ 对象,没办法保证析构顺序符合要求。该如何保证两个c++对象析构的前后顺序? - -参考方案: -先标记可释放状态,当A和B都为可释放状态时同时释放C++对象 -原理:将所有依赖对象的释放逻辑集中在最后一个被销毁的 JS 对象的 finalize_cb 中处理。 -实现步骤: -在 jsObjA 的 finalize_cb 中标记 cppObjA 为待销毁(不立即释放)。 -在 jsObjB 的 finalize_cb 中标记 cppObjB 为待销毁(不立即释放)。 -jsObjA 和 jsObjB 都为待销毁状态时,按顺序销毁A和B。 -示例代码: -```cpp -struct ObjectPair { - CppObjA* objA; - CppObjB* objB; - bool objADestroyedA = false; - bool objADestroyedB = false; -}; - -// jsObjA 的 finalize 回调 -void FinalizeA(napi_env env, void* data, void* hint) { - ObjectPair* pair = static_cast(data); - pair->objADestroyedA = true; - if (pair->objADestroyedA && pair->objADestroyedB) { - delete pair->objA; // 确保先销毁 A - delete pair->objB; // 再销毁 B - delete pair; // 释放包装结构 - } -} - -// jsObjB 的 finalize 回调 -void FinalizeB(napi_env env, void* data, void* hint) { - ObjectPair* pair = static_cast(data); - pair->objADestroyedB = true; - if (pair->objADestroyedA && pair->objADestroyedB) { - delete pair->objA; // 确保先销毁 A - delete pair->objB; // 再销毁 B - delete pair; // 释放包装结构 - } -} -``` diff --git a/zh-cn/application-dev/website.md b/zh-cn/application-dev/website.md index c524bad5ed2cc0d9475311c7ca3492eb02bcde97..72700374f9f4d4cb54cafc1cfb42dcf56637d626 100644 --- a/zh-cn/application-dev/website.md +++ b/zh-cn/application-dev/website.md @@ -1835,7 +1835,11 @@ - [使用Node-API接口创建、切换和销毁上下文环境](napi/use-napi-about-context.md) - [使用Node-API接口产生的异常日志/崩溃分析](napi/use-napi-about-crash.md) - [使用Node-API调用返回值为promise的ArkTS方法](napi/use-napi-method-promise.md) - - [Node-API常见问题](napi/use-napi-faqs.md) + - Node-API常见问题汇总 + - [Node-API常见问题](napi/use-napi-faqs.md) + - [稳定性相关问题汇总](napi/napi-faq-about-stability.md) + - [内存泄漏相关问题汇总](napi/napi-faq-about-memory-leak.md) + - [常见基本功能问题汇总](napi/napi-faq-about-common-basic.md) - 使用JSVM-API实现JS与C/C++语言交互 - [JSVM-API简介](napi/jsvm-introduction.md) - [JSVM-API支持的数据类型和接口](napi/jsvm-data-types-interfaces.md)