diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index bcb131aa5b3..f3281382c87 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -692,6 +692,12 @@ task freeze3(type: RunKonanTest) { source = "runtime/workers/freeze3.kt" } +task freeze4(type: RunKonanTest) { + disabled = (project.testTarget == 'wasm32') // No exceptions on WASM. + goldValue = "OK\n" + source = "runtime/workers/freeze4.kt" +} + task atomic0(type: RunKonanTest) { disabled = (project.testTarget == 'wasm32') // Workers need pthreads. goldValue = "35\n" + "20\n" + "OK\n" diff --git a/backend.native/tests/runtime/workers/freeze4.kt b/backend.native/tests/runtime/workers/freeze4.kt new file mode 100644 index 00000000000..620a95cbef6 --- /dev/null +++ b/backend.native/tests/runtime/workers/freeze4.kt @@ -0,0 +1,24 @@ +package runtime.workers.freeze4 + +import kotlin.test.* + +import konan.worker.* + +data class Data(val x: Int, val s: String, val next: Data? = null) + +@Test fun runTest() { + val data1 = Data(1, "") + data1.freeze() + assertFailsWith { + data1.ensureNeverFrozen() + } + + val dataNF = Data(42, "42") + dataNF.ensureNeverFrozen() + val data2 = Data(2, "2", dataNF) + assertFailsWith { + data2.freeze() + } + assert(!data2.isFrozen) + println("OK") +} diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index ed940f2c49f..0a51fea116f 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -223,7 +223,6 @@ constexpr const char* MemoryStatistic::indexToName[]; #endif // COLLECT_STATISTIC struct MemoryState { - #if TRACE_MEMORY // Set of all containers. ContainerHeaderSet* containers; @@ -419,7 +418,7 @@ RUNTIME_USED ContainerHeader theStaticObjectsContainer = { void objc_release(void* ptr); void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject); -RUNTIME_NORETURN void ThrowFreezingException(); +RUNTIME_NORETURN void ThrowFreezingException(KRef toFreeze, KRef blocker); } // extern "C" @@ -1536,11 +1535,18 @@ bool ClearSubgraphReferences(ObjHeader* root, bool checked) { * - not 'marked' and not 'seen' as WHITE marker (object is unprocessed) * When we see GREY during DFS, it means we see cycle. */ -void depthFirstTraversal(ContainerHeader* container, bool* hasCycles, KStdVector& order) { +void depthFirstTraversal(ContainerHeader* container, bool* hasCycles, + KRef* firstBlocker, KStdVector& order) { // Mark GRAY. container->setSeen(); - traverseContainerReferredObjects(container, [hasCycles, &order](ObjHeader* obj) { + traverseContainerReferredObjects(container, [hasCycles, firstBlocker, &order](ObjHeader* obj) { + if (*firstBlocker != nullptr) + return; + if (obj->has_meta_object() && ((obj->meta_object()->flags_ & MF_NEVER_FROZEN) != 0)) { + *firstBlocker = obj; + return; + } ContainerHeader* objContainer = obj->container(); if (!objContainer->permanentOrFrozen()) { // Marked GREY, there's cycle. @@ -1548,7 +1554,7 @@ void depthFirstTraversal(ContainerHeader* container, bool* hasCycles, KStdVector // Go deeper if WHITE. if (!objContainer->seen() && !objContainer->marked()) { - depthFirstTraversal(objContainer, hasCycles, order); + depthFirstTraversal(objContainer, hasCycles, firstBlocker, order); } } }); @@ -1687,8 +1693,12 @@ void FreezeSubgraph(ObjHeader* root) { // Do DFS cycle detection. bool hasCycles = false; + KRef firstBlocker = nullptr; KStdVector order; - depthFirstTraversal(rootContainer, &hasCycles, order); + depthFirstTraversal(rootContainer, &hasCycles, &firstBlocker, order); + if (firstBlocker != nullptr) { + ThrowFreezingException(root, firstBlocker); + } // Now unmark all marked objects, and freeze them, if no cycles detected. if (hasCycles) { freezeCyclic(rootContainer, order); @@ -1743,4 +1753,12 @@ OBJ_GETTER(ReadRefLocked, ObjHeader** location, int32_t* spinlock) { return value; } +void EnsureNeverFrozen(KRef object) { + if (object->container()->frozen()) + ThrowFreezingException(object, object); + // TODO: note, that this API could not not be called on frozen objects, so no need to care much about concurrency, + // although there's subtle race with case, where other thread freezes the same object after check. + object->meta_object()->flags_ |= MF_NEVER_FROZEN; +} + } // extern "C" diff --git a/runtime/src/main/cpp/Memory.h b/runtime/src/main/cpp/Memory.h index 481cf38f0d1..9c43f0dd6d7 100644 --- a/runtime/src/main/cpp/Memory.h +++ b/runtime/src/main/cpp/Memory.h @@ -246,6 +246,8 @@ struct MetaObjHeader { const TypeInfo* typeInfo_; // Strong reference to counter object. ObjHeader* counter_; + // Flags for object state. + int32_t flags_; #ifdef KONAN_OBJC_INTEROP void* associatedObject_; @@ -470,7 +472,9 @@ OBJ_GETTER(AdoptStablePointer, void*) RUNTIME_NOTHROW; // Check mutability state. void MutationCheck(ObjHeader* obj); // Freeze object subgraph. -void FreezeSubgraph(ObjHeader* root); +void FreezeSubgraph(ObjHeader* obj); +// Ensure this object shall block freezing. +void EnsureNeverFrozen(ObjHeader* obj); #ifdef __cplusplus } #endif diff --git a/runtime/src/main/cpp/TypeInfo.h b/runtime/src/main/cpp/TypeInfo.h index 8700c0e4d05..20fa19dd5f0 100644 --- a/runtime/src/main/cpp/TypeInfo.h +++ b/runtime/src/main/cpp/TypeInfo.h @@ -61,6 +61,10 @@ enum Konan_TypeFlags { TF_IMMUTABLE = 1 << 0 }; +enum Konan_MetaFlags { + MF_NEVER_FROZEN = 1 << 0 +}; + // Extended information about a type. struct ExtendedTypeInfo { // Number of fields (negated Konan_RuntimeType for array types). diff --git a/runtime/src/main/cpp/Worker.cpp b/runtime/src/main/cpp/Worker.cpp index bc3681c5ccd..aef6f332c92 100644 --- a/runtime/src/main/cpp/Worker.cpp +++ b/runtime/src/main/cpp/Worker.cpp @@ -577,4 +577,8 @@ KBoolean Kotlin_Worker_isFrozenInternal(KRef object) { return object == nullptr || object->container()->permanentOrFrozen(); } +void Kotlin_Worker_ensureNeverFrozen(KRef object) { + EnsureNeverFrozen(object); +} + } // extern "C" diff --git a/runtime/src/main/kotlin/konan/worker/Freezing.kt b/runtime/src/main/kotlin/konan/worker/Freezing.kt index 67f4b0feea3..5f5932a0d40 100644 --- a/runtime/src/main/kotlin/konan/worker/Freezing.kt +++ b/runtime/src/main/kotlin/konan/worker/Freezing.kt @@ -21,7 +21,8 @@ import konan.internal.ExportForCppRuntime /** * Exception thrown whenever freezing is not possible. */ -public class FreezingException() : RuntimeException() +public class FreezingException(toFreeze: Any, blocker: Any) : + RuntimeException("freezing of $toFreeze has failed, first blocker is $blocker") /** * Exception thrown whenever we attempt to mutate frozen objects. @@ -41,6 +42,14 @@ fun T.freeze(): T { val Any?.isFrozen get() = isFrozenInternal(this) + +/** + * This function ensures that if we see such an object during freezing attempt - freeze fails and FreezingException + * is thrown. Is object is already frozen - FreezingException is thrown immediately. + */ +@SymbolName("Kotlin_Worker_ensureNeverFrozen") +external fun Any.ensureNeverFrozen() + @SymbolName("Kotlin_Worker_freezeInternal") internal external fun freezeInternal(it: Any?) @@ -48,7 +57,8 @@ internal external fun freezeInternal(it: Any?) internal external fun isFrozenInternal(it: Any?): Boolean @ExportForCppRuntime -internal fun ThrowFreezingException(): Nothing = throw FreezingException() +internal fun ThrowFreezingException(toFreeze: Any, blocker: Any): Nothing = + throw FreezingException(toFreeze, blocker) @ExportForCppRuntime internal fun ThrowInvalidMutabilityException(where: Any): Nothing = throw InvalidMutabilityException(where)