📢 【三方库】aki v1.3.0 鸿蒙平台稳定性与质量提升

aki v1.3.0 已发布。本次升级聚焦该库稳定性与质量提升,修复多项内存泄漏、use-after-free 与线程安全问题,加固类型检查与错误处理,兼容升级,开发者无需修改现有代码即可享受本次改进。推荐所有该库用户升级至本版本。

版本概览

类型 内容
稳定性 SafetyCallback 引入调用超时、env 生命周期守卫与自持式调用,防止 env 销毁后崩溃;ClassWrapper 新增 Invalidate()/IsValid() 防 UAF;修复 Persistent 内存泄漏(绑定 env_);修复异步方法 receiver 裸指针生命周期;修复 Promise 重复 settle 与被 GC 提前回收;修复异步调用子线程异常不传播 / 取消时不 reject;修复 moduleMap 多线程无锁访问;修复 BindInfo/constructor ref 模块卸载泄漏;加固全部 napi API 调用;修复 AsyncWorker 析构未清理资源
质量提升 TaskRunner 重构为 napi_threadsafe_function;JSBind 新增 GetJSFunctionRef()/CleanupInvalidSessions()/DestroyTaskRunner()int64_t 入参兼容 BigInt;修复 Value::IsObject 误判数组;修复 ArrayBuffer 拷贝语义;统一 uint64/int64 类型校验与数值范围校验;加固 std::array<T,N> 越界处理;bindFunction/unbindFunction 增加入参校验;新增 FORTIFY-SOURCE 编译选项;统一代码风格

稳定性提升

一、SafetyCallback 调用超时、env 生命周期守卫与自持式调用

问题SafetyCallbackinclude/aki/callback/napi/safety_callback.h)通过 ThreadSafeContext 把子线程调用桥接到 JS 线程。旧实现存在多个高危缺陷:

  1. 子线程同步调用 Invoke()future.get() 无限阻塞——若 JS 线程卡死或 env 已退出,调用方将永久挂起,进而拖垮整个 worker 线程。
  2. ThreadSafeContext 析构与 napi_add_env_cleanup_hook 之间无关联:env 销毁(worker 退出)后,cbRefnapi_ref)可能已被清理,而 C++ 侧仍持有 ThreadSafeContext 并尝试 napi_delete_reference(env, cbRef),导致在已销毁的 env 上操作 napi,引发崩溃。
  3. Invoke() 跨线程时把 Data 通过 napi_call_threadsafe_function 投递,但 Data 内只保存了裸的 promise,未持有 ThreadSafeContext 自身。若 SafetyCallback 析构、ThreadSafeContext 释放,CallJs 回调里访问 ctx->forwardCallJs 即为悬空访问。

修复:该问题在 ff2bf60459d6923c6f9efd4419f6997e480 多个提交中分阶段完成,涉及 include/aki/callback/napi/safety_callback.hinclude/aki/value/napi/js_function.h

  1. 调用超时机制:在 Data 结构体新增 std::shared_ptr<ThreadSafeContext> ctx 字段实现自持(safety_callback.h)。跨线程 Invoke(std::shared_ptr<ThreadSafeContext> self, ...) 中,投递的 Data 持有 ctxshared_ptr,保证 ThreadSafeContext 在异步回调完成前不被析构;同时将无限阻塞的 future.get() 改为先 future.wait_for(std::chrono::milliseconds(AKI_INVOKE_TIMEOUT_MS)),超时(默认 10000ms,可由 AKI_INVOKE_TIMEOUT_MS 宏配置)则抛出 std::runtime_error("SafetyCallback::Invoke timed out waiting for JS callback")

    if (future.wait_for(std::chrono::milliseconds(AKI_INVOKE_TIMEOUT_MS)) == std::future_status::timeout) {
        throw std::runtime_error("SafetyCallback::Invoke timed out waiting for JS callback");
    }
    return future.get();
    

    js_function.hJsFunction::Invoke / InvokeAsync 同步改为传入 ctx_ctx_->template Invoke<R, P...>(ctx_, std::move(args)...)),完成自持式调用重载。

  2. env 生命周期守卫:新增 EnvGuardstd::atomic<bool> valid{true})与 CleanupHookArg,在构造时 napi_add_env_cleanup_hook(env, EnvCleanupHook, cleanupHookArg_) 注册清理钩子。EnvCleanupHook 在 env 销毁时将 guard->validfalse。所有 Invoke() / Invoke(void* data) 入口先检查 envGuard_->valid.load(),env 已销毁则直接抛 runtime_error 或返回 false,不再触碰 napi。析构函数据此判断:env 已失效时跳过 N-API 清理,仅记录告警,避免在已销毁 env 上调用 napi_delete_reference

  3. 子线程异常传播ForwardCallJs 重构,JS 回调抛出的异常经 napi_is_exception_pending 检测后,由 SetExceptionFromNapi() 提取错误字符串并 data->result->set_exception(...) 回传给等待的 C++ 子线程;CallJsenv==nullptr/cb==nullptr 等异常路径下也通过 cleanupData 回调 reject promise 并释放 Data,杜绝资源泄漏与无限等待。

