From 54b9bf509fdb703c47cd23ae296003bebad2fae6 Mon Sep 17 00:00:00 2001 From: liuyics15 <1179805842@qq.com> Date: Mon, 23 Jun 2025 09:36:14 +0800 Subject: [PATCH] support cj & arkts cycle reference resolve Signed-off-by: liuyics15 <1179805842@qq.com> --- .../cjffi/ark_interop/ark_interop_global.cpp | 127 ++++++++-- .../cjffi/ark_interop/ark_interop_napi.cpp | 89 +++++++ .../cjffi/ark_interop/ark_interop_napi.h | 14 ++ test/unittest/cj_native/test_ark_interop.cpp | 224 ++++++++++++++++++ 4 files changed, 439 insertions(+), 15 deletions(-) diff --git a/interfaces/inner_api/cjffi/ark_interop/ark_interop_global.cpp b/interfaces/inner_api/cjffi/ark_interop/ark_interop_global.cpp index c6ee0a5cd..8a2ddf6dd 100644 --- a/interfaces/inner_api/cjffi/ark_interop/ark_interop_global.cpp +++ b/interfaces/inner_api/cjffi/ark_interop/ark_interop_global.cpp @@ -15,6 +15,7 @@ #include "ark_interop_internal.h" #include "ark_interop_napi.h" +#include "ark_interop_log.h" #include #include @@ -23,6 +24,64 @@ using namespace panda; using namespace panda::ecmascript; +struct ARKTS_Global_ { + ARKTS_Global_(EcmaVM *vm, const Local& value): ref(vm, value), isDisposed(false) {} + + ~ARKTS_Global_() + { + if (!isDisposed) { + ref.FreeGlobalHandleAddr(); + } + } + + void SetWeak() + { + if (isDisposed) { + return; + } + if (ref.IsWeak()) { + return; + } + ref.SetWeakCallback(this, [](void* handle) { + auto global = P_CAST(handle, ARKTS_Global); + if (!global) { + return; + } + global->isDisposed = true; + global->ref.FreeGlobalHandleAddr(); + }, [](void*) {}); + } + + void ClearWeak() + { + if (isDisposed) { + return; + } + if (ref.IsEmpty() || !ref.IsWeak()) { + return; + } + ref.ClearWeak(); + } + + ARKTS_Value GetValue() const + { + if (isDisposed) { + return ARKTS_CreateUndefined(); + } + auto result = BIT_CAST(ref, const Local); + return ARKTS_FromHandle(result); + } + + bool IsAlive() const + { + return !isDisposed && !ref.IsEmpty(); + } + +private: + Global ref; + bool isDisposed; +}; + namespace { class __attribute__((capability("mutex"))) SpinLock final { public: @@ -69,7 +128,7 @@ GlobalManager::GlobalManager(EcmaVM* vm) vm_ = vm; } -void GlobalManager::AsyncDisposer(ARKTS_Env env, void* data) +void GlobalManager::AsyncDisposer(ARKTS_Env /*env*/, void* data) { auto manager = (GlobalManager*)data; std::vector toDispose; @@ -79,8 +138,7 @@ void GlobalManager::AsyncDisposer(ARKTS_Env env, void* data) manager->handleMutex_.Release(); for (auto handle : toDispose) { - auto global = P_CAST(handle, Global*); - global->FreeGlobalHandleAddr(); + auto global = P_CAST(handle, ARKTS_Global); delete global; } } @@ -112,23 +170,16 @@ ARKTS_Global ARKTS_CreateGlobal(ARKTS_Env env, ARKTS_Value value) auto vm = P_CAST(env, EcmaVM*); auto handle = BIT_CAST(value, Local); - auto result = new Global(vm, handle); + auto result = new ARKTS_Global_(vm, handle); return P_CAST(result, ARKTS_Global); } -/** - * no actual action about this convert, the data is in the same form with JSHandle, - * it does not mean global is equal to local, the memory they are allocated are controlled by different system, - * have no idea the consequence of NativeScope escape a global variable - */ ARKTS_Value ARKTS_GetGlobalValue(ARKTS_Global global) { ARKTS_ASSERT_P(global, "global is null"); - auto result = *P_CAST(global, Local*); - - return ARKTS_FromHandle(result); + return global->GetValue(); } /** @@ -147,7 +198,53 @@ void ARKTS_DisposeGlobalSync(ARKTS_Env env, ARKTS_Global global) ARKTS_ASSERT_V(env, "env is null"); ARKTS_ASSERT_V(global, "handle is null"); - auto ref = P_CAST(global, Global*); - ref->FreeGlobalHandleAddr(); - delete ref; + delete global; +} + +void ARKTS_GlobalSetWeak(ARKTS_Env env, ARKTS_Global global) +{ + ARKTS_ASSERT_V(env, "env is null"); + ARKTS_ASSERT_V(global, "global is null"); + global->SetWeak(); +} + +void ARKTS_GlobalClearWeak(ARKTS_Env env, ARKTS_Global global) +{ + ARKTS_ASSERT_V(env, "env is null"); + ARKTS_ASSERT_V(global, "global is null"); + global->ClearWeak(); +} + +constexpr uint64_t GLOBAL_TAG = 0b11111ULL << 48; +constexpr uint64_t GLOBAL_MASK = 0x0000'FFFF'FFFF'FFFF; + +ARKTS_Value ARKTS_GlobalToValue(ARKTS_Env env, ARKTS_Global global) +{ + ARKTS_ASSERT_P(env, "env is null"); + ARKTS_ASSERT_P(global, "global is null"); + + auto value = reinterpret_cast(global) & GLOBAL_MASK; + value = value | GLOBAL_TAG; + auto dValue = static_cast(value); + return ARKTS_CreateF64(dValue); +} + +ARKTS_Global ARKTS_GlobalFromValue(ARKTS_Env env, ARKTS_Value value) +{ + ARKTS_ASSERT_P(env, "env is null"); + ARKTS_ASSERT_P(ARKTS_IsNumber(value), "value is a number"); + + auto dValue = ARKTS_GetValueNumber(value); + auto iValue = static_cast(dValue); + ARKTS_ASSERT_P((iValue & GLOBAL_TAG) == GLOBAL_TAG, "invalid tag value"); + iValue = iValue & GLOBAL_MASK; + return BIT_CAST(iValue, ARKTS_Global); +} + +bool ARKTS_GlobalIsAlive(ARKTS_Env env, ARKTS_Global global) +{ + ARKTS_ASSERT_F(env, "env is null"); + ARKTS_ASSERT_F(global, "global is null"); + + return global->IsAlive(); } \ No newline at end of file diff --git a/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.cpp b/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.cpp index 1bafbbe1e..9c59e3955 100644 --- a/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.cpp +++ b/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.cpp @@ -791,3 +791,92 @@ void ARKTS_UpdateStackInfo(unsigned long long vmAddress, void *subStackInfo, uns auto vm = reinterpret_cast(vmAddress); panda::JSNApi::UpdateStackInfo(vm, subStackInfo, opKind); } + +namespace { +ARKTS_CycleFreeCallback* g_cycleFreeCallback = nullptr; +} + +void ARKTS_RegisterCycleFreeCallback(ARKTS_CycleFreeCallback callback) +{ + if (g_cycleFreeCallback) { + LOGE("register cycle free callback failed, already registered."); + return; + } + g_cycleFreeCallback = new ARKTS_CycleFreeCallback(callback); +} + +static Local CycleFreeFuncInvoker(JsiRuntimeCallInfo *callInfo) +{ + Local result; + auto vm = callInfo->GetVM(); + if (!vm) { + LOGE("failed to invoke cycleFree func, vm is null"); + return result; + } + result = JSValueRef::Undefined(vm); + auto data = reinterpret_cast(callInfo->GetData()); + if (!data) { + LOGE("failed to invoke cycleFree func, LambdaData is null"); + return result; + } + if (!g_cycleFreeCallback) { + LOGE("failed to invoke cycleFree func, cycleFreeCallback is null"); + return result; + } + auto value = g_cycleFreeCallback->funcInvoker(P_CAST(callInfo, ARKTS_CallInfo), data->lambdaId); + auto res = ARKTS_ToResult(vm, value); + result = BIT_CAST(res, Local); + return result; +} + +static void CycleFreeFuncReleaser(void* /*env*/, void* /*data*/, void* hint) +{ + auto data = reinterpret_cast(hint); + if (!data) { + return; + } + if (!g_cycleFreeCallback) { + delete data; + return; + } + g_cycleFreeCallback->refRelease(data->lambdaId); + delete data; +} + +ARKTS_Value ARKTS_CreateCycleFreeFunc(ARKTS_Env env, int64_t id) +{ + ARKTS_ASSERT_P(env, "env is null"); + auto vm = P_CAST(env, EcmaVM*); + auto data = new LambdaData {env, id}; + if (!data) { + return ARKTS_CreateUndefined(); + } + auto result = FunctionRef::New(vm, CycleFreeFuncInvoker, CycleFreeFuncReleaser, data); + return ARKTS_FromHandle(result); +} + +static void CycleFreeObjectReleaser(void* /*env*/, void* nativePointer, void* /*hint*/) +{ + if (!g_cycleFreeCallback) { + return; + } + auto id = static_cast(reinterpret_cast(nativePointer)); + g_cycleFreeCallback->refRelease(id); +} + +ARKTS_Value ARKTS_CreateCycleFreeExtern(ARKTS_Env env, int64_t id) +{ + ARKTS_ASSERT_P(env, "env is null"); + auto vm = P_CAST(env, EcmaVM*); + auto data = reinterpret_cast(static_cast(id)); + auto result = NativePointerRef::New(vm, data, CycleFreeObjectReleaser, nullptr); + return ARKTS_FromHandle(result); +} + +ARKTS_Value ARKTS_GetExceptionAndClear(ARKTS_Env env) +{ + ARKTS_ASSERT_P(env, "env is null"); + auto vm = P_CAST(env, EcmaVM*); + auto exception = JSNApi::GetAndClearUncaughtException(vm); + return ARKTS_FromHandle(exception); +} \ No newline at end of file diff --git a/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.h b/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.h index 37e3d8f99..17703fc15 100644 --- a/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.h +++ b/interfaces/inner_api/cjffi/ark_interop/ark_interop_napi.h @@ -155,6 +155,7 @@ EXPORT ARKTS_Value ARKTS_GetElement(ARKTS_Env env, ARKTS_Value array, uint32_t i EXPORT bool ARKTS_IsArray(ARKTS_Env env, ARKTS_Value value); EXPORT ARKTS_Global ARKTS_CreateGlobal(ARKTS_Env env, ARKTS_Value value); +EXPORT bool ARKTS_GlobalIsAlive(ARKTS_Env env, ARKTS_Global global); EXPORT ARKTS_Value ARKTS_GetGlobalValue(ARKTS_Global global); EXPORT void ARKTS_DisposeGlobal(ARKTS_Env env, ARKTS_Global global); EXPORT void ARKTS_DisposeGlobalSync(ARKTS_Env env, ARKTS_Global global); @@ -234,6 +235,19 @@ EXPORT void* ARKTS_GetGlobalNapiEnv(ARKTS_Env env); EXPORT void ARKTS_UpdateStackInfo(unsigned long long vmAddress, void *subStackInfo, unsigned int opKind); +EXPORT void ARKTS_GlobalSetWeak(ARKTS_Env env, ARKTS_Global global); +EXPORT void ARKTS_GlobalClearWeak(ARKTS_Env env, ARKTS_Global global); +EXPORT ARKTS_Value ARKTS_GlobalToValue(ARKTS_Env env, ARKTS_Global global); +EXPORT ARKTS_Global ARKTS_GlobalFromValue(ARKTS_Env env, ARKTS_Value value); +EXPORT ARKTS_Value ARKTS_CreateCycleFreeFunc(ARKTS_Env env, int64_t id); +EXPORT ARKTS_Value ARKTS_CreateCycleFreeExtern(ARKTS_Env env, int64_t id); +typedef struct { + ARKTS_Value (*funcInvoker)(ARKTS_CallInfo callInfo, int64_t id); + void (*refRelease)(int64_t id); +} ARKTS_CycleFreeCallback; +EXPORT void ARKTS_RegisterCycleFreeCallback(ARKTS_CycleFreeCallback callback); +EXPORT ARKTS_Value ARKTS_GetExceptionAndClear(ARKTS_Env env); + DECL_END #endif //NAPI_ARK_INTEROP_NAPI_H diff --git a/test/unittest/cj_native/test_ark_interop.cpp b/test/unittest/cj_native/test_ark_interop.cpp index 74c2d9176..83cc873a2 100644 --- a/test/unittest/cj_native/test_ark_interop.cpp +++ b/test/unittest/cj_native/test_ark_interop.cpp @@ -118,15 +118,25 @@ public: result = items_.size(); items_.push_back({std::move(item), -1}); } + ++length; return result; } void Del(int64_t id) { auto& removing = items_[id]; + if (!removing.data.has_value()) { + return; + } removing.data = std::nullopt; removing.prev = last_; last_ = id; + --length; + } + + size_t Size() const + { + return length; } T& Get(int64_t id) @@ -145,6 +155,7 @@ private: }; std::vector items_ {}; int64_t last_ = -1; + size_t length = 0; }; class MockContext { @@ -298,6 +309,15 @@ protected: { } + virtual ARKTS_Value InvokeCycleFreeFunc(ARKTS_CallInfo callInfo, uint32_t id) + { + return ARKTS_CreateUndefined(); + } + + virtual void ReleaseCycleFreeExt(uint32_t id) + { + } + int64_t ToId(int64_t index) const { return GetPrefixMask() | index; @@ -368,6 +388,20 @@ void MockContext::Init() } }; ARKTS_SetCJModuleCallback(&callbacks); + static ARKTS_CycleFreeCallback cycleFreeCallback { + .funcInvoker = [](ARKTS_CallInfo callInfo, int64_t id) { + if (instance_) { + return instance_->InvokeCycleFreeFunc(callInfo, id); + } + return ARKTS_CreateUndefined(); + }, + .refRelease = [](int64_t id) { + if (instance_) { + instance_->ReleaseCycleFreeExt(id); + } + } + }; + ARKTS_RegisterCycleFreeCallback(cycleFreeCallback); } MockContext* MockContext::instance_ = nullptr; @@ -375,6 +409,8 @@ MockContext* MockContext::instance_ = nullptr; class ErrorCaptureContext : public MockContext { public: ErrorCaptureContext() = default; + explicit ErrorCaptureContext(ARKTS_Engine engine, bool needDestroyEngine = true) + : MockContext(engine, needDestroyEngine) {} ~ErrorCaptureContext() { if (hasJSError_ || hasNativeError_) { @@ -423,6 +459,45 @@ private: bool hasNativeError_ = false; }; +class CycleFreeContext : public ErrorCaptureContext { +public: + CycleFreeContext() = default; + explicit CycleFreeContext(ARKTS_Engine engine, bool needDestroyEngine = true) + : ErrorCaptureContext(engine, needDestroyEngine) {} + uint32_t StoreCycleFreeFunc(std::function callback) + { + std::lock_guard lock(callbackMutex_); + return callbacks_.Add(std::move(callback)); + } + + size_t GetCycleFreeFuncCount() const + { + return callbacks_.Size(); + } + +protected: + ARKTS_Value InvokeCycleFreeFunc(ARKTS_CallInfo callInfo, uint32_t id) override + { + std::lock_guard lock(callbackMutex_); + auto callback = callbacks_.Get(id); + if (!callback) { + return ARKTS_CreateUndefined(); + } + return callback(callInfo); + } + + void ReleaseCycleFreeExt(uint32_t id) override + { + callbacks_.Del(id); + } +private: + static Slab> callbacks_; + static std::mutex callbackMutex_; +}; + +Slab> CycleFreeContext::callbacks_; +std::mutex CycleFreeContext::callbackMutex_; + void ArkInteropTest::TestPrime() { auto env = MockContext::GetInstance()->GetEnv(); @@ -1088,6 +1163,155 @@ TEST_F(ArkInteropTest, PromiseThen) EXPECT_TRUE(waitTimes > 0); } +TEST_F(ArkInteropTest, CycleFreeFunc) +{ + CycleFreeContext local; + auto env = local.GetEnv(); + auto funcCount = local.GetCycleFreeFuncCount(); + auto scope = ARKTS_OpenScope(env); + auto isCalled = false; + auto id = local.StoreCycleFreeFunc([&isCalled](ARKTS_CallInfo callInfo) { + isCalled = true; + return ARKTS_CreateUndefined(); + }); + EXPECT_EQ(local.GetCycleFreeFuncCount(), funcCount + 1); + auto func = ARKTS_CreateCycleFreeFunc(local.GetEnv(), id); + EXPECT_TRUE(ARKTS_IsCallable(env, func)); + ARKTS_Call(env, func, ARKTS_CreateUndefined(), 0, nullptr); + EXPECT_TRUE(isCalled); + ARKTS_CloseScope(env, scope); +} + +TEST_F(ArkInteropTest, CycleFreeExtern) +{ + CycleFreeContext local; + auto env = local.GetEnv(); + auto funcCount = local.GetCycleFreeFuncCount(); + { + auto scope = ARKTS_OpenScope(env); + auto id = local.StoreCycleFreeFunc(nullptr); + EXPECT_EQ(local.GetCycleFreeFuncCount(), funcCount + 1); + auto object = ARKTS_CreateCycleFreeExtern(local.GetEnv(), id); + EXPECT_TRUE(ARKTS_IsExternal(env, object)); + auto handle = ARKTS_GetExternalData(env, object); + uint32_t resid = reinterpret_cast(handle); + EXPECT_EQ(resid, id); + + ARKTS_CloseScope(env, scope); + } +} + +class GlobalWeakTest { +public: + GlobalWeakTest(): local(ARKTS_CreateEngineWithNewThread()), status(CREATING) + { + ScheduleNext(); + } + + void WaitForComplete() + { + std::mutex mutex; + std::unique_lock lock(mutex); + while (status != COMPLETE) { + cv.wait(lock); + } + } + +private: + static constexpr int objectCnt = 1000; + + void CreateWeakObjects() + { + auto env = local.GetEnv(); + auto scope = ARKTS_OpenScope(env); + for (auto i = 0; i < objectCnt; i++) { + auto object = ARKTS_CreateObject(env); + auto global = ARKTS_CreateGlobal(env, object); + ARKTS_GlobalSetWeak(env, global); + globals.push_back(global); + } + ARKTS_CloseScope(env, scope); + panda::JSNApi::TriggerGC(P_CAST(env, EcmaVM*), panda::JSNApi::TRIGGER_GC_TYPE::FULL_GC); + } + + void DoAssertion() + { + for (auto one : globals) { + EXPECT_TRUE(!ARKTS_GlobalIsAlive(local.GetEnv(), one)); + } + } + + void ReleaseGlobals() + { + for (auto one : globals) { + ARKTS_DisposeGlobalSync(local.GetEnv(), one); + } + } + + enum Status { + CREATING, + ASSERTION, + DISPOSE, + COMPLETE + }; + + void ScheduleNext() + { + auto id = local.StoreAsyncFunc([this] { + DoNext(); + }); + ARKTS_CreateAsyncTask(local.GetEnv(), id); + } + + void DoNext() + { + switch (status) { + case CREATING: + CreateWeakObjects(); + status = ASSERTION; + break; + case ASSERTION: + DoAssertion(); + status = DISPOSE; + break; + case DISPOSE: + ReleaseGlobals(); + status = COMPLETE; + cv.notify_all(); + return; + default: ; + } + ScheduleNext(); + } + + CycleFreeContext local; + Status status; + std::condition_variable cv; + std::vector globals; +}; + +TEST_F(ArkInteropTest, GlobalWeak) +{ + GlobalWeakTest weakTest; + weakTest.WaitForComplete(); +} + +TEST_F(ArkInteropTest, GlobalToValue) +{ + MockContext local; + auto env = local.GetEnv(); + { + auto scope = ARKTS_OpenScope(env); + auto object = ARKTS_CreateObject(env); + auto global = ARKTS_CreateGlobal(env, object); + auto value = ARKTS_GlobalToValue(env, global); + auto received = ARKTS_GlobalFromValue(env, value); + EXPECT_EQ(received, global); + ARKTS_DisposeGlobalSync(env, global); + ARKTS_CloseScope(env, scope); + } +} + HWTEST_F(ArkInteropTest, ArkTSInteropNapiCreateEngineNew, TestSize.Level1) { MockContext local(ARKTS_CreateEngineWithNewThread()); -- Gitee