diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt index 92a9a3e8a81..990e77c0218 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/KonanFqNames.kt @@ -25,6 +25,7 @@ object KonanFqNames { val threadLocal = FqName("kotlin.native.concurrent.ThreadLocal") val sharedImmutable = FqName("kotlin.native.concurrent.SharedImmutable") val frozen = FqName("kotlin.native.internal.Frozen") + val leakDetectorCandidate = FqName("kotlin.native.internal.LeakDetectorCandidate") val typedIntrinsic = FqName("kotlin.native.internal.TypedIntrinsic") val objCMethod = FqName("kotlinx.cinterop.ObjCMethod") } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt index 97e5b1612a8..08417430cc7 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/RTTIGenerator.kt @@ -60,6 +60,9 @@ internal class RTTIGenerator(override val context: Context) : ContextUtils { result = result or TF_ACYCLIC } } + if (irClass.hasAnnotation(KonanFqNames.leakDetectorCandidate)) { + result = result or TF_LEAK_DETECTOR_CANDIDATE + } if (irClass.isInterface) result = result or TF_INTERFACE return result @@ -569,3 +572,6 @@ internal class RTTIGenerator(override val context: Context) : ContextUtils { private const val TF_IMMUTABLE = 1 private const val TF_ACYCLIC = 2 private const val TF_INTERFACE = 4 +private const val TF_OBJC_DYNAMIC = 8 +private const val TF_LEAK_DETECTOR_CANDIDATE = 16 + diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 060f8295408..b46aa691d72 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -2721,6 +2721,12 @@ standaloneTest("memory_only_gc") { source = "runtime/memory/only_gc.kt" } +standaloneTest("leak_detector") { + disabled = project.globalTestArgs.contains('-opt') + flags = ['-g'] + source = "runtime/memory/leak_detector.kt" +} + standaloneTest("mpp1") { source = "codegen/mpp/mpp1.kt" flags = ['-tr', '-Xmulti-platform'] diff --git a/backend.native/tests/runtime/memory/leak_detector.kt b/backend.native/tests/runtime/memory/leak_detector.kt new file mode 100644 index 00000000000..641c2d7e930 --- /dev/null +++ b/backend.native/tests/runtime/memory/leak_detector.kt @@ -0,0 +1,61 @@ +import kotlin.native.concurrent.* +import kotlin.native.internal.GC +import kotlin.test.* + +/* + * Typical snippet for the leak detector usage. + */ +fun dumpLeaks() { + GC.collect() + GC.detectCycles()?.let { cycles -> + cycles.firstOrNull()?.let { root -> + val cycle = GC.findCycle(root) + println(cycle?.contentToString()) + } + } +} + +fun test1() { + val a = AtomicReference(null) + a.value = a + val cycles = GC.detectCycles()!! + assertEquals(1, cycles.size) + assertTrue(arrayOf(a).contentEquals(GC.findCycle(cycles[0])!!)) + a.value = null +} + +class Holder(var other: Any?) + +fun test2() { + val array = arrayOf(AtomicReference(null), AtomicReference(null)) + val obj1 = Holder(array).freeze() + array[0].value = obj1 + val cycles = GC.detectCycles()!! + assertEquals(1, cycles.size) + assertTrue(arrayOf(obj1, array, array[0]).contentEquals(GC.findCycle(cycles[0])!!)) + array[0].value = null +} + +fun test3() { + val a1 = FreezableAtomicReference(null) + val head = Holder(null) + var current = head + repeat(30) { + val next = Holder(null) + current.other = next + current = next + } + a1.value = head + current.other = a1 + val cycles = GC.detectCycles()!! + assertEquals(1, cycles.size) + val cycle = GC.findCycle(cycles[0])!! + assertEquals(32, cycle.size) + a1.value = null +} + +fun main() { + test1() + test2() + test3() +} \ No newline at end of file diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index 24ef2458410..2cd01cbea0b 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -89,8 +89,13 @@ constexpr size_t kMaxGcAllocThreshold = 8 * 1024 * 1024; typedef KStdUnorderedSet ContainerHeaderSet; typedef KStdVector ContainerHeaderList; -typedef KStdVector KRefPtrList; typedef KStdDeque ContainerHeaderDeque; +typedef KStdVector KRefList; +typedef KStdVector KRefPtrList; +typedef KStdUnorderedSet KRefSet; +typedef KStdUnorderedMap KRefIntMap; +typedef KStdDeque KRefDeque; +typedef KStdDeque KRefListDeque; // A little hack that allows to enable -O2 optimizations // Prevents clang from replacing FrameOverlay struct @@ -104,6 +109,10 @@ volatile int aliveMemoryStatesCount = 0; KBoolean g_checkLeaks = KonanNeedDebugInfo; +// Only used by the leak detector. +KRef g_leakCheckerGlobalList = nullptr; +KInt g_leakCheckerGlobalLock = 0; + // TODO: can we pass this variable as an explicit argument? THREAD_LOCAL_VARIABLE MemoryState* memoryState = nullptr; THREAD_LOCAL_VARIABLE FrameOverlay* currentFrame = nullptr; @@ -689,24 +698,38 @@ inline container_size_t objectSize(const ObjHeader* obj) { return alignUp(size, kObjectAlignment); } +template +inline void traverseObjectFields(ObjHeader* obj, func process) { + const TypeInfo* typeInfo = obj->type_info(); + if (typeInfo != theArrayTypeInfo) { + for (int index = 0; index < typeInfo->objOffsetsCount_; index++) { + ObjHeader** location = reinterpret_cast( + reinterpret_cast(obj) + typeInfo->objOffsets_[index]); + process(location); + } + } else { + ArrayHeader* array = obj->array(); + for (int index = 0; index < array->count_; index++) { + process(ArrayAddressOfElementAt(array, index)); + } + } +} + +template +inline void traverseReferredObjects(ObjHeader* obj, func process) { + traverseObjectFields(obj, [process](ObjHeader** location) { + ObjHeader* ref = *location; + if (ref != nullptr) process(ref); + }); +} + template inline void traverseContainerObjectFields(ContainerHeader* container, func process) { RuntimeAssert(!isAggregatingFrozenContainer(container), "Must not be called on such containers"); ObjHeader* obj = reinterpret_cast(container + 1); + for (int object = 0; object < container->objectCount(); object++) { - const TypeInfo* typeInfo = obj->type_info(); - if (typeInfo != theArrayTypeInfo) { - for (int index = 0; index < typeInfo->objOffsetsCount_; index++) { - ObjHeader** location = reinterpret_cast( - reinterpret_cast(obj) + typeInfo->objOffsets_[index]); - process(location); - } - } else { - ArrayHeader* array = obj->array(); - for (int index = 0; index < array->count_; index++) { - process(ArrayAddressOfElementAt(array, index)); - } - } + traverseObjectFields(obj, process); obj = reinterpret_cast( reinterpret_cast(obj) + objectSize(obj)); } @@ -897,14 +920,29 @@ void freeAggregatingFrozenContainer(ContainerHeader* container) { void runDeallocationHooks(ContainerHeader* container) { ObjHeader* obj = reinterpret_cast(container + 1); - for (int index = 0; index < container->objectCount(); index++) { if (obj->has_meta_object()) { + if (KonanNeedDebugInfo && (obj->type_info()->flags_ & TF_LEAK_DETECTOR_CANDIDATE) != 0 && g_checkLeaks) { + // Remove the object from the double-linked list of potentially cyclic objects. + auto* meta = obj->meta_object(); + lock(&g_leakCheckerGlobalLock); + // Get previous. + auto* previous = meta->LeakDetector.previous_; + auto* previousMeta = (previous != nullptr) ? previous->meta_object() : nullptr; + auto* next = meta->LeakDetector.next_; + auto* nextMeta = (next != nullptr) ? next->meta_object() : nullptr; + // Remove current. + if (previous != nullptr) + previous->meta_object()->LeakDetector.next_ = next; + if (next != nullptr) + next->meta_object()->LeakDetector.previous_ = previous; + if (obj == g_leakCheckerGlobalList) + g_leakCheckerGlobalList = next; + unlock(&g_leakCheckerGlobalLock); + } ObjHeader::destroyMetaObject(&obj->typeInfoOrMeta_); } - - obj = reinterpret_cast( - reinterpret_cast(obj) + objectSize(obj)); + obj = reinterpret_cast(reinterpret_cast(obj) + objectSize(obj)); } } @@ -1364,8 +1402,8 @@ void collectWhite(MemoryState* state, ContainerHeader* start) { toVisit.push_front(childContainer); } }); - runDeallocationHooks(container); - scheduleDestroyContainer(state, container); + runDeallocationHooks(container); + scheduleDestroyContainer(state, container); } } #endif @@ -1877,6 +1915,18 @@ OBJ_GETTER(allocInstance, const TypeInfo* type_info) { checkIfGcNeeded(state); #endif // USE_GC auto container = ObjectContainer(state, type_info); + ObjHeader* obj = container.GetPlace(); + if (KonanNeedDebugInfo && g_checkLeaks && (type_info->flags_ & TF_LEAK_DETECTOR_CANDIDATE) != 0) { + // Add newly allocated object to the double-linked list of potentially cyclic objects. + MetaObjHeader* meta = obj->meta_object(); + lock(&g_leakCheckerGlobalLock); + KRef old = g_leakCheckerGlobalList; + g_leakCheckerGlobalList = obj; + meta->LeakDetector.next_ = old; + if (old != nullptr) + old->meta_object()->LeakDetector.previous_ = obj; + unlock(&g_leakCheckerGlobalLock); + } #if USE_GC if (Strict) { rememberNewContainer(container.header()); @@ -1884,7 +1934,7 @@ OBJ_GETTER(allocInstance, const TypeInfo* type_info) { makeShareable(container.header()); } #endif // USE_GC - RETURN_OBJ(container.GetPlace()); + RETURN_OBJ(obj); } template @@ -2455,6 +2505,7 @@ void ensureNeverFrozen(ObjHeader* object) { object->meta_object()->flags_ |= MF_NEVER_FROZEN; } +// TODO: incorrect, use 3-color scheme. KBoolean ensureAcyclicAndSet(ObjHeader* where, KInt index, ObjHeader* what) { RuntimeAssert(where->container() != nullptr && where->container()->frozen(), "Must be used on frozen objects only"); RuntimeAssert(what == nullptr || isPermanentOrFrozen(what), @@ -2504,6 +2555,99 @@ void shareAny(ObjHeader* obj) { container->makeShared(); } +OBJ_GETTER0(detectCyclicReferences) { + // Collect rootset, hold references to simplify remaining code. + KRefList rootset; + lock(&g_leakCheckerGlobalLock); + auto* candidate = g_leakCheckerGlobalList; + while (candidate != nullptr) { + addHeapRef(candidate); + rootset.push_back(candidate); + candidate = candidate->meta_object()->LeakDetector.next_; + } + unlock(&g_leakCheckerGlobalLock); + KRefSet cyclic; + KRefSet seen; + KRefDeque toVisit; + for (auto* root: rootset) { + seen.clear(); + toVisit.clear(); + traverseReferredObjects(root, [&toVisit](ObjHeader* obj) { toVisit.push_front(obj); }); + bool seenToRoot = false; + while (!toVisit.empty() && !seenToRoot) { + KRef current = toVisit.front(); + toVisit.pop_front(); + if (current == root) seenToRoot = true; + // TODO: racy against concurrent mutators. + if (seen.count(current) == 0) { + traverseReferredObjects(current, [&toVisit](ObjHeader* obj) { + toVisit.push_front(obj); + }); + seen.insert(current); + } + } + if (seenToRoot) { + cyclic.insert(root); + } + } + int numElements = cyclic.size(); + ArrayHeader* result = AllocArrayInstance(theArrayTypeInfo, numElements, OBJ_RESULT)->array(); + KRef* place = ArrayAddressOfElementAt(result, 0); + for (auto* it: cyclic) { + UpdateHeapRef(place++, it); + } + for (auto* root: rootset) { + ReleaseHeapRef(root); + } + RETURN_OBJ(result->obj()); +} + +OBJ_GETTER(findCycle, KRef root) { + KRefSet seen; + KRefListDeque queue; + KRefDeque toVisit; + KRefList path; + traverseReferredObjects(root, [&toVisit](ObjHeader* obj) { toVisit.push_front(obj); }); + bool isFound = false; + while (!toVisit.empty() && !isFound) { + KRef current = toVisit.front(); + toVisit.pop_front(); + // Do DFS path search. + KRefList first; + first.push_back(current); + queue.emplace_back(first); + seen.clear(); + while (!queue.empty()) { + KRefList currentPath = queue.back(); + queue.pop_back(); + KRef node = currentPath[currentPath.size() - 1]; + if (node == root) { + isFound = true; + path = currentPath; + break; + } + if (seen.count(node) == 0) { + // TODO: racy against concurrent mutators. + traverseReferredObjects(node, [&queue, ¤tPath](ObjHeader* obj) { + KRefList newPath(currentPath); + newPath.push_back(obj); + queue.emplace_back(newPath); + }); + seen.insert(node); + } + } + } + ArrayHeader* result = nullptr; + if (isFound) { + result = AllocArrayInstance(theArrayTypeInfo, path.size(), OBJ_RESULT)->array(); + KRef* place = ArrayAddressOfElementAt(result, 0); + for (auto* it: path) { + UpdateHeapRef(place++, it); + } + } + RETURN_OBJ(result->obj()); +} + } // namespace MetaObjHeader* ObjHeader::createMetaObject(TypeInfo** location) { @@ -2535,9 +2679,9 @@ MetaObjHeader* ObjHeader::createMetaObject(TypeInfo** location) { void ObjHeader::destroyMetaObject(TypeInfo** location) { MetaObjHeader* meta = clearPointerBits(*(reinterpret_cast(location)), OBJECT_TAG_MASK); *const_cast(location) = meta->typeInfo_; - if (meta->counter_ != nullptr) { - WeakReferenceCounterClear(meta->counter_); - ZeroHeapRef(&meta->counter_); + if (meta->WeakReference.counter_ != nullptr) { + WeakReferenceCounterClear(meta->WeakReference.counter_); + ZeroHeapRef(&meta->WeakReference.counter_); } #ifdef KONAN_OBJC_INTEROP @@ -2906,6 +3050,15 @@ KBoolean Kotlin_native_internal_GC_getTuneThreshold(KRef) { #endif } +OBJ_GETTER(Kotlin_native_internal_GC_detectCycles, KRef) { + if (!KonanNeedDebugInfo || !g_checkLeaks) RETURN_OBJ(nullptr); + RETURN_RESULT_OF0(detectCyclicReferences); +} + +OBJ_GETTER(Kotlin_native_internal_GC_findCycle, KRef, KRef root) { + RETURN_RESULT_OF(findCycle, root); +} + KNativePtr CreateStablePointer(KRef any) { return createStablePointer(any); } diff --git a/runtime/src/main/cpp/Memory.h b/runtime/src/main/cpp/Memory.h index fdb7dfca0db..7d914f619bf 100644 --- a/runtime/src/main/cpp/Memory.h +++ b/runtime/src/main/cpp/Memory.h @@ -315,16 +315,27 @@ ALWAYS_INLINE bool hasPointerBits(T* ptr, unsigned bits) { struct MetaObjHeader { // Pointer to the type info. Must be first, to match ArrayHeader and ObjHeader layout. const TypeInfo* typeInfo_; - // Strong reference to the counter object. - ObjHeader* counter_; // Container pointer. ContainerHeader* container_; + #ifdef KONAN_OBJC_INTEROP void* associatedObject_; #endif // Flags for the object state. int32_t flags_; + + // TODO: maybe make it a union for the orthogonal features. + struct { + // Strong reference to the counter object. + ObjHeader* counter_; + } WeakReference; + struct { + // Leak detector's previous list element. + ObjHeader* previous_; + // Leak detector's next list element. + ObjHeader* next_; + } LeakDetector; }; // Header of every object. @@ -403,16 +414,16 @@ extern "C" { returnType name(__VA_ARGS__) RUNTIME_NOTHROW; \ returnType name##Strict(__VA_ARGS__) RUNTIME_NOTHROW; \ returnType name##Relaxed(__VA_ARGS__) RUNTIME_NOTHROW; -#define RETURN_OBJ(value) { ObjHeader* obj = value; \ - UpdateReturnRef(OBJ_RESULT, obj); \ - return obj; } +#define RETURN_OBJ(value) { ObjHeader* __obj = value; \ + UpdateReturnRef(OBJ_RESULT, __obj); \ + return __obj; } #define RETURN_RESULT_OF0(name) { \ - ObjHeader* obj = name(OBJ_RESULT); \ - return obj; \ + ObjHeader* __obj = name(OBJ_RESULT); \ + return __obj; \ } #define RETURN_RESULT_OF(name, ...) { \ - ObjHeader* result = name(__VA_ARGS__, OBJ_RESULT); \ - return result; \ + ObjHeader* __result = name(__VA_ARGS__, OBJ_RESULT); \ + return __result; \ } struct MemoryState; diff --git a/runtime/src/main/cpp/TypeInfo.h b/runtime/src/main/cpp/TypeInfo.h index 866349a0224..65898ac26d5 100644 --- a/runtime/src/main/cpp/TypeInfo.h +++ b/runtime/src/main/cpp/TypeInfo.h @@ -57,7 +57,8 @@ enum Konan_TypeFlags { TF_IMMUTABLE = 1 << 0, TF_ACYCLIC = 1 << 1, TF_INTERFACE = 1 << 2, - TF_OBJC_DYNAMIC = 1 << 3 + TF_OBJC_DYNAMIC = 1 << 3, + TF_LEAK_DETECTOR_CANDIDATE = 1 << 4 }; // Flags per object instance. diff --git a/runtime/src/main/cpp/Weak.cpp b/runtime/src/main/cpp/Weak.cpp index a9bcf35768d..ee8c9136cfc 100644 --- a/runtime/src/main/cpp/Weak.cpp +++ b/runtime/src/main/cpp/Weak.cpp @@ -61,13 +61,13 @@ OBJ_GETTER(Konan_getWeakReferenceImpl, ObjHeader* referred) { } #endif // KONAN_OBJC_INTEROP - if (meta->counter_ == nullptr) { + if (meta->WeakReference.counter_ == nullptr) { ObjHolder counterHolder; // Cast unneeded, just to emphasize we store an object reference as void*. ObjHeader* counter = makeWeakReferenceCounter(reinterpret_cast(referred), counterHolder.slot()); - UpdateHeapRefIfNull(&meta->counter_, counter); + UpdateHeapRefIfNull(&meta->WeakReference.counter_, counter); } - RETURN_OBJ(meta->counter_); + RETURN_OBJ(meta->WeakReference.counter_); } // Materialize a weak reference to either null or the real reference. diff --git a/runtime/src/main/kotlin/kotlin/native/concurrent/Atomics.kt b/runtime/src/main/kotlin/kotlin/native/concurrent/Atomics.kt index 7ea41931502..f2a15cc8664 100644 --- a/runtime/src/main/kotlin/kotlin/native/concurrent/Atomics.kt +++ b/runtime/src/main/kotlin/kotlin/native/concurrent/Atomics.kt @@ -7,6 +7,7 @@ package kotlin.native.concurrent import kotlin.native.internal.ExportTypeInfo import kotlin.native.internal.Frozen +import kotlin.native.internal.LeakDetectorCandidate import kotlin.native.internal.NoReorderFields import kotlin.native.SymbolName import kotlinx.cinterop.NativePtr @@ -204,12 +205,22 @@ public class AtomicNativePtr(private var value_: NativePtr) { private external fun getImpl(): NativePtr } + +private fun idString(value: Any) = "${value.hashCode().toUInt().toString(16)}" + +private fun debugString(value: Any?): String { + if (value == null) return "null" + return "${value::class.qualifiedName}: ${idString(value)}" +} + /** * An atomic reference to a frozen Kotlin object. Can be used in concurrent scenarious - * but frequently shall be of nullable type and be zeroed out (with `compareAndSwap(get(), null)`) - * once no longer needed. Otherwise memory leak could happen. + * but frequently shall be of nullable type and be zeroed out once no longer needed. + * Otherwise memory leak could happen. To detect such leaks [kotlin.native.internal.GC.detectCycles] + * in debug mode could be helpful. */ @Frozen +@LeakDetectorCandidate @NoReorderFields public class AtomicReference(private var value_: T) { // A spinlock to fix potential ARC race. @@ -263,7 +274,8 @@ public class AtomicReference(private var value_: T) { * * @return string representation of this object */ - public override fun toString(): String = "Atomic reference to $value" + public override fun toString(): String = + "${debugString(this)} -> ${debugString(value)}" // Implementation details. @SymbolName("Kotlin_AtomicReference_set") @@ -271,15 +283,16 @@ public class AtomicReference(private var value_: T) { @SymbolName("Kotlin_AtomicReference_get") private external fun getImpl(): Any? - } - /** * An atomic reference to a Kotlin object. Can be used in concurrent scenarious, but must be frozen first, - * otherwise behaves as regular box for the value. + * otherwise behaves as regular box for the value. If frozen, shall be zeroed out once no longer needed. + * Otherwise memory leak could happen. To detect such leaks [kotlin.native.internal.GC.detectCycles] + * in debug mode could be helpful. */ @NoReorderFields +@LeakDetectorCandidate @ExportTypeInfo("theFreezableAtomicReferenceTypeInfo") public class FreezableAtomicReference(private var value_: T) { // A spinlock to fix potential ARC race. @@ -342,7 +355,8 @@ public class FreezableAtomicReference(private var value_: T) { * * @return string representation of this object */ - public override fun toString(): String = "Freezable atomic reference to $value" + public override fun toString(): String = + "${debugString(this)} -> ${debugString(value)}" // Implementation details. @SymbolName("Kotlin_AtomicReference_set") diff --git a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt index a366e379829..f9e35e80c6c 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/Annotations.kt @@ -115,4 +115,10 @@ annotation class Independent */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.BINARY) -@PublishedApi internal annotation class FilterExceptions \ No newline at end of file +@PublishedApi internal annotation class FilterExceptions + +/** + * Marks a class whose instances to be added to the list of leak detector candidates. + */ +@Target(AnnotationTarget.CLASS) +@PublishedApi internal annotation class LeakDetectorCandidate \ No newline at end of file diff --git a/runtime/src/main/kotlin/kotlin/native/internal/GC.kt b/runtime/src/main/kotlin/kotlin/native/internal/GC.kt index 1b43bb3d1c5..f90a6e9cb92 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/GC.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/GC.kt @@ -78,6 +78,24 @@ object GC { get() = getTuneThreshold() set(value) = setTuneThreshold(value) + /** + * Detect cyclic references going via atomic references and return list of cycle-inducing objects + * or `null` if the leak detector is not available. Use [Platform.isMemoryLeakCheckerActive] to check + * leak detector availability. + * Note that cycle detector requires reference graph stability, thus it may not work as + * expected or even crash for mutating graphs. + */ + @SymbolName("Kotlin_native_internal_GC_detectCycles") + external fun detectCycles(): Array? + + /** + * Find a reference cycle including from the given object, `null` if no cycles detected. + * Note that cycle detector requires reference graph stability, thus it may not work as + * expected or even crash for mutating graphs. + */ + @SymbolName("Kotlin_native_internal_GC_findCycle") + external fun findCycle(root: Any): Array? + @SymbolName("Kotlin_native_internal_GC_getThreshold") private external fun getThreshold(): Int diff --git a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt index 473000a7ffb..6f091136107 100644 --- a/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt +++ b/runtime/src/main/kotlin/kotlin/native/internal/RuntimeUtils.kt @@ -174,17 +174,21 @@ internal fun getProgressionLast(start: Long, end: Long, step: Long): Long = // Called by the debugger. @ExportForCppRuntime internal fun KonanObjectToUtf8Array(value: Any?): ByteArray { - val string = when (value) { - is Array<*> -> value.contentToString() - is CharArray -> value.contentToString() - is BooleanArray -> value.contentToString() - is ByteArray -> value.contentToString() - is ShortArray -> value.contentToString() - is IntArray -> value.contentToString() - is LongArray -> value.contentToString() - is FloatArray -> value.contentToString() - is DoubleArray -> value.contentToString() - else -> value.toString() + val string = try { + when (value) { + is Array<*> -> value.contentToString() + is CharArray -> value.contentToString() + is BooleanArray -> value.contentToString() + is ByteArray -> value.contentToString() + is ShortArray -> value.contentToString() + is IntArray -> value.contentToString() + is LongArray -> value.contentToString() + is FloatArray -> value.contentToString() + is DoubleArray -> value.contentToString() + else -> value.toString() + } + } catch (error: Throwable) { + "" } return string.encodeToByteArray() }