二、修复 threadsafe context 中 cbRef 悬空指针与 env 跨线程使用问题

问题ThreadSafeContext::CallJs(JS 线程回调)与 ~ThreadSafeContext(任意线程析构)并发访问 cbRef 时无任何同步——一边 napi_get_reference_value(env, cbRef, &cb),另一边 napi_delete_reference(env, cbRef) 并置空,产生数据竞争与悬空指针。此外 cbRef 原本作为 ThreadSafeContext 成员存储,而 env 清理钩子通过 napi_ref* 指针间接置空它,指针别名关系脆弱。

修复d4419f6997e480include/aki/callback/napi/safety_callback.h):

  • 引入 mutable std::mutex cbRefMutex_CallJs 与析构函数在读取/删除 cbRef 时加 std::lock_guard,确保 delete_referenceget_reference_value 不会交错执行。
  • 997e480 中进一步重构:将 cbRefThreadSafeContext 成员移入 CleanupHookArg(含 napi_ref cbRefmutable std::mutex mutexstd::atomic<bool> cleanupDone),并将其作为 napi_create_threadsafe_functionfinalize_data 传入。清理逻辑统一收敛到 DoNapiCleanup(),用 cleanupDone.compare_exchange_strong 保证 EnvCleanupHookFinalizeThreadsafeFunction 只执行一次真正的 napi_delete_reference,并自动 napi_remove_env_cleanup_hookCallJs 改为拷贝 localCbRef 后释放锁再调用,缩小临界区。最终 ~ThreadSafeContext 仅保留 napi_release_threadsafe_function,彻底消除 env 跨线程误用。

三、ClassWrapper 新增 Invalidate()/IsValid() 防止 use-after-free

问题IS_POINTER 模式下 C++ 侧拥有对象所有权。当 C++ 侧 delete 底层对象后,若 JS 仍持有该 wrapper 引用并再次访问,ClassWrapper<C>::UnWrapper() 返回的 unsafeObj_ 即为悬空指针,引发 UAF。

修复ff2bf603c6f9efinclude/aki/class/class_wrapper.h):

  • ClassWrapperBase 新增 void Invalidate() { unsafeObj_ = nullptr; }bool IsValid() const { return unsafeObj_ != nullptr; }。C++ 侧在销毁对象前调用 Invalidate() 标记失效。
  • UnWrapper()GetShared() 在 unwrap 后检查 wrapperObj->IsValid(),失效时打印 ClassWrapper unwrapped object has been invalidated 并返回 nullptr/空 shared_ptr,阻止悬空访问。
  • DestructorCallback 中检测到 IS_POINTER 模式下 unsafeObj_ != nullptr(即未调用 Invalidate() 就销毁)时打印 ERROR 告警并防御性置空。
  • 配套在 napi_method_binder.hnapi_getter_binder.hnapi_setter_binder.hnapi_pmethod_binder.hSafetyWrapper 中对 UnWrapper 返回值增加空指针校验,receiver == nullptrnapi_throw_errorreturn nullptr,避免后续解引用空指针。

四、修复 Persistent 内存泄漏

问题Persistentsrc/persistent/persistent.cppinclude/aki/persistent/persistent.h)通过 napi_ref 持有 JS 对象引用,但所有 napi_reference_ref/unref/delete 操作都通过 JSBind::GetScopedEnv() 动态获取 env。在 worker 退出、env 销毁后,~Persistent 拿到的 envnullptr,此时直接 return不释放 ref_,造成 napi_ref 泄漏;拷贝赋值运算符未处理自赋值与旧 ref_ 未释放的情况,同样泄漏。此外 Value::ObjectAs()new NapiValueMaker<aki::Value> 创建的对象无对应 delete,存在堆内存泄漏。

