------------------ Original ------------------
From:  "谭小凡";<xiaofan@iscas.ac.cn>;
Send time: Monday, Jul 26, 2021 11:53 AM
To: "dev"<dev@openharmony.io>;
Subject:  [Dev] Re: 标准系统开发n-api的JS模块的一些问题以及解决方法

由于上封邮件格式存在问题,详情请阅读附件。

-----原始邮件-----
发件人:"谭小凡" <xiaofan@iscas.ac.cn>
发送时间:2021-07-26 11:30:35 (星期一)
收件人: dev@openharmony.io
抄送:
主题: [Dev] 标准系统开发n-api的JS模块的一些问题以及解决方法

存在的问题

问题1. NAPI_CALL一族函数可能会导致函数体返回,造成内存泄露

  • (2021/7/25更新)该状态已经得到缓解,目前代码中看到的是在外层函数中申请资源,调用Wrap函数去实际干活。Wrap函数中随意使用NAPI_CALL宏,一旦异常返回,外层函数负责回收申请到的资源。

    但是这种机制未解决了所有内存泄露的问题,以NAPI_GetProcessName函数为例(为了板书简短,下面代码做了适当精简)

    napi_value NAPI_GetProcessName(napi_env env, napi_callback_info info) {
        ProcessNameCB *processNameCB = CreateProcessNameCBInfo(env); if (processNameCB == nullptr) return nullptr;
        napi_value ret = GetProcessNameWrap(env, info, processNameCB); if (ret == nullptr && processNameCB != nullptr) { delete processNameCB;
         processNameCB = nullptr;
        } return ret;
    }
    
    napi_value GetProcessNameWrap(napi_env env, napi_callback_info info, ProcessNameCB *processNameCB) { if (processNameCB == nullptr) return nullptr;
        size_t argcAsync = 1; const size_t argcPromise = 0; const size_t argCountWithAsync = argcPromise + ARGS_ASYNC_COUNT;
        napi_value args[ARGS_MAX_COUNT] = {nullptr};
        napi_value ret = nullptr;
        NAPI_CALL(env, napi_get_cb_info(env, info, &argcAsync, args, nullptr, nullptr)); if (argcAsync > argCountWithAsync || argcAsync > ARGS_MAX_COUNT) return nullptr; if (argcAsync > argcPromise)
            ret = GetProcessNameAsync(env, args, argcAsync, argcPromise, processNameCB); else ret = GetProcessNamePromise(env, processNameCB); return ret;
    }
    
    napi_value GetProcessNameAsync(
        napi_env env, napi_value *args, size_t argcAsync, const size_t argcPromise, ProcessNameCB *processNameCB) { if (args == nullptr || processNameCB == nullptr) return nullptr;
        napi_value resourceName = nullptr;
        NAPI_CALL(env, napi_create_string_latin1(env, __func__, NAPI_AUTO_LENGTH, &resourceName));
        napi_valuetype valuetype = napi_undefined;
        NAPI_CALL(env, napi_typeof(env, args[argcPromise], &valuetype)); if (valuetype == napi_function)
            NAPI_CALL(env, napi_create_reference(env, args[argcPromise], 1, &processNameCB->cbBase.cbInfo.callback));
        NAPI_CALL(env,
            napi_create_async_work(env, nullptr,
                resourceName,
                GetProcessNameExecuteCB,
                GetProcessNameAsyncCompleteCB,
                (void *)processNameCB,
                &processNameCB->cbBase.asyncWork));
        NAPI_CALL(env, napi_queue_async_work(env, processNameCB->cbBase.asyncWork));
        napi_value result = nullptr;
        NAPI_CALL(env, napi_get_null(env, &result)); return result;
    } void GetProcessNameAsyncCompleteCB(napi_env env, napi_status status, void *data) {
        ProcessNameCB *processNameCB = (ProcessNameCB *)data;
        napi_value callback = nullptr;
        napi_value undefined = nullptr;
        napi_value result[ARGS2] = {nullptr};
        napi_value callResult = nullptr;
        NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
        result[PARAM0] = GetCallbackErrorValue(env, NO_ERROR);
        result[PARAM1] = WrapProcessName(env, processNameCB);
        NAPI_CALL_RETURN_VOID(env, napi_get_reference_value(env, processNameCB->cbBase.cbInfo.callback, &callback));
        NAPI_CALL_RETURN_VOID(env, napi_call_function(env, undefined, callback, ARGS2, &result[PARAM0], &callResult)); if (processNameCB->cbBase.cbInfo.callback != nullptr)
            NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, processNameCB->cbBase.cbInfo.callback));
        NAPI_CALL_RETURN_VOID(env, napi_delete_async_work(env, processNameCB->cbBase.asyncWork)); delete processNameCB;
        processNameCB = nullptr;
    } 

