【三方库】aki v1.3.0 鸿蒙平台稳定性与质量提升
📢 【三方库】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 生命周期守卫与自持式调用
问题:SafetyCallback(include/aki/callback/napi/safety_callback.h)通过 ThreadSafeContext 把子线程调用桥接到 JS 线程。旧实现存在多个高危缺陷:
- 子线程同步调用
Invoke()后future.get()无限阻塞——若 JS 线程卡死或 env 已退出,调用方将永久挂起,进而拖垮整个 worker 线程。 ThreadSafeContext析构与napi_add_env_cleanup_hook之间无关联:env 销毁(worker 退出)后,cbRef(napi_ref)可能已被清理,而 C++ 侧仍持有ThreadSafeContext并尝试napi_delete_reference(env, cbRef),导致在已销毁的 env 上操作 napi,引发崩溃。Invoke()跨线程时把Data通过napi_call_threadsafe_function投递,但Data内只保存了裸的promise,未持有ThreadSafeContext自身。若SafetyCallback析构、ThreadSafeContext释放,CallJs回调里访问ctx->forwardCallJs即为悬空访问。
修复:该问题在 ff2bf60、459d692、3c6f9ef、d4419f6、997e480 多个提交中分阶段完成,涉及 include/aki/callback/napi/safety_callback.h 与 include/aki/value/napi/js_function.h。
-
调用超时机制:在
Data结构体新增std::shared_ptr<ThreadSafeContext> ctx字段实现自持(safety_callback.h)。跨线程Invoke(std::shared_ptr<ThreadSafeContext> self, ...)中,投递的Data持有ctx的shared_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.h中JsFunction::Invoke/InvokeAsync同步改为传入ctx_(ctx_->template Invoke<R, P...>(ctx_, std::move(args)...)),完成自持式调用重载。 -
env 生命周期守卫:新增
EnvGuard(std::atomic<bool> valid{true})与CleanupHookArg,在构造时napi_add_env_cleanup_hook(env, EnvCleanupHook, cleanupHookArg_)注册清理钩子。EnvCleanupHook在 env 销毁时将guard->valid置false。所有Invoke()/Invoke(void* data)入口先检查envGuard_->valid.load(),env 已销毁则直接抛runtime_error或返回false,不再触碰 napi。析构函数据此判断:env 已失效时跳过 N-API 清理,仅记录告警,避免在已销毁 env 上调用napi_delete_reference。 -
子线程异常传播:
ForwardCallJs重构,JS 回调抛出的异常经napi_is_exception_pending检测后,由SetExceptionFromNapi()提取错误字符串并data->result->set_exception(...)回传给等待的 C++ 子线程;CallJs在env==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* 指针间接置空它,指针别名关系脆弱。
修复(d4419f6、997e480,include/aki/callback/napi/safety_callback.h):
- 引入
mutable std::mutex cbRefMutex_,CallJs与析构函数在读取/删除cbRef时加std::lock_guard,确保delete_reference与get_reference_value不会交错执行。 - 在
997e480中进一步重构:将cbRef从ThreadSafeContext成员移入CleanupHookArg(含napi_ref cbRef、mutable std::mutex mutex、std::atomic<bool> cleanupDone),并将其作为napi_create_threadsafe_function的finalize_data传入。清理逻辑统一收敛到DoNapiCleanup(),用cleanupDone.compare_exchange_strong保证EnvCleanupHook与FinalizeThreadsafeFunction只执行一次真正的napi_delete_reference,并自动napi_remove_env_cleanup_hook。CallJs改为拷贝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。
修复(ff2bf60、3c6f9ef,include/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.h、napi_getter_binder.h、napi_setter_binder.h、napi_pmethod_binder.h的SafetyWrapper中对UnWrapper返回值增加空指针校验,receiver == nullptr时napi_throw_error并return nullptr,避免后续解引用空指针。
四、修复 Persistent 内存泄漏
问题:Persistent(src/persistent/persistent.cpp、include/aki/persistent/persistent.h)通过 napi_ref 持有 JS 对象引用,但所有 napi_reference_ref/unref/delete 操作都通过 JSBind::GetScopedEnv() 动态获取 env。在 worker 退出、env 销毁后,~Persistent 拿到的 env 为 nullptr,此时直接 return 而不释放 ref_,造成 napi_ref 泄漏;拷贝赋值运算符未处理自赋值与旧 ref_ 未释放的情况,同样泄漏。此外 Value::ObjectAs() 中 new NapiValueMaker<aki::Value> 创建的对象无对应 delete,存在堆内存泄漏。
修复(c0ebb64、ff2bf60):
persistent.h新增napi_env env_ = nullptr;成员,在构造时绑定创建引用时的 env;后续operator=、拷贝构造、析构、GetValue()全部改用env_而非GetScopedEnv(),保证即使 scoped env 失效仍可用原始 env 正确napi_reference_unref/napi_delete_reference。~Persistent在env_==nullptr时打印 ERROR 并置空ref_。operator=补充自赋值检查与旧ref_的 unref/delete 逻辑;~Persistent在env==nullptr时提前return(c0ebb64)。napi_value_trait.h的Value::ObjectAs()将new NapiValueMaker<aki::Value>改为栈对象NapiValueMaker<aki::Value> val(...),消除堆泄漏。
五、修复异步方法 receiver 裸指针生命周期
问题:NapiPMethodBinder(include/aki/binder/napi/napi_pmethod_binder.h)的异步 InvokeWorkInfo 中,receiver 为 C* 裸指针,源自 ClassWrapper<C>::UnWrapper(env, jsThis)。异步任务在子线程执行 WrapperForward 时,若 JS 侧对象在此期间被 GC 回收或 C++ 对象被释放,裸指针即悬空,解引用导致 UAF。
修复(0deb0ab,include/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++ 对象生命周期至异步任务完成。WrapperForward由C* receiver = arg->receiver; ... InnerWrapper(*receiver, ...)改为C& receiver = *arg->receiver; ... InnerWrapper(receiver, ...),保持调用语义不变。
六、修复 Promise 重复 settle 与被 GC 提前回收
问题:aki::Promise(include/aki/value/promise.h、src/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 / NapiPMethodBinder(include/aki/binder/napi/napi_pfunction_binder.h、napi_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;。AsyncInvokeWork用try { ... } catch(...) { arg->invokeFailed = true; AKI_LOG(ERROR) << "Exception occurred in async invoke work"; }包裹业务执行,捕获子线程异常。AsyncInvokeDone在status != napi_ok(取消)时构造Error("Async work was cancelled")并napi_reject_deferred,随后napi_delete_async_work、置空arg->deferred、delete arg,补齐清理;在arg->invokeFailed为真时同样 reject"Exception occurred in async invoke work"并清理;ToNapiValue转换也加 try-catch 防止返回值转换异常逃逸。
八、修复 moduleMap 多线程无锁访问
问题:src/napi/napi_init.cpp 中 static 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 销毁)时无任何回收逻辑,造成泄漏。
修复(ff2bf60,src/napi/napi_init.cpp):新增 EnvCleanupData 结构体收集所有 constructorRefs 与 bindInfos,BindMethodAndField/Init/BindSymbols 中每次 new 时同步 cleanupData->bindInfos.push_back(info) / constructorRefs.push_back(constructor)。最后 napi_add_env_cleanup_hook(env, ModuleCleanup, cleanupData) 注册清理钩子,ModuleCleanup 在 env 销毁时遍历 napi_delete_reference 并 delete 全部 BindInfo 与 napi_ref*,实现自动释放。
十、加固全部 napi API 调用
问题:旧代码大量使用 AKI_DCHECK(status == napi_ok) 断言——该宏仅在 Debug 构建生效,Release 构建下 napi 调用失败被静默忽略,后续基于失败结果继续执行,引发空指针、越界等二次故障。
修复(ff2bf60、ab2e48d,跨多文件):将 DCHECK 升级为真实错误处理:
ThreadSafeContext::Invoke(void* data)由void改为bool,napi_acquire_threadsafe_function/napi_call_threadsafe_function失败时记录 ERROR、napi_release_threadsafe_function并返回false,调用方据此set_exception并delete data。ScopeHandle(include/aki/scopehandle/ScopeHandle.h)对napi_open_escapable_handle_scope/napi_open_handle_scope失败时打印 ERROR 并将句柄置nullptr。NapiValueBase(src/value/napi/napi_value_base.cpp)所有GetBool/GetUint8/GetInt8/.../GetDouble/GetString/GetArrayBuffer在 napi 调用失败时记录 ERROR 并返回安全默认值。AsyncWorker::CreateAsyncWorker/Queue(src/asyncworker/asyncworker.cpp)对napi_create_promise/napi_create_string_utf8/napi_create_async_work/napi_queue_async_work失败时 reject deferred、释放asyncData_并返回。TaskRunner::PostTask(src/task_runner/task_runner.cpp)对uv_queue_work/napi_call_threadsafe_function失败时释放work/taskPtr。NapiOverloader::Wrapper/CreateValue(src/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.h、src/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.h、src/value/napi/napi_value_base.cpp、src/value.cpp、include/aki/value/array_buffer.h、src/value/array_buffer.cpp、include/aki/value/napi/napi_value_trait.h:
- int64_t/uint64_t 兼容 BigInt(
66274f7):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,接口不变。 - 数值范围校验加固(
ff2bf60、eabad02、66274f7):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 误判数组(
ab2e48d,src/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_length与length > 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并返回-1;unbindFunction校验空名称及函数不存在(GetJSFunctionMap().find未命中)时抛错返回-1,防止操作未注册或空名函数。
五、编译选项与代码风格统一
- FORTIFY-SOURCE(
ad555dd,CMakeLists.txt):新增add_compile_options(-D_FORTIFY_SOURCE=2),启用 GCC 缓冲区溢出运行时检测,提升内存安全。 - 代码风格统一(
030e89a、fc3abd4、51f3927、3453951、940e102):跨 60+ 文件统一代码风格,修复函数定义与声明参数名不一致、删除注释掉的代码段、构造函数补explicit、移除冗余空行与多余ScopeHandle堆分配(napi_value_trait.h中new 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
更多推荐



所有评论(0)