修复c0ebb64ff2bf60):

  • persistent.h 新增 napi_env env_ = nullptr; 成员,在构造时绑定创建引用时的 env;后续 operator=、拷贝构造、析构、GetValue() 全部改用 env_ 而非 GetScopedEnv(),保证即使 scoped env 失效仍可用原始 env 正确 napi_reference_unref/napi_delete_reference~Persistentenv_==nullptr 时打印 ERROR 并置空 ref_
  • operator= 补充自赋值检查与旧 ref_ 的 unref/delete 逻辑;~Persistentenv==nullptr 时提前 returnc0ebb64)。
  • napi_value_trait.hValue::ObjectAs()new NapiValueMaker<aki::Value> 改为栈对象 NapiValueMaker<aki::Value> val(...),消除堆泄漏。

五、修复异步方法 receiver 裸指针生命周期

问题NapiPMethodBinderinclude/aki/binder/napi/napi_pmethod_binder.h)的异步 InvokeWorkInfo 中,receiverC* 裸指针,源自 ClassWrapper<C>::UnWrapper(env, jsThis)。异步任务在子线程执行 WrapperForward 时,若 JS 侧对象在此期间被 GC 回收或 C++ 对象被释放,裸指针即悬空,解引用导致 UAF。

修复0deb0abinclude/aki/binder/napi/napi_pmethod_binder.h):

  • InvokeWorkInfo::receiver 类型由 C* 改为 std::shared_ptr<C>,构建时由 ClassWrapper<C>::UnWrapper(env, jsThis) 改为 *ClassWrapper<C>::GetShared(env, jsThis),借助 shared_ptr 延长 C++ 对象生命周期至异步任务完成。
  • WrapperForwardC* receiver = arg->receiver; ... InnerWrapper(*receiver, ...) 改为 C& receiver = *arg->receiver; ... InnerWrapper(receiver, ...),保持调用语义不变。

六、修复 Promise 重复 settle 与被 GC 提前回收

问题aki::Promiseinclude/aki/value/promise.hsrc/value/promise.cpp)旧实现仅持有 napi_value promise_napi_deferred deferred_,存在两个缺陷:一是 Resolve/Reject 无幂等保护,重复调用会触发 napi 内部错误;二是 napi_value 不增加引用计数,Promise 对象可能在没有 resolve/reject 之前被 GC 回收,导致后续 napi_resolve_deferred 操作失效对象。

修复ab2e48d):

  • 新增 std::shared_ptr<std::atomic<bool>> settled_ 标志,构造时 napi_create_reference(env_, promise_, 1, &promiseRef_) 建立 napi_ref 防止 Promise 被 GC 提前回收,并新增 CleanupRef() 在 settle 后释放引用。
  • 构造函数绑定 env_ 成员并完善 napi_create_promise 失败处理;补齐拷贝构造与赋值运算符(共享同一 settled_promiseRef_),保证跨拷贝的 settle 幂等。

七、修复异步调用子线程异常不传播与取消时不 reject Promise

问题NapiPFunctionBinder / NapiPMethodBinderinclude/aki/binder/napi/napi_pfunction_binder.hnapi_pmethod_binder.h)的 AsyncInvokeWork(子线程 execute 回调)直接调用业务函数,无 try-catch;子线程抛出的 C++ 异常会跨越 napi 边界,行为未定义,且 Promise 永远不会 resolve/reject,调用方无限等待。AsyncInvokeDone(complete 回调)在 status != napi_ok(如 env 退出导致 async work 被取消)时虽 reject,但未清理 deferred/worker/arg,存在资源泄漏。

修复eabad02):

  • InvokeWorkInfo 新增 bool invokeFailed = false;AsyncInvokeWorktry { ... } catch(...) { arg->invokeFailed = true; AKI_LOG(ERROR) << "Exception occurred in async invoke work"; } 包裹业务执行,捕获子线程异常。
  • AsyncInvokeDonestatus != napi_ok(取消)时构造 Error("Async work was cancelled")napi_reject_deferred,随后 napi_delete_async_work、置空 arg->deferreddelete arg,补齐清理;在 arg->invokeFailed 为真时同样 reject "Exception occurred in async invoke work" 并清理;ToNapiValue 转换也加 try-catch 防止返回值转换异常逃逸。

八、修复 moduleMap 多线程无锁访问

问题src/napi/napi_init.cppstatic std::map<std::string, bool> moduleMap 用于主线程 so 单次加载判断,在 worker 多线程场景下被并发读写而无锁保护,存在数据竞争。

修复:新增 static std::mutex moduleMapMutex;BindSymbols 的查询与插入路径均加 std::lock_guard<std::mutex> lock(moduleMapMutex),保证线程安全。