目前仍然存在的问题

  • cbInfo.callbackasyncWork泄露

    GetProcessNameAsync调用了napi_create_reference创建了一个processNameCB->cbBase.cbInfo.callback

    一旦后续步骤发生失败,将返回空指针到NAPI_GetProcessName

    就会发生NAPI_GetProcessName仅删除了processNameCB资源,并未删除processNameCB->cbBase.cbInfo.callback

    同理napi_queue_async_work函数运行失败,也会造成processNameCB->cbBase.asyncWork这个资源泄露

  • NAPI_CALL_RETURN_VOID导致泄露

    async_workcomplete回调中,并未采用Wrap函数这种技巧,可能导致无法顺利执行资源释放的工作。

    processNameCB,cbInfo.callback,asyncWork都将有未释放的风险

  • 对不再使用的局部变量置空是无意义的写法

    delete processNameCB;
    processNameCB = nullptr; 

问题2. 何时使用NAPI_CALL这一族宏定义来检查n-api函数是否成功

从目前代码中来看该族函数,大部分napi函数都使用了NAPI_CALL这一族宏定义来判断状态,但还是有很多未检查的代码,例如

napi_value NAPI_KillProcessesByBundleName(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value argv[argc];
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
    std::string bundleName;
    ParseBundleName(env, bundleName, argv[0]); // 内部使用了NAPI_CALL但外部未检查返回值是否为空指针 bool callBackMode = false; if (argc >= 2) {
        napi_valuetype valuetype;
        NAPI_CALL(env, napi_typeof(env, argv[1], &valuetype));
        NAPI_ASSERT(env, valuetype == napi_function, "Wrong argument type. Function expected.");
        callBackMode = true;
    }
    AsyncKillProcessCallbackInfo *async_callback_info = new (std::nothrow) AsyncKillProcessCallbackInfo{.env = env, .asyncWork = nullptr, .deferred = nullptr}; if (async_callback_info == nullptr) return nullptr;
    async_callback_info->bundleName = bundleName; if (callBackMode)
        napi_create_reference(env, argv[1], 1, &async_callback_info->callback[0]); // 未检查 napi_value ret = NAPI_KillProcessesByBundleNameWrap(env, info, callBackMode, async_callback_info); if (ret == nullptr) { delete async_callback_info;
        async_callback_info = nullptr;
    } return ((callBackMode) ? (nullptr) : (ret));
}

