From d47193d36ff67ea1686b8cb841087d1b7aa88529 Mon Sep 17 00:00:00 2001 From: Alexander Shabalin Date: Tue, 28 Jun 2022 06:38:36 +0000 Subject: [PATCH] [K/N] Add tracking of application state to the GC scheduler. Merge-request: KT-MR-6435 Merged-by: Alexander Shabalin --- .../kotlin/backend/konan/AppStateTracking.kt | 17 + .../kotlin/backend/konan/BinaryOptions.kt | 2 + .../kotlin/backend/konan/KonanConfig.kt | 4 + .../kotlin/backend/konan/llvm/IrToBitcode.kt | 1 + kotlin-native/konan/konan.properties | 42 +- .../src/gc/common/cpp/GCSchedulerImpl.hpp | 7 + .../src/gc/common/cpp/GCSchedulerTest.cpp | 37 ++ .../src/main/cpp/CompilerConstants.cpp | 7 +- .../src/main/cpp/CompilerConstants.hpp | 6 + .../NSNotificationSubscription.hpp | 44 ++ .../NSNotificationSubscription.mm | 39 ++ .../NSNotificationSubscriptionTest.mm | 228 +++++++++ .../src/main/cpp/objc_support/ObjCForward.hpp | 12 + .../src/main/cpp/objc_support/ObjectPtr.hpp | 164 +++++++ .../src/main/cpp/objc_support/ObjectPtr.mm | 66 +++ .../main/cpp/objc_support/ObjectPtrTest.mm | 460 ++++++++++++++++++ .../runtime/src/mm/cpp/AppStateTracking.hpp | 39 ++ .../src/mm/cpp/AppStateTrackingDefault.cpp | 18 + .../mm/cpp/AppStateTrackingTestSupport.hpp | 24 + .../src/mm/cpp/AppStateTrackingUIKit.mm | 48 ++ .../runtime/src/mm/cpp/GlobalData.hpp | 3 + 21 files changed, 1246 insertions(+), 22 deletions(-) create mode 100644 kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/AppStateTracking.kt create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.hpp create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.mm create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscriptionTest.mm create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/ObjCForward.hpp create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.hpp create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.mm create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtrTest.mm create mode 100644 kotlin-native/runtime/src/mm/cpp/AppStateTracking.hpp create mode 100644 kotlin-native/runtime/src/mm/cpp/AppStateTrackingDefault.cpp create mode 100644 kotlin-native/runtime/src/mm/cpp/AppStateTrackingTestSupport.hpp create mode 100644 kotlin-native/runtime/src/mm/cpp/AppStateTrackingUIKit.mm diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/AppStateTracking.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/AppStateTracking.kt new file mode 100644 index 00000000000..b430e34a917 --- /dev/null +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/AppStateTracking.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.konan + +/** + * Controls whether K/N runtime is allowed track application state. + * + * Can be turned off via [BinaryOptions] to workaround bugs in implementation. + */ +// Must match `AppStateTracking` in CompilerConstants.hpp +enum class AppStateTracking(val value: Int) { + DISABLED(0), + ENABLED(1), +} diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt index ec25bd738da..e890153c5bd 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/BinaryOptions.kt @@ -35,6 +35,8 @@ object BinaryOptions : BinaryOptionRegistry() { val bundleId by stringOption() val bundleShortVersionString by stringOption() val bundleVersion by stringOption() + + val appStateTracking by option() } open class BinaryOption( diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt index 7d17bca5e15..c2d602cf016 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanConfig.kt @@ -163,6 +163,10 @@ class KonanConfig(val project: Project, val configuration: CompilerConfiguration get() = configuration.get(KonanConfigKeys.VERIFY_COMPILER) ?: (optimizationsEnabled || CompilerVersion.CURRENT.meta != MetaVersion.RELEASE) + val appStateTracking: AppStateTracking by lazy { + configuration.get(BinaryOptions.appStateTracking) ?: AppStateTracking.DISABLED + } + init { if (!platformManager.isEnabled(target)) { error("Target ${target.visibleName} is not available on the ${HostManager.hostName} host") diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index 30c5a3a3463..5cf24021e96 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -2770,6 +2770,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map scheduleGC) noexcept : config_(config), + appStateTracking_(mm::GlobalData::Instance().appStateTracking()), heapGrowthController_(config), regularIntervalPacer_(config), scheduleGC_(std::move(scheduleGC)), timer_("GC Timer thread", config_.regularGcInterval(), [this] { + if (appStateTracking_.state() == mm::AppStateTracking::State::kBackground) { + return; + } if (regularIntervalPacer_.NeedsGC()) { scheduleGC_(); } @@ -155,6 +161,7 @@ public: private: gc::GCSchedulerConfig& config_; + mm::AppStateTracking& appStateTracking_; HeapGrowthController heapGrowthController_; RegularIntervalPacer regularIntervalPacer_; std::function scheduleGC_; diff --git a/kotlin-native/runtime/src/gc/common/cpp/GCSchedulerTest.cpp b/kotlin-native/runtime/src/gc/common/cpp/GCSchedulerTest.cpp index 2880a8f2192..95bb70c2265 100644 --- a/kotlin-native/runtime/src/gc/common/cpp/GCSchedulerTest.cpp +++ b/kotlin-native/runtime/src/gc/common/cpp/GCSchedulerTest.cpp @@ -11,6 +11,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "AppStateTrackingTestSupport.hpp" #include "ClockTestSupport.hpp" #include "GCSchedulerImpl.hpp" #include "SingleThreadExecutor.hpp" @@ -710,6 +711,42 @@ TEST_F(GCSchedulerDataWithTimerTest, TuneTargetHeap) { EXPECT_THAT(config.targetHeapBytes.load(), 5); } +TEST_F(GCSchedulerDataWithTimerTest, DoNotCollectOnTimerInBackground) { + constexpr int mutatorsCount = kDefaultThreadCount; + + GCSchedulerConfig config; + config.regularGcIntervalMicroseconds = 10; + config.autoTune = false; + config.targetHeapBytes = std::numeric_limits::max(); + GCSchedulerDataWithTimerTestApi schedulerTestApi(config); + + // TODO: Not a global, please. + mm::AppStateTrackingTestSupport appStateTracking(mm::GlobalData::Instance().appStateTracking()); + + // Wait until the timer is initialized. + test_support::manual_clock::waitForPending(test_support::manual_clock::now() + microseconds(10)); + + // Now go into the background. + ASSERT_THAT(mm::GlobalData::Instance().appStateTracking().state(), mm::AppStateTracking::State::kForeground); + appStateTracking.setState(mm::AppStateTracking::State::kBackground); + + // Timer works in the background, but does nothing. + EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()).Times(0); + schedulerTestApi.advance_time(microseconds(10)); + test_support::manual_clock::waitForPending(test_support::manual_clock::now() + microseconds(10)); + testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC()); + + // Now go back into the foreground. + appStateTracking.setState(mm::AppStateTracking::State::kForeground); + + EXPECT_CALL(schedulerTestApi.scheduleGC(), Call()); + schedulerTestApi.advance_time(microseconds(10)); + test_support::manual_clock::waitForPending(test_support::manual_clock::now() + microseconds(10)); + testing::Mock::VerifyAndClearExpectations(&schedulerTestApi.scheduleGC()); + schedulerTestApi.OnPerformFullGC(); + schedulerTestApi.UpdateAliveSetBytes(0); +} + // These tests require a stack trace to contain call site addresses but // on Windows a trace contains function addresses instead. // So skip these tests on Windows. diff --git a/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp b/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp index 3149b87ed78..c4d894e51b8 100644 --- a/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp +++ b/kotlin-native/runtime/src/main/cpp/CompilerConstants.cpp @@ -25,6 +25,8 @@ RUNTIME_WEAK Kotlin_getSourceInfo_FunctionType Kotlin_getSourceInfo_Function = n #ifdef KONAN_ANDROID RUNTIME_WEAK int32_t Kotlin_printToAndroidLogcat = 1; #endif +// Keep it 0 even when the compiler defaults to 1: if the overriding mechanism breaks, keeping it disabled is safer. +RUNTIME_WEAK int32_t Kotlin_appStateTracking = 0; ALWAYS_INLINE compiler::DestroyRuntimeMode compiler::destroyRuntimeMode() noexcept { return static_cast(Kotlin_destroyRuntimeMode); @@ -39,6 +41,9 @@ ALWAYS_INLINE bool compiler::suspendFunctionsFromAnyThreadFromObjCEnabled() noex return Kotlin_suspendFunctionsFromAnyThreadFromObjC != 0; } +ALWAYS_INLINE compiler::AppStateTracking compiler::appStateTracking() noexcept { + return static_cast(Kotlin_appStateTracking); +} #ifdef KONAN_ANDROID ALWAYS_INLINE bool compiler::printToAndroidLogcat() noexcept { @@ -52,4 +57,4 @@ ALWAYS_INLINE int compiler::getSourceInfo(void* addr, SourceInfo *result, int re } else { return Kotlin_getSourceInfo_Function(addr, result, result_size); } -} \ No newline at end of file +} diff --git a/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp b/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp index acf39284a75..ca4a3739e4d 100644 --- a/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp +++ b/kotlin-native/runtime/src/main/cpp/CompilerConstants.hpp @@ -72,6 +72,11 @@ enum class GCSchedulerType { kAggressive = 3, }; +// Must match AppStateTracking in AppStateTracking.kt +enum class AppStateTracking { + kDisabled = 0, + kEnabled = 1, +}; ALWAYS_INLINE inline bool shouldContainDebugInfo() noexcept { return Kotlin_needDebugInfo != 0; @@ -101,6 +106,7 @@ ALWAYS_INLINE inline GCSchedulerType getGCSchedulerType() noexcept { WorkerExceptionHandling workerExceptionHandling() noexcept; DestroyRuntimeMode destroyRuntimeMode() noexcept; bool suspendFunctionsFromAnyThreadFromObjCEnabled() noexcept; +AppStateTracking appStateTracking() noexcept; int getSourceInfo(void* addr, SourceInfo *result, int result_size) noexcept; #ifdef KONAN_ANDROID diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.hpp b/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.hpp new file mode 100644 index 00000000000..2ca0e5e904c --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.hpp @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2022 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 + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include +#include + +#include "ObjCForward.hpp" +#include "ObjectPtr.hpp" +#include "Utils.hpp" + +OBJC_FORWARD_DECLARE(NSNotificationCenter); +OBJC_FORWARD_DECLARE(NSString); + +namespace kotlin::objc_support { + +class NSNotificationSubscription : private MoveOnly { +public: + NSNotificationSubscription(NSNotificationCenter* center, NSString* name, std::function handler) noexcept; + NSNotificationSubscription(NSString* name, std::function handler) noexcept; + + NSNotificationSubscription(NSNotificationSubscription&&) = default; + NSNotificationSubscription& operator=(NSNotificationSubscription&&) = default; + + ~NSNotificationSubscription() { reset(); } + + void reset() noexcept; + bool subscribed() const noexcept; + explicit operator bool() const noexcept { return subscribed(); } + +private: + object_ptr center_; + // center_ will hold the strong reference to token_ anyway. + id token_; +}; + +} // namespace kotlin::objc_support + +#endif diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.mm b/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.mm new file mode 100644 index 00000000000..45c416deb73 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscription.mm @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2022 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. + */ + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include "NSNotificationSubscription.hpp" + +#import + +using namespace kotlin; + +objc_support::NSNotificationSubscription::NSNotificationSubscription( + NSNotificationCenter* center, NSString* name, std::function handler) noexcept : + center_([center retain]), + token_([center addObserverForName:name + object:nil + queue:nil + usingBlock:^(NSNotification* notification) { + handler(); + }]) {} + +objc_support::NSNotificationSubscription::NSNotificationSubscription(NSString* name, std::function handler) noexcept : + NSNotificationSubscription([NSNotificationCenter defaultCenter], name, std::move(handler)) {} + +bool objc_support::NSNotificationSubscription::subscribed() const noexcept { + return token_ != nil; +} + +void objc_support::NSNotificationSubscription::reset() noexcept { + @autoreleasepool { + [*center_ removeObserver:token_]; + center_.reset(); + token_ = nil; + } +} + +#endif diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscriptionTest.mm b/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscriptionTest.mm new file mode 100644 index 00000000000..efc5ae80891 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/NSNotificationSubscriptionTest.mm @@ -0,0 +1,228 @@ +/* + * Copyright 2010-2022 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. + */ + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include "NSNotificationSubscription.hpp" + +#import +#import + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "std_support/Memory.hpp" + +using namespace kotlin; + +using testing::_; + +namespace { + +class WithDestructorHook; + +using DestructorHook = void(WithDestructorHook*); + +class WithDestructorHook : private Pinned { +public: + explicit WithDestructorHook(std::function hook) : hook_(std::move(hook)) {} + + ~WithDestructorHook() { hook_(this); } + +private: + std::function hook_; +}; + +} // namespace + +@interface Kotlin_objc_support_NSNotificationSubscriptionTest : NSObject { + NSNotificationCenter* center_; + std::function handler_; +} + +- (instancetype)initWithNotificationCenter:(NSNotificationCenter*)center + name:(NSNotificationName)name + handler:(std::function)handler; + +- (void)reset; + +- (void)onNotification:(NSNotification*)notification; + +@end + +@implementation Kotlin_objc_support_NSNotificationSubscriptionTest + +- (instancetype)initWithNotificationCenter:(NSNotificationCenter*)center + name:(NSNotificationName)name + handler:(std::function)handler { + if ((self = [super init])) { + center_ = center; + handler_ = std::move(handler); + + [center_ addObserver:self selector:@selector(onNotification:) name:name object:nil]; + } + return self; +} + +- (void)reset { + [center_ removeObserver:self]; +} + +- (void)onNotification:(NSNotification*)notification { + handler_(); +} + +@end + +class NSNotificationSubscriptionTest : public testing::Test { +public: + objc_support::NSNotificationSubscription subscribe(const char* name, std::function handler) noexcept { + return objc_support::NSNotificationSubscription(center_, [NSString stringWithUTF8String:name], std::move(handler)); + } + + objc_support::object_ptr subscribeOther( + const char* name, std::function handler) noexcept { + return objc_support::object_ptr( + [[Kotlin_objc_support_NSNotificationSubscriptionTest alloc] initWithNotificationCenter:center_ + name:[NSString stringWithUTF8String:name] + handler:std::move(handler)]); + } + + void post(const char* name) noexcept { [center_ postNotificationName:[NSString stringWithUTF8String:name] object:nil]; } + +private: + NSNotificationCenter* center_ = [[NSNotificationCenter alloc] init]; +}; + +TEST_F(NSNotificationSubscriptionTest, Subscribed) { + constexpr const char* name = "NOTIFICATION_NAME"; + testing::StrictMock> handler; + + auto subscription = subscribe(name, handler.AsStdFunction()); + EXPECT_TRUE(subscription.subscribed()); + EXPECT_TRUE(subscription); + + subscription.reset(); + EXPECT_FALSE(subscription.subscribed()); + EXPECT_FALSE(subscription); +} + +TEST_F(NSNotificationSubscriptionTest, Post) { + constexpr const char* name = "NOTIFICATION_NAME"; + testing::StrictMock> handler; + + auto subscription = subscribe(name, handler.AsStdFunction()); + + EXPECT_CALL(handler, Call()); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler); +} + +TEST_F(NSNotificationSubscriptionTest, PostWrongName) { + constexpr const char* name = "NOTIFICATION_NAME"; + constexpr const char* wrongName = "NOTIFICATION_NAME_WRONG"; + testing::StrictMock> handler; + + auto subscription = subscribe(name, handler.AsStdFunction()); + + EXPECT_CALL(handler, Call()).Times(0); + post(wrongName); + testing::Mock::VerifyAndClearExpectations(&handler); +} + +TEST_F(NSNotificationSubscriptionTest, PostAfterReset) { + constexpr const char* name = "NOTIFICATION_NAME"; + testing::StrictMock> handler; + + auto subscription = subscribe(name, handler.AsStdFunction()); + subscription.reset(); + + EXPECT_CALL(handler, Call()).Times(0); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler); +} + +TEST_F(NSNotificationSubscriptionTest, PostAfterDtor) { + constexpr const char* name = "NOTIFICATION_NAME"; + testing::StrictMock> handler; + + { + // Create and destroy subscription object. + subscribe(name, handler.AsStdFunction()); + } + + EXPECT_CALL(handler, Call()).Times(0); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler); +} + +TEST_F(NSNotificationSubscriptionTest, MultipleSubscribers) { + constexpr const char* name = "NOTIFICATION_NAME"; + testing::StrictMock> handler1; + testing::StrictMock> handler2; + testing::StrictMock> handler3; + testing::StrictMock> handler4; + + auto subscription1 = subscribeOther(name, handler1.AsStdFunction()); + auto subscription2 = subscribe(name, handler2.AsStdFunction()); + auto subscription3 = subscribe(name, handler3.AsStdFunction()); + auto subscription4 = subscribeOther(name, handler4.AsStdFunction()); + + EXPECT_CALL(handler1, Call()); + EXPECT_CALL(handler2, Call()); + EXPECT_CALL(handler3, Call()); + EXPECT_CALL(handler4, Call()); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler1); + testing::Mock::VerifyAndClearExpectations(&handler2); + testing::Mock::VerifyAndClearExpectations(&handler3); + testing::Mock::VerifyAndClearExpectations(&handler4); + + subscription3.reset(); + + EXPECT_CALL(handler1, Call()); + EXPECT_CALL(handler2, Call()); + EXPECT_CALL(handler4, Call()); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler1); + testing::Mock::VerifyAndClearExpectations(&handler2); + testing::Mock::VerifyAndClearExpectations(&handler4); + + [*subscription4 reset]; + subscription4.reset(); + + EXPECT_CALL(handler1, Call()); + EXPECT_CALL(handler2, Call()); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler1); + testing::Mock::VerifyAndClearExpectations(&handler2); + + subscription2.reset(); + + EXPECT_CALL(handler1, Call()); + post(name); + testing::Mock::VerifyAndClearExpectations(&handler1); + + // Make sure to unsubscribe. + [*subscription1 reset]; +} + +TEST_F(NSNotificationSubscriptionTest, DestroysHandler) { + constexpr const char* name = "NOTIFICATION_NAME"; + + testing::StrictMock> destructorHook; + + EXPECT_CALL(destructorHook, Call(_)).Times(0); + auto subscription = + subscribe(name, [withDestructorHook = std_support::make_shared(destructorHook.AsStdFunction())] {}); + post(name); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CALL(destructorHook, Call(_)); + subscription.reset(); + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +#endif diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/ObjCForward.hpp b/kotlin-native/runtime/src/main/cpp/objc_support/ObjCForward.hpp new file mode 100644 index 00000000000..cc72bcc8271 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/ObjCForward.hpp @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2022 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 + +#ifdef __OBJC__ +#define OBJC_FORWARD_DECLARE(clazz) @class clazz +#else +#define OBJC_FORWARD_DECLARE(clazz) class clazz +#endif diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.hpp b/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.hpp new file mode 100644 index 00000000000..464eb074f0a --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.hpp @@ -0,0 +1,164 @@ +/* + * Copyright 2010-2022 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 + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#if defined(__has_feature) && __has_feature(objc_arc) +#error "Assumes that ARC is not used" +#endif + +#include +#include + +#include "ObjCForward.hpp" + +OBJC_FORWARD_DECLARE(NSObject); + +namespace kotlin::objc_support { + +namespace internal { + +class ObjectPtrImpl { +public: + ObjectPtrImpl() noexcept; + explicit ObjectPtrImpl(NSObject* object) noexcept; + + ObjectPtrImpl(const ObjectPtrImpl& rhs) noexcept; + ObjectPtrImpl(ObjectPtrImpl&& rhs) noexcept; + + ~ObjectPtrImpl(); + + ObjectPtrImpl& operator=(const ObjectPtrImpl& rhs) noexcept { + ObjectPtrImpl tmp(rhs); + swap(tmp); + return *this; + } + + ObjectPtrImpl& operator=(ObjectPtrImpl&& rhs) noexcept { + ObjectPtrImpl tmp(std::move(rhs)); + swap(tmp); + return *this; + } + + void swap(ObjectPtrImpl& rhs) noexcept; + + NSObject* get() const noexcept; + bool valid() const noexcept; + + void reset() noexcept; + void reset(NSObject* object) noexcept; + + bool operator==(const ObjectPtrImpl& rhs) const noexcept; + bool operator<(const ObjectPtrImpl& rhs) const noexcept; + + std::size_t computeHash() const noexcept; + +private: + NSObject* object_; +}; + +} // namespace internal + +// `std::shared_ptr`-like smart pointer for ObjC objects. +// +// IMPORTANT: Must be constructed from a retained ObjC object. +// This optimizes for the common case of constructing directly from `[[T alloc] init...]`. +// +// Unlike a regular C++ smart pointer `operator*` does not return a reference but return a pointer +// for convenient syntax of calling ObjC methods: `[*smartPtr methodName]`. +// Use `(*smartPtr).propertyName` to access properties. +// Use `smartPtr->fieldName` to access fields. +// +// Implements hashes and comparisons making it suitable to store in associative containers. +// Equality comparison is shallow (does not use `[NSObject isEqual:]`) mirroring regular C++ smart pointers. +// +// When transfering ownership, prefer to move because it avoids calling `retain` and `release`. +template +class object_ptr { +public: + // Construct empty `object_ptr`. + object_ptr() noexcept = default; + + // Construct from ObjC object. + // + // IMPORTANT: this does not `retain` `ptr`. + explicit object_ptr(T* ptr) noexcept : impl_(ptr) {} + + void swap(object_ptr& rhs) noexcept { impl_.swap(rhs.impl_); } + + T* get() const noexcept { return (T*)impl_.get(); } + T* operator*() const noexcept { return (T*)impl_.get(); } + T* operator->() const noexcept { return (T*)impl_.get(); } + + explicit operator bool() const noexcept { return impl_.valid(); } + + // Release stored pointer and become empty. + void reset() noexcept { impl_.reset(); } + + // Release stored pointer and store `ptr` instead. + // + // IMPORTANT: this does not `retain` `ptr`. + void reset(T* ptr) noexcept { impl_.reset(ptr); } + + template + bool operator==(const object_ptr& rhs) const noexcept { + return impl_ == rhs.impl_; + } + + template + bool operator<(const object_ptr& rhs) const noexcept { + return impl_ < rhs.impl_; + } + +private: + friend struct std::hash; + + template + friend class object_ptr; + + internal::ObjectPtrImpl impl_; +}; + +template +bool operator!=(const object_ptr& lhs, const object_ptr& rhs) noexcept { + return !(lhs == rhs); +} + +template +bool operator>(const object_ptr& lhs, const object_ptr& rhs) noexcept { + return rhs < lhs; +} + +template +bool operator<=(const object_ptr& lhs, const object_ptr& rhs) noexcept { + return !(lhs > rhs); +} + +template +bool operator>=(const object_ptr& lhs, const object_ptr& rhs) noexcept { + return !(lhs < rhs); +} + +} // namespace kotlin::objc_support + +namespace std { + +template +void swap(kotlin::objc_support::object_ptr& lhs, kotlin::objc_support::object_ptr& rhs) noexcept { + lhs.swap(rhs); +} + +template +struct hash> { + std::size_t operator()(const kotlin::objc_support::object_ptr& value) { return std::hash()(value.impl_.get()); } +}; + +// TODO: std::atomic specialization? + +} // namespace std + +#endif diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.mm b/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.mm new file mode 100644 index 00000000000..aeaf39e3cca --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtr.mm @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2022 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. + */ + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include "ObjectPtr.hpp" + +#import + +using namespace kotlin; + +objc_support::internal::ObjectPtrImpl::ObjectPtrImpl() noexcept : object_(nil) {} + +objc_support::internal::ObjectPtrImpl::ObjectPtrImpl(NSObject* object) noexcept : object_(object) {} + +objc_support::internal::ObjectPtrImpl::ObjectPtrImpl(const ObjectPtrImpl& rhs) noexcept : object_([rhs.object_ retain]) {} + +objc_support::internal::ObjectPtrImpl::ObjectPtrImpl(ObjectPtrImpl&& rhs) noexcept : object_(rhs.object_) { + rhs.object_ = nil; +} + +objc_support::internal::ObjectPtrImpl::~ObjectPtrImpl() { + @autoreleasepool { + [object_ release]; + } +} + +void objc_support::internal::ObjectPtrImpl::swap(ObjectPtrImpl& rhs) noexcept { + using std::swap; + swap(object_, rhs.object_); +} + +NSObject* objc_support::internal::ObjectPtrImpl::get() const noexcept { + return object_; +} + +bool objc_support::internal::ObjectPtrImpl::valid() const noexcept { + return object_ != nil; +} + +void objc_support::internal::ObjectPtrImpl::reset() noexcept { + reset(nil); +} + +void objc_support::internal::ObjectPtrImpl::reset(NSObject* object) noexcept { + @autoreleasepool { + [object_ release]; + object_ = object; + } +} + +std::size_t objc_support::internal::ObjectPtrImpl::computeHash() const noexcept { + return object_.hash; +} + +bool objc_support::internal::ObjectPtrImpl::operator==(const ObjectPtrImpl& rhs) const noexcept { + return object_ == rhs.object_; +} + +bool objc_support::internal::ObjectPtrImpl::operator<(const ObjectPtrImpl& rhs) const noexcept { + return std::less<>()(object_, rhs.object_); +} + +#endif diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtrTest.mm b/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtrTest.mm new file mode 100644 index 00000000000..ca68857d5f7 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/ObjectPtrTest.mm @@ -0,0 +1,460 @@ +/* + * Copyright 2010-2022 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. + */ + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include "ObjectPtr.hpp" + +#include + +#import + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "std_support/Memory.hpp" +#include "Utils.hpp" + +using namespace kotlin; + +namespace { + +class WithDestructorHook; + +using DestructorHook = void(WithDestructorHook*); + +class WithDestructorHook : private Pinned { +public: + explicit WithDestructorHook(std::function hook) : hook_(std::move(hook)) {} + + ~WithDestructorHook() { hook_(this); } + +private: + std::function hook_; +}; + +} // namespace + +@interface WithDestructorHookObjC : NSObject { + std_support::unique_ptr impl_; +} + +@property(readonly) WithDestructorHook* impl; + +- (instancetype)initWithDestructorHook:(std::function)hook; + +@end + +@implementation WithDestructorHookObjC + +- (instancetype)initWithDestructorHook:(std::function)hook { + if ((self = [super init])) { + impl_ = std_support::make_unique(hook); + } + return self; +} + +- (WithDestructorHook*)impl { + return impl_.get(); +} + +@end + +#define EXPECT_CONTAINS(smartPtr, ptr) \ + do { \ + EXPECT_THAT(static_cast(smartPtr), (ptr) != nil); \ + EXPECT_THAT(*(smartPtr), (ptr)); \ + EXPECT_THAT((smartPtr).get(), (ptr)); \ + } while (false) + +TEST(ObjectPtrTest, DefaultCtor) { + objc_support::object_ptr obj; + EXPECT_CONTAINS(obj, nil); +} + +TEST(ObjectPtrTest, ObjectCtor) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj(ptr); + EXPECT_CONTAINS(obj, ptr); + EXPECT_CALL(destructorHook, Call(ptr.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, CopyCtor) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj1(ptr); + objc_support::object_ptr obj2(obj1); + EXPECT_CONTAINS(obj1, ptr); + EXPECT_CONTAINS(obj2, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)).Times(0); + obj1.reset(); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + EXPECT_CONTAINS(obj1, nil); + EXPECT_CONTAINS(obj2, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, MoveCtor) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj1(ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)).Times(0); + objc_support::object_ptr obj2(std::move(obj1)); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + EXPECT_CONTAINS(obj1, nil); + EXPECT_CONTAINS(obj2, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, CopyAssignmentIntoEmpty) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj2; + { + objc_support::object_ptr obj1(ptr); + obj2 = obj1; + EXPECT_CONTAINS(obj1, ptr); + EXPECT_CONTAINS(obj2, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)).Times(0); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CONTAINS(obj2, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, CopyAssignmentIntoSet) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj2(ptr2); + { + objc_support::object_ptr obj1(ptr1); + + EXPECT_CALL(destructorHook, Call(ptr2.impl)); + obj2 = obj1; + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CONTAINS(obj1, ptr1); + EXPECT_CONTAINS(obj2, ptr1); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)).Times(0); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CONTAINS(obj2, ptr1); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, MoveAssignmentIntoEmpty) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj2; + { + EXPECT_CALL(destructorHook, Call(ptr.impl)).Times(0); + + objc_support::object_ptr obj1(ptr); + obj2 = std::move(obj1); + EXPECT_CONTAINS(obj1, nil); + EXPECT_CONTAINS(obj2, ptr); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CONTAINS(obj2, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, MoveAssignmentIntoSet) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + { + objc_support::object_ptr obj2(ptr2); + { + objc_support::object_ptr obj1(ptr1); + + EXPECT_CALL(destructorHook, Call(ptr2.impl)); + EXPECT_CALL(destructorHook, Call(ptr1.impl)).Times(0); + obj2 = std::move(obj1); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CONTAINS(obj1, nil); + EXPECT_CONTAINS(obj2, ptr1); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + EXPECT_CONTAINS(obj2, ptr1); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); + } + testing::Mock::VerifyAndClearExpectations(&destructorHook); +} + +TEST(ObjectPtrTest, SwapEmptyEmpty) { + objc_support::object_ptr obj1; + objc_support::object_ptr obj2; + obj1.swap(obj2); + EXPECT_CONTAINS(obj1, nil); + EXPECT_CONTAINS(obj2, nil); +} + +TEST(ObjectPtrTest, SwapEmptySet) { + testing::StrictMock> destructorHook; + + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj1; + objc_support::object_ptr obj2(ptr2); + obj1.swap(obj2); + EXPECT_CONTAINS(obj1, ptr2); + EXPECT_CONTAINS(obj2, nil); + + EXPECT_CALL(destructorHook, Call(ptr2.impl)); +} + +TEST(ObjectPtrTest, SwapSetEmpty) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj1(ptr1); + objc_support::object_ptr obj2; + obj1.swap(obj2); + EXPECT_CONTAINS(obj1, nil); + EXPECT_CONTAINS(obj2, ptr1); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); +} + +TEST(ObjectPtrTest, SwapSetSet) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj1(ptr1); + objc_support::object_ptr obj2(ptr2); + obj1.swap(obj2); + EXPECT_CONTAINS(obj1, ptr2); + EXPECT_CONTAINS(obj2, ptr1); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); + EXPECT_CALL(destructorHook, Call(ptr2.impl)); +} + +TEST(ObjectPtrTest, ResetEmptyFromEmpty) { + objc_support::object_ptr obj; + obj.reset(); + EXPECT_CONTAINS(obj, nil); +} + +TEST(ObjectPtrTest, ResetEmptyFromSet) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj(ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)); + obj.reset(); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + EXPECT_CONTAINS(obj, nil); +} + +TEST(ObjectPtrTest, ResetObjectFromEmpty) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj; + + obj.reset(ptr); + EXPECT_CONTAINS(obj, ptr); + + EXPECT_CALL(destructorHook, Call(ptr.impl)); +} + +TEST(ObjectPtrTest, ResetObjectFromSet) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj(ptr1); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); + obj.reset(ptr2); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + EXPECT_CONTAINS(obj, ptr2); + + EXPECT_CALL(destructorHook, Call(ptr2.impl)); +} + +#define EXPECT_EQUAL(expr1, expr2) \ + do { \ + EXPECT_TRUE((expr1) == (expr2)); \ + EXPECT_FALSE((expr1) != (expr2)); \ + EXPECT_FALSE((expr1) < (expr2)); \ + EXPECT_TRUE((expr1) <= (expr2)); \ + EXPECT_FALSE((expr1) > (expr2)); \ + EXPECT_TRUE((expr1) >= (expr2)); \ + } while (false) + +#define EXPECT_LESS(expr1, expr2) \ + do { \ + EXPECT_FALSE((expr1) == (expr2)); \ + EXPECT_TRUE((expr1) != (expr2)); \ + EXPECT_TRUE((expr1) < (expr2)); \ + EXPECT_TRUE((expr1) <= (expr2)); \ + EXPECT_FALSE((expr1) > (expr2)); \ + EXPECT_FALSE((expr1) >= (expr2)); \ + } while (false) + +#define EXPECT_GREATER(expr1, expr2) \ + do { \ + EXPECT_FALSE((expr1) == (expr2)); \ + EXPECT_TRUE((expr1) != (expr2)); \ + EXPECT_FALSE((expr1) < (expr2)); \ + EXPECT_FALSE((expr1) <= (expr2)); \ + EXPECT_TRUE((expr1) > (expr2)); \ + EXPECT_TRUE((expr1) >= (expr2)); \ + } while (false) + +TEST(ObjectPtrTest, ComparisonsEmpty) { + testing::StrictMock> destructorHook; + + auto* ptr = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr empty; + objc_support::object_ptr reset(ptr); + EXPECT_CALL(destructorHook, Call(ptr.impl)); + reset.reset(); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + objc_support::object_ptr emptyOtherType; + + EXPECT_EQUAL(empty, empty); + EXPECT_EQUAL(empty, objc_support::object_ptr()); + EXPECT_EQUAL(empty, reset); + EXPECT_EQUAL(empty, emptyOtherType); + EXPECT_EQUAL(reset, empty); + EXPECT_EQUAL(reset, reset); + EXPECT_EQUAL(reset, emptyOtherType); +} + +TEST(ObjectPtrTest, ComparisonsEmptySet) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr3 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj(ptr1); + objc_support::object_ptr empty; + objc_support::object_ptr reset(ptr2); + EXPECT_CALL(destructorHook, Call(ptr2.impl)); + reset.reset(); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + objc_support::object_ptr emptyOtherType; + objc_support::object_ptr objOtherType(ptr3); + + EXPECT_GREATER(obj, empty); + EXPECT_GREATER(obj, reset); + EXPECT_GREATER(obj, emptyOtherType); + EXPECT_LESS(empty, obj); + EXPECT_LESS(empty, objOtherType); + EXPECT_LESS(reset, obj); + EXPECT_LESS(reset, objOtherType); + EXPECT_GREATER(objOtherType, empty); + EXPECT_GREATER(objOtherType, reset); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); + EXPECT_CALL(destructorHook, Call(ptr3.impl)); +} + +TEST(ObjectPtrTest, ComparisonsSetSet) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + if (ptr2 < ptr1) { + std::swap(ptr1, ptr2); + } + ASSERT_LT(ptr1, ptr2); + + objc_support::object_ptr obj1(ptr1); + objc_support::object_ptr obj2(ptr2); + objc_support::object_ptr obj3(obj1); + objc_support::object_ptr objOtherType([ptr1 retain]); + + EXPECT_EQUAL(obj1, obj1); + EXPECT_LESS(obj1, obj2); + EXPECT_EQUAL(obj1, obj3); + EXPECT_EQUAL(obj1, objOtherType); + EXPECT_GREATER(obj2, obj1); + EXPECT_EQUAL(obj2, obj2); + EXPECT_GREATER(obj2, obj3); + EXPECT_GREATER(obj2, objOtherType); + EXPECT_EQUAL(obj3, obj1); + EXPECT_LESS(obj3, obj2); + EXPECT_EQUAL(obj3, obj3); + EXPECT_EQUAL(obj3, objOtherType); + EXPECT_EQUAL(objOtherType, obj1); + EXPECT_LESS(objOtherType, obj2); + EXPECT_EQUAL(objOtherType, obj3); + EXPECT_EQUAL(objOtherType, objOtherType); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); + EXPECT_CALL(destructorHook, Call(ptr2.impl)); +} + +TEST(ObjectPtrTest, Hashing) { + testing::StrictMock> destructorHook; + + auto* ptr1 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + auto* ptr2 = [[WithDestructorHookObjC alloc] initWithDestructorHook:destructorHook.AsStdFunction()]; + objc_support::object_ptr obj(ptr1); + objc_support::object_ptr empty; + objc_support::object_ptr reset(ptr2); + EXPECT_CALL(destructorHook, Call(ptr2.impl)); + reset.reset(); + testing::Mock::VerifyAndClearExpectations(&destructorHook); + + using Hash = std::hash>; + using HashImpl = std::hash; + + EXPECT_THAT(Hash()(obj), HashImpl()(ptr1)); + EXPECT_THAT(Hash()(empty), HashImpl()(nullptr)); + EXPECT_THAT(Hash()(reset), HashImpl()(nullptr)); + + EXPECT_CALL(destructorHook, Call(ptr1.impl)); +} + +#endif diff --git a/kotlin-native/runtime/src/mm/cpp/AppStateTracking.hpp b/kotlin-native/runtime/src/mm/cpp/AppStateTracking.hpp new file mode 100644 index 00000000000..a5235dbb5e5 --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/AppStateTracking.hpp @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2022 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 + +#include "Utils.hpp" +#include "std_support/Memory.hpp" + +namespace kotlin::mm { + +class AppStateTracking : private Pinned { +public: + enum class State { + kForeground, + kBackground, + }; + + AppStateTracking() noexcept; + ~AppStateTracking(); + + State state() const noexcept { return state_; } + +private: + friend class AppStateTrackingTestSupport; + + void setState(State state) noexcept { state_ = state; } + + class Impl; + + // TODO: The initial value might be incorrect. + std::atomic state_ = State::kForeground; + std_support::unique_ptr impl_; +}; + +} // namespace kotlin::mm diff --git a/kotlin-native/runtime/src/mm/cpp/AppStateTrackingDefault.cpp b/kotlin-native/runtime/src/mm/cpp/AppStateTrackingDefault.cpp new file mode 100644 index 00000000000..eba6d3f34ce --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/AppStateTrackingDefault.cpp @@ -0,0 +1,18 @@ +/* + * Copyright 2010-2022 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. + */ + +#if !KONAN_HAS_UIKIT_FRAMEWORK + +#include "AppStateTracking.hpp" + +using namespace kotlin; + +class mm::AppStateTracking::Impl {}; + +mm::AppStateTracking::AppStateTracking() noexcept = default; + +mm::AppStateTracking::~AppStateTracking() = default; + +#endif diff --git a/kotlin-native/runtime/src/mm/cpp/AppStateTrackingTestSupport.hpp b/kotlin-native/runtime/src/mm/cpp/AppStateTrackingTestSupport.hpp new file mode 100644 index 00000000000..c6c620084ec --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/AppStateTrackingTestSupport.hpp @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2022 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 "AppStateTracking.hpp" + +#include "Utils.hpp" + +namespace kotlin::mm { + +class AppStateTrackingTestSupport : private Pinned { +public: + AppStateTrackingTestSupport(AppStateTracking& appStateTracking) noexcept : appStateTracking_(appStateTracking) {} + + void setState(AppStateTracking::State state) noexcept { appStateTracking_.setState(state); } + +private: + AppStateTracking& appStateTracking_; +}; + +} // namespace kotlin::mm diff --git a/kotlin-native/runtime/src/mm/cpp/AppStateTrackingUIKit.mm b/kotlin-native/runtime/src/mm/cpp/AppStateTrackingUIKit.mm new file mode 100644 index 00000000000..9be4200f3f4 --- /dev/null +++ b/kotlin-native/runtime/src/mm/cpp/AppStateTrackingUIKit.mm @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2022 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. + */ + +#if KONAN_HAS_UIKIT_FRAMEWORK + +#include "AppStateTracking.hpp" + +#include + +// Workaround from https://youtrack.jetbrains.com/issue/KT-48807#focus=Comments-27-5210791.0-0 +// TODO: remove this once our clang supports NSAttributedString as a valid return from the formatter function. +#define NS_FORMAT_ARGUMENT(x) +#import +#undef NS_FORMAT_ARGUMENT + +#include "CompilerConstants.hpp" +#include "objc_support/NSNotificationSubscription.hpp" + +using namespace kotlin; + +class mm::AppStateTracking::Impl : private Pinned { +public: + explicit Impl(std::function handler) noexcept : + handler_(std::move(handler)), + didEnterBackground_(UIApplicationDidEnterBackgroundNotification, [this] { handler_(State::kBackground); }), + willEnterForeground_(UIApplicationWillEnterForegroundNotification, [this] { handler_(State::kForeground); }) {} + +private: + std::function handler_; + objc_support::NSNotificationSubscription didEnterBackground_; + objc_support::NSNotificationSubscription willEnterForeground_; +}; + +mm::AppStateTracking::AppStateTracking() noexcept { + switch (compiler::appStateTracking()) { + case compiler::AppStateTracking::kDisabled: + break; + case compiler::AppStateTracking::kEnabled: + impl_ = std_support::make_unique([this](State state) noexcept { setState(state); }); + break; + } +} + +mm::AppStateTracking::~AppStateTracking() = default; + +#endif diff --git a/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp b/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp index b1fa0049b45..3a8b1de9700 100644 --- a/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp +++ b/kotlin-native/runtime/src/mm/cpp/GlobalData.hpp @@ -14,6 +14,7 @@ #include "ThreadRegistry.hpp" #include "Utils.hpp" #include "ExtraObjectDataFactory.hpp" +#include "AppStateTracking.hpp" namespace kotlin { namespace mm { @@ -28,6 +29,7 @@ public: StableRefRegistry& stableRefRegistry() noexcept { return stableRefRegistry_; } ExtraObjectDataFactory& extraObjectDataFactory() noexcept { return extraObjectDataFactory_; } gc::GC& gc() noexcept { return gc_; } + AppStateTracking& appStateTracking() noexcept { return appStateTracking_; } private: GlobalData(); @@ -37,6 +39,7 @@ private: static GlobalData instance_; ThreadRegistry threadRegistry_; + AppStateTracking appStateTracking_; GlobalsRegistry globalsRegistry_; StableRefRegistry stableRefRegistry_; ExtraObjectDataFactory extraObjectDataFactory_;