九、修复 BindInfo/constructor ref 模块卸载时内存泄漏

问题Init()src/napi/napi_init.cpp)与 BindSymbols() 中通过 new aki::BindInfo()new napi_ref 创建大量构造函数引用与方法绑定信息,但模块卸载(env 销毁)时无任何回收逻辑,造成泄漏。

修复ff2bf60src/napi/napi_init.cpp):新增 EnvCleanupData 结构体收集所有 constructorRefsbindInfosBindMethodAndField/Init/BindSymbols 中每次 new 时同步 cleanupData->bindInfos.push_back(info) / constructorRefs.push_back(constructor)。最后 napi_add_env_cleanup_hook(env, ModuleCleanup, cleanupData) 注册清理钩子,ModuleCleanup 在 env 销毁时遍历 napi_delete_referencedelete 全部 BindInfonapi_ref*,实现自动释放。

十、加固全部 napi API 调用

问题:旧代码大量使用 AKI_DCHECK(status == napi_ok) 断言——该宏仅在 Debug 构建生效,Release 构建下 napi 调用失败被静默忽略,后续基于失败结果继续执行,引发空指针、越界等二次故障。

修复ff2bf60ab2e48d,跨多文件):将 DCHECK 升级为真实错误处理:

  • ThreadSafeContext::Invoke(void* data)void 改为 boolnapi_acquire_threadsafe_function / napi_call_threadsafe_function 失败时记录 ERROR、napi_release_threadsafe_function 并返回 false,调用方据此 set_exceptiondelete data
  • ScopeHandleinclude/aki/scopehandle/ScopeHandle.h)对 napi_open_escapable_handle_scope / napi_open_handle_scope 失败时打印 ERROR 并将句柄置 nullptr
  • NapiValueBasesrc/value/napi/napi_value_base.cpp)所有 GetBool/GetUint8/GetInt8/.../GetDouble/GetString/GetArrayBuffer 在 napi 调用失败时记录 ERROR 并返回安全默认值。
  • AsyncWorker::CreateAsyncWorker / Queuesrc/asyncworker/asyncworker.cpp)对 napi_create_promise/napi_create_string_utf8/napi_create_async_work/napi_queue_async_work 失败时 reject deferred、释放 asyncData_ 并返回。
  • TaskRunner::PostTasksrc/task_runner/task_runner.cpp)对 uv_queue_work/napi_call_threadsafe_function 失败时释放 work/taskPtr
  • NapiOverloader::Wrapper / CreateValuesrc/overloader/napi/napi_overloader.cpp)新增 argc >= methods.size()constructorWrapper == nullptr 越界检查,napi_throw_type_error 防止越界访问 methods[argc]

十一、修复 AsyncWorker 析构未清理资源

问题AsyncWorker::~AsyncWorker()src/asyncworker/asyncworker.cpp)旧实现为空。若 AsyncWorker 对象在 Queue() 之前或异常路径下析构,asyncData_(含 deferred_worker_)不会被释放,造成内存与 napi 句柄泄漏。

修复ff2bf60):析构函数增加清理逻辑——asyncData_ != nullptr 时,若 deferred_ 非空则 napi_reject_deferred,若 worker_ 非空则 napi_delete_async_work,最后 delete asyncData_ 并置空;Queue() 成功后将 asyncData_ 所有权转移并置空,避免重复释放。

质量提升

一、TaskRunner 重构为 napi_threadsafe_function

src/task_runner/task_runner.cpp 全面重构:弃用基于 libuv uv_queue_work 的实现,改为 napi_create_threadsafe_function 创建线程安全函数 tsfn_,任务经 napi_call_threadsafe_function 投递,在 CallJs 回调中于 JS 线程执行并自动 Binding::SetScopedEnv(env)。新增 TaskRunner::RemoveTaskRunner()(加 runnersMutex_ 锁 erase),并在 JSBind 暴露 DestroyTaskRunner(name) 接口(include/aki/jsbind.hsrc/napi/napi_init.cpp 注册为 destroyTaskRunner 方法),供开发者主动销毁 TaskRunner 实例。该重构使 TaskRunner 不再直接依赖 libuv loop,生命周期与 env 绑定更安全。

二、JSBind 新增接口与生命周期管理

include/aki/jsbind.h 新增 GetJSFunctionRef(name)(返回 std::shared_ptr<JSFunction>,以共享所有权延长 JSFunction 生命周期)与 CleanupInvalidSessions()(清理失效会话)接口,配合 DestroyTaskRunner() 完善运行时会话管理。