napi_value NAPI_KillProcessesByBundleNameWrap(
    napi_env env, napi_callback_info info, bool callBackMode, AsyncKillProcessCallbackInfo *async_callback_info) { if (callBackMode) {
        napi_value resourceName;
        napi_create_string_latin1(env, "NAPI_KillProcessesByBundleNameCallBack", NAPI_AUTO_LENGTH, &resourceName); // 未检查 napi_create_async_work(env, nullptr,
            resourceName,
            [](napi_env env, void *data) {
                HILOG_INFO("killProcessesByBundleName called(CallBack Mode)...");
                AsyncKillProcessCallbackInfo *async_callback_info = (AsyncKillProcessCallbackInfo *)data;
                async_callback_info->result = GetAbilityManagerInstance()->KillProcess(async_callback_info->bundleName);
            },
            [](napi_env env, napi_status status, void *data) {
                HILOG_INFO("killProcessesByBundleName compeleted(CallBack Mode)...");
                AsyncKillProcessCallbackInfo *async_callback_info = (AsyncKillProcessCallbackInfo *)data;
                napi_value result[2] = {0};
                napi_value callback;
                napi_value undefined;
                napi_value callResult = 0;

                result[0] = GetCallbackErrorValue(async_callback_info->env, BUSINESS_ERROR_CODE_OK); // 内部使用了NAPI_CALL但外部未检查返回值是否为空指针 napi_create_int32(async_callback_info->env, async_callback_info->result, &result[1]); // 未检查 napi_get_undefined(env, &undefined); // 未检查 napi_get_reference_value(env, async_callback_info->callback[0], &callback); // 未检查 napi_call_function(env, undefined, callback, 2, &result[0], &callResult); // 未检查 if (async_callback_info->callback[0] != nullptr) {
                    napi_delete_reference(env, async_callback_info->callback[0]); // 未检查,但此处确实无需检查 }

                napi_delete_async_work(env, async_callback_info->asyncWork); // 未检查,但此处确实无需检查 delete async_callback_info;
            },
            (void *)async_callback_info,
            &async_callback_info->asyncWork); // 未检查 NAPI_CALL(env, napi_queue_async_work(env, async_callback_info->asyncWork));
        napi_value ret = 0;
        NAPI_CALL(env, napi_create_int32(env, 0, &ret)); // 若此函数失败程序可能会崩溃,因为会出现double delete的问题。 return ret;
    } else {
        napi_value resourceName;
        napi_create_string_latin1(env, "NAPI_KillProcessesByBundleNamePromise", NAPI_AUTO_LENGTH, &resourceName); // 未检查 napi_deferred deferred;
        napi_value promise;
        NAPI_CALL(env, napi_create_promise(env, &deferred, &promise));
        async_callback_info->deferred = deferred;

        napi_create_async_work(env, nullptr,
            resourceName,
            [](napi_env env, void *data) {
                HILOG_INFO("killProcessesByBundleName called(Promise Mode)...");
                AsyncKillProcessCallbackInfo *async_callback_info = (AsyncKillProcessCallbackInfo *)data;
                async_callback_info->result = GetAbilityManagerInstance()->KillProcess(async_callback_info->bundleName);
            },
            [](napi_env env, napi_status status, void *data) {
                HILOG_INFO("killProcessesByBundleName compeleted(Promise Mode)...");
                AsyncKillProcessCallbackInfo *async_callback_info = (AsyncKillProcessCallbackInfo *)data;
                napi_value result;
                napi_create_int32(async_callback_info->env, async_callback_info->result, &result); // 未检查 napi_resolve_deferred(async_callback_info->env, async_callback_info->deferred, result); // 未检查 napi_delete_async_work(env, async_callback_info->asyncWork); // 未检查,但此处确实无需检查 delete async_callback_info;
            },
            (void *)async_callback_info,
            &async_callback_info->asyncWork); // 未检查 napi_queue_async_work(env, async_callback_info->asyncWork); // 未检查 return promise;
    }
} 

