# Node-API常è§é—®é¢˜ ## 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接å£åœ¨è°ƒç”¨ç»“æŸå‰ä¼šè¿›è¡Œæ£€æŸ¥ï¼Œæ£€æŸ¥è™šæ‹Ÿæœºä¸æ˜¯å¦å˜åœ¨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/log.h> // hilog, 输出日志, 需链接 libhilog_ndk.z.so #include <thread> // 创建线程 #include <unistd.h> // çº¿ç¨‹ä¼‘çœ // å®šä¹‰è¾“å‡ºæ—¥å¿—çš„æ ‡ç¾å’ŒåŸŸ #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<std::mutex> 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<MyTsfnContext *>(data); napi_threadsafe_function tsfn = that->GetTsfn(); std::unique_lock<std::mutex> 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<MyTsfnContext *>(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<MyTsfnContext *>(context); char *str = reinterpret_cast<char *>(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();; } ```