三、类型系统加固(int64_t/uint64_t BigInt 兼容、数值范围、IsObject、ArrayBuffer、std::array)

本组变更集中在新数值类型兼容与边界校验,涉及 include/aki/value/napi/napi_value.hsrc/value/napi/napi_value_base.cppsrc/value.cppinclude/aki/value/array_buffer.hsrc/value/array_buffer.cppinclude/aki/value/napi/napi_value_trait.h

  • int64_t/uint64_t 兼容 BigInt66274f7):NapiValue<uint64_t> / NapiValue<int64_t>CheckType 由仅接受 Number 改为 CheckNumberType(env, value) || CheckBigIntType(env, value),修复"CheckType 只认 Number 但 ToNapiValue 返回 BigInt"导致 JS 传 BigInt 时校验失败的矛盾。GetUInt64() / GetInt64()napi_typeof 分支:BigInt 走 napi_get_value_bigint_* 并校验 lossless;Number 走原接口,超过 Number.MAX_SAFE_INTEGER (2^53-1) 时打印 WARNING 提示精度丢失、建议改用 BigInt。int64_t 返回值仍保持 Number,接口不变。
  • 数值范围校验加固ff2bf60eabad0266274f7):GetUint8/GetInt8/GetUint16/GetInt16/GetFloat/GetUInt64 的范围检查由 AKI_DCHECK(仅 Debug)升级为 napi_throw_range_error(Release 生效),超界时抛出 range_error 并返回安全值;GetUInt64 改用 napi_get_value_bigint_uint64 修正原先误用 bigint_int64 的不一致。
  • Value::IsObject 误判数组ab2e48dsrc/value.cpp):Value::IsObject() 新增 napi_is_array 判断,返回 type == napi_object && !isArr,排除数组类型。
  • ArrayBuffer 拷贝语义c0ebb64):ArrayBuffer 新增拷贝构造与赋值运算符,拷贝后当 staging_ 非空时将 data_ 重新指向 staging_.data(),修复原先浅拷贝后 data_ 仍指向源对象 staging_ 的悬空问题。
  • std::array<T,N> 越界处理eabad02):NapiValue<std::array<T,N>>::CheckType 新增 napi_get_array_lengthlength > N 告警;GetArray()length > N 时截断为 N 并打印 WARNING,避免越界写入定长数组。

四、接口健壮性(AsyncWorker 返回值、bindFunction 校验)

  • AsyncWorker 返回值类型include/aki/asyncworker/asyncworker.h):SetData/SetExecuteCallback/SetCompleteCallback 返回类型由 void 改为 int,可在回调设置失败时上报错误码。
  • bindFunction/unbindFunction 入参校验src/jsbind.cpp):bindFunction 校验 name.empty()napi_throw_error 并返回 -1unbindFunction 校验空名称及函数不存在(GetJSFunctionMap().find 未命中)时抛错返回 -1,防止操作未注册或空名函数。

五、编译选项与代码风格统一

  • FORTIFY-SOURCEad555ddCMakeLists.txt):新增 add_compile_options(-D_FORTIFY_SOURCE=2),启用 GCC 缓冲区溢出运行时检测,提升内存安全。
  • 代码风格统一030e89afc3abd451f39273453951940e102):跨 60+ 文件统一代码风格,修复函数定义与声明参数名不一致、删除注释掉的代码段、构造函数补 explicit、移除冗余空行与多余 ScopeHandle 堆分配(napi_value_trait.hnew ScopeHandle 改为栈对象);并补充覆盖 ArrayBuffer/Callback/JSFunction/Napi/Promise/Value 等未覆盖接口的单元测试(060ffba,新增约 509 行测试)。

兼容性说明

  • 无 Breaking Changes,公开 API 接口保持不变;int64_t 返回值仍为 Number,仅入参新增 BigInt 兼容。
  • 本次变更以缺陷修复与稳定性/质量加固为主,现有代码无需修改。
  • 从 v1.2.25 升级至 v1.3.0 均为兼容性升级。

升级方式

  • 鸿蒙原生:oh-package.json5"@ohos/aki": "^1.3.0",执行 ohpm install

相关链接

  • 仓库:https://gitcode.com/CPF-ApplicationTPC/aki
  • 文档:https://gitcode.com/CPF-ApplicationTPC/aki/blob/master/README.md
  • CHANGELOG:https://gitcode.com/CPF-ApplicationTPC/aki/blob/master/changelog.md
  • 反馈:https://gitcode.com/CPF-ApplicationTPC/aki/issues
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