问题3. Promise模式和Callback模式代码混乱

  • promise和callback的语义不统一

    void AsyncCompleteCallback(napi_env env, napi_status status, void *data)
    {
        AsyncPermissionCallbackInfo *asyncCallbackInfo = (AsyncPermissionCallbackInfo *)data; if (asyncCallbackInfo == nullptr) return; const size_t cbCount = 2;
        napi_value callback = 0;
        napi_value undefined = 0;
        napi_get_undefined(env, &undefined);
        napi_value result = 0;
        napi_value callResult = 0;
        NAPI_GetNapiValue(env, &asyncCallbackInfo->native_data, &result); if (asyncCallbackInfo->run_status) { if (asyncCallbackInfo->callback[0] != nullptr) {
                napi_get_reference_value(env, asyncCallbackInfo->callback[0], &callback);
                napi_call_function(env, undefined, callback, 1, &result, &callResult);
                napi_delete_reference(env, asyncCallbackInfo->callback[0]);
            }
        } else { if (asyncCallbackInfo->callback[1] != nullptr) {
                napi_value rev[2] = {nullptr};
                napi_create_int32(env, asyncCallbackInfo->error_code, &rev[0]);
                napi_create_int32(env, asyncCallbackInfo->run_status ? 0 : -1, &rev[1]);
                napi_get_reference_value(env, asyncCallbackInfo->callback[1], &callback);
                napi_call_function(env, undefined, callback, cbCount, rev, &callResult);
                napi_delete_reference(env, asyncCallbackInfo->callback[1]);
            }
        }
        napi_delete_async_work(env, asyncCallbackInfo->asyncWork); delete asyncCallbackInfo;
    } void PromiseCompleteCallback(napi_env env, napi_status status, void *data)
    {
        AsyncPermissionCallbackInfo *asyncCallbackInfo = (AsyncPermissionCallbackInfo *)data; if (asyncCallbackInfo != nullptr) {
            napi_value result = 0;
            NAPI_GetNapiValue(env, &asyncCallbackInfo->native_data, &result);
            napi_resolve_deferred(env, asyncCallbackInfo->deferred, result);
            napi_delete_async_work(env, asyncCallbackInfo->asyncWork); delete asyncCallbackInfo;
        }
    } 

    以上述代码为例,callback分为成功回调和失败回调。那为何promise始终都用napi_resolve_deferred而不用napi_reject_deferred呢?

  • 现存代码中callback有两种写法

    1. 传入一个或两个callback,第一个表success_cb,第二个表fail_cb
    2. 传入一个callback对象,其中携带success, fail, complete三个回调
  • 代码过于重复

    大部分同时支持promise模式和callback模式的函数拥有着高度重复的代码,严重降低代码的可维护性,代码数量快速膨胀。

解决思路

问题1,2

这两个问题的根源内存泄露,其实只要解决了内存泄露,那问题1,2就能迎刃而解了

  • 建议为asyncCallbackInfo添加析构函数,由析构函数负责释其所管理的所有资源。

    具体的来说,在析构函数中调用napi_delete_reference,api_delete_async_work等资源清理函数

  • 建议用std::unique_ptr来管理asyncCallbackInfo指针

    • 当函数因失败而返回,或者正常退出且需要释放asyncCallbackInfo的场合,std::unique_ptr可以自动释放资源
    • 当函数正常结束无需释放asyncCallbackInfo的场合,调用std::unique_ptr<T>::release()接口即可取消自动释放资源的功能

问题3

  • 为async_callback_data抽象一个共同的基类来屏蔽promisecallback的差异,实现只需要写一份代码,就可以同时实现两种模式

    class AsyncWorkData
    { public: // 构造函数,需要持有一个napi_env对象 AsyncWorkData(napi_env env); // 析构函数,释放本对象管理的所有资源 ~AsyncWorkData(); // argc,argv //   argc == 0: promise模式 //   argc == 1: callback模式 //     * 如果type(argv[0]) == napi_function //       则将argv作为success_cb //     * 如果type(argv[0]) == napi_object 的话 //      则将argv[0].success作为success_cb,argv[0].fail作为fail_cb,argv[0].complete作为complete_cb //   argc == 2: callback模式 //     将argv[0]作为success_cb, argv[1]作为fail_cb // retval //   若为promise模式,则使*retval = promise对象 //   若为callback模式,则使*retval = nullptr bool parse_args(size_t argc, napi_value argv[], napi_value *retval); // 若为promise模式: //  根据resolved值来调用napi_resolve_deferred或napi_reject_deferred函数,result作为resolve/reject的参数。 // 若为callback模式 //   根据resolved值来调用complete_cb(result)或fail_cb(result)。如果存在complete_cb,则调用complete_cb() void notify(napi_value result, bool resolved = true); // 每个这个成员与参数解析无关,但他是所有async_callback_data的共同成员,所以放在基类里统一管理 napi_async_work async_work; protected:
        napi_env env; private:
        napi_ref success_cb;
        napi_ref fail_cb;
        napi_ref complete_cb;
        napi_deferred deferred;
    }; 

    使用这种技巧编写这种同时支持callback和promise的应用将会非常简单,例如开发一个getProcessInfo的函数

    struct GetProcessNameAsyncInfo : public AsyncWorkData
    {
        GetProcessInfoAsyncInfo(napi_env env);
        Ability *ability;
        std::shared_ptr<ProcessInfo> process_info;
    }; static napi_value NAPI_getProcessInfo(napi_env env, napi_callback_info info) {
        size_t argc = 1;
        napi_value argv[1], retval, resourceName;
    
        NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
        GetProcessInfoAsyncInfo *async_callback_info = new GetProcessInfoAsyncInfo { env };
        std::unique_ptr<GetProcessInfoAsyncInfo> cleaner { async_callback_info }; // 如果回调参数下标是从n开始的话,可以写作parse_args(argc-n, argv+n, &retval) if (!async_callback_info->parse_args(argc, argv, &retval)) return nullptr;
    
        NAPI_CALL(env, util_get_ability(env, &async_callback_info->ability));
        NAPI_CALL(env, napi_create_string_latin1(env, __func__, NAPI_AUTO_LENGTH, &resourceName));
        NAPI_CALL(env, napi_create_async_work(
            env, nullptr,
            resourceName,
            [](napi_env env, void *data) {
                GetProcessInfoAsyncInfo *async_callback_info = reinterpret_cast<GetProcessInfoAsyncInfo*>(data);
                async_callback_info->process_info = async_callback_info->ability->GetProcessInfo();
            },
            [](napi_env env, napi_status status, void *data) {
                GetProcessInfoAsyncInfo *async_callback_info = reinterpret_cast<GetProcessInfoAsyncInfo*>(data);
         std::unique_ptr<GetProcessInfoAsyncInfo> cleaner { async_callback_info };
    
                ProcessInfo &process_info = *async_callback_info->process_info;
                napi_value result;
                napi_value pid, name;
                NAPI_CALL_RETURN_VOID(env, napi_create_object(env, &result));
                NAPI_CALL_RETURN_VOID(env, util_napi_value_from_string_utf8(env, process_info.GetProcessName(), &name));
                NAPI_CALL_RETURN_VOID(env, napi_create_uint32(env, (uint32_t)process_info.GetPid(), &pid));
                NAPI_CALL_RETURN_VOID(env, napi_set_named_property(env, result, "processName", name));
                NAPI_CALL_RETURN_VOID(env, napi_set_named_property(env, result, "pid", pid));
                async_callback_info->notify(result); // 如果此处有失败分支的话,令第三个参数为false即可。 }, reinterpret_cast<void*>(async_callback_info),
            &async_callback_info->async_work));
        NAPI_CALL(env, napi_queue_async_work(env, async_callback_info->async_work));
        cleaner.release(); return retval;
    } 

    这样一来,以上代码同时支持callback模式和promise模式

    vm = this // callback风格: 只传一个success_cb getProcessInfo(info => { vm.message = 'name: ' + info.processName }) // callback风格: 传两个回调函数 getProcessInfo(info => { vm.message = 'name: ' + info.processName },
                   err => { vm.message = 'fail: ' + err.message }) // callback风格: 传入一个携带success,fail,complete信息的对象 getProcessInfo({
        success: info => { vm.message = 'name: ' + info.processName },
        fail: err => { vm.message = 'fail: ' + err.message },
        complete: () => { console.log('complete') }
    }) // callback风格: 传入一个不完整的回调对象 getProcessInfo({
        success: info => { vm.message = 'name: ' + info.processName },
    }) // promise风格 getProcessInfo()
        .then(info => { vm.message = info.processName })
     .catch(err => { vm.message = 'fail: ' + err.message })