[K/N] Process objc run loop on the finalizer thread ^KT-58851
Merge-request: KT-MR-11024 Merged-by: Alexey Glushko <aleksei.glushko@jetbrains.com>
This commit is contained in:
committed by
Space Team
parent
6ac76aa3e9
commit
1eda35a59c
@@ -4147,6 +4147,10 @@ if (PlatformInfo.isAppleTarget(project)) {
|
|||||||
it.defFile 'interop/objc/kt56402/objclib.def'
|
it.defFile 'interop/objc/kt56402/objclib.def'
|
||||||
it.headers "$projectDir/interop/objc/kt56402/objclib.h"
|
it.headers "$projectDir/interop/objc/kt56402/objclib.h"
|
||||||
}
|
}
|
||||||
|
createInterop("objc_autorelease_from_dealloc") {
|
||||||
|
it.defFile 'interop/objc/autorelease_from_dealloc/objclib.def'
|
||||||
|
it.headers "$projectDir/interop/objc/autorelease_from_dealloc/objclib.h"
|
||||||
|
}
|
||||||
createInterop("objCAction") {
|
createInterop("objCAction") {
|
||||||
it.defFile 'interop/objc/objCAction/objclib.def'
|
it.defFile 'interop/objc/objCAction/objclib.def'
|
||||||
it.headers "$projectDir/interop/objc/objCAction/objclib.h"
|
it.headers "$projectDir/interop/objc/objCAction/objclib.h"
|
||||||
@@ -4934,6 +4938,7 @@ if (PlatformInfo.isAppleTarget(project)) {
|
|||||||
mkdir(buildDir)
|
mkdir(buildDir)
|
||||||
project.extensions.execClang.execClangForCompilerTests(project.target) {
|
project.extensions.execClang.execClangForCompilerTests(project.target) {
|
||||||
args "$projectDir/interop/objc/kt56402/objclib.m"
|
args "$projectDir/interop/objc/kt56402/objclib.m"
|
||||||
|
args "-g"
|
||||||
args "-lobjc", '-fobjc-arc'
|
args "-lobjc", '-fobjc-arc'
|
||||||
args '-fPIC', '-shared', '-o', "$buildDir/libobjc_kt56402.dylib"
|
args '-fPIC', '-shared', '-o', "$buildDir/libobjc_kt56402.dylib"
|
||||||
args '-framework', 'AppKit'
|
args '-framework', 'AppKit'
|
||||||
@@ -4944,6 +4949,26 @@ if (PlatformInfo.isAppleTarget(project)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interopTest("interop_objc_autorelease_from_dealloc") {
|
||||||
|
enabled = !isNoopGC // requires some GC
|
||||||
|
source = 'interop/objc/autorelease_from_dealloc/main.kt'
|
||||||
|
interop = "objc_autorelease_from_dealloc"
|
||||||
|
flags = ["-tr"]
|
||||||
|
|
||||||
|
doBeforeBuild {
|
||||||
|
mkdir(buildDir)
|
||||||
|
project.extensions.execClang.execClangForCompilerTests(project.target) {
|
||||||
|
args "$projectDir/interop/objc/autorelease_from_dealloc/objclib.m"
|
||||||
|
args "-lobjc", '-fno-objc-arc'
|
||||||
|
args '-fPIC', '-shared', '-o', "$buildDir/libobjc_autorelease_from_dealloc.dylib"
|
||||||
|
}
|
||||||
|
if (UtilsKt.isSimulatorTarget(project, project.target)) {
|
||||||
|
UtilsKt.codesign(project, "$buildDir/libobjc_autorelease_from_dealloc.dylib")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UtilsKt.dependsOnPlatformLibs(it)
|
||||||
|
}
|
||||||
|
|
||||||
interopTest("interop_objCAction") {
|
interopTest("interop_objCAction") {
|
||||||
source = 'interop/objc/objCAction/main.kt'
|
source = 'interop/objc/objCAction/main.kt'
|
||||||
interop = "objCAction"
|
interop = "objCAction"
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2023 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.
|
||||||
|
*/
|
||||||
|
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class)
|
||||||
|
|
||||||
|
import objclib.*
|
||||||
|
import platform.objc.*
|
||||||
|
import platform.Foundation.*
|
||||||
|
|
||||||
|
import kotlin.concurrent.*
|
||||||
|
import kotlin.native.concurrent.*
|
||||||
|
import kotlin.native.internal.test.testLauncherEntryPoint
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
import kotlin.test.*
|
||||||
|
import kotlin.time.*
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlinx.cinterop.*
|
||||||
|
|
||||||
|
val timeout = 10.seconds
|
||||||
|
|
||||||
|
fun allocCollectable(ctor: () -> ULong): ULong = autoreleasepool {
|
||||||
|
ctor()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event() : NSObject() {
|
||||||
|
@Volatile
|
||||||
|
private var triggered = false
|
||||||
|
|
||||||
|
fun isTriggered() = triggered
|
||||||
|
|
||||||
|
@ObjCAction
|
||||||
|
fun trigger() {
|
||||||
|
assertFalse(isTriggered())
|
||||||
|
triggered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val trigerSelector = sel_registerName("trigger")
|
||||||
|
|
||||||
|
fun waitTriggered(event: Event) {
|
||||||
|
val timeoutMark = TimeSource.Monotonic.markNow() + timeout
|
||||||
|
|
||||||
|
kotlin.native.internal.GC.collect()
|
||||||
|
while (true) {
|
||||||
|
if (event.isTriggered()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assertFalse(timeoutMark.hasPassedNow(), "Timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All the tests are run on a secondary (non main) thread in order to prevent `objcDisposeOnMain` hacks from messing things up
|
||||||
|
@Test
|
||||||
|
fun testAutorelease() {
|
||||||
|
val event = withWorker {
|
||||||
|
execute(TransferMode.SAFE, {}) {
|
||||||
|
val event = Event()
|
||||||
|
assertFalse(event.isTriggered())
|
||||||
|
|
||||||
|
val victimId = allocCollectable {
|
||||||
|
val v = OnDestroyHook {
|
||||||
|
event.trigger()
|
||||||
|
}
|
||||||
|
retain(v.identity())
|
||||||
|
v.identity()
|
||||||
|
}
|
||||||
|
|
||||||
|
allocCollectable {
|
||||||
|
OnDestroyHook {
|
||||||
|
autorelease(victimId)
|
||||||
|
}.identity()
|
||||||
|
}
|
||||||
|
|
||||||
|
event
|
||||||
|
}.result
|
||||||
|
}
|
||||||
|
waitTriggered(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTimer() {
|
||||||
|
val event = Event()
|
||||||
|
|
||||||
|
withWorker {
|
||||||
|
execute(TransferMode.SAFE, { event }) { event ->
|
||||||
|
allocCollectable {
|
||||||
|
OnDestroyHook {
|
||||||
|
assertFalse(event.isTriggered())
|
||||||
|
NSTimer.scheduledTimerWithTimeInterval(0.0, target=event, selector=trigerSelector, userInfo=null, repeats=false)
|
||||||
|
}.identity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitTriggered(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSelector() {
|
||||||
|
val event = Event()
|
||||||
|
|
||||||
|
withWorker {
|
||||||
|
execute(TransferMode.SAFE, { event }) { event ->
|
||||||
|
allocCollectable {
|
||||||
|
OnDestroyHook {
|
||||||
|
assertFalse(event.isTriggered())
|
||||||
|
NSRunLoop.currentRunLoop().performSelector(trigerSelector, target=event, argument=null, order=0, modes=listOf(NSDefaultRunLoopMode))
|
||||||
|
}.identity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitTriggered(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPerformBlock() {
|
||||||
|
val event = Event()
|
||||||
|
|
||||||
|
withWorker {
|
||||||
|
execute(TransferMode.SAFE, { event }) { event ->
|
||||||
|
allocCollectable {
|
||||||
|
OnDestroyHook {
|
||||||
|
NSRunLoop.currentRunLoop().performBlock({
|
||||||
|
assertFalse(event.isTriggered())
|
||||||
|
event.trigger()
|
||||||
|
});
|
||||||
|
}.identity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitTriggered(event)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
language = Objective-C
|
||||||
|
headerFilter = **/objclib.h
|
||||||
|
linkerOpts = -lobjc_autorelease_from_dealloc
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include <inttypes.h>
|
||||||
|
#include <objc/NSObject.h>
|
||||||
|
|
||||||
|
@interface OnDestroyHook: NSObject
|
||||||
|
-(instancetype)init:(void(^)(uintptr_t))onDestroy;
|
||||||
|
-(uintptr_t)identity;
|
||||||
|
@end
|
||||||
|
|
||||||
|
void retain(uint64_t);
|
||||||
|
void release(uint64_t);
|
||||||
|
void autorelease(uint64_t);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#include "objclib.h"
|
||||||
|
|
||||||
|
@implementation OnDestroyHook {
|
||||||
|
void (^_onDestroy)(uintptr_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(uintptr_t)identity {
|
||||||
|
return (uintptr_t)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(instancetype)init:(void (^)(uintptr_t))onDestroy {
|
||||||
|
if (self = [super init]) {
|
||||||
|
_onDestroy = [onDestroy retain];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void)dealloc {
|
||||||
|
_onDestroy([self identity]);
|
||||||
|
[_onDestroy release];
|
||||||
|
[super dealloc];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
void retain(uint64_t obj) {
|
||||||
|
[((id) obj) retain];
|
||||||
|
}
|
||||||
|
|
||||||
|
void release(uint64_t obj) {
|
||||||
|
[((id) obj) release];
|
||||||
|
}
|
||||||
|
|
||||||
|
void autorelease(uint64_t obj) {
|
||||||
|
[((id) obj) autorelease];
|
||||||
|
}
|
||||||
@@ -17,6 +17,13 @@
|
|||||||
#include "Runtime.h"
|
#include "Runtime.h"
|
||||||
#include "ScopedThread.hpp"
|
#include "ScopedThread.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "Logging.hpp"
|
||||||
|
|
||||||
|
#if KONAN_OBJC_INTEROP
|
||||||
|
#include "ObjCMMAPI.h"
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <CoreFoundation/CFRunLoop.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace kotlin::gc {
|
namespace kotlin::gc {
|
||||||
|
|
||||||
@@ -26,7 +33,7 @@ public:
|
|||||||
// epochDoneCallback could be called on any subset of them.
|
// epochDoneCallback could be called on any subset of them.
|
||||||
// If no new tasks are set, epochDoneCallback will be eventually called on last epoch
|
// If no new tasks are set, epochDoneCallback will be eventually called on last epoch
|
||||||
explicit FinalizerProcessor(std::function<void(int64_t)> epochDoneCallback) noexcept :
|
explicit FinalizerProcessor(std::function<void(int64_t)> epochDoneCallback) noexcept :
|
||||||
epochDoneCallback_(std::move(epochDoneCallback)) {}
|
epochDoneCallback_(std::move(epochDoneCallback)), processingLoop_(*this) {}
|
||||||
|
|
||||||
~FinalizerProcessor() { StopFinalizerThread(); }
|
~FinalizerProcessor() { StopFinalizerThread(); }
|
||||||
|
|
||||||
@@ -40,7 +47,7 @@ public:
|
|||||||
StartFinalizerThreadIfNone();
|
StartFinalizerThreadIfNone();
|
||||||
FinalizerQueueTraits::add(finalizerQueue_, std::move(tasks));
|
FinalizerQueueTraits::add(finalizerQueue_, std::move(tasks));
|
||||||
finalizerQueueEpoch_ = epoch;
|
finalizerQueueEpoch_ = epoch;
|
||||||
finalizerQueueCondVar_.notify_all();
|
processingLoop_.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
void StopFinalizerThread() noexcept {
|
void StopFinalizerThread() noexcept {
|
||||||
@@ -48,7 +55,7 @@ public:
|
|||||||
std::unique_lock guard(finalizerQueueMutex_);
|
std::unique_lock guard(finalizerQueueMutex_);
|
||||||
if (!finalizerThread_.joinable()) return;
|
if (!finalizerThread_.joinable()) return;
|
||||||
shutdownFlag_ = true;
|
shutdownFlag_ = true;
|
||||||
finalizerQueueCondVar_.notify_all();
|
processingLoop_.notify();
|
||||||
}
|
}
|
||||||
finalizerThread_.join();
|
finalizerThread_.join();
|
||||||
shutdownFlag_ = false;
|
shutdownFlag_ = false;
|
||||||
@@ -71,26 +78,7 @@ public:
|
|||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
}
|
}
|
||||||
initializedCondVar_.notify_all();
|
initializedCondVar_.notify_all();
|
||||||
int64_t finalizersEpoch = 0;
|
processingLoop_.body();
|
||||||
while (true) {
|
|
||||||
std::unique_lock lock(finalizerQueueMutex_);
|
|
||||||
finalizerQueueCondVar_.wait(lock, [this, &finalizersEpoch] {
|
|
||||||
return !FinalizerQueueTraits::isEmpty(finalizerQueue_) || finalizerQueueEpoch_ != finalizersEpoch || shutdownFlag_;
|
|
||||||
});
|
|
||||||
if (FinalizerQueueTraits::isEmpty(finalizerQueue_) && finalizerQueueEpoch_ == finalizersEpoch) {
|
|
||||||
newTasksAllowed_ = false;
|
|
||||||
RuntimeAssert(shutdownFlag_, "Nothing to do, but no shutdownFlag_ is set on wakeup");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
auto queue = std::move(finalizerQueue_);
|
|
||||||
finalizersEpoch = finalizerQueueEpoch_;
|
|
||||||
lock.unlock();
|
|
||||||
if (!FinalizerQueueTraits::isEmpty(queue)) {
|
|
||||||
ThreadStateGuard guard(ThreadState::kRunnable);
|
|
||||||
FinalizerQueueTraits::process(std::move(queue));
|
|
||||||
}
|
|
||||||
epochDoneCallback_(finalizersEpoch);
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
std::unique_lock guard(initializedMutex_);
|
std::unique_lock guard(initializedMutex_);
|
||||||
initialized_ = false;
|
initialized_ = false;
|
||||||
@@ -105,6 +93,119 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// should be called under the finalizerQueueMutex_
|
||||||
|
bool shouldShutdown(int64_t lastProcessedEpoch) noexcept {
|
||||||
|
bool shouldShutdown = FinalizerQueueTraits::isEmpty(finalizerQueue_) && finalizerQueueEpoch_ == lastProcessedEpoch;
|
||||||
|
if (shouldShutdown) {
|
||||||
|
RuntimeAssert(shutdownFlag_, "Nothing to do, but no shutdownFlag_ is set on wakeup");
|
||||||
|
}
|
||||||
|
return shouldShutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
void processSingle(FinalizerQueue&& queue, int64_t currentEpoch) noexcept {
|
||||||
|
if (!FinalizerQueueTraits::isEmpty(queue)) {
|
||||||
|
#if KONAN_OBJC_INTEROP
|
||||||
|
konan::AutoreleasePool autoreleasePool;
|
||||||
|
#endif
|
||||||
|
ThreadStateGuard guard(ThreadState::kRunnable);
|
||||||
|
FinalizerQueueTraits::process(std::move(queue));
|
||||||
|
}
|
||||||
|
epochDoneCallback_(currentEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if KONAN_OBJC_INTEROP
|
||||||
|
class ProcessingLoop {
|
||||||
|
public:
|
||||||
|
explicit ProcessingLoop(FinalizerProcessor& owner) :
|
||||||
|
owner_(owner),
|
||||||
|
sourceContext_{
|
||||||
|
0, this, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||||
|
[](void* info) {
|
||||||
|
auto& self = *reinterpret_cast<ProcessingLoop*>(info);
|
||||||
|
self.handleNewFinalizers();
|
||||||
|
}},
|
||||||
|
runLoopSource_(CFRunLoopSourceCreate(nullptr, 0, &sourceContext_)) {}
|
||||||
|
|
||||||
|
~ProcessingLoop() {
|
||||||
|
CFRelease(runLoopSource_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notify() {
|
||||||
|
// wait until runLoop_ ptr is published
|
||||||
|
while (runLoop_.load(std::memory_order_acquire) == nullptr) {
|
||||||
|
std::this_thread::yield();
|
||||||
|
}
|
||||||
|
// notify
|
||||||
|
CFRunLoopSourceSignal(runLoopSource_);
|
||||||
|
CFRunLoopWakeUp(runLoop_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void body() {
|
||||||
|
konan::AutoreleasePool autoreleasePool;
|
||||||
|
auto mode = kCFRunLoopDefaultMode;
|
||||||
|
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource_, mode);
|
||||||
|
runLoop_.store(CFRunLoopGetCurrent(), std::memory_order_release);
|
||||||
|
|
||||||
|
CFRunLoopRun();
|
||||||
|
|
||||||
|
runLoop_.store(nullptr, std::memory_order_release);
|
||||||
|
|
||||||
|
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource_, mode);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
void handleNewFinalizers() {
|
||||||
|
std::unique_lock lock(owner_.finalizerQueueMutex_);
|
||||||
|
if (owner_.shouldShutdown(finishedEpoch_)) {
|
||||||
|
owner_.newTasksAllowed_ = false;
|
||||||
|
CFRunLoopStop(runLoop_.load(std::memory_order_acquire));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto queue = std::move(owner_.finalizerQueue_);
|
||||||
|
int64_t currentEpoch = owner_.finalizerQueueEpoch_;
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
owner_.processSingle(std::move(queue), currentEpoch);
|
||||||
|
finishedEpoch_ = currentEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
FinalizerProcessor& owner_;
|
||||||
|
int64_t finishedEpoch_ = 0;
|
||||||
|
CFRunLoopSourceContext sourceContext_;
|
||||||
|
std::atomic<CFRunLoopRef> runLoop_ = nullptr;
|
||||||
|
CFRunLoopSourceRef runLoopSource_;
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
class ProcessingLoop {
|
||||||
|
public:
|
||||||
|
explicit ProcessingLoop(FinalizerProcessor& owner) : owner_(owner) {}
|
||||||
|
|
||||||
|
void notify() {
|
||||||
|
owner_.finalizerQueueCondVar_.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void body() {
|
||||||
|
int64_t finishedEpoch = 0;
|
||||||
|
while (true) {
|
||||||
|
std::unique_lock lock(owner_.finalizerQueueMutex_);
|
||||||
|
owner_.finalizerQueueCondVar_.wait(lock, [this, &finishedEpoch] {
|
||||||
|
return !FinalizerQueueTraits::isEmpty(owner_.finalizerQueue_) || owner_.finalizerQueueEpoch_ != finishedEpoch || owner_.shutdownFlag_;
|
||||||
|
});
|
||||||
|
if (owner_.shouldShutdown(finishedEpoch)) {
|
||||||
|
owner_.newTasksAllowed_ = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto queue = std::move(owner_.finalizerQueue_);
|
||||||
|
auto currentEpoch = owner_.finalizerQueueEpoch_;
|
||||||
|
lock.unlock();
|
||||||
|
owner_.processSingle(std::move(queue), currentEpoch);
|
||||||
|
finishedEpoch = currentEpoch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
FinalizerProcessor& owner_;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
ScopedThread finalizerThread_;
|
ScopedThread finalizerThread_;
|
||||||
FinalizerQueue finalizerQueue_;
|
FinalizerQueue finalizerQueue_;
|
||||||
std::condition_variable finalizerQueueCondVar_;
|
std::condition_variable finalizerQueueCondVar_;
|
||||||
@@ -114,11 +215,14 @@ private:
|
|||||||
bool shutdownFlag_ = false;
|
bool shutdownFlag_ = false;
|
||||||
bool newTasksAllowed_ = true;
|
bool newTasksAllowed_ = true;
|
||||||
|
|
||||||
|
ProcessingLoop processingLoop_;
|
||||||
|
|
||||||
std::mutex initializedMutex_;
|
std::mutex initializedMutex_;
|
||||||
std::condition_variable initializedCondVar_;
|
std::condition_variable initializedCondVar_;
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
|
|
||||||
std::mutex threadCreatingMutex_;
|
std::mutex threadCreatingMutex_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace kotlin::gc
|
} // namespace kotlin::gc
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ konan::AutoreleasePool::AutoreleasePool()
|
|||||||
: handle(objc_autoreleasePoolPush()) {}
|
: handle(objc_autoreleasePoolPush()) {}
|
||||||
|
|
||||||
konan::AutoreleasePool::~AutoreleasePool() {
|
konan::AutoreleasePool::~AutoreleasePool() {
|
||||||
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative);
|
kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative, true);
|
||||||
objc_autoreleasePoolPop(handle);
|
objc_autoreleasePoolPop(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user