[K/N] Fix a race in MemorySharedRefs ^KT-56233
This commit is contained in:
committed by
Space Team
parent
8a8aeed998
commit
4cd1f2ff82
+1
-1
@@ -95,7 +95,7 @@ private fun PatchBuilder.addObjCPatches() {
|
||||
addProtocolImport("NSCopying")
|
||||
|
||||
addPrivateSelector("toKotlin:")
|
||||
addPrivateSelector("releaseAsAssociatedObject:")
|
||||
addPrivateSelector("releaseAsAssociatedObject")
|
||||
|
||||
addPrivateClass("KIteratorAsNSEnumerator", "iteratorHolder")
|
||||
addPrivateClass("KListAsNSArray", "listHolder")
|
||||
|
||||
@@ -58,7 +58,6 @@ bool SweepExtraObject(ExtraObjectCell* extraObjectCell, AtomicStack<ExtraObjectC
|
||||
}
|
||||
extraObject->ClearRegularWeakReferenceImpl();
|
||||
if (extraObject->HasAssociatedObject()) {
|
||||
extraObject->DetachAssociatedObject();
|
||||
extraObject->setFlag(mm::ExtraObjectData::FLAGS_IN_FINALIZER_QUEUE);
|
||||
finalizerQueue.Push(extraObjectCell);
|
||||
KeepAlive(baseObject);
|
||||
|
||||
@@ -50,6 +50,13 @@ struct SweepTraits {
|
||||
}
|
||||
};
|
||||
|
||||
struct ProcessWeaksTraits {
|
||||
static bool IsMarked(ObjHeader* obj) noexcept {
|
||||
auto& objectData = mm::ObjectFactory<gc::ConcurrentMarkAndSweep>::NodeRef::From(obj).ObjectData();
|
||||
return objectData.marked();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void gc::ConcurrentMarkAndSweep::ThreadData::SafePointAllocation(size_t size) noexcept {
|
||||
@@ -179,13 +186,17 @@ bool gc::ConcurrentMarkAndSweep::PerformFullGC(int64_t epoch) noexcept {
|
||||
auto markStats = gcHandle.getMarked();
|
||||
scheduler.gcData().UpdateAliveSetBytes(markStats.totalObjectsSize);
|
||||
|
||||
#ifndef CUSTOM_ALLOCATOR
|
||||
auto extraObjectFactoryIterable = mm::GlobalData::Instance().extraObjectDataFactory().LockForIter();
|
||||
gc::SweepExtraObjects<SweepTraits>(gcHandle, extraObjectFactoryIterable);
|
||||
gc::processWeaks<ProcessWeaksTraits>(gcHandle, mm::SpecialRefRegistry::instance());
|
||||
|
||||
#ifndef CUSTOM_ALLOCATOR
|
||||
// Taking the locks before the pause is completed. So that any destroying thread
|
||||
// would not publish into the global state at an unexpected time.
|
||||
auto extraObjectFactoryIterable = mm::GlobalData::Instance().extraObjectDataFactory().LockForIter();
|
||||
auto objectFactoryIterable = objectFactory_.LockForIter();
|
||||
mm::ResumeThreads();
|
||||
gcHandle.threadsAreResumed();
|
||||
|
||||
gc::SweepExtraObjects<SweepTraits>(gcHandle, extraObjectFactoryIterable);
|
||||
auto finalizerQueue = gc::Sweep<SweepTraits>(gcHandle, objectFactoryIterable);
|
||||
kotlin::compactObjectPoolInMainThread();
|
||||
#else
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "SingleThreadExecutor.hpp"
|
||||
#include "TestSupport.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "WeakRef.hpp"
|
||||
#include "std_support/Vector.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
@@ -196,6 +197,7 @@ test_support::RegularWeakReferenceImpl& InstallWeakReference(mm::ThreadData& thr
|
||||
mm::AllocateObject(&threadData, theRegularWeakReferenceImplTypeInfo, location);
|
||||
auto& weakReference = test_support::RegularWeakReferenceImpl::FromObjHeader(*location);
|
||||
auto& extraObjectData = mm::ExtraObjectData::GetOrInstall(objHeader);
|
||||
weakReference->weakRef = static_cast<mm::RawSpecialRef*>(mm::WeakRef::create(objHeader));
|
||||
weakReference->referred = objHeader;
|
||||
auto* setWeakRef = extraObjectData.GetOrSetRegularWeakReferenceImpl(objHeader, weakReference.header());
|
||||
EXPECT_EQ(setWeakRef, weakReference.header());
|
||||
@@ -211,6 +213,7 @@ public:
|
||||
|
||||
~ConcurrentMarkAndSweepTest() {
|
||||
mm::GlobalsRegistry::Instance().ClearForTests();
|
||||
mm::SpecialRefRegistry::instance().clearForTests();
|
||||
mm::GlobalData::Instance().extraObjectDataFactory().ClearForTests();
|
||||
mm::GlobalData::Instance().gc().ClearForTests();
|
||||
}
|
||||
@@ -337,7 +340,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectsWithFinalizers) {
|
||||
}
|
||||
|
||||
TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithFreeWeak) {
|
||||
RunInNewThread([](mm::ThreadData& threadData) {
|
||||
RunInNewThread([this](mm::ThreadData& threadData) {
|
||||
auto& object1 = AllocateObject(threadData);
|
||||
auto& weak1 = ([&threadData, &object1]() -> test_support::RegularWeakReferenceImpl& {
|
||||
ObjHolder holder;
|
||||
@@ -349,6 +352,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithFreeWeak) {
|
||||
ASSERT_THAT(IsMarked(weak1.header()), false);
|
||||
ASSERT_THAT(weak1.get(), object1.header());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak1.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
@@ -1112,6 +1116,7 @@ TEST_P(ConcurrentMarkAndSweepTest, FreeObjectWithFreeWeakReversedOrder) {
|
||||
|
||||
global1->field1 = nullptr;
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak.load()->header()));
|
||||
threadData.gc().ScheduleAndWaitFullGC();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global1.header()));
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <cinttypes>
|
||||
#include <mutex>
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
extern "C" {
|
||||
void Kotlin_Internal_GC_GCInfoBuilder_setEpoch(KRef thiz, KLong value);
|
||||
@@ -370,4 +371,14 @@ GCHandle::GCMarkScope::~GCMarkScope() {
|
||||
objectsCount, getStageTime(), konan::currentThreadId());
|
||||
}
|
||||
|
||||
} // namespace kotlin::gc
|
||||
gc::GCHandle::GCProcessWeaksScope::GCProcessWeaksScope(gc::GCHandle& handle) noexcept : handle_(handle) {}
|
||||
|
||||
gc::GCHandle::GCProcessWeaksScope::~GCProcessWeaksScope() {
|
||||
GCLogDebug(
|
||||
handle_.getEpoch(),
|
||||
"Processed special refs in %" PRIu64 " microseconds. %" PRIu64 " are undisposed, %" PRIu64 " are alive, %" PRIu64
|
||||
" are nulled out",
|
||||
getStageTime(), undisposedCount_, aliveCount_, nulledCount_);
|
||||
}
|
||||
|
||||
} // namespace kotlin::gc
|
||||
|
||||
@@ -7,12 +7,18 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "Common.h"
|
||||
#include "ThreadData.hpp"
|
||||
#include "Porting.h"
|
||||
#include "Utils.hpp"
|
||||
|
||||
#define GCLogInfo(epoch, format, ...) RuntimeLogInfo({kTagGC}, "Epoch #%" PRIu64 ": " format, epoch, ##__VA_ARGS__)
|
||||
#define GCLogDebug(epoch, format, ...) RuntimeLogDebug({kTagGC}, "Epoch #%" PRIu64 ": " format, epoch, ##__VA_ARGS__)
|
||||
|
||||
namespace kotlin::mm {
|
||||
class ThreadData;
|
||||
}
|
||||
|
||||
namespace kotlin::gc {
|
||||
|
||||
class GCHandle;
|
||||
@@ -87,6 +93,21 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class GCProcessWeaksScope : GCStageScopeUsTimer, Pinned {
|
||||
GCHandle& handle_;
|
||||
uint64_t undisposedCount_ = 0;
|
||||
uint64_t aliveCount_ = 0;
|
||||
uint64_t nulledCount_ = 0;
|
||||
|
||||
public:
|
||||
explicit GCProcessWeaksScope(GCHandle& handle) noexcept;
|
||||
~GCProcessWeaksScope();
|
||||
|
||||
void addUndisposed() noexcept { ++undisposedCount_; }
|
||||
void addAlive() noexcept { ++aliveCount_; }
|
||||
void addNulled() noexcept { ++nulledCount_; }
|
||||
};
|
||||
|
||||
private:
|
||||
uint64_t epoch_;
|
||||
explicit GCHandle(uint64_t epoch) : epoch_(epoch) {}
|
||||
@@ -117,6 +138,7 @@ public:
|
||||
GCGlobalRootSetScope collectGlobalRoots() { return GCGlobalRootSetScope(*this); }
|
||||
GCThreadRootSetScope collectThreadRoots(mm::ThreadData& threadData) { return GCThreadRootSetScope(*this, threadData); }
|
||||
GCMarkScope mark() { return GCMarkScope(*this); }
|
||||
GCProcessWeaksScope processWeaks() noexcept { return GCProcessWeaksScope(*this); }
|
||||
|
||||
MemoryUsage getMarked();
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "ObjectTraversal.hpp"
|
||||
#include "RootSet.hpp"
|
||||
#include "Runtime.h"
|
||||
#include "StableRefRegistry.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "Types.h"
|
||||
|
||||
@@ -116,7 +116,6 @@ void SweepExtraObjects(GCHandle handle, typename Traits::ExtraObjectsFactory::It
|
||||
if (!extraObject.getFlag(mm::ExtraObjectData::FLAGS_IN_FINALIZER_QUEUE) && !Traits::IsMarkedByExtraObject(extraObject)) {
|
||||
extraObject.ClearRegularWeakReferenceImpl();
|
||||
if (extraObject.HasAssociatedObject()) {
|
||||
extraObject.DetachAssociatedObject();
|
||||
extraObject.setFlag(mm::ExtraObjectData::FLAGS_IN_FINALIZER_QUEUE);
|
||||
++it;
|
||||
} else {
|
||||
@@ -184,7 +183,6 @@ void collectRootSetForThread(GCHandle gcHandle, typename Traits::MarkQueue& mark
|
||||
template <typename Traits>
|
||||
void collectRootSetGlobals(GCHandle gcHandle, typename Traits::MarkQueue& markQueue) noexcept {
|
||||
auto handle = gcHandle.collectGlobalRoots();
|
||||
mm::StableRefRegistry::Instance().ProcessDeletions();
|
||||
// TODO: Remove useless mm::GlobalRootSet abstraction.
|
||||
for (auto value : mm::GlobalRootSet()) {
|
||||
if (internal::collectRoot<Traits>(markQueue, value.object)) {
|
||||
@@ -213,6 +211,28 @@ void collectRootSet(GCHandle handle, typename Traits::MarkQueue& markQueue, F&&
|
||||
collectRootSetGlobals<Traits>(handle, markQueue);
|
||||
}
|
||||
|
||||
template <typename Traits>
|
||||
void processWeaks(GCHandle gcHandle, mm::SpecialRefRegistry& registry) noexcept {
|
||||
auto handle = gcHandle.processWeaks();
|
||||
for (auto& object : registry.lockForIter()) {
|
||||
auto* obj = object;
|
||||
if (!obj) {
|
||||
// We already processed it at some point.
|
||||
handle.addUndisposed();
|
||||
continue;
|
||||
}
|
||||
if (obj->permanent() || Traits::IsMarked(obj)) {
|
||||
// TODO: Let's not put permanent objects in here at all?
|
||||
// Object is alive. Nothing to do.
|
||||
handle.addAlive();
|
||||
continue;
|
||||
}
|
||||
// Object is not alive. Clear it out.
|
||||
object = nullptr;
|
||||
handle.addNulled();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gc
|
||||
} // namespace kotlin
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "ObjectFactory.hpp"
|
||||
#include "ObjectTestSupport.hpp"
|
||||
#include "ExtraObjectDataFactory.hpp"
|
||||
#include "WeakRef.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
@@ -154,6 +155,13 @@ struct SweepTraits {
|
||||
}
|
||||
};
|
||||
|
||||
struct ProcessWeakTraits {
|
||||
static bool IsMarked(ObjHeader* object) noexcept {
|
||||
auto& objectData = ObjectFactory::NodeRef::From(object).ObjectData();
|
||||
return objectData.state != ObjectFactoryTraits::ObjectData::State::kUnmarked;
|
||||
}
|
||||
};
|
||||
|
||||
class MarkAndSweepUtilsSweepTest : public ::testing::Test {
|
||||
public:
|
||||
~MarkAndSweepUtilsSweepTest() override {
|
||||
@@ -186,6 +194,7 @@ public:
|
||||
}
|
||||
|
||||
std_support::vector<ObjHeader*> Sweep() {
|
||||
gc::processWeaks<ProcessWeakTraits>(gc::GCHandle::getByEpoch(0), specialRefRegistry_);
|
||||
gc::SweepExtraObjects<SweepTraits>(gc::GCHandle::getByEpoch(0), extraObjectFactory_);
|
||||
auto finalizers = gc::Sweep<SweepTraits>(gc::GCHandle::getByEpoch(0), objectFactory_);
|
||||
std_support::vector<ObjHeader*> objects;
|
||||
@@ -244,7 +253,9 @@ public:
|
||||
auto& extraObjectData = InstallExtraData(objHeader);
|
||||
auto* setHeader = extraObjectData.GetOrSetRegularWeakReferenceImpl(objHeader, weakReference.header());
|
||||
EXPECT_EQ(setHeader, weakReference.header());
|
||||
weakReference->weakRef = static_cast<mm::RawSpecialRef*>(specialRefRegistryThreadQueue_.createWeakRef(objHeader));
|
||||
weakReference->referred = objHeader;
|
||||
specialRefRegistryThreadQueue_.publish();
|
||||
return weakReference;
|
||||
}
|
||||
|
||||
@@ -258,6 +269,9 @@ private:
|
||||
ObjectFactory::ThreadQueue objectFactoryThreadQueue_{objectFactory_, gc::Allocator()};
|
||||
ExtraObjectsDataFactory extraObjectFactory_;
|
||||
ExtraObjectsDataFactory::ThreadQueue extraObjectFactoryThreadQueue_{extraObjectFactory_};
|
||||
mm::SpecialRefRegistry specialRefRegistry_;
|
||||
mm::SpecialRefRegistry::ThreadQueue specialRefRegistryThreadQueue_{specialRefRegistry_};
|
||||
|
||||
std_support::vector<ObjectFactory::FinalizerQueue> finalizers_;
|
||||
};
|
||||
|
||||
@@ -445,8 +459,10 @@ TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectWithWeakReference) {
|
||||
|
||||
auto finalizers = Sweep();
|
||||
|
||||
EXPECT_THAT(finalizers, testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(finalizers, testing::UnorderedElementsAre(weakReference.header()));
|
||||
EXPECT_THAT(Alive(), testing::UnorderedElementsAre());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weakReference.header()));
|
||||
}
|
||||
|
||||
TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectArrayWithWeakReference) {
|
||||
@@ -456,8 +472,10 @@ TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleObjectArrayWithWeakReference) {
|
||||
|
||||
auto finalizers = Sweep();
|
||||
|
||||
EXPECT_THAT(finalizers, testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(finalizers, testing::UnorderedElementsAre(weakReference.header()));
|
||||
EXPECT_THAT(Alive(), testing::UnorderedElementsAre());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weakReference.header()));
|
||||
}
|
||||
|
||||
TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleCharArrayWithWeakReference) {
|
||||
@@ -467,8 +485,10 @@ TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleCharArrayWithWeakReference) {
|
||||
|
||||
auto finalizers = Sweep();
|
||||
|
||||
EXPECT_THAT(finalizers, testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(finalizers, testing::UnorderedElementsAre(weakReference.header()));
|
||||
EXPECT_THAT(Alive(), testing::UnorderedElementsAre());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weakReference.header()));
|
||||
}
|
||||
|
||||
TEST_F(MarkAndSweepUtilsSweepTest, SweepSingleMarkedObjectWithWeakReference) {
|
||||
|
||||
@@ -44,6 +44,13 @@ struct FinalizeTraits {
|
||||
using ObjectFactory = mm::ObjectFactory<gc::SameThreadMarkAndSweep>;
|
||||
};
|
||||
|
||||
struct ProcessWeaksTraits {
|
||||
static bool IsMarked(ObjHeader* obj) noexcept {
|
||||
auto& objectData = mm::ObjectFactory<gc::SameThreadMarkAndSweep>::NodeRef::From(obj).ObjectData();
|
||||
return objectData.marked();
|
||||
}
|
||||
};
|
||||
|
||||
// Global, because it's accessed on a hot path: avoid memory load from `this`.
|
||||
std::atomic<gc::SameThreadMarkAndSweep::SafepointFlag> gSafepointFlag = gc::SameThreadMarkAndSweep::SafepointFlag::kNone;
|
||||
|
||||
@@ -130,6 +137,9 @@ bool gc::SameThreadMarkAndSweep::PerformFullGC() noexcept {
|
||||
gc::Mark<internal::MarkTraits>(gcHandle, markQueue_);
|
||||
auto markStats = gcHandle.getMarked();
|
||||
scheduler.gcData().UpdateAliveSetBytes(markStats.totalObjectsSize);
|
||||
|
||||
gc::processWeaks<ProcessWeaksTraits>(gcHandle, mm::SpecialRefRegistry::instance());
|
||||
|
||||
gc::SweepExtraObjects<SweepTraits>(gcHandle, extraObjectsDataFactory);
|
||||
finalizerQueue = gc::Sweep<SweepTraits>(gcHandle, objectFactory_);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "SingleThreadExecutor.hpp"
|
||||
#include "TestSupport.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "WeakRef.hpp"
|
||||
#include "std_support/Memory.hpp"
|
||||
#include "std_support/Vector.hpp"
|
||||
|
||||
@@ -197,6 +198,7 @@ test_support::RegularWeakReferenceImpl& InstallWeakReference(mm::ThreadData& thr
|
||||
mm::AllocateObject(&threadData, theRegularWeakReferenceImplTypeInfo, location);
|
||||
auto& weakReference = test_support::RegularWeakReferenceImpl::FromObjHeader(*location);
|
||||
auto& extraObjectData = mm::ExtraObjectData::GetOrInstall(objHeader);
|
||||
weakReference->weakRef = static_cast<mm::RawSpecialRef*>(mm::WeakRef::create(objHeader));
|
||||
weakReference->referred = objHeader;
|
||||
auto* setWeakRef = extraObjectData.GetOrSetRegularWeakReferenceImpl(objHeader, weakReference.header());
|
||||
EXPECT_EQ(setWeakRef, weakReference.header());
|
||||
@@ -207,6 +209,7 @@ class SameThreadMarkAndSweepTest : public testing::Test {
|
||||
public:
|
||||
~SameThreadMarkAndSweepTest() {
|
||||
mm::GlobalsRegistry::Instance().ClearForTests();
|
||||
mm::SpecialRefRegistry::instance().clearForTests();
|
||||
mm::GlobalData::Instance().extraObjectDataFactory().ClearForTests();
|
||||
mm::GlobalData::Instance().gc().impl().objectFactory().ClearForTests();
|
||||
}
|
||||
@@ -333,7 +336,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectsWithFinalizers) {
|
||||
}
|
||||
|
||||
TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithFreeWeak) {
|
||||
RunInNewThread([](mm::ThreadData& threadData) {
|
||||
RunInNewThread([this](mm::ThreadData& threadData) {
|
||||
auto& object1 = AllocateObject(threadData);
|
||||
auto& weak1 = ([&threadData, &object1]() -> test_support::RegularWeakReferenceImpl& {
|
||||
ObjHolder holder;
|
||||
@@ -345,6 +348,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithFreeWeak) {
|
||||
ASSERT_THAT(IsMarked(weak1.header()), false);
|
||||
ASSERT_THAT(weak1.get(), object1.header());
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak1.header()));
|
||||
threadData.gc().ScheduleAndWaitFullGC();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre());
|
||||
@@ -1096,6 +1100,7 @@ TEST_F(SameThreadMarkAndSweepTest, FreeObjectWithFreeWeakReversedOrder) {
|
||||
|
||||
global1->field1 = nullptr;
|
||||
|
||||
EXPECT_CALL(finalizerHook(), Call(weak.load()->header()));
|
||||
threadData.gc().ScheduleAndWaitFullGC();
|
||||
|
||||
EXPECT_THAT(Alive(threadData), testing::UnorderedElementsAre(global1.header()));
|
||||
|
||||
@@ -3085,7 +3085,7 @@ void ObjHeader::destroyMetaObject(ObjHeader* object) {
|
||||
}
|
||||
|
||||
#ifdef KONAN_OBJC_INTEROP
|
||||
Kotlin_ObjCExport_detachAndReleaseAssociatedObject(meta->associatedObject_);
|
||||
Kotlin_ObjCExport_releaseAssociatedObject(meta->associatedObject_);
|
||||
#endif
|
||||
|
||||
std_support::allocator_delete(objectAllocator, meta);
|
||||
@@ -3730,6 +3730,10 @@ RUNTIME_NOTHROW ALWAYS_INLINE void Kotlin_processEmptyObjectInMark(void* state,
|
||||
// no-op, used by the new MM only.
|
||||
}
|
||||
|
||||
RUNTIME_NOTHROW void DisposeRegularWeakReferenceImpl(ObjHeader* counter) {
|
||||
RuntimeFail("New MM only");
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#if !KONAN_NO_EXCEPTIONS
|
||||
|
||||
@@ -183,8 +183,8 @@ void BackRefFromAssociatedObject::detach() {
|
||||
obj_ = nullptr; // Handled in addRef/tryAddRef/releaseRef/ref.
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void BackRefFromAssociatedObject::assertDetached() {
|
||||
RuntimeAssert(obj_ == nullptr, "Expecting this=%p to be detached, but found obj_=%p", this, obj_);
|
||||
void BackRefFromAssociatedObject::dealloc() {
|
||||
RuntimeFail("New MM only");
|
||||
}
|
||||
|
||||
template <ErrorPolicy errorPolicy>
|
||||
|
||||
@@ -86,6 +86,10 @@ ALWAYS_INLINE inline RuntimeAssertsMode runtimeAssertsMode() noexcept {
|
||||
return static_cast<RuntimeAssertsMode>(Kotlin_runtimeAssertsMode);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE inline bool runtimeAssertsEnabled() noexcept {
|
||||
return runtimeAssertsMode() != RuntimeAssertsMode::kIgnore;
|
||||
}
|
||||
|
||||
ALWAYS_INLINE inline std::string_view runtimeLogs() noexcept {
|
||||
return Kotlin_runtimeLogs == nullptr ? std::string_view() : std::string_view(Kotlin_runtimeLogs);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ NO_INLINE void RunFinalizerHooksImpl(ObjHeader* object, const TypeInfo* type) no
|
||||
DisposeCleaner(object);
|
||||
} else if (type == theWorkerBoundReferenceTypeInfo) {
|
||||
DisposeWorkerBoundReference(object);
|
||||
} else if (type == theRegularWeakReferenceImplTypeInfo) {
|
||||
DisposeRegularWeakReferenceImpl(object);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,8 @@ CODEGEN_INLINE_POLICY RUNTIME_NOTHROW void Kotlin_mm_switchThreadStateRunnable()
|
||||
CODEGEN_INLINE_POLICY void Kotlin_mm_safePointFunctionPrologue() RUNTIME_NOTHROW;
|
||||
CODEGEN_INLINE_POLICY void Kotlin_mm_safePointWhileLoopBody() RUNTIME_NOTHROW;
|
||||
|
||||
RUNTIME_NOTHROW void DisposeRegularWeakReferenceImpl(ObjHeader* counter);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "ManuallyScoped.hpp"
|
||||
#include "Memory.h"
|
||||
#include "Mutex.hpp"
|
||||
|
||||
// TODO: Generalize for uses outside this file.
|
||||
enum class ErrorPolicy {
|
||||
@@ -30,16 +32,14 @@ class KRefSharedHolder {
|
||||
|
||||
void dispose();
|
||||
|
||||
void disposeFromNative() {
|
||||
kotlin::CalledFromNativeGuard guard;
|
||||
dispose();
|
||||
}
|
||||
|
||||
OBJ_GETTER0(describe) const;
|
||||
|
||||
private:
|
||||
ObjHeader* obj_;
|
||||
ForeignRefContext context_;
|
||||
union {
|
||||
ForeignRefContext context_; // Legacy MM.
|
||||
kotlin::mm::RawSpecialRef* ref_; // New MM.
|
||||
};
|
||||
};
|
||||
|
||||
static_assert(std::is_trivially_destructible_v<KRefSharedHolder>, "KRefSharedHolder destructor is not guaranteed to be called.");
|
||||
@@ -47,6 +47,7 @@ static_assert(std::is_trivially_destructible_v<KRefSharedHolder>, "KRefSharedHol
|
||||
class BackRefFromAssociatedObject {
|
||||
public:
|
||||
void initForPermanentObject(ObjHeader* obj);
|
||||
|
||||
void initAndAddRef(ObjHeader* obj);
|
||||
|
||||
// Error if refCount is zero and it's called from the wrong worker with non-frozen obj_.
|
||||
@@ -59,8 +60,11 @@ class BackRefFromAssociatedObject {
|
||||
|
||||
void releaseRef();
|
||||
|
||||
// This does nothing with the new MM.
|
||||
void detach();
|
||||
void assertDetached();
|
||||
|
||||
// This does nothing with legacy MM.
|
||||
void dealloc();
|
||||
|
||||
// Error if called from the wrong worker with non-frozen obj_.
|
||||
template <ErrorPolicy errorPolicy>
|
||||
@@ -69,9 +73,18 @@ class BackRefFromAssociatedObject {
|
||||
ObjHeader* refPermanent() const;
|
||||
|
||||
private:
|
||||
ObjHeader* obj_; // May be null before [initAndAddRef] or after [detach].
|
||||
ForeignRefContext context_;
|
||||
volatile int refCount;
|
||||
union {
|
||||
struct {
|
||||
ObjHeader* obj_; // May be null before [initAndAddRef] or after [detach].
|
||||
ForeignRefContext context_;
|
||||
volatile int refCount;
|
||||
}; // Legacy MM
|
||||
struct {
|
||||
kotlin::mm::RawSpecialRef* ref_;
|
||||
kotlin::ManuallyScoped<kotlin::RWSpinLock<kotlin::MutexThreadStateHandling::kIgnore>> deallocMutex_;
|
||||
}; // New MM. Regular object.
|
||||
ObjHeader* permanentObj_; // New MM. Permanent object.
|
||||
};
|
||||
};
|
||||
|
||||
static_assert(
|
||||
|
||||
@@ -99,9 +99,9 @@ static Class getOrCreateClass(const TypeInfo* typeInfo);
|
||||
|
||||
namespace {
|
||||
|
||||
ALWAYS_INLINE void send_releaseAsAssociatedObject(void* associatedObject, ReleaseMode mode) {
|
||||
auto msgSend = reinterpret_cast<void (*)(void* self, SEL cmd, ReleaseMode mode)>(&objc_msgSend);
|
||||
msgSend(associatedObject, Kotlin_ObjCExport_releaseAsAssociatedObjectSelector, mode);
|
||||
ALWAYS_INLINE void send_releaseAsAssociatedObject(void* associatedObject) {
|
||||
auto msgSend = reinterpret_cast<void (*)(void* self, SEL cmd)>(&objc_msgSend);
|
||||
msgSend(associatedObject, Kotlin_ObjCExport_releaseAsAssociatedObjectSelector);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -109,22 +109,7 @@ ALWAYS_INLINE void send_releaseAsAssociatedObject(void* associatedObject, Releas
|
||||
extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject) {
|
||||
if (associatedObject != nullptr) {
|
||||
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative);
|
||||
send_releaseAsAssociatedObject(associatedObject, ReleaseMode::kRelease);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_detachAndReleaseAssociatedObject(void* associatedObject) {
|
||||
if (associatedObject != nullptr) {
|
||||
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative);
|
||||
send_releaseAsAssociatedObject(associatedObject, ReleaseMode::kDetachAndRelease);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_detachAssociatedObject(void* associatedObject) {
|
||||
if (associatedObject != nullptr) {
|
||||
// Switching to Native state is not required, because detach is fast and can't call user code.
|
||||
// Also switching is not possible, because this is called from GC.
|
||||
send_releaseAsAssociatedObject(associatedObject, ReleaseMode::kDetach);
|
||||
send_releaseAsAssociatedObject(associatedObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +285,7 @@ static OBJ_GETTER(blockToKotlinImp, id self, SEL cmd);
|
||||
static OBJ_GETTER(boxedBooleanToKotlinImp, NSNumber* self, SEL cmd);
|
||||
|
||||
static OBJ_GETTER(SwiftObject_toKotlinImp, id self, SEL cmd);
|
||||
static void SwiftObject_releaseAsAssociatedObjectImp(id self, SEL cmd, ReleaseMode mode);
|
||||
static void SwiftObject_releaseAsAssociatedObjectImp(id self, SEL cmd);
|
||||
|
||||
static void initTypeAdaptersFrom(const ObjCTypeAdapter** adapters, int count) {
|
||||
for (int index = 0; index < count; ++index) {
|
||||
@@ -361,7 +346,7 @@ static void Kotlin_ObjCExport_initializeImpl() {
|
||||
swiftRootClass, releaseAsAssociatedObjectSelector,
|
||||
(IMP)SwiftObject_releaseAsAssociatedObjectImp, releaseAsAssociatedObjectTypeEncoding
|
||||
);
|
||||
RuntimeAssert(added, "Unable to add 'releaseAsAssociatedObject:' method to SwiftObject class");
|
||||
RuntimeAssert(added, "Unable to add 'releaseAsAssociatedObject' method to SwiftObject class");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,9 +366,7 @@ static OBJ_GETTER(SwiftObject_toKotlinImp, id self, SEL cmd) {
|
||||
RETURN_RESULT_OF(Kotlin_ObjCExport_convertUnmappedObjCObject, self);
|
||||
}
|
||||
|
||||
static void SwiftObject_releaseAsAssociatedObjectImp(id self, SEL cmd, ReleaseMode mode) {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
static void SwiftObject_releaseAsAssociatedObjectImp(id self, SEL cmd) {
|
||||
objc_release(self);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,32 +18,6 @@
|
||||
+(instancetype)createRetainedWrapper:(ObjHeader*)obj;
|
||||
@end
|
||||
|
||||
enum class ReleaseMode {
|
||||
kRelease,
|
||||
kDetachAndRelease,
|
||||
kDetach,
|
||||
};
|
||||
|
||||
inline bool ReleaseModeHasDetach(ReleaseMode mode) {
|
||||
switch (mode) {
|
||||
case ReleaseMode::kRelease:
|
||||
return false;
|
||||
case ReleaseMode::kDetachAndRelease:
|
||||
case ReleaseMode::kDetach:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool ReleaseModeHasRelease(ReleaseMode mode) {
|
||||
switch (mode) {
|
||||
case ReleaseMode::kRelease:
|
||||
case ReleaseMode::kDetachAndRelease:
|
||||
return true;
|
||||
case ReleaseMode::kDetach:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void Kotlin_ObjCExport_initializeClass(Class clazz);
|
||||
extern "C" const TypeInfo* Kotlin_ObjCExport_getAssociatedTypeInfo(Class clazz);
|
||||
extern "C" OBJ_GETTER(Kotlin_ObjCExport_convertUnmappedObjCObject, id obj);
|
||||
|
||||
@@ -115,8 +115,18 @@ void releaseImp(id self, SEL _cmd) {
|
||||
getBackRef(self)->releaseRef();
|
||||
}
|
||||
|
||||
void releaseAsAssociatedObjectImp(id self, SEL _cmd, ReleaseMode mode) {
|
||||
void releaseAsAssociatedObjectImp(id self, SEL _cmd) {
|
||||
auto* classData = GetKotlinClassData(self);
|
||||
if (CurrentMemoryModel == MemoryModel::kExperimental) {
|
||||
// No need for any special handling. Weak reference handling machinery
|
||||
// has already cleaned up the reference to Kotlin object.
|
||||
// [super release]
|
||||
Class clazz = classData->objcClass;
|
||||
struct objc_super s = {self, clazz};
|
||||
auto messenger = reinterpret_cast<void (*) (struct objc_super*, SEL _cmd)>(objc_msgSendSuper2);
|
||||
messenger(&s, @selector(release));
|
||||
return;
|
||||
}
|
||||
|
||||
// This function is called by the GC. It made a decision to reclaim Kotlin object, and runs
|
||||
// deallocation hooks at the moment, including deallocation of the "associated object" ([self])
|
||||
@@ -132,24 +142,28 @@ void releaseAsAssociatedObjectImp(id self, SEL _cmd, ReleaseMode mode) {
|
||||
// Generally retaining and releasing Kotlin object that is being deallocated would lead to
|
||||
// use-after-dispose and double-dispose problems (with unpredictable consequences) or to an assertion failure.
|
||||
// To workaround this, detach the back ref from the Kotlin object:
|
||||
if (ReleaseModeHasDetach(mode)) {
|
||||
backRef->detach();
|
||||
} else {
|
||||
// With Mark&Sweep this object should already have been detached earlier.
|
||||
backRef->assertDetached();
|
||||
}
|
||||
backRef->detach();
|
||||
|
||||
// So retain/release/etc. on [self] won't affect the Kotlin object, and an attempt to get
|
||||
// the reference to it (e.g. when calling Kotlin method on [self]) would crash.
|
||||
// The latter is generally ok, because by the time superclass dealloc gets launched, subclass state
|
||||
// should already be deinitialized, and Kotlin methods operate on the subclass.
|
||||
if (ReleaseModeHasRelease(mode)) {
|
||||
// [super release]
|
||||
Class clazz = classData->objcClass;
|
||||
struct objc_super s = {self, clazz};
|
||||
auto messenger = reinterpret_cast<void (*) (struct objc_super*, SEL _cmd)>(objc_msgSendSuper2);
|
||||
messenger(&s, @selector(release));
|
||||
}
|
||||
// [super release]
|
||||
Class clazz = classData->objcClass;
|
||||
struct objc_super s = {self, clazz};
|
||||
auto messenger = reinterpret_cast<void (*) (struct objc_super*, SEL _cmd)>(objc_msgSendSuper2);
|
||||
messenger(&s, @selector(release));
|
||||
}
|
||||
|
||||
void deallocImp(id self, SEL _cmd) {
|
||||
getBackRef(self)->dealloc();
|
||||
|
||||
// [super dealloc]
|
||||
auto* classData = GetKotlinClassData(self);
|
||||
Class clazz = classData->objcClass;
|
||||
struct objc_super s = {self, clazz};
|
||||
auto messenger = reinterpret_cast<void (*) (struct objc_super*, SEL _cmd)>(objc_msgSendSuper2);
|
||||
messenger(&s, @selector(dealloc));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -277,6 +291,9 @@ void* CreateKotlinObjCClass(const KotlinObjCClassInfo* info) {
|
||||
AddNSObjectOverride(false, newClass, @selector(release), (void*)&releaseImp);
|
||||
AddNSObjectOverride(false, newClass, Kotlin_ObjCExport_releaseAsAssociatedObjectSelector,
|
||||
(void*)&releaseAsAssociatedObjectImp);
|
||||
if (CurrentMemoryModel == MemoryModel::kExperimental) {
|
||||
AddNSObjectOverride(false, newClass, @selector(dealloc), (void*)&deallocImp);
|
||||
}
|
||||
|
||||
AddMethods(newClass, info->instanceMethods, info->instanceMethodsNum);
|
||||
AddMethods(newMetaclass, info->classMethods, info->classMethodsNum);
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
#if KONAN_OBJC_INTEROP
|
||||
|
||||
extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject);
|
||||
extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_detachAndReleaseAssociatedObject(void* associatedObject);
|
||||
extern "C" ALWAYS_INLINE void Kotlin_ObjCExport_detachAssociatedObject(void* associatedObject);
|
||||
|
||||
namespace konan {
|
||||
class AutoreleasePool : private kotlin::Pinned {
|
||||
|
||||
@@ -345,6 +345,7 @@ public:
|
||||
};
|
||||
|
||||
struct RegularWeakReferenceImplPayload {
|
||||
void* weakRef;
|
||||
void* referred;
|
||||
|
||||
using Field = ObjHeader* RegularWeakReferenceImplPayload::*;
|
||||
|
||||
@@ -22,7 +22,7 @@ import kotlin.native.internal.*
|
||||
* [weak1] [weak2]
|
||||
* \ /
|
||||
* V V
|
||||
* ...... [WeakReferenceImpl] <------
|
||||
* ... [RegularWeakReferenceImpl] <-
|
||||
* . |
|
||||
* . |
|
||||
* ->[Object] -> [ExtraObjectData]-
|
||||
@@ -50,8 +50,10 @@ internal class WeakReferenceCounterLegacyMM(var referred: COpaquePointer?) : Wea
|
||||
|
||||
@NoReorderFields
|
||||
@ExportTypeInfo("theRegularWeakReferenceImplTypeInfo")
|
||||
@HasFinalizer // TODO: Consider just using Cleaners.
|
||||
internal class RegularWeakReferenceImpl(
|
||||
val referred: COpaquePointer,
|
||||
val weakRef: COpaquePointer,
|
||||
val referred: COpaquePointer, // TODO: This exists only for the ExtraObjectData's sake. Refactor and remove.
|
||||
) : WeakReferenceImpl() {
|
||||
@GCUnsafeCall("Konan_RegularWeakReferenceImpl_get")
|
||||
external override fun get(): Any?
|
||||
@@ -73,7 +75,7 @@ internal fun makeWeakReferenceCounterLegacyMM(referred: COpaquePointer) = WeakRe
|
||||
|
||||
// Create a counter object.
|
||||
@ExportForCppRuntime
|
||||
internal fun makeRegularWeakReferenceImpl(referred: COpaquePointer) = RegularWeakReferenceImpl(referred)
|
||||
internal fun makeRegularWeakReferenceImpl(weakRef: COpaquePointer, referred: COpaquePointer) = RegularWeakReferenceImpl(weakRef, referred)
|
||||
|
||||
internal class PermanentWeakReferenceImpl(val referred: Any): kotlin.native.ref.WeakReferenceImpl() {
|
||||
override fun get(): Any? = referred
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "Memory.h"
|
||||
|
||||
#include "StableRefRegistry.hpp"
|
||||
#include "StableRef.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
|
||||
@@ -14,22 +14,16 @@ using namespace kotlin;
|
||||
namespace {
|
||||
|
||||
#if !KONAN_NO_EXCEPTIONS
|
||||
class ExceptionObjHolderImpl : public ExceptionObjHolder {
|
||||
class ExceptionObjHolderImpl : public ExceptionObjHolder, private Pinned {
|
||||
public:
|
||||
explicit ExceptionObjHolderImpl(ObjHeader* obj) noexcept {
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
stableRef_ = threadData->stableRefThreadQueue().Insert(obj);
|
||||
}
|
||||
explicit ExceptionObjHolderImpl(ObjHeader* obj) noexcept : stableRef_(mm::StableRef::create(obj)) {}
|
||||
|
||||
~ExceptionObjHolderImpl() override {
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
threadData->stableRefThreadQueue().Erase(stableRef_);
|
||||
}
|
||||
~ExceptionObjHolderImpl() override { std::move(stableRef_).dispose(); }
|
||||
|
||||
ObjHeader* obj() noexcept { return **stableRef_; }
|
||||
ObjHeader* obj() noexcept { return *stableRef_; }
|
||||
|
||||
private:
|
||||
mm::StableRefRegistry::Node* stableRef_;
|
||||
mm::StableRef stableRef_;
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -21,11 +21,9 @@ namespace {
|
||||
class ExceptionObjHolderTest : public ::testing::Test {
|
||||
public:
|
||||
static std_support::vector<ObjHeader*> Collect(mm::ThreadData& threadData) {
|
||||
auto& stableRefs = mm::StableRefRegistry::Instance();
|
||||
stableRefs.ProcessThread(&threadData);
|
||||
stableRefs.ProcessDeletions();
|
||||
threadData.specialRefRegistry().publish();
|
||||
std_support::vector<ObjHeader*> result;
|
||||
for (const auto& obj : stableRefs.LockForIter()) {
|
||||
for (const auto& obj : mm::SpecialRefRegistry::instance().roots()) {
|
||||
result.push_back(obj);
|
||||
}
|
||||
return result;
|
||||
@@ -66,7 +64,7 @@ TEST_F(ExceptionObjHolderTest, ThrowInsideCatch) {
|
||||
try {
|
||||
ExceptionObjHolder::Throw(&exception2);
|
||||
} catch (...) {
|
||||
EXPECT_THAT(Collect(threadData), testing::ElementsAre(&exception1, &exception2));
|
||||
EXPECT_THAT(Collect(threadData), testing::UnorderedElementsAre(&exception1, &exception2));
|
||||
}
|
||||
EXPECT_THAT(Collect(threadData), testing::ElementsAre(&exception1));
|
||||
}
|
||||
@@ -94,7 +92,7 @@ TEST_F(ExceptionObjHolderTest, StoreException) {
|
||||
} catch (...) {
|
||||
storedException2 = std::current_exception();
|
||||
}
|
||||
EXPECT_THAT(Collect(threadData), testing::ElementsAre(&exception1, &exception2));
|
||||
EXPECT_THAT(Collect(threadData), testing::UnorderedElementsAre(&exception1, &exception2));
|
||||
|
||||
storedException1 = std::exception_ptr();
|
||||
EXPECT_THAT(Collect(threadData), testing::ElementsAre(&exception2));
|
||||
|
||||
@@ -54,7 +54,9 @@ mm::ExtraObjectData& mm::ExtraObjectData::Install(ObjHeader* object) noexcept {
|
||||
void mm::ExtraObjectData::Uninstall() noexcept {
|
||||
auto *object = GetBaseObject();
|
||||
atomicSetRelease(const_cast<const TypeInfo**>(&object->typeInfoOrMeta_), typeInfo_);
|
||||
RuntimeAssert(!object->has_meta_object(), "Object has metaobject after removing metaobject");
|
||||
RuntimeAssert(
|
||||
!object->has_meta_object(), "Object %p has metaobject %p after removing metaobject %p", object, object->meta_object_or_null(),
|
||||
this);
|
||||
|
||||
#ifdef KONAN_OBJC_INTEROP
|
||||
Kotlin_ObjCExport_releaseAssociatedObject(associatedObject_);
|
||||
@@ -62,12 +64,6 @@ void mm::ExtraObjectData::Uninstall() noexcept {
|
||||
#endif
|
||||
}
|
||||
|
||||
void mm::ExtraObjectData::DetachAssociatedObject() noexcept {
|
||||
#ifdef KONAN_OBJC_INTEROP
|
||||
Kotlin_ObjCExport_detachAssociatedObject(associatedObject_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool mm::ExtraObjectData::HasAssociatedObject() noexcept {
|
||||
#ifdef KONAN_OBJC_INTEROP
|
||||
return associatedObject_ != nullptr;
|
||||
@@ -77,10 +73,7 @@ bool mm::ExtraObjectData::HasAssociatedObject() noexcept {
|
||||
}
|
||||
|
||||
void mm::ExtraObjectData::ClearRegularWeakReferenceImpl() noexcept {
|
||||
if (!HasRegularWeakReferenceImpl()) return;
|
||||
|
||||
auto *object = GetBaseObject();
|
||||
disposeRegularWeakReferenceImpl(GetRegularWeakReferenceImpl());
|
||||
// Not using `mm::SetHeapRef here`, because this code is called during sweep phase by the GC thread,
|
||||
// and so cannot affect marking.
|
||||
// TODO: Asserts on the above?
|
||||
@@ -88,9 +81,16 @@ void mm::ExtraObjectData::ClearRegularWeakReferenceImpl() noexcept {
|
||||
}
|
||||
|
||||
mm::ExtraObjectData::~ExtraObjectData() {
|
||||
RuntimeAssert(!HasRegularWeakReferenceImpl(), "Object must have cleared weak references");
|
||||
auto* weakReference = weakReferenceOrBaseObject_.load(std::memory_order_relaxed);
|
||||
if (hasPointerBits(weakReference, WEAK_REF_TAG)) {
|
||||
weakReference = clearPointerBits(weakReference, WEAK_REF_TAG);
|
||||
} else {
|
||||
weakReference = nullptr;
|
||||
}
|
||||
RuntimeAssert(weakReference == nullptr, "ExtraObjectData %p must have cleared weak reference %p", this, weakReference);
|
||||
|
||||
#ifdef KONAN_OBJC_INTEROP
|
||||
RuntimeAssert(associatedObject_ == nullptr, "Object must have cleared associated object");
|
||||
auto* associatedObject = associatedObject_.load(std::memory_order_relaxed);
|
||||
RuntimeAssert(associatedObject == nullptr, "ExtraObjectData %p must have cleared associated object %p", this, associatedObject);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -47,13 +47,12 @@ public:
|
||||
std::atomic<void*>& AssociatedObject() noexcept { return associatedObject_; }
|
||||
#endif
|
||||
bool HasAssociatedObject() noexcept;
|
||||
void DetachAssociatedObject() noexcept;
|
||||
|
||||
bool getFlag(Flags value) noexcept { return (flags_.load() & (1u << static_cast<uint32_t>(value))) != 0; }
|
||||
void setFlag(Flags value) noexcept { flags_.fetch_or(1u << static_cast<uint32_t>(value)); }
|
||||
|
||||
bool HasRegularWeakReferenceImpl() noexcept { return hasPointerBits(weakReferenceOrBaseObject_.load(), WEAK_REF_TAG); }
|
||||
void ClearRegularWeakReferenceImpl() noexcept;
|
||||
void ClearRegularWeakReferenceImpl() noexcept; // TODO: Only exists for the sake of GetBaseObject. Refactor to remove the need for it.
|
||||
ObjHeader* GetRegularWeakReferenceImpl() noexcept {
|
||||
auto* pointer = weakReferenceOrBaseObject_.load();
|
||||
if (hasPointerBits(pointer, WEAK_REF_TAG)) return clearPointerBits(pointer, WEAK_REF_TAG);
|
||||
|
||||
@@ -39,9 +39,5 @@ void mm::ExtraObjectDataFactory::ProcessThread(mm::ThreadData* threadData) noexc
|
||||
threadData->extraObjectDataThreadQueue().Publish();
|
||||
}
|
||||
|
||||
void mm::ExtraObjectDataFactory::ProcessDeletions() noexcept {
|
||||
extraObjects_.ApplyDeletions();
|
||||
}
|
||||
|
||||
mm::ExtraObjectDataFactory::ExtraObjectDataFactory() = default;
|
||||
mm::ExtraObjectDataFactory::~ExtraObjectDataFactory() = default;
|
||||
|
||||
@@ -44,9 +44,6 @@ public:
|
||||
// when it's asked by GC to stop.
|
||||
void ProcessThread(mm::ThreadData* threadData) noexcept;
|
||||
|
||||
// Lock registry and apply deletions. Should be called on GC thread after all threads have published, and before `LockForIter`.
|
||||
void ProcessDeletions() noexcept;
|
||||
|
||||
// Lock registry for safe iteration.
|
||||
Iterable LockForIter() noexcept { return extraObjects_.LockForIter(); }
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "GlobalsRegistry.hpp"
|
||||
#include "GC.hpp"
|
||||
#include "GCScheduler.hpp"
|
||||
#include "StableRefRegistry.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "ExtraObjectDataFactory.hpp"
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
|
||||
ThreadRegistry& threadRegistry() noexcept { return threadRegistry_; }
|
||||
GlobalsRegistry& globalsRegistry() noexcept { return globalsRegistry_; }
|
||||
StableRefRegistry& stableRefRegistry() noexcept { return stableRefRegistry_; }
|
||||
SpecialRefRegistry& specialRefRegistry() noexcept { return specialRefRegistry_; }
|
||||
ExtraObjectDataFactory& extraObjectDataFactory() noexcept { return extraObjectDataFactory_; }
|
||||
gc::GC& gc() noexcept { return gc_; }
|
||||
AppStateTracking& appStateTracking() noexcept { return appStateTracking_; }
|
||||
@@ -41,7 +41,7 @@ private:
|
||||
ThreadRegistry threadRegistry_;
|
||||
AppStateTracking appStateTracking_;
|
||||
GlobalsRegistry globalsRegistry_;
|
||||
StableRefRegistry stableRefRegistry_;
|
||||
SpecialRefRegistry specialRefRegistry_;
|
||||
ExtraObjectDataFactory extraObjectDataFactory_;
|
||||
gc::GC gc_;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "ObjectOps.hpp"
|
||||
#include "Porting.h"
|
||||
#include "Runtime.h"
|
||||
#include "StableRefRegistry.hpp"
|
||||
#include "StableRef.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
#include "ThreadState.hpp"
|
||||
@@ -24,29 +24,6 @@
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
// TODO: This name does not make sense anymore.
|
||||
// Delete all means of creating this type directly as it only serves
|
||||
// as a typedef for `mm::StableRefRegistry::Node`.
|
||||
class ForeignRefManager : Pinned {
|
||||
public:
|
||||
ForeignRefManager() = delete;
|
||||
~ForeignRefManager() = delete;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// `reinterpret_cast` to it and back to the same type
|
||||
// will yield precisely the same pointer, so it's safe.
|
||||
ALWAYS_INLINE ForeignRefManager* ToForeignRefManager(mm::StableRefRegistry::Node* data) {
|
||||
return reinterpret_cast<ForeignRefManager*>(data);
|
||||
}
|
||||
|
||||
ALWAYS_INLINE mm::StableRefRegistry::Node* FromForeignRefManager(ForeignRefManager* manager) {
|
||||
return reinterpret_cast<mm::StableRefRegistry::Node*>(manager);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ObjHeader* ObjHeader::GetWeakCounter() {
|
||||
RuntimeFail("Only for legacy MM");
|
||||
}
|
||||
@@ -464,16 +441,6 @@ extern "C" RUNTIME_NOTHROW void PerformFullGC(MemoryState* memory) {
|
||||
threadData->gc().ScheduleAndWaitFullGCWithFinalizers();
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW OBJ_GETTER(TryRef, ObjHeader* object) {
|
||||
// TODO: With CMS this needs:
|
||||
// * during marking phase if `object` is unmarked: barrier (might be automatic because of the stack write)
|
||||
// and return `object`;
|
||||
// * during marking phase if `object` is marked: return `object`;
|
||||
// * during sweeping phase if `object` is unmarked: return nullptr;
|
||||
// * during sweeping phase if `object` is marked: return `object`;
|
||||
RETURN_OBJ(object);
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW bool ClearSubgraphReferences(ObjHeader* root, bool checked) {
|
||||
// TODO: Remove when legacy MM is gone.
|
||||
return true;
|
||||
@@ -483,24 +450,23 @@ extern "C" RUNTIME_NOTHROW void* CreateStablePointer(ObjHeader* object) {
|
||||
if (!object)
|
||||
return nullptr;
|
||||
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
AssertThreadState(threadData, ThreadState::kRunnable);
|
||||
return mm::StableRefRegistry::Instance().RegisterStableRef(threadData, object);
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
return static_cast<mm::RawSpecialRef*>(mm::StableRef::create(object));
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW void DisposeStablePointer(void* pointer) {
|
||||
DisposeStablePointerFor(kotlin::mm::GetMemoryState(), pointer);
|
||||
if (!pointer) return;
|
||||
|
||||
// Can be safely called in any thread state.
|
||||
mm::StableRef(static_cast<mm::RawSpecialRef*>(pointer)).dispose();
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW void DisposeStablePointerFor(MemoryState* memoryState, void* pointer) {
|
||||
if (!pointer)
|
||||
return;
|
||||
|
||||
auto* threadData = memoryState->GetThreadData();
|
||||
AssertThreadState(threadData, ThreadState::kRunnable);
|
||||
|
||||
auto* node = static_cast<mm::StableRefRegistry::Node*>(pointer);
|
||||
mm::StableRefRegistry::Instance().UnregisterStableRef(threadData, node);
|
||||
// Can be safely called in any thread state.
|
||||
mm::StableRef(static_cast<mm::RawSpecialRef*>(pointer)).disposeOn(*mm::FromMemoryState(memoryState)->Get());
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW OBJ_GETTER(DerefStablePointer, void* pointer) {
|
||||
@@ -508,24 +474,19 @@ extern "C" RUNTIME_NOTHROW OBJ_GETTER(DerefStablePointer, void* pointer) {
|
||||
RETURN_OBJ(nullptr);
|
||||
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
|
||||
auto* node = static_cast<mm::StableRefRegistry::Node*>(pointer);
|
||||
ObjHeader* object = **node;
|
||||
RETURN_OBJ(object);
|
||||
RETURN_OBJ(*mm::StableRef(static_cast<mm::RawSpecialRef*>(pointer)));
|
||||
}
|
||||
|
||||
extern "C" RUNTIME_NOTHROW OBJ_GETTER(AdoptStablePointer, void* pointer) {
|
||||
if (!pointer)
|
||||
RETURN_OBJ(nullptr);
|
||||
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
AssertThreadState(threadData, ThreadState::kRunnable);
|
||||
auto* node = static_cast<mm::StableRefRegistry::Node*>(pointer);
|
||||
ObjHeader* object = **node;
|
||||
// Make sure `object` stays in the rootset: put it on the stack before removing it from `StableRefRegistry`.
|
||||
mm::SetStackRef(OBJ_RESULT, object);
|
||||
mm::StableRefRegistry::Instance().UnregisterStableRef(threadData, node);
|
||||
return object;
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
mm::StableRef stableRef(static_cast<mm::RawSpecialRef*>(pointer));
|
||||
auto* obj = *stableRef;
|
||||
mm::SetStackRef(OBJ_RESULT, obj);
|
||||
std::move(stableRef).dispose();
|
||||
return obj;
|
||||
}
|
||||
|
||||
extern "C" void MutationCheck(ObjHeader* obj) {
|
||||
@@ -554,22 +515,6 @@ extern "C" void EnsureNeverFrozen(ObjHeader* obj) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" ForeignRefContext InitForeignRef(ObjHeader* object) {
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
auto* node = mm::StableRefRegistry::Instance().RegisterStableRef(threadData, object);
|
||||
return ToForeignRefManager(node);
|
||||
}
|
||||
|
||||
extern "C" void DeinitForeignRef(ObjHeader* object, ForeignRefContext context) {
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
RuntimeAssert(context != nullptr, "DeinitForeignRef must not be called for InitLocalForeignRef");
|
||||
auto* threadData = mm::ThreadRegistry::Instance().CurrentThreadData();
|
||||
auto* node = FromForeignRefManager(context);
|
||||
RuntimeAssert(object == **node, "Must correspond to the same object");
|
||||
mm::StableRefRegistry::Instance().UnregisterStableRef(threadData, node);
|
||||
}
|
||||
|
||||
extern "C" void CheckGlobalsAccessible() {
|
||||
// TODO: Remove when legacy MM is gone.
|
||||
// Always accessible
|
||||
@@ -659,3 +604,7 @@ RUNTIME_NOTHROW extern "C" OBJ_GETTER(Konan_WeakReferenceCounterLegacyMM_get, Ob
|
||||
RUNTIME_NOTHROW extern "C" OBJ_GETTER(Konan_RegularWeakReferenceImpl_get, ObjHeader* weakRef) {
|
||||
RETURN_RESULT_OF(mm::derefRegularWeakReferenceImpl, weakRef);
|
||||
}
|
||||
|
||||
RUNTIME_NOTHROW extern "C" void DisposeRegularWeakReferenceImpl(ObjHeader* weakRef) {
|
||||
mm::disposeRegularWeakReferenceImpl(weakRef);
|
||||
}
|
||||
|
||||
@@ -39,10 +39,4 @@ extern "C" struct MemoryState : kotlin::Pinned {
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
ForeignRefContext InitForeignRef(ObjHeader* object);
|
||||
void DeinitForeignRef(ObjHeader* object, ForeignRefContext context);
|
||||
RUNTIME_NOTHROW OBJ_GETTER(TryRef, ObjHeader* object);
|
||||
}
|
||||
|
||||
#endif //RUNTIME_MEMORYPRIVATE_HPP
|
||||
|
||||
@@ -5,19 +5,22 @@
|
||||
|
||||
#include "MemorySharedRefs.hpp"
|
||||
|
||||
#include "MemoryPrivate.hpp"
|
||||
#include <shared_mutex>
|
||||
|
||||
#include "ObjCBackRef.hpp"
|
||||
#include "StableRef.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
void KRefSharedHolder::initLocal(ObjHeader* obj) {
|
||||
RuntimeAssert(obj != nullptr, "must not be null");
|
||||
context_ = nullptr;
|
||||
ref_ = nullptr;
|
||||
obj_ = obj;
|
||||
}
|
||||
|
||||
void KRefSharedHolder::init(ObjHeader* obj) {
|
||||
RuntimeAssert(obj != nullptr, "must not be null");
|
||||
context_ = InitForeignRef(obj);
|
||||
ref_ = static_cast<mm::RawSpecialRef*>(mm::StableRef::create(obj));
|
||||
obj_ = obj;
|
||||
}
|
||||
|
||||
@@ -33,46 +36,30 @@ template ObjHeader* KRefSharedHolder::ref<ErrorPolicy::kThrow>() const;
|
||||
template ObjHeader* KRefSharedHolder::ref<ErrorPolicy::kTerminate>() const;
|
||||
|
||||
void KRefSharedHolder::dispose() {
|
||||
if (obj_ == nullptr) {
|
||||
// To handle the case when it is not initialized. See [KotlinMutableSet/Dictionary dealloc].
|
||||
// Handles the case when it is not initialized. See [KotlinMutableSet/Dictionary dealloc].
|
||||
if (!ref_) {
|
||||
return;
|
||||
}
|
||||
|
||||
DeinitForeignRef(obj_, context_);
|
||||
std::move(mm::StableRef::reinterpret(ref_)).dispose();
|
||||
// obj_ is dangling now.
|
||||
}
|
||||
|
||||
void BackRefFromAssociatedObject::initForPermanentObject(ObjHeader* obj) {
|
||||
RuntimeAssert(obj != nullptr, "must not be null");
|
||||
RuntimeAssert(obj->permanent(), "Can only be called with permanent object");
|
||||
obj_ = obj;
|
||||
permanentObj_ = obj;
|
||||
}
|
||||
|
||||
void BackRefFromAssociatedObject::initAndAddRef(ObjHeader* obj) {
|
||||
RuntimeAssert(obj != nullptr, "must not be null");
|
||||
RuntimeAssert(!obj->permanent(), "Can only be called with non-permanent object");
|
||||
obj_ = obj;
|
||||
|
||||
// Generally a specialized addRef below:
|
||||
context_ = InitForeignRef(obj);
|
||||
refCount = 1;
|
||||
ref_ = static_cast<mm::RawSpecialRef*>(mm::ObjCBackRef::create(obj));
|
||||
deallocMutex_.construct();
|
||||
}
|
||||
|
||||
template <ErrorPolicy errorPolicy>
|
||||
void BackRefFromAssociatedObject::addRef() {
|
||||
static_assert(errorPolicy != ErrorPolicy::kDefaultValue, "Cannot use default return value here");
|
||||
|
||||
// Can be called both from Native state (if ObjC or Swift code adds RC)
|
||||
// and from Runnable state (Kotlin_ObjCExport_refToObjC).
|
||||
|
||||
if (atomicAdd(&refCount, 1) == 1) {
|
||||
if (obj_ == nullptr) return; // E.g. after [detach].
|
||||
|
||||
kotlin::CalledFromNativeGuard guard(/* reentrant */ true);
|
||||
|
||||
// Foreign reference has already been deinitialized (see [releaseRef]).
|
||||
// Create a new one:
|
||||
context_ = InitForeignRef(obj_);
|
||||
}
|
||||
mm::ObjCBackRef::reinterpret(ref_).retain();
|
||||
}
|
||||
|
||||
template void BackRefFromAssociatedObject::addRef<ErrorPolicy::kThrow>();
|
||||
@@ -80,54 +67,36 @@ template void BackRefFromAssociatedObject::addRef<ErrorPolicy::kTerminate>();
|
||||
|
||||
template <ErrorPolicy errorPolicy>
|
||||
bool BackRefFromAssociatedObject::tryAddRef() {
|
||||
static_assert(errorPolicy != ErrorPolicy::kDefaultValue, "Cannot use default return value here");
|
||||
kotlin::CalledFromNativeGuard guard;
|
||||
|
||||
if (obj_ == nullptr) return false; // E.g. after [detach].
|
||||
|
||||
ObjHolder holder;
|
||||
ObjHeader* obj = TryRef(obj_, holder.slot());
|
||||
// Failed to lock weak reference.
|
||||
if (obj == nullptr) return false;
|
||||
RuntimeAssert(obj == obj_, "Mismatched locked weak. obj=%p obj_=%p", obj, obj_);
|
||||
// TODO: This is a very weird way to ask for "unsafe" addRef.
|
||||
addRef<ErrorPolicy::kIgnore>();
|
||||
return true;
|
||||
// Only this method can be called in parallel with dealloc.
|
||||
std::shared_lock guard(*deallocMutex_, std::try_to_lock);
|
||||
if (!guard) {
|
||||
// That means `dealloc` is running in parallel, so
|
||||
// cannot possibly retain.
|
||||
return false;
|
||||
}
|
||||
return mm::ObjCBackRef::reinterpret(ref_).tryRetain();
|
||||
}
|
||||
|
||||
template bool BackRefFromAssociatedObject::tryAddRef<ErrorPolicy::kThrow>();
|
||||
template bool BackRefFromAssociatedObject::tryAddRef<ErrorPolicy::kTerminate>();
|
||||
|
||||
void BackRefFromAssociatedObject::releaseRef() {
|
||||
ForeignRefContext context = context_;
|
||||
if (atomicAdd(&refCount, -1) == 0) {
|
||||
if (obj_ == nullptr) return; // E.g. after [detach].
|
||||
|
||||
kotlin::CalledFromNativeGuard guard;
|
||||
|
||||
// Note: by this moment "subsequent" addRef may have already happened and patched context_.
|
||||
// So use the value loaded before refCount update:
|
||||
DeinitForeignRef(obj_, context);
|
||||
// From this moment [context] is generally a dangling pointer.
|
||||
// This is handled in [IsForeignRefAccessible] and [addRef].
|
||||
// TODO: This probably isn't fine in new MM. Make sure it works.
|
||||
}
|
||||
mm::ObjCBackRef::reinterpret(ref_).release();
|
||||
}
|
||||
|
||||
void BackRefFromAssociatedObject::detach() {
|
||||
RuntimeAssert(atomicGet(&refCount) == 0, "unexpected refCount");
|
||||
obj_ = nullptr; // Handled in addRef/tryAddRef/releaseRef/ref.
|
||||
RuntimeFail("Legacy MM only");
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void BackRefFromAssociatedObject::assertDetached() {
|
||||
RuntimeAssert(obj_ == nullptr, "Expecting this=%p to be detached, but found obj_=%p", this, obj_);
|
||||
void BackRefFromAssociatedObject::dealloc() {
|
||||
// This will wait for all `tryAddRef` to finish.
|
||||
std::unique_lock guard(*deallocMutex_);
|
||||
std::move(mm::ObjCBackRef::reinterpret(ref_)).dispose();
|
||||
}
|
||||
|
||||
template <ErrorPolicy errorPolicy>
|
||||
ObjHeader* BackRefFromAssociatedObject::ref() const {
|
||||
kotlin::AssertThreadState(kotlin::ThreadState::kRunnable);
|
||||
RuntimeAssert(obj_ != nullptr, "no valid Kotlin object found");
|
||||
return obj_;
|
||||
return *mm::ObjCBackRef::reinterpret(ref_);
|
||||
}
|
||||
|
||||
template ObjHeader* BackRefFromAssociatedObject::ref<ErrorPolicy::kDefaultValue>() const;
|
||||
@@ -135,5 +104,5 @@ template ObjHeader* BackRefFromAssociatedObject::ref<ErrorPolicy::kThrow>() cons
|
||||
template ObjHeader* BackRefFromAssociatedObject::ref<ErrorPolicy::kTerminate>() const;
|
||||
|
||||
ObjHeader* BackRefFromAssociatedObject::refPermanent() const {
|
||||
return obj_;
|
||||
return permanentObj_;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "ObjCBackRef.hpp"
|
||||
|
||||
#include "ThreadData.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
// static
|
||||
mm::ObjCBackRef mm::ObjCBackRef::create(ObjHeader* obj) noexcept {
|
||||
RuntimeAssert(obj != nullptr, "Creating ObjCBackRef for null object");
|
||||
return mm::ThreadRegistry::Instance().CurrentThreadData()->specialRefRegistry().createObjCBackRef(obj);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Memory.h"
|
||||
#include "RawPtr.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace kotlin::mm {
|
||||
|
||||
// Reference from an ObjC associated object back into a Kotlin object.
|
||||
// GC automatically tracks references with refcount > 0 as roots, and invalidates references with refcount = 0 when the Kotlin object is
|
||||
// collected. Use `create` and `dispose` to create and destroy the back reference.
|
||||
class ObjCBackRef : private MoveOnly {
|
||||
public:
|
||||
ObjCBackRef() noexcept = default;
|
||||
|
||||
// Cast raw ref into a back reference.
|
||||
explicit ObjCBackRef(RawSpecialRef* raw) noexcept : node_(SpecialRefRegistry::Node::fromRaw(raw)) {}
|
||||
|
||||
// Cast back reference into a raw ref
|
||||
[[nodiscard("must be manually disposed")]] explicit operator RawSpecialRef*() && noexcept {
|
||||
// Make sure to move out from node_.
|
||||
auto node = std::move(node_);
|
||||
return node->asRaw();
|
||||
}
|
||||
|
||||
// Create new back reference for `obj`.
|
||||
[[nodiscard("must be manually disposed")]] static ObjCBackRef create(ObjHeader* obj) noexcept;
|
||||
|
||||
// Dispose back reference.
|
||||
void dispose() && noexcept {
|
||||
RuntimeAssert(node_, "Disposing null ObjCBackRef");
|
||||
// Make sure to move out from node_.
|
||||
auto node = std::move(node_);
|
||||
// Can be safely called with any thread state.
|
||||
node->dispose();
|
||||
}
|
||||
|
||||
// Increment refcount.
|
||||
void retain() noexcept {
|
||||
// In objc import if KtClass inherits from ObjCClass
|
||||
// calling [self retain] inside [ObjCClass dealloc] will lead to
|
||||
// this->retain() being called after this->dispose()
|
||||
if (!node_) return;
|
||||
// Can be safely called with any thread state.
|
||||
node_->retainRef();
|
||||
}
|
||||
|
||||
// Decrement refcount.
|
||||
void release() noexcept {
|
||||
// In objc import if KtClass inherits from ObjCClass
|
||||
// calling [self release] inside [ObjCClass dealloc] will lead to
|
||||
// this->release() being called after this->dispose()
|
||||
if (!node_) return;
|
||||
// Can be safely called with any thread state.
|
||||
node_->releaseRef();
|
||||
}
|
||||
|
||||
// Try incrementing refcount. Will fail if the underlying object is not alive.
|
||||
[[nodiscard("refcount change must be processed")]] bool tryRetain() noexcept {
|
||||
// In objc export if ObjCClass is objc_setAssociatedObject with KtClass
|
||||
// calling [KtClass _tryRetain] inside [ObjCClass dealloc] will lead to
|
||||
// this->tryRetain() being called after this->dispose()
|
||||
if (!node_) return false;
|
||||
CalledFromNativeGuard guard;
|
||||
return tryRetainIgnoreState();
|
||||
}
|
||||
|
||||
// Get the underlying object.
|
||||
// The result is only safe to use only with refcount > 0.
|
||||
[[nodiscard("expensive pure function")]] ObjHeader* operator*() const noexcept {
|
||||
// In objc import if KtClass inherits from ObjCClass
|
||||
// calling [self retain] inside [ObjCClass dealloc] and then passing the retained
|
||||
// reference back to Kotlin will lead to
|
||||
// this->operator*() being called after this->dispose()
|
||||
if (!node_) return nullptr;
|
||||
return node_->ref();
|
||||
}
|
||||
|
||||
bool tryRetainForTests() noexcept { return tryRetainIgnoreState(); }
|
||||
|
||||
static ObjCBackRef& reinterpret(RawSpecialRef*& raw) noexcept { return reinterpret_cast<ObjCBackRef&>(raw); }
|
||||
|
||||
static const ObjCBackRef& reinterpret(RawSpecialRef* const& raw) noexcept { return reinterpret_cast<const ObjCBackRef&>(raw); }
|
||||
|
||||
private:
|
||||
bool tryRetainIgnoreState() noexcept {
|
||||
ObjHolder holder;
|
||||
if (auto* obj = node_->tryRef(holder.slot())) {
|
||||
node_->retainRef();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
raw_ptr<SpecialRefRegistry::Node> node_;
|
||||
};
|
||||
|
||||
static_assert(sizeof(ObjCBackRef) == sizeof(void*), "ObjCBackRef must be a thin wrapper around pointer");
|
||||
static_assert(alignof(ObjCBackRef) == alignof(void*), "ObjCBackRef must be a thin wrapper around pointer");
|
||||
static_assert(
|
||||
std::is_trivially_destructible_v<ObjCBackRef>, "ObjCBackRef must be trivially destructible. Destruction is manual via dispose()");
|
||||
|
||||
} // namespace kotlin::mm
|
||||
@@ -89,7 +89,7 @@ mm::GlobalRootSet::Value mm::GlobalRootSet::Iterator::operator*() noexcept {
|
||||
case Phase::kGlobals:
|
||||
return {**globalsIterator_, Source::kGlobal};
|
||||
case Phase::kStableRefs:
|
||||
return {*stableRefsIterator_, Source::kStableRef};
|
||||
return {*specialRefsIterator_, Source::kStableRef};
|
||||
case Phase::kDone:
|
||||
RuntimeFail("Cannot dereference");
|
||||
}
|
||||
@@ -102,7 +102,7 @@ mm::GlobalRootSet::Iterator& mm::GlobalRootSet::Iterator::operator++() noexcept
|
||||
Init();
|
||||
return *this;
|
||||
case Phase::kStableRefs:
|
||||
++stableRefsIterator_;
|
||||
++specialRefsIterator_;
|
||||
Init();
|
||||
return *this;
|
||||
case Phase::kDone:
|
||||
@@ -121,7 +121,7 @@ bool mm::GlobalRootSet::Iterator::operator==(const Iterator& rhs) const noexcept
|
||||
case Phase::kGlobals:
|
||||
return globalsIterator_ == rhs.globalsIterator_;
|
||||
case Phase::kStableRefs:
|
||||
return stableRefsIterator_ == rhs.stableRefsIterator_;
|
||||
return specialRefsIterator_ == rhs.specialRefsIterator_;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +131,10 @@ void mm::GlobalRootSet::Iterator::Init() noexcept {
|
||||
case Phase::kGlobals:
|
||||
if (globalsIterator_ != owner_.globalsIterable_.end()) return;
|
||||
phase_ = Phase::kStableRefs;
|
||||
stableRefsIterator_ = owner_.stableRefsIterable_.begin();
|
||||
specialRefsIterator_ = owner_.specialRefsIterable_.begin();
|
||||
break;
|
||||
case Phase::kStableRefs:
|
||||
if (stableRefsIterator_ != owner_.stableRefsIterable_.end()) return;
|
||||
if (specialRefsIterator_ != owner_.specialRefsIterable_.end()) return;
|
||||
phase_ = Phase::kDone;
|
||||
break;
|
||||
case Phase::kDone:
|
||||
@@ -146,4 +146,4 @@ void mm::GlobalRootSet::Iterator::Init() noexcept {
|
||||
mm::ThreadRootSet::ThreadRootSet(ThreadData& threadData) noexcept : ThreadRootSet(threadData.shadowStack(), threadData.tls()) {}
|
||||
|
||||
mm::GlobalRootSet::GlobalRootSet() noexcept :
|
||||
GlobalRootSet(mm::GlobalData::Instance().globalsRegistry(), mm::GlobalData::Instance().stableRefRegistry()) {}
|
||||
GlobalRootSet(mm::GlobalData::Instance().globalsRegistry(), mm::GlobalData::Instance().specialRefRegistry()) {}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#include "GlobalsRegistry.hpp"
|
||||
#include "ShadowStack.hpp"
|
||||
#include "StableRefRegistry.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "ThreadLocalStorage.hpp"
|
||||
|
||||
struct ObjHeader;
|
||||
@@ -18,6 +18,7 @@ namespace mm {
|
||||
|
||||
class ThreadData;
|
||||
|
||||
// TODO: Extremely useless class. Remove.
|
||||
class ThreadRootSet {
|
||||
public:
|
||||
enum class Source {
|
||||
@@ -78,6 +79,7 @@ private:
|
||||
ThreadLocalStorage& tls_;
|
||||
};
|
||||
|
||||
// TODO: Extremely useless class. Remove.
|
||||
class GlobalRootSet {
|
||||
public:
|
||||
enum class Source {
|
||||
@@ -86,7 +88,7 @@ public:
|
||||
};
|
||||
|
||||
struct Value {
|
||||
ObjHeader*& object;
|
||||
ObjHeader* object;
|
||||
Source source;
|
||||
|
||||
bool operator==(const Value& rhs) const noexcept { return object == rhs.object && source == rhs.source; }
|
||||
@@ -123,12 +125,12 @@ public:
|
||||
Phase phase_;
|
||||
union {
|
||||
GlobalsRegistry::Iterator globalsIterator_;
|
||||
StableRefRegistry::Iterator stableRefsIterator_;
|
||||
SpecialRefRegistry::RootsIterator specialRefsIterator_;
|
||||
};
|
||||
};
|
||||
|
||||
GlobalRootSet(GlobalsRegistry& globalsRegistry, StableRefRegistry& stableRefRegistry) noexcept :
|
||||
globalsIterable_(globalsRegistry.LockForIter()), stableRefsIterable_(stableRefRegistry.LockForIter()) {}
|
||||
GlobalRootSet(GlobalsRegistry& globalsRegistry, SpecialRefRegistry& stableRefRegistry) noexcept :
|
||||
globalsIterable_(globalsRegistry.LockForIter()), specialRefsIterable_(stableRefRegistry.roots()) {}
|
||||
GlobalRootSet() noexcept;
|
||||
|
||||
Iterator begin() noexcept { return Iterator(Iterator::begin, *this); }
|
||||
@@ -138,7 +140,7 @@ private:
|
||||
// TODO: These use separate locks, which is inefficient, and slightly dangerous. In practice it's
|
||||
// fine, because this is the only place where these two locks are taken simultaneously.
|
||||
GlobalsRegistry::Iterable globalsIterable_;
|
||||
StableRefRegistry::Iterable stableRefsIterable_;
|
||||
SpecialRefRegistry::RootsIterable specialRefsIterable_;
|
||||
};
|
||||
|
||||
} // namespace mm
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "ShadowStack.hpp"
|
||||
#include "StableRef.hpp"
|
||||
#include "std_support/Memory.hpp"
|
||||
#include "std_support/Vector.hpp"
|
||||
|
||||
@@ -97,19 +98,19 @@ TEST(GlobalRootSetTest, Basic) {
|
||||
globalsProducer.Insert(&global1);
|
||||
globalsProducer.Insert(&global2);
|
||||
|
||||
mm::StableRefRegistry stableRefs;
|
||||
mm::StableRefRegistry::ThreadQueue stableRefsProducer(stableRefs);
|
||||
mm::SpecialRefRegistry specialRefsRegistry;
|
||||
mm::SpecialRefRegistry::ThreadQueue stableRefsProducer(specialRefsRegistry);
|
||||
ObjHeader* stableRef1 = reinterpret_cast<ObjHeader*>(3);
|
||||
ObjHeader* stableRef2 = reinterpret_cast<ObjHeader*>(4);
|
||||
ObjHeader* stableRef3 = reinterpret_cast<ObjHeader*>(5);
|
||||
stableRefsProducer.Insert(stableRef1);
|
||||
stableRefsProducer.Insert(stableRef2);
|
||||
stableRefsProducer.Insert(stableRef3);
|
||||
auto stableRefHandle1 = stableRefsProducer.createStableRef(stableRef1);
|
||||
auto stableRefHandle2 = stableRefsProducer.createStableRef(stableRef2);
|
||||
auto stableRefHandle3 = stableRefsProducer.createStableRef(stableRef3);
|
||||
|
||||
globalsProducer.Publish();
|
||||
stableRefsProducer.Publish();
|
||||
stableRefsProducer.publish();
|
||||
|
||||
mm::GlobalRootSet iter(globals, stableRefs);
|
||||
mm::GlobalRootSet iter(globals, specialRefsRegistry);
|
||||
|
||||
std_support::vector<mm::GlobalRootSet::Value> actual;
|
||||
for (auto object : iter) {
|
||||
@@ -120,15 +121,19 @@ TEST(GlobalRootSetTest, Basic) {
|
||||
auto asStableRef = [](ObjHeader*& object) -> mm::GlobalRootSet::Value { return {object, mm::GlobalRootSet::Source::kStableRef}; };
|
||||
EXPECT_THAT(
|
||||
actual,
|
||||
testing::ElementsAre(
|
||||
testing::UnorderedElementsAre(
|
||||
asGlobal(global1), asGlobal(global2), asStableRef(stableRef1), asStableRef(stableRef2), asStableRef(stableRef3)));
|
||||
|
||||
std::move(stableRefHandle1).dispose();
|
||||
std::move(stableRefHandle2).dispose();
|
||||
std::move(stableRefHandle3).dispose();
|
||||
}
|
||||
|
||||
TEST(GlobalRootSetTest, Empty) {
|
||||
mm::GlobalsRegistry globals;
|
||||
mm::StableRefRegistry stableRefs;
|
||||
mm::SpecialRefRegistry specialRefsRegistry;
|
||||
|
||||
mm::GlobalRootSet iter(globals, stableRefs);
|
||||
mm::GlobalRootSet iter(globals, specialRefsRegistry);
|
||||
|
||||
std_support::vector<mm::GlobalRootSet::Value> actual;
|
||||
for (auto object : iter) {
|
||||
|
||||
@@ -28,6 +28,12 @@ class ShadowStack : private Pinned {
|
||||
public:
|
||||
class Iterator {
|
||||
public:
|
||||
using difference_type = ptrdiff_t;
|
||||
using value_type = ObjHeader*;
|
||||
using pointer = value_type*;
|
||||
using reference = value_type&;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
||||
explicit Iterator(FrameOverlay* frame) noexcept : frame_(frame), object_(begin()), end_(end()) { Init(); }
|
||||
|
||||
ObjHeader*& operator*() noexcept { return *object_; }
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
|
||||
#include "GlobalData.hpp"
|
||||
#include "MemoryPrivate.hpp"
|
||||
#include "ObjCBackRef.hpp"
|
||||
#include "StableRef.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "ThreadState.hpp"
|
||||
#include "WeakRef.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
mm::StableRef mm::SpecialRefRegistry::ThreadQueue::createStableRef(ObjHeader* object) noexcept {
|
||||
return mm::StableRef(registerNode(object, 1, true).asRaw());
|
||||
}
|
||||
|
||||
mm::WeakRef mm::SpecialRefRegistry::ThreadQueue::createWeakRef(ObjHeader* object) noexcept {
|
||||
return mm::WeakRef(registerNode(object, 0, false).asRaw());
|
||||
}
|
||||
|
||||
mm::ObjCBackRef mm::SpecialRefRegistry::ThreadQueue::createObjCBackRef(ObjHeader* object) noexcept {
|
||||
return mm::ObjCBackRef(registerNode(object, 1, false).asRaw());
|
||||
}
|
||||
|
||||
void mm::SpecialRefRegistry::ThreadQueue::deleteNodeIfLocal(Node& node) noexcept {
|
||||
// This is a very weird optimization.
|
||||
// * We're saving some time during root scanning and some memory by
|
||||
// deleting some short-lived nodes without ever publishing them.
|
||||
// * But in order to do that we have to be in a runnable state, so
|
||||
// we potentially force a native state thread to go wait for the GC.
|
||||
if (node.owner_ == this) {
|
||||
queue_.erase(node.position_);
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
mm::SpecialRefRegistry& mm::SpecialRefRegistry::instance() noexcept {
|
||||
return GlobalData::Instance().specialRefRegistry();
|
||||
}
|
||||
|
||||
mm::SpecialRefRegistry::Node* mm::SpecialRefRegistry::nextRoot(Node* current) noexcept {
|
||||
RuntimeAssert(current != nullptr, "current cannot be null");
|
||||
RuntimeAssert(current != rootsTail(), "current cannot be tail");
|
||||
Node* candidate = current->nextRoot_.load(std::memory_order_relaxed);
|
||||
// Not an infinite loop, `candidate` always moves forward and since insertions can only
|
||||
// happen in the head, they will always happen before `candidate`.
|
||||
while (true) {
|
||||
RuntimeAssert(candidate != nullptr, "candidate cannot be null");
|
||||
if (candidate == rootsTail())
|
||||
// Reached tail, nothing to do anymore
|
||||
return candidate;
|
||||
if (candidate->rc_.load(std::memory_order_relaxed) > 0) {
|
||||
// Keeping acquire-release for nextRoot_.
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
// Perfectly good node. Stop right there.
|
||||
return candidate;
|
||||
}
|
||||
// Bad node. Let's remove it from the roots.
|
||||
// Racy if someone concurrently inserts in the middle. Or iterates.
|
||||
// But we don't have that here. Inserts are only in the beginning.
|
||||
// Iteration also happens only here.
|
||||
auto [candidatePrev, candidateNext] = eraseFromRoots(current, candidate);
|
||||
// We removed candidate. But should we have?
|
||||
if (candidate->rc_.load(std::memory_order_relaxed) > 0) {
|
||||
RuntimeAssert(candidate->obj_ != nullptr, "candidate cannot have a null obj_");
|
||||
// Ooops. Let's put it back. Okay to put into the head.
|
||||
insertIntoRootsHead(*candidate);
|
||||
}
|
||||
// eraseFromRoots and insertIntoRootsHead are both acquire-release fences.
|
||||
// This means they play nice with each other and we don't need an extra fence
|
||||
// here to ensure synchronization with 0->1 rc_ change:
|
||||
// * We read rc_ after eraseFromRoots.
|
||||
// * retainRef writes rc_ before insertIntoRootsHead.
|
||||
// So the write to rc_ in retainRef happens before the read here.
|
||||
//
|
||||
// Okay, properly deleted. Our new `candidate` is the next of previous candidate,
|
||||
// and our `current` then is our best guess at the previous node of the `candidate`.
|
||||
current = candidatePrev;
|
||||
candidate = candidateNext;
|
||||
// `current` has either moved forward or stayed where it is.
|
||||
// `candidate` has definitely moved forward.
|
||||
// `current` is only used in `eraseFromRoots` which itself ensures that no
|
||||
// infinite loop can happen.
|
||||
// So, this loop is also not infinite.
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<mm::SpecialRefRegistry::Node*, mm::SpecialRefRegistry::Node*> mm::SpecialRefRegistry::eraseFromRoots(
|
||||
Node* prev, Node* node) noexcept {
|
||||
RuntimeAssert(node != rootsHead(), "node cannot be head");
|
||||
RuntimeAssert(node != rootsTail(), "node cannot be tail");
|
||||
Node* next = node->nextRoot_.load(std::memory_order_acquire);
|
||||
RuntimeAssert(next != nullptr, "node@%p next cannot be null", node);
|
||||
do {
|
||||
Node* prevExpectedNext = node;
|
||||
bool removed =
|
||||
prev->nextRoot_.compare_exchange_strong(prevExpectedNext, next, std::memory_order_release, std::memory_order_acquire);
|
||||
if (removed) {
|
||||
auto* actualNext = node->nextRoot_.exchange(nullptr, std::memory_order_acq_rel);
|
||||
RuntimeAssert(actualNext == next, "node@%p next expected %p actual %p", node, next, actualNext);
|
||||
return {prev, next};
|
||||
}
|
||||
prev = prevExpectedNext;
|
||||
RuntimeAssert(prev != rootsHead(), "prev cannot be head");
|
||||
RuntimeAssert(prev != rootsTail(), "prev cannot be tail");
|
||||
// We moved `prev` forward, nothing can insert after `prev` anymore, this
|
||||
// cannot be an infinite loop, then.
|
||||
} while (true);
|
||||
}
|
||||
|
||||
void mm::SpecialRefRegistry::insertIntoRootsHead(Node& node) noexcept {
|
||||
Node* next = rootsHead()->nextRoot_.load(std::memory_order_acquire);
|
||||
Node* nodeExpectedNext = nullptr;
|
||||
do {
|
||||
RuntimeAssert(next != nullptr, "head's next cannot be null");
|
||||
if (!node.nextRoot_.compare_exchange_strong(nodeExpectedNext, next, std::memory_order_release, std::memory_order_acquire)) {
|
||||
// So:
|
||||
// * `node` is already in the roots list
|
||||
// * some other thread is inserting it in the roots list
|
||||
// * GC thread may be removing it from the roots list, but
|
||||
// will recheck rc afterwards and insert it back if needed
|
||||
// In either case, do not touch anything anymore here.
|
||||
return;
|
||||
}
|
||||
// CAS was successfull, so we need to update the expected value of node.nextRoot_
|
||||
nodeExpectedNext = next;
|
||||
} while (!rootsHead()->nextRoot_.compare_exchange_weak(next, &node, std::memory_order_release, std::memory_order_acquire));
|
||||
}
|
||||
|
||||
std_support::list<mm::SpecialRefRegistry::Node>::iterator mm::SpecialRefRegistry::findAliveNode(
|
||||
std_support::list<Node>::iterator it) noexcept {
|
||||
while (it != all_.end() && it->rc_.load(std::memory_order_relaxed) == Node::disposedMarker) {
|
||||
// Synchronization with `Node::dispose()`
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
// Removing disposed nodes.
|
||||
if (it->nextRoot_.load(std::memory_order_relaxed) != nullptr) {
|
||||
// Wait, it's in the roots list. Lets wait until the next GC
|
||||
// for it to get cleaned up from there.
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
// If we observe both `nextRoot_ == nullptr` and `rc_ == disposedMarker`, we
|
||||
// can be sure, that no mutator can change `nextRoot_` later.
|
||||
// Proof:
|
||||
// For the mutator to change `nextRoot_`, it would need to perform 0->1 `rc_` transition.
|
||||
// But `rc_` can only be set to `disposedMarker` during `dispose()` call,
|
||||
// which can only happen after any `retainRef()` call. So, it's impossible
|
||||
// to observe any changes in `nextRoot_` after we observe `disposedMarker` in `nextRoot_`.
|
||||
it = all_.erase(it);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "Memory.h"
|
||||
#include "RawPtr.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
#include "std_support/List.hpp"
|
||||
|
||||
namespace kotlin::mm {
|
||||
|
||||
class ObjCBackRef;
|
||||
class StableRef;
|
||||
class WeakRef;
|
||||
|
||||
// Registry for all special references to objects:
|
||||
// * stable references (i.e. always part of the root set)
|
||||
// * weak references
|
||||
// * ObjC back references. A mix between stable and weak references
|
||||
// created for ObjC part of Kotlin objects. Have a count of external
|
||||
// references. When > 0 - stable reference. When = 0 - weak reference.
|
||||
//
|
||||
// Consists of 2 global lists and 1 thread local list of `Node`s.
|
||||
// Each `Node` has a reference `obj_` to an object and an external references counter `rc_`.
|
||||
// Invariants:
|
||||
// * `rc_ > 0` -> alive externally referenced object, must be in the root set.
|
||||
// * `rc_ == 0` -> alive externally unreferenced object, must eventually be out of the root set.
|
||||
// * `rc_ == disposedMarker` -> the `Node` itself is no longer externally referenced and can be eventually deleted.
|
||||
// * `rc_` can be increased and decreased by any thread in any state.
|
||||
// * In practice 0 -> 1 only happens in mutator threads in runnable state, but the implementation of `SpecialRefRegistry`
|
||||
// does not depend on it.
|
||||
// * `Node`s are owned either by a global `std::list` or by thread local `std::list`s. Global list is protected by a mutex.
|
||||
// * Insertion into the global list happens by moving from thread local list during STW and when a thread gets destroyed.
|
||||
// * Only the GC thread traverses and removes elements from the global list. Removal happens only if `Node` has `rc_ == disposedMarker`
|
||||
// and it's not in the roots list.
|
||||
// * During global list traversal `Node`s `obj_` referenced may get nulled out by the GC.
|
||||
// * Insertion into thread local lists happens in runnable state.
|
||||
// * Removal from thread local list happens during STW, thread destruction, or in the runnable state for `Node`s that can never
|
||||
// go through 0 -> 1 rc transition (created via mm::StableRef).
|
||||
// * `Node`s are additionally linked into an intrusive global roots list.
|
||||
// * Any thread in any state can insert into the roots list. Insertion only happens into the head.
|
||||
// * Only the GC thread can remove from the roots list during root scanning. If after removal
|
||||
// the `rc_` of the `Node` is `> 0`, the GC thread will make sure the node is inserted
|
||||
// into the head
|
||||
// * During roots list traversal all nodes to the left are either marked or inserted into the mark queue.
|
||||
class SpecialRefRegistry : private Pinned {
|
||||
// TODO: Consider using a real mutex.
|
||||
using Mutex = SpinLock<MutexThreadStateHandling::kIgnore>;
|
||||
|
||||
class Node : private Pinned {
|
||||
public:
|
||||
using Rc = int32_t;
|
||||
inline static constexpr Rc disposedMarker = std::numeric_limits<Rc>::min();
|
||||
static_assert(disposedMarker < 0, "disposedMarker must be an impossible Rc value");
|
||||
|
||||
Node(ObjHeader* obj, Rc rc) noexcept : obj_(obj), rc_(rc) {
|
||||
RuntimeAssert(obj != nullptr, "Creating StableRef for null object");
|
||||
RuntimeAssert(rc >= 0, "Creating StableRef with negative rc %d", rc);
|
||||
}
|
||||
|
||||
~Node() {
|
||||
if (compiler::runtimeAssertsEnabled()) {
|
||||
auto rc = rc_.load(std::memory_order_relaxed);
|
||||
RuntimeAssert(rc == disposedMarker, "Deleting StableRef@%p with rc %d", this, rc);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() noexcept {
|
||||
// Synchronization with `findAliveNode()`.
|
||||
// TODO: When assertions are disabled, exchange may pollute the
|
||||
// generated assembly. Check if this a problem.
|
||||
auto rc = rc_.exchange(disposedMarker, std::memory_order_release);
|
||||
if (compiler::runtimeAssertsEnabled()) {
|
||||
if (rc > 0) {
|
||||
// In objc export if ObjCClass extends from KtClass
|
||||
// doing retain+autorelease inside [ObjCClass dealloc] will cause
|
||||
// this->dispose() be called after this->retain() but before
|
||||
// subsequent this->release().
|
||||
// However, since this happens in dealloc, the stored object must
|
||||
// have been cleared already.
|
||||
RuntimeAssert(obj_ == nullptr, "Disposing StableRef@%p with rc %d and uncleaned object %p", this, rc, obj_);
|
||||
}
|
||||
RuntimeAssert(rc >= 0, "Disposing StableRef@%p with rc %d", this, rc);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard("expensive pure function")]] ObjHeader* ref() const noexcept {
|
||||
if (compiler::runtimeAssertsEnabled()) {
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
auto rc = rc_.load(std::memory_order_relaxed);
|
||||
RuntimeAssert(rc >= 0, "Dereferencing StableRef@%p with rc %d", this, rc);
|
||||
}
|
||||
return obj_;
|
||||
}
|
||||
|
||||
OBJ_GETTER0(tryRef) noexcept {
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
// TODO: Weak read barrier with CMS.
|
||||
RETURN_OBJ(obj_);
|
||||
}
|
||||
|
||||
void retainRef() noexcept {
|
||||
auto rc = rc_.fetch_add(1, std::memory_order_relaxed);
|
||||
RuntimeAssert(rc >= 0, "Retaining StableRef@%p with rc %d", this, rc);
|
||||
if (rc == 0) {
|
||||
RuntimeAssert(
|
||||
position_ == std_support::list<Node>::iterator{},
|
||||
"Retaining StableRef@%p with fast deletion optimization is disallowed", this);
|
||||
|
||||
if (!obj_) {
|
||||
// In objc export if ObjCClass extends from KtClass
|
||||
// calling retain inside [ObjCClass dealloc] will cause
|
||||
// node.retainRef() be called after node.obj_ was cleared but
|
||||
// before node.dispose().
|
||||
// We could place it into the root set, and it'll be removed
|
||||
// from it at some later point. But let's just skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: With CMS barrier for marking `obj_` should be here.
|
||||
// Until we have the barrier, the object must already be in the roots.
|
||||
// If 0->1 happened from `[ObjCClass _tryRetain]`, it would first hold the object
|
||||
// on the stack via `tryRef`.
|
||||
// If 0->1 happened during construction:
|
||||
// * First of all, currently it's impossible because the `Node` is created with rc=1 and not inserted
|
||||
// into the roots list until publishing.
|
||||
// * Even if the above changes, for the construction, the object must have been passed in from somewhere,
|
||||
// so it must be reachable anyway.
|
||||
// If 0->1 happened because an object is passing through the interop border for the second time (or more)
|
||||
// (e.g. accessing a non-permanent global a couple of times). Follows the construction case above:
|
||||
// "the object must have been passed in from somewhere, so it must be reachable anyway".
|
||||
|
||||
// 0->1 changes require putting this node into the root set.
|
||||
SpecialRefRegistry::instance().insertIntoRootsHead(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void releaseRef() noexcept {
|
||||
auto rc = rc_.fetch_sub(1, std::memory_order_relaxed);
|
||||
RuntimeAssert(rc > 0, "Releasing StableRef@%p with rc %d", this, rc);
|
||||
}
|
||||
|
||||
RawSpecialRef* asRaw() noexcept { return reinterpret_cast<RawSpecialRef*>(this); }
|
||||
static Node* fromRaw(RawSpecialRef* ref) noexcept { return reinterpret_cast<Node*>(ref); }
|
||||
|
||||
private:
|
||||
friend class SpecialRefRegistry;
|
||||
friend class SpecialRefRegistryTest;
|
||||
|
||||
// obj_ is set in the constructor and can be nulled out only by the
|
||||
// GC thread when processing weaks. It's the responsibility of the
|
||||
// GC to make sure nulling out obj_ is synchronized with mutators:
|
||||
// * via STW: nulling obj_ only happens when mutators are paused.
|
||||
// * via weak read barriers: when GC enters a weak processing phase,
|
||||
// it enables weak read barriers which do not read obj_ if obj_ will
|
||||
// be nulled, and disable the barriers when the phase is completed.
|
||||
// Synchronization between GC and mutators happens via enabling/disabling
|
||||
// the barriers.
|
||||
ObjHeader* obj_ = nullptr;
|
||||
// Only ever updated using relaxed memory ordering. Any synchronization
|
||||
// with nextRoot_ is achieved via acquire-release of nextRoot_.
|
||||
std::atomic<Rc> rc_ = 0; // After dispose() will be disposedMarker.
|
||||
// Singly linked lock free list. Using acquire-release throughout.
|
||||
std::atomic<Node*> nextRoot_ = nullptr;
|
||||
// This and the next one only serve fast deletion optimization for shortly lived StableRefs.
|
||||
// TODO: Consider discarding this optimization completely.
|
||||
// If we were to use custom allocator for these nodes as well they better
|
||||
// be only deleted in the sweep anyway.
|
||||
// Alternative: keep stable refs completely separate.
|
||||
void* owner_ = nullptr;
|
||||
std_support::list<Node>::iterator position_{};
|
||||
};
|
||||
|
||||
public:
|
||||
class ThreadQueue : private Pinned {
|
||||
public:
|
||||
explicit ThreadQueue(SpecialRefRegistry& registry) : owner_(registry) {}
|
||||
|
||||
~ThreadQueue() { publish(); }
|
||||
|
||||
void publish() noexcept {
|
||||
for (auto& node : queue_) {
|
||||
// No need to synchronize. These two can only be updated in the runnable state.
|
||||
// TODO: If we were to remove this optimization, we could avoid scanning
|
||||
// the whole queue here and just have the nodes inserted into the roots
|
||||
// when they're created.
|
||||
node.owner_ = nullptr;
|
||||
node.position_ = std_support::list<Node>::iterator();
|
||||
RuntimeAssert(node.obj_ != nullptr, "Publishing Node with null obj_");
|
||||
// If the node was created with a positive refcount, we must ensure its put into
|
||||
// the roots.
|
||||
auto rc = node.rc_.load(std::memory_order_relaxed);
|
||||
if (rc > 0) {
|
||||
// Regular publishing happens before the global root scanning,
|
||||
// so this insertion will definitely be processed.
|
||||
// Publishing during the thread destruction is a bit more complicated.
|
||||
// But the GC makes sure to process all threads before scanning global
|
||||
// roots. So, it'll either publish the dying thread itself, or
|
||||
// if the dying thread has already deregistered, it means it published
|
||||
// itself. In any case, global root scanning happens afterwards.
|
||||
// TODO: With CMS barrier for marking `node.obj_` should be here.
|
||||
owner_.insertIntoRootsHead(node);
|
||||
}
|
||||
}
|
||||
std::unique_lock guard(owner_.mutex_);
|
||||
RuntimeAssert(owner_.all_.get_allocator() == queue_.get_allocator(), "allocators must match");
|
||||
owner_.all_.splice(owner_.all_.end(), std::move(queue_));
|
||||
}
|
||||
|
||||
void clearForTests() noexcept { queue_.clear(); }
|
||||
|
||||
[[nodiscard("must be manually disposed")]] StableRef createStableRef(ObjHeader* object) noexcept;
|
||||
[[nodiscard("must be manually disposed")]] WeakRef createWeakRef(ObjHeader* object) noexcept;
|
||||
[[nodiscard("must be manually disposed")]] ObjCBackRef createObjCBackRef(ObjHeader* object) noexcept;
|
||||
|
||||
private:
|
||||
friend class StableRef;
|
||||
friend class SpecialRefRegistryTest;
|
||||
|
||||
[[nodiscard("must be manually disposed")]] Node& registerNode(ObjHeader* obj, Node::Rc rc, bool allowFastDeletion) noexcept {
|
||||
RuntimeAssert(obj != nullptr, "Creating node for null object");
|
||||
queue_.emplace_back(obj, rc);
|
||||
auto& node = queue_.back();
|
||||
if (allowFastDeletion) {
|
||||
node.owner_ = this;
|
||||
node.position_ = std::prev(queue_.end());
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
void deleteNodeIfLocal(Node& node) noexcept;
|
||||
|
||||
SpecialRefRegistry& owner_;
|
||||
std_support::list<Node> queue_;
|
||||
};
|
||||
|
||||
class RootsIterator {
|
||||
public:
|
||||
ObjHeader* operator*() const noexcept {
|
||||
// Ignoring rc here. If someone nulls out rc during root
|
||||
// scanning, it's okay to be conservative and still make it a root.
|
||||
return node_->obj_;
|
||||
}
|
||||
|
||||
RootsIterator& operator++() noexcept {
|
||||
node_ = owner_->nextRoot(node_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const RootsIterator& rhs) const noexcept { return node_ == rhs.node_; }
|
||||
|
||||
bool operator!=(const RootsIterator& rhs) const noexcept { return !(*this == rhs); }
|
||||
|
||||
private:
|
||||
friend class SpecialRefRegistry;
|
||||
|
||||
RootsIterator(SpecialRefRegistry& owner, Node* node) noexcept : owner_(&owner), node_(node) {}
|
||||
|
||||
SpecialRefRegistry* owner_;
|
||||
Node* node_;
|
||||
};
|
||||
|
||||
class RootsIterable : private MoveOnly {
|
||||
public:
|
||||
RootsIterator begin() const noexcept { return RootsIterator(*owner_, owner_->nextRoot(owner_->rootsHead())); }
|
||||
|
||||
RootsIterator end() const noexcept { return RootsIterator(*owner_, owner_->rootsTail()); }
|
||||
|
||||
private:
|
||||
friend class SpecialRefRegistry;
|
||||
|
||||
explicit RootsIterable(SpecialRefRegistry& owner) noexcept : owner_(&owner) {}
|
||||
|
||||
raw_ptr<SpecialRefRegistry> owner_;
|
||||
};
|
||||
|
||||
class Iterator {
|
||||
public:
|
||||
ObjHeader*& operator*() noexcept { return iterator_->obj_; }
|
||||
|
||||
Iterator& operator++() noexcept {
|
||||
iterator_ = owner_->findAliveNode(std::next(iterator_));
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const Iterator& rhs) const noexcept { return iterator_ == rhs.iterator_; }
|
||||
|
||||
bool operator!=(const Iterator& rhs) const noexcept { return iterator_ != rhs.iterator_; }
|
||||
|
||||
private:
|
||||
friend class SpecialRefRegistry;
|
||||
friend class SpecialRefRegistryTest;
|
||||
|
||||
Iterator(SpecialRefRegistry& owner, std_support::list<Node>::iterator iterator) noexcept : owner_(&owner), iterator_(iterator) {}
|
||||
|
||||
SpecialRefRegistry* owner_;
|
||||
std_support::list<Node>::iterator iterator_;
|
||||
};
|
||||
|
||||
class Iterable : private MoveOnly {
|
||||
public:
|
||||
Iterator begin() noexcept { return Iterator(owner_, owner_.findAliveNode(owner_.all_.begin())); }
|
||||
Iterator end() noexcept { return Iterator(owner_, owner_.all_.end()); }
|
||||
|
||||
private:
|
||||
friend class SpecialRefRegistry;
|
||||
|
||||
Iterable(SpecialRefRegistry& owner) noexcept : owner_(owner), guard_(owner_.mutex_) {}
|
||||
|
||||
SpecialRefRegistry& owner_;
|
||||
std::unique_lock<Mutex> guard_;
|
||||
};
|
||||
|
||||
SpecialRefRegistry() noexcept { rootsHead()->nextRoot_.store(rootsTail(), std::memory_order_relaxed); }
|
||||
|
||||
~SpecialRefRegistry() = default;
|
||||
|
||||
static SpecialRefRegistry& instance() noexcept;
|
||||
|
||||
void clearForTests() noexcept {
|
||||
rootsHead()->nextRoot_ = rootsTail();
|
||||
for (auto& node : all_) {
|
||||
// Allow the tests not to run the finalizers for weaks.
|
||||
node.rc_ = Node::disposedMarker;
|
||||
}
|
||||
all_.clear();
|
||||
}
|
||||
|
||||
// Should be called on the GC thread after all threads have published.
|
||||
RootsIterable roots() noexcept { return RootsIterable(*this); }
|
||||
|
||||
// Should be called on the GC thread after marking is complete.
|
||||
// Locks the registry and allows safe iteration over it.
|
||||
Iterable lockForIter() noexcept { return Iterable(*this); }
|
||||
|
||||
private:
|
||||
friend class ObjCBackRef;
|
||||
friend class StableRef;
|
||||
friend class WeakRef;
|
||||
friend class SpecialRefRegistryTest;
|
||||
|
||||
Node* nextRoot(Node* current) noexcept;
|
||||
// Erase `node` from the roots list. `prev` is the current guess of the node
|
||||
// previous to `node`. Returns two nodes between which `node` was deleted.
|
||||
std::pair<Node*, Node*> eraseFromRoots(Node* prev, Node* node) noexcept;
|
||||
void insertIntoRootsHead(Node& node) noexcept;
|
||||
std_support::list<Node>::iterator findAliveNode(std_support::list<Node>::iterator it) noexcept;
|
||||
|
||||
Node* rootsHead() noexcept { return reinterpret_cast<Node*>(rootsHeadStorage_); }
|
||||
const Node* rootsHead() const noexcept { return reinterpret_cast<const Node*>(rootsHeadStorage_); }
|
||||
static Node* rootsTail() noexcept { return reinterpret_cast<Node*>(rootsTailStorage_); }
|
||||
|
||||
// TODO: Iteration over `all_` will be slow, because it's `std_support::list`
|
||||
// collected at different times from different threads, and so the nodes
|
||||
// are all over the memory. Consider using custom allocator for that.
|
||||
std_support::list<Node> all_;
|
||||
Mutex mutex_;
|
||||
alignas(Node) char rootsHeadStorage_[sizeof(Node)] = {0};
|
||||
alignas(Node) static inline char rootsTailStorage_[sizeof(Node)] = {0};
|
||||
};
|
||||
|
||||
} // namespace kotlin::mm
|
||||
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "ObjCBackRef.hpp"
|
||||
#include "StableRef.hpp"
|
||||
#include "TestSupport.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
#include "WeakRef.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
namespace {
|
||||
|
||||
class Waiter : private Pinned {
|
||||
public:
|
||||
void allow() noexcept {
|
||||
{
|
||||
std::unique_lock guard(mutex_);
|
||||
allow_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
};
|
||||
|
||||
void wait() noexcept {
|
||||
std::unique_lock guard(mutex_);
|
||||
cv_.wait(guard, [this] { return allow_; });
|
||||
}
|
||||
|
||||
private:
|
||||
bool allow_ = false;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class SpecialRefRegistryTest : public testing::Test {
|
||||
public:
|
||||
~SpecialRefRegistryTest() {
|
||||
// Clean up safely.
|
||||
roots();
|
||||
all();
|
||||
}
|
||||
|
||||
void publish() noexcept { mm::ThreadRegistry::Instance().CurrentThreadData()->specialRefRegistry().publish(); }
|
||||
|
||||
template <typename... Invalidated>
|
||||
std::vector<ObjHeader*> all(Invalidated&&... invalidated) noexcept {
|
||||
std::set<ObjHeader*> invalidatedSet({std::forward<Invalidated>(invalidated)...});
|
||||
std::vector<ObjHeader*> result;
|
||||
for (auto& obj : mm::SpecialRefRegistry::instance().lockForIter()) {
|
||||
if (invalidatedSet.find(obj) != invalidatedSet.end()) {
|
||||
obj = nullptr;
|
||||
}
|
||||
result.push_back(obj);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<ObjHeader*> roots() noexcept {
|
||||
std::vector<ObjHeader*> result;
|
||||
for (auto* obj : mm::SpecialRefRegistry::instance().roots()) {
|
||||
result.push_back(obj);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ObjHeader* tryRef(mm::WeakRef& weakRef) noexcept {
|
||||
ObjHeader* result;
|
||||
return weakRef.tryRef(&result);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterStableRefWithoutPublish) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::StableRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterStableRef) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::StableRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterWeakRefWithoutPublish) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::WeakRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(tryRef(ref), obj);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterWeakRef) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::WeakRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(tryRef(ref), obj);
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(tryRef(ref), obj);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterObjCRefWithoutPublish) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::ObjCBackRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
ref.release();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterObjCRef) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::ObjCBackRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
ref.release();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterAllRefsWithoutPublish) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref1 = mm::StableRef::create(obj);
|
||||
auto ref2 = mm::WeakRef::create(obj);
|
||||
auto ref3 = mm::ObjCBackRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
|
||||
std::move(ref1).dispose();
|
||||
std::move(ref2).dispose();
|
||||
ref3.release();
|
||||
std::move(ref3).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, RegisterAllRefs) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref1 = mm::StableRef::create(obj);
|
||||
auto ref2 = mm::WeakRef::create(obj);
|
||||
auto ref3 = mm::ObjCBackRef::create(obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre(obj, obj));
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj, obj, obj));
|
||||
|
||||
std::move(ref1).dispose();
|
||||
std::move(ref2).dispose();
|
||||
ref3.release();
|
||||
std::move(ref3).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, InvalidateWeakRef) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::WeakRef::create(obj);
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(obj), testing::UnorderedElementsAre(nullptr));
|
||||
EXPECT_THAT(tryRef(ref), nullptr);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(nullptr));
|
||||
EXPECT_THAT(tryRef(ref), nullptr);
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, InvalidateObjCRef) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::ObjCBackRef::create(obj);
|
||||
ref.release();
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(obj), testing::UnorderedElementsAre(nullptr));
|
||||
EXPECT_FALSE(ref.tryRetainForTests());
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(nullptr));
|
||||
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, TryObjCRef) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::ObjCBackRef::create(obj);
|
||||
ref.release();
|
||||
publish();
|
||||
|
||||
EXPECT_TRUE(ref.tryRetainForTests());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
ref.release();
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, ReRetainObjCRefBeforePublish) {
|
||||
RunInNewThread([this] {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
auto ref = mm::ObjCBackRef::create(obj);
|
||||
ref.release();
|
||||
ref.retain();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
publish();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre(obj));
|
||||
EXPECT_THAT(*ref, obj);
|
||||
|
||||
ref.release();
|
||||
std::move(ref).dispose();
|
||||
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, StressStableRef) {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
Waiter waiter;
|
||||
std::vector<ScopedThread> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back([&, this] {
|
||||
ScopedMemoryInit scope;
|
||||
ObjHolder holder(obj);
|
||||
waiter.wait();
|
||||
auto ref = mm::StableRef::create(obj);
|
||||
publish();
|
||||
std::move(ref).dispose();
|
||||
});
|
||||
}
|
||||
waiter.allow();
|
||||
mutators.clear();
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, StressWeakRef) {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
Waiter waiter;
|
||||
std::vector<ScopedThread> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back([&, this] {
|
||||
ScopedMemoryInit scope;
|
||||
ObjHolder holder(obj);
|
||||
waiter.wait();
|
||||
auto ref = mm::WeakRef::create(obj);
|
||||
publish();
|
||||
std::move(ref).dispose();
|
||||
});
|
||||
}
|
||||
waiter.allow();
|
||||
mutators.clear();
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, StressObjCRef) {
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
Waiter waiter;
|
||||
std::vector<ScopedThread> mutators;
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back([&, this] {
|
||||
ScopedMemoryInit scope;
|
||||
ObjHolder holder(obj);
|
||||
waiter.wait();
|
||||
auto ref = mm::ObjCBackRef::create(obj);
|
||||
publish();
|
||||
ref.release();
|
||||
std::move(ref).dispose();
|
||||
});
|
||||
}
|
||||
waiter.allow();
|
||||
mutators.clear();
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
}
|
||||
|
||||
TEST_F(SpecialRefRegistryTest, StressObjCRefRetainRelease) {
|
||||
RunInNewThread([this] {
|
||||
constexpr int kGCCycles = 10000;
|
||||
constexpr int kRefsCount = 3;
|
||||
ObjHeader* obj = reinterpret_cast<ObjHeader*>(1);
|
||||
ObjHolder holder(obj);
|
||||
Waiter waiter;
|
||||
std::atomic<bool> canStop = false;
|
||||
std::vector<mm::ObjCBackRef> refs;
|
||||
for (int i = 0; i < kRefsCount; ++i) {
|
||||
refs.emplace_back(mm::ObjCBackRef::create(obj));
|
||||
refs.back().release();
|
||||
}
|
||||
publish();
|
||||
std::vector<ScopedThread> mutators;
|
||||
mutators.emplace_back([&, this] {
|
||||
waiter.wait();
|
||||
for (int i = 0; i < kGCCycles; ++i) {
|
||||
roots();
|
||||
all();
|
||||
}
|
||||
canStop.store(true, std::memory_order_release);
|
||||
});
|
||||
for (int i = 0; i < kDefaultThreadCount; ++i) {
|
||||
mutators.emplace_back([i, obj, &refs, &waiter, &canStop] {
|
||||
ScopedMemoryInit scope;
|
||||
ObjHolder holder(obj);
|
||||
waiter.wait();
|
||||
auto& ref = refs[i % kRefsCount];
|
||||
while (!canStop.load(std::memory_order_acquire)) {
|
||||
ref.retain();
|
||||
ref.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
waiter.allow();
|
||||
mutators.clear();
|
||||
for (auto& ref : refs) {
|
||||
std::move(ref).dispose();
|
||||
}
|
||||
EXPECT_THAT(roots(), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(all(), testing::UnorderedElementsAre());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "StableRef.hpp"
|
||||
|
||||
#include "MemoryPrivate.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
#include "ThreadState.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
// static
|
||||
mm::StableRef mm::StableRef::create(ObjHeader* obj) noexcept {
|
||||
RuntimeAssert(obj != nullptr, "Creating StableRef for null object");
|
||||
return mm::ThreadRegistry::Instance().CurrentThreadData()->specialRefRegistry().createStableRef(obj);
|
||||
}
|
||||
|
||||
// static
|
||||
void mm::StableRef::tryToDeleteImmediately(raw_ptr<SpecialRefRegistry::Node> node) noexcept {
|
||||
// When we're on the registered thread, perform oportunistic quick deletion.
|
||||
if (auto* threadNode = mm::ThreadRegistry::Instance().CurrentThreadDataNodeOrNull()) {
|
||||
tryToDeleteImmediately(*threadNode->Get(), std::move(node));
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
void mm::StableRef::tryToDeleteImmediately(mm::ThreadData& thread, raw_ptr<SpecialRefRegistry::Node> node) noexcept {
|
||||
auto lastState = SwitchThreadState(&thread, ThreadState::kRunnable, /* reentrant = */ true);
|
||||
thread.specialRefRegistry().deleteNodeIfLocal(*node);
|
||||
SwitchThreadState(&thread, lastState, /* reentrant = */ true);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Memory.h"
|
||||
#include "RawPtr.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace kotlin::mm {
|
||||
|
||||
class ThreadData;
|
||||
|
||||
// Stable reference to a Kotlin object.
|
||||
// Every stable reference makes Kotlin object be in the root set.
|
||||
// Use `create` and `dispose` to create and destroy the stable reference.
|
||||
class StableRef : private MoveOnly {
|
||||
public:
|
||||
StableRef() noexcept = default;
|
||||
|
||||
// Cast raw ref into a stable reference.
|
||||
explicit StableRef(RawSpecialRef* raw) noexcept : node_(SpecialRefRegistry::Node::fromRaw(raw)) {}
|
||||
|
||||
// Cast stable reference into raw ref.
|
||||
[[nodiscard("must be manually disposed")]] explicit operator RawSpecialRef*() && noexcept {
|
||||
// Make sure to move out from node_.
|
||||
auto node = std::move(node_);
|
||||
return node->asRaw();
|
||||
}
|
||||
|
||||
// Create new stable reference for `obj`.
|
||||
[[nodiscard("must be manually disposed")]] static StableRef create(ObjHeader* obj) noexcept;
|
||||
|
||||
// Dispose stable reference.
|
||||
void dispose() && noexcept {
|
||||
auto node = std::move(*this).disposeImpl();
|
||||
tryToDeleteImmediately(std::move(node));
|
||||
}
|
||||
|
||||
// Dispose stable reference using `thread` for opportunistic deletion.
|
||||
// Note: `thread` should still be the current thread, and it's used
|
||||
// when the thread is being destroyed and its TLS deallocating.
|
||||
void disposeOn(mm::ThreadData& thread) && noexcept {
|
||||
auto node = std::move(*this).disposeImpl();
|
||||
tryToDeleteImmediately(thread, std::move(node));
|
||||
}
|
||||
|
||||
// Get the underlying object.
|
||||
// Always safe, because the object is guaranteed to be in the root set.
|
||||
[[nodiscard("expensive pure function")]] ObjHeader* operator*() const noexcept {
|
||||
RuntimeAssert(node_, "operator* on null StableRef");
|
||||
return node_->ref();
|
||||
}
|
||||
|
||||
static StableRef& reinterpret(RawSpecialRef*& raw) noexcept { return reinterpret_cast<StableRef&>(raw); }
|
||||
|
||||
static const StableRef& reinterpret(RawSpecialRef* const& raw) noexcept { return reinterpret_cast<const StableRef&>(raw); }
|
||||
|
||||
private:
|
||||
raw_ptr<SpecialRefRegistry::Node> disposeImpl() && noexcept {
|
||||
RuntimeAssert(node_, "Disposing null StableRef");
|
||||
// Make sure to move out from node_.
|
||||
auto node = std::move(node_);
|
||||
// Can be safely called with any thread state.
|
||||
node->releaseRef();
|
||||
// Can be safely called with any thread state.
|
||||
node->dispose();
|
||||
return node;
|
||||
}
|
||||
|
||||
static void tryToDeleteImmediately(raw_ptr<SpecialRefRegistry::Node> node) noexcept;
|
||||
static void tryToDeleteImmediately(mm::ThreadData& thread, raw_ptr<SpecialRefRegistry::Node> node) noexcept;
|
||||
|
||||
raw_ptr<SpecialRefRegistry::Node> node_;
|
||||
};
|
||||
|
||||
static_assert(sizeof(StableRef) == sizeof(void*), "StableRef must be a thin wrapper around pointer");
|
||||
static_assert(alignof(StableRef) == alignof(void*), "StableRef must be a thin wrapper around pointer");
|
||||
static_assert(std::is_trivially_destructible_v<StableRef>, "StableRef must be trivially destructible. Destruction is manual via dispose()");
|
||||
|
||||
} // namespace kotlin::mm
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "StableRefRegistry.hpp"
|
||||
|
||||
#include "GlobalData.hpp"
|
||||
#include "ThreadData.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
// static
|
||||
mm::StableRefRegistry& mm::StableRefRegistry::Instance() noexcept {
|
||||
return GlobalData::Instance().stableRefRegistry();
|
||||
}
|
||||
|
||||
mm::StableRefRegistry::Node* mm::StableRefRegistry::RegisterStableRef(mm::ThreadData* threadData, ObjHeader* object) noexcept {
|
||||
return threadData->stableRefThreadQueue().Insert(object);
|
||||
}
|
||||
|
||||
void mm::StableRefRegistry::UnregisterStableRef(mm::ThreadData* threadData, Node* node) noexcept {
|
||||
threadData->stableRefThreadQueue().Erase(node);
|
||||
}
|
||||
|
||||
void mm::StableRefRegistry::ProcessThread(mm::ThreadData* threadData) noexcept {
|
||||
threadData->stableRefThreadQueue().Publish();
|
||||
}
|
||||
|
||||
void mm::StableRefRegistry::ProcessDeletions() noexcept {
|
||||
stableRefs_.ApplyDeletions();
|
||||
}
|
||||
|
||||
mm::StableRefRegistry::StableRefRegistry() = default;
|
||||
mm::StableRefRegistry::~StableRefRegistry() = default;
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#ifndef RUNTIME_MM_STABLE_REF_REGISTRY_H
|
||||
#define RUNTIME_MM_STABLE_REF_REGISTRY_H
|
||||
|
||||
#include "Memory.h"
|
||||
#include "MultiSourceQueue.hpp"
|
||||
#include "ThreadRegistry.hpp"
|
||||
|
||||
namespace kotlin {
|
||||
namespace mm {
|
||||
|
||||
// Registry for all objects that have references outside of Kotlin.
|
||||
class StableRefRegistry : Pinned {
|
||||
using Mutex = SpinLock<MutexThreadStateHandling::kIgnore>;
|
||||
|
||||
public:
|
||||
class ThreadQueue : public MultiSourceQueue<ObjHeader*, Mutex>::Producer {
|
||||
public:
|
||||
explicit ThreadQueue(StableRefRegistry& registry) : Producer(registry.stableRefs_) {}
|
||||
// Do not add fields as this is just a wrapper and Producer does not have virtual destructor.
|
||||
};
|
||||
|
||||
using Iterable = MultiSourceQueue<ObjHeader*, Mutex>::Iterable;
|
||||
using Iterator = MultiSourceQueue<ObjHeader*, Mutex>::Iterator;
|
||||
using Node = MultiSourceQueue<ObjHeader*, Mutex>::Node;
|
||||
|
||||
StableRefRegistry();
|
||||
~StableRefRegistry();
|
||||
|
||||
static StableRefRegistry& Instance() noexcept;
|
||||
|
||||
Node* RegisterStableRef(mm::ThreadData* threadData, ObjHeader* object) noexcept;
|
||||
|
||||
void UnregisterStableRef(mm::ThreadData* threadData, Node* node) noexcept;
|
||||
|
||||
// Collect stable references from thread corresponding to `threadData`. Must be called by the thread
|
||||
// when it's asked by GC to stop.
|
||||
void ProcessThread(mm::ThreadData* threadData) noexcept;
|
||||
|
||||
// Lock registry and apply deletions. Should be called on GC thread after all threads have published, and before `LockForIter`.
|
||||
void ProcessDeletions() noexcept;
|
||||
|
||||
// Lock registry for safe iteration.
|
||||
// TODO: Iteration over `stableRefs_` will be slow, because it's `std_support::list` collected at different times from
|
||||
// different threads, and so the nodes are all over the memory. Use metrics to understand how
|
||||
// much of a problem is it.
|
||||
Iterable LockForIter() noexcept { return stableRefs_.LockForIter(); }
|
||||
|
||||
void ClearForTests() noexcept { stableRefs_.ClearForTests(); }
|
||||
|
||||
private:
|
||||
// Current approach optimizes for creating and disposing of stable refs:
|
||||
// * creation just enqueues ref, disposing either queues or deletes the ref immediately (if it still resides in the current queue).
|
||||
// * when thread is stopped, it'll scan through the local queue (to mark that refs no longer reside in it) and push creation and
|
||||
// deletion queues to the global registry.
|
||||
// * during marking GC will have to `ProcessDeletions` to actually delete the refs that were enqueued for deletion.
|
||||
// So, we sacrifice memory (to keep deleted queues) and marking time (to process these queues) to improve creation and disposal times.
|
||||
//
|
||||
// Other alternatives:
|
||||
// * Use a single global collection (e.g. lock free doubly linked list).
|
||||
// * Sacrifice disposal time to try to delete as early as possible (e.g. post directly into owning producer, so it processes deletions
|
||||
// before posting queue to the global registry)
|
||||
//
|
||||
// TODO: Measure to understand, if this approach is problematic.
|
||||
MultiSourceQueue<ObjHeader*, Mutex> stableRefs_;
|
||||
};
|
||||
|
||||
} // namespace mm
|
||||
} // namespace kotlin
|
||||
|
||||
#endif // RUNTIME_MM_STABLE_REF_REGISTRY_H
|
||||
@@ -46,12 +46,12 @@ extern "C" void Kotlin_TestSupport_AssertClearGlobalState() {
|
||||
// Validate that global registries are empty.
|
||||
auto globals = mm::GlobalsRegistry::Instance().LockForIter();
|
||||
auto extraObjects = mm::GlobalData::Instance().extraObjectDataFactory().LockForIter();
|
||||
auto stableRefs = mm::StableRefRegistry::Instance().LockForIter();
|
||||
auto specialRefs = mm::SpecialRefRegistry::instance().lockForIter();
|
||||
auto threads = mm::ThreadRegistry::Instance().LockForIter();
|
||||
|
||||
EXPECT_THAT(collectCopy(globals), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(collectPointers(extraObjects), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(collectCopy(stableRefs), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(collectPointers(specialRefs), testing::UnorderedElementsAre());
|
||||
EXPECT_THAT(collectPointers(threads), testing::UnorderedElementsAre());
|
||||
gc::AssertClear(mm::GlobalData::Instance().gc());
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#include "ObjectFactory.hpp"
|
||||
#include "ExtraObjectDataFactory.hpp"
|
||||
#include "ShadowStack.hpp"
|
||||
#include "StableRefRegistry.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "ThreadLocalStorage.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "ThreadSuspension.hpp"
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
explicit ThreadData(int threadId) noexcept :
|
||||
threadId_(threadId),
|
||||
globalsThreadQueue_(GlobalsRegistry::Instance()),
|
||||
stableRefThreadQueue_(StableRefRegistry::Instance()),
|
||||
specialRefRegistry_(SpecialRefRegistry::instance()),
|
||||
extraObjectDataThreadQueue_(ExtraObjectDataFactory::Instance()),
|
||||
gc_(GlobalData::Instance().gc(), *this),
|
||||
suspensionData_(ThreadState::kNative, *this) {}
|
||||
@@ -46,7 +46,7 @@ public:
|
||||
|
||||
ThreadLocalStorage& tls() noexcept { return tls_; }
|
||||
|
||||
StableRefRegistry::ThreadQueue& stableRefThreadQueue() noexcept { return stableRefThreadQueue_; }
|
||||
SpecialRefRegistry::ThreadQueue& specialRefRegistry() noexcept { return specialRefRegistry_; }
|
||||
|
||||
ExtraObjectDataFactory::ThreadQueue& extraObjectDataThreadQueue() noexcept { return extraObjectDataThreadQueue_; }
|
||||
|
||||
@@ -65,14 +65,14 @@ public:
|
||||
void Publish() noexcept {
|
||||
// TODO: These use separate locks, which is inefficient.
|
||||
globalsThreadQueue_.Publish();
|
||||
stableRefThreadQueue_.Publish();
|
||||
specialRefRegistry_.publish();
|
||||
extraObjectDataThreadQueue_.Publish();
|
||||
gc_.Publish();
|
||||
}
|
||||
|
||||
void ClearForTests() noexcept {
|
||||
globalsThreadQueue_.ClearForTests();
|
||||
stableRefThreadQueue_.ClearForTests();
|
||||
specialRefRegistry_.clearForTests();
|
||||
extraObjectDataThreadQueue_.ClearForTests();
|
||||
gc_.ClearForTests();
|
||||
}
|
||||
@@ -81,7 +81,7 @@ private:
|
||||
const int threadId_;
|
||||
GlobalsRegistry::ThreadQueue globalsThreadQueue_;
|
||||
ThreadLocalStorage tls_;
|
||||
StableRefRegistry::ThreadQueue stableRefThreadQueue_;
|
||||
SpecialRefRegistry::ThreadQueue specialRefRegistry_;
|
||||
ExtraObjectDataFactory::ThreadQueue extraObjectDataThreadQueue_;
|
||||
ShadowStack shadowStack_;
|
||||
gc::GC::ThreadData gc_;
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
RuntimeAssert(currentThreadDataNode_ != nullptr, "Thread is not attached to the runtime");
|
||||
return currentThreadDataNode_;
|
||||
}
|
||||
Node* CurrentThreadDataNodeOrNull() const noexcept { return currentThreadDataNode_; }
|
||||
|
||||
bool IsCurrentThreadRegistered() const noexcept { return currentThreadDataNode_ != nullptr; }
|
||||
|
||||
|
||||
@@ -6,20 +6,21 @@
|
||||
#include "Weak.hpp"
|
||||
|
||||
#include "ExtraObjectData.hpp"
|
||||
#include "ObjectOps.hpp"
|
||||
#include "WeakRef.hpp"
|
||||
#include "ThreadState.hpp"
|
||||
#include "Types.h"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
extern "C" {
|
||||
OBJ_GETTER(makeRegularWeakReferenceImpl, void*);
|
||||
OBJ_GETTER(makeRegularWeakReferenceImpl, void*, void*);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct RegularWeakReferenceImpl {
|
||||
ObjHeader header;
|
||||
mm::WeakRef weakRef;
|
||||
void* referred;
|
||||
};
|
||||
|
||||
@@ -38,18 +39,17 @@ OBJ_GETTER(mm::createRegularWeakReferenceImpl, ObjHeader* object) noexcept {
|
||||
RETURN_OBJ(weakRef);
|
||||
}
|
||||
ObjHolder holder;
|
||||
auto* weakRef = makeRegularWeakReferenceImpl(object, holder.slot());
|
||||
auto* weakRef = makeRegularWeakReferenceImpl(static_cast<mm::RawSpecialRef*>(mm::WeakRef::create(object)), object, holder.slot());
|
||||
auto* setWeakRef = extraObject.GetOrSetRegularWeakReferenceImpl(object, weakRef);
|
||||
RETURN_OBJ(setWeakRef);
|
||||
}
|
||||
|
||||
void mm::disposeRegularWeakReferenceImpl(ObjHeader* weakRef) noexcept {
|
||||
asRegularWeakReferenceImpl(weakRef)->referred = nullptr;
|
||||
std::move(asRegularWeakReferenceImpl(weakRef)->weakRef).dispose();
|
||||
}
|
||||
|
||||
OBJ_GETTER(mm::derefRegularWeakReferenceImpl, ObjHeader* weakRef) noexcept {
|
||||
ObjHeader** location = reinterpret_cast<ObjHeader**>(&asRegularWeakReferenceImpl(weakRef)->referred);
|
||||
RETURN_RESULT_OF(mm::ReadHeapRefAtomic, location);
|
||||
RETURN_RESULT_OF0(asRegularWeakReferenceImpl(weakRef)->weakRef.tryRef);
|
||||
}
|
||||
|
||||
ObjHeader* mm::regularWeakReferenceImplBaseObjectUnsafe(ObjHeader* weakRef) noexcept {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "WeakRef.hpp"
|
||||
|
||||
#include "ThreadData.hpp"
|
||||
|
||||
using namespace kotlin;
|
||||
|
||||
// static
|
||||
mm::WeakRef mm::WeakRef::create(ObjHeader* obj) noexcept {
|
||||
RuntimeAssert(obj != nullptr, "Creating WeakRef for null object");
|
||||
return mm::ThreadRegistry::Instance().CurrentThreadData()->specialRefRegistry().createWeakRef(obj);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Memory.h"
|
||||
#include "RawPtr.hpp"
|
||||
#include "SpecialRefRegistry.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace kotlin::mm {
|
||||
|
||||
// Weak reference to a Kotlin object.
|
||||
// GC automatically invalidates the reference when the Kotlin object is collected.
|
||||
// Use `create` and `dispose` to create and destroy the weak reference.
|
||||
class WeakRef : private MoveOnly {
|
||||
public:
|
||||
WeakRef() noexcept = default;
|
||||
|
||||
// Cast raw ref into a weak reference.
|
||||
explicit WeakRef(RawSpecialRef* raw) noexcept : node_(SpecialRefRegistry::Node::fromRaw(raw)) {}
|
||||
|
||||
// Cast weak reference into raw ref.
|
||||
[[nodiscard("must be manually disposed")]] explicit operator RawSpecialRef*() && noexcept {
|
||||
// Make sure to move out from node_.
|
||||
auto node = std::move(node_);
|
||||
return node->asRaw();
|
||||
}
|
||||
|
||||
// Create new weak reference for `obj`.
|
||||
[[nodiscard("must be manually disposed")]] static WeakRef create(ObjHeader* obj) noexcept;
|
||||
|
||||
// Dispose weak reference.
|
||||
void dispose() && noexcept {
|
||||
RuntimeAssert(node_, "Disposing null WeakRef");
|
||||
// Make sure to move out from node_.
|
||||
auto node = std::move(node_);
|
||||
// Can be safely called with any thread state.
|
||||
node->dispose();
|
||||
}
|
||||
|
||||
// Safely dereference weak reference. Returns null if the underlying object
|
||||
// is not alive.
|
||||
OBJ_GETTER0(tryRef) const noexcept {
|
||||
RuntimeAssert(node_, "tryRef on null WeakRef");
|
||||
AssertThreadState(ThreadState::kRunnable);
|
||||
RETURN_RESULT_OF0(node_->tryRef);
|
||||
}
|
||||
|
||||
static WeakRef& reinterpret(RawSpecialRef*& raw) noexcept { return reinterpret_cast<WeakRef&>(raw); }
|
||||
|
||||
static const WeakRef& reinterpret(RawSpecialRef* const& raw) noexcept { return reinterpret_cast<const WeakRef&>(raw); }
|
||||
|
||||
private:
|
||||
raw_ptr<SpecialRefRegistry::Node> node_;
|
||||
};
|
||||
|
||||
static_assert(sizeof(WeakRef) == sizeof(void*), "WeakRef must be a thin wrapper around pointer");
|
||||
static_assert(alignof(WeakRef) == alignof(void*), "WeakRef must be a thin wrapper around pointer");
|
||||
static_assert(std::is_trivially_destructible_v<WeakRef>, "WeakRef must be trivially destructible. Destruction is manual via dispose()");
|
||||
|
||||
} // namespace kotlin::mm
|
||||
@@ -105,7 +105,7 @@ static void injectToRuntime();
|
||||
{
|
||||
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative);
|
||||
candidate->refHolder.releaseRef();
|
||||
[candidate releaseAsAssociatedObject:ReleaseMode::kDetachAndRelease];
|
||||
[candidate releaseAsAssociatedObject];
|
||||
}
|
||||
return objc_retain(old);
|
||||
}
|
||||
@@ -142,9 +142,16 @@ static void injectToRuntime();
|
||||
}
|
||||
}
|
||||
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
-(void)releaseAsAssociatedObject {
|
||||
RuntimeAssert(!permanent, "Cannot be called on permanent objects");
|
||||
|
||||
if (CurrentMemoryModel == MemoryModel::kExperimental) {
|
||||
// No need for any special handling. Weak reference handling machinery
|
||||
// has already cleaned up the reference to Kotlin object.
|
||||
[super release];
|
||||
return;
|
||||
}
|
||||
|
||||
// This function is called by the GC. It made a decision to reclaim Kotlin object, and runs
|
||||
// deallocation hooks at the moment, including deallocation of the "associated object" ([self])
|
||||
// using the [super release] call below.
|
||||
@@ -155,20 +162,25 @@ static void injectToRuntime();
|
||||
// Generally retaining and releasing Kotlin object that is being deallocated would lead to
|
||||
// use-after-dispose and double-dispose problems (with unpredictable consequences) or to an assertion failure.
|
||||
// To workaround this, detach the back ref from the Kotlin object:
|
||||
if (ReleaseModeHasDetach(mode)) {
|
||||
refHolder.detach();
|
||||
} else {
|
||||
// With Mark&Sweep this object should already have been detached earlier.
|
||||
refHolder.assertDetached();
|
||||
}
|
||||
refHolder.detach();
|
||||
|
||||
// So retain/release/etc. on [self] won't affect the Kotlin object, and an attempt to get
|
||||
// the reference to it (e.g. when calling Kotlin method on [self]) would crash.
|
||||
// The latter is generally ok because can be triggered only by user-defined Swift/Obj-C
|
||||
// subclasses of Kotlin classes.
|
||||
if (ReleaseModeHasRelease(mode)) {
|
||||
[super release];
|
||||
[super release];
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
if (CurrentMemoryModel == MemoryModel::kExperimental) {
|
||||
if (!permanent) {
|
||||
refHolder.dealloc();
|
||||
}
|
||||
[super dealloc];
|
||||
return;
|
||||
}
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
@@ -186,9 +198,7 @@ static void injectToRuntime();
|
||||
RETURN_RESULT_OF(Kotlin_ObjCExport_convertUnmappedObjCObject, self);
|
||||
}
|
||||
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_release(self);
|
||||
}
|
||||
@end
|
||||
@@ -261,7 +271,7 @@ static void injectToRuntimeImpl() {
|
||||
Kotlin_ObjCExport_toKotlinSelector = @selector(toKotlin:);
|
||||
|
||||
RuntimeCheck(Kotlin_ObjCExport_releaseAsAssociatedObjectSelector == nullptr, errorMessage);
|
||||
Kotlin_ObjCExport_releaseAsAssociatedObjectSelector = @selector(releaseAsAssociatedObject:);
|
||||
Kotlin_ObjCExport_releaseAsAssociatedObjectSelector = @selector(releaseAsAssociatedObject);
|
||||
}
|
||||
|
||||
static void injectToRuntime() {
|
||||
|
||||
@@ -93,9 +93,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
RETURN_RESULT_OF(invokeAndAssociate, Kotlin_NSArrayAsKList_create, objc_retain(self));
|
||||
}
|
||||
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_release(self);
|
||||
}
|
||||
@end
|
||||
@@ -108,9 +106,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
RETURN_RESULT_OF(invokeAndAssociate, Kotlin_NSMutableArrayAsKMutableList_create, objc_retain(self));
|
||||
}
|
||||
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_release(self);
|
||||
}
|
||||
@end
|
||||
@@ -124,9 +120,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
RETURN_RESULT_OF(invokeAndAssociate, Kotlin_NSSetAsKSet_create, objc_retain(self));
|
||||
}
|
||||
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_release(self);
|
||||
}
|
||||
|
||||
@@ -140,9 +134,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
RETURN_RESULT_OF(invokeAndAssociate, Kotlin_NSDictionaryAsKMap_create, objc_retain(self));
|
||||
}
|
||||
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_release(self);
|
||||
}
|
||||
|
||||
@@ -156,7 +148,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
iteratorHolder.disposeFromNative();
|
||||
iteratorHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -186,7 +178,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
listHolder.disposeFromNative();
|
||||
listHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -222,7 +214,7 @@ static inline KInt objCIndexToKotlinOrThrow(NSUInteger index) {
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
listHolder.disposeFromNative();
|
||||
listHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -302,7 +294,7 @@ static inline id KSet_getElement(KRef set, id object) {
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
setHolder.disposeFromNative();
|
||||
setHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -390,7 +382,7 @@ static inline id KSet_getElement(KRef set, id object) {
|
||||
// Note: since setHolder initialization is not performed directly with alloc,
|
||||
// it is possible that it wasn't initialized properly.
|
||||
// Fortunately setHolder.dispose() handles the zero-initialized case too.
|
||||
setHolder.disposeFromNative();
|
||||
setHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -464,7 +456,7 @@ static inline id KMap_get(KRef map, id aKey) {
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
mapHolder.disposeFromNative();
|
||||
mapHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -510,7 +502,7 @@ static inline id KMap_get(KRef map, id aKey) {
|
||||
// Note: since mapHolder initialization is not performed directly with alloc,
|
||||
// it is possible that it wasn't initialized properly.
|
||||
// Fortunately mapHolder.dispose() handles the zero-initialized case too.
|
||||
mapHolder.disposeFromNative();
|
||||
mapHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -599,9 +591,7 @@ static inline id KMap_get(KRef map, id aKey) {
|
||||
@end
|
||||
|
||||
@implementation NSEnumerator (NSEnumeratorAsAssociatedObject)
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_release(self);
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
-(void)dealloc {
|
||||
refHolder.disposeFromNative();
|
||||
refHolder.dispose();
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -74,9 +74,7 @@ void objc_release(id obj);
|
||||
}
|
||||
|
||||
// Called when removing Kotlin object.
|
||||
-(void)releaseAsAssociatedObject:(ReleaseMode)mode {
|
||||
if (!ReleaseModeHasRelease(mode))
|
||||
return;
|
||||
-(void)releaseAsAssociatedObject {
|
||||
objc_destroyWeak(&referred);
|
||||
objc_release(self);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ kotlin::test_support::TypeInfoHolder theWorkerBoundReferenceTypeInfoHolder{
|
||||
kotlin::test_support::TypeInfoHolder::ObjectBuilder<EmptyPayload>()};
|
||||
kotlin::test_support::TypeInfoHolder theCleanerImplTypeInfoHolder{kotlin::test_support::TypeInfoHolder::ObjectBuilder<EmptyPayload>()};
|
||||
kotlin::test_support::TypeInfoHolder theRegularWeakReferenceImplTypeInfoHolder{
|
||||
kotlin::test_support::TypeInfoHolder::ObjectBuilder<kotlin::test_support::RegularWeakReferenceImplPayload>()};
|
||||
kotlin::test_support::TypeInfoHolder::ObjectBuilder<kotlin::test_support::RegularWeakReferenceImplPayload>().addFlag(
|
||||
TF_HAS_FINALIZER)};
|
||||
|
||||
ArrayHeader theEmptyStringImpl = {theStringTypeInfoHolder.typeInfo(), /* element count */ 0};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user