JetBrains / kotlin-native

Kotlin/Native infrastructure
Apache License 2.0
7.02k stars 566 forks source link

Memory.cpp:356: runtime assert: Unable to execute Kotlin code on uninitialized thread #2207

Closed yuriry closed 5 years ago

yuriry commented 5 years ago

My code is running into this line when it calls a method from iOS (calling from Android works fine). The method getFoo() looks like this:

internal expect val ApplicationDispatcher: CoroutineDispatcher

class RESTClient(val host: String) {
    private val client = HttpClient()

    fun getFoo(path: String?, extraHeaders: Map<String, String>? = null, callback: (Foo?, Exception?) -> Unit) {
        GlobalScope.launch(ApplicationDispatcher) {
            try {
                val jsonText: String = client.get {
                    url {
                        protocol = URLProtocol.HTTPS
                        host = this@RESTClient.host
                        encodedPath = ...
                        extraHeaders?.forEach { header(it.key, it.value)  }
                    }
                }
                val foo = Foo.parse(jsonText)
                callback(foo, null)
            } catch (e: Exception) {
                callback(null, e)
            }
        }
    }

My current version information is as follows:

kotlin_version=1.3.0-rc-57
kotlin_native_version=0.9.2
serialization_version=0.8.1-rc13
ktor_version=0.9.5-rc13-conf2
android_gradle_version=3.2.0

Any help on how to properly initialize treads when calling Kotlin methods on iOS would be greatly appreciated.

yuriry commented 5 years ago

More information: iOS version of ApplicationDispatcher looks like this:

internal actual val ApplicationDispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())

internal class NsQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatchQueue) {
            block.run()
        }
    }
}
yuriry commented 5 years ago

If I replace NsQueueDispatcher with Dispatchers.Default on iOS side, my method gets called, but it still fails later on.

0x0000000105bca146 kfun:kotlin.Exception.<init>(kotlin.String?)kotlin.Exception + 70
0x0000000105bca066 kfun:kotlin.RuntimeException.<init>(kotlin.String?)kotlin.RuntimeException + 70
0x0000000105bd66b6 kfun:kotlin.IllegalStateException.<init>(kotlin.String?)kotlin.IllegalStateException + 70
0x0000000105cedbbf kfun:kotlinx.coroutines.takeEventLoop#internal + 239
0x0000000105ceda76 kfun:kotlinx.coroutines.DefaultExecutor.dispatch(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.Runnable) + 86
0x0000000105ce2e8c kfun:kotlinx.coroutines.resumeCancellable$kotlinx-coroutines-core-native@kotlin.coroutines.Continuation<#GENERIC>.(#GENERIC)Generic + 380
0x0000000105cebe56 kfun:kotlinx.coroutines.intrinsics.startCoroutineCancellable@kotlin.coroutines.SuspendFunction1<#GENERIC,#GENERIC>.(#GENERIC;kotlin.coroutines.Continuation<#GENERIC>)Generic + 118
0x0000000105cebd6b kfun:kotlinx.coroutines.CoroutineStart.invoke(kotlin.coroutines.SuspendFunction1<#GENERIC,#GENERIC>;#GENERIC;kotlin.coroutines.Continuation<#GENERIC>)Generic + 155
0x0000000105cebc8e kfun:kotlinx.coroutines.AbstractCoroutine.start(kotlinx.coroutines.CoroutineStart;#GENERIC;kotlin.coroutines.SuspendFunction1<#GENERIC,#GENERIC>)Generic + 110
0x0000000105cec260 kfun:kotlinx.coroutines.launch@kotlinx.coroutines.CoroutineScope.(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.CoroutineStart;kotlin.Function1<kotlin.Throwable?,kotlin.Unit>?;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,kotlin.Unit>)kotlinx.coroutines.Job + 288
0x0000000105cf3440 kfun:kotlinx.coroutines.launch$default@kotlinx.coroutines.CoroutineScope.(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.CoroutineStart;kotlin.Function1<kotlin.Throwable?,kotlin.Unit>?;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,kotlin.Unit>;kotlin.Int)kotlinx.coroutines.Job + 384
0x0000000105b9d282 kfun:org.example.serialization.RESTClient.getFoo(kotlin.String?;kotlin.collections.Map<kotlin.String,kotlin.String>?;kotlin.Function2<org.example.serialization.Foo?,kotlin.Exception?,kotlin.Unit>) + 258
Alex009 commented 5 years ago

@yuriry for dispatch_async my worked coroutinedispatcher is:

class MainQueueDispatcher : ContinuationDispatcher() {
    private val mQueue = dispatch_get_main_queue()

    override fun <T> dispatchResume(value: T, continuation: Continuation<T>): Boolean {
        dispatch_async_f(mQueue,
                DetachedObjectGraph<Pair<T, Continuation<T>>>(TransferMode.UNSAFE) { Pair(value, continuation) }.asCPointer(),
                staticCFunction { it ->
                    initRuntimeIfNeeded()
                    val pair = DetachedObjectGraph<Pair<T, Continuation<T>>>(it).attach()
                    pair.second.resume(pair.first)
                })
        return true
    }

    override fun dispatchResumeWithException(exception: Throwable, continuation: Continuation<*>): Boolean {
        dispatch_async_f(mQueue,
                DetachedObjectGraph<Pair<Throwable, Continuation<*>>>(TransferMode.UNSAFE) { Pair(exception, continuation) }.asCPointer(),
                staticCFunction { it ->
                    initRuntimeIfNeeded()
                    val pair = DetachedObjectGraph<Pair<Throwable, Continuation<*>>>(it).attach()
                    pair.second.resumeWithException(pair.first)
                })
        return true
    }
}

i think it's can be unstable in some cases, but in my case it's work

olonho commented 5 years ago

Can you try build with 973cc7bbdb0333b4d1e241c59499e88c086fb02c?

yuriry commented 5 years ago

@olonho I built the distribution when I had Xcode 9.x, but now with Xcode 10.0 there are many errors like below when I run ./gradlew bundle:

.../kotlin-native/runtime/src/main/cpp/Exceptions.cpp:16:10: fatal error: 'stdio.h' file not found
#include <stdio.h>
         ^~~~~~~~~
1 error generated.
In file included from .../kotlin-native/runtime/src/main/cpp/ObjCExport.mm:17:
.../kotlin-native/runtime/src/main/cpp/Types.h:20:10: fatal error: 'stdlib.h' file not found
#include <stdlib.h>
         ^~~~~~~~~~

I removed ~/.konan, re-run ./gradlew dependencies:update, but running ./gradlew bundle fails. Edit: The failure is on master branch after syncing to 973cc7b

olonho commented 5 years ago

Likely you didn't run command-line tools installation, just run Xcode and it will prompt you to do so.

yuriry commented 5 years ago

Installing Command Line tools now. Strange that when I had Xcode 9.x, the Kotlin Native distribution didn't compile with Command Line tools installed. I had to remove them and re-boot the laptop in order to compile.

yuriry commented 5 years ago

Installed command line tools, installed updates, rebooted the laptop, removed ~/.konan, ran ./gradlew clean, ran ./gradlew dependencies:update. Still have similar issues when running ./gradlew bundle

> Task :common:android_arm32Hash FAILED
.../kotlin-native/common/src/hash/cpp/Base64.cpp:16:10: fatal error: 'string.h' file not found
#include <string.h>
         ^~~~~~~~~~
1 error generated.
.../kotlin-native/common/src/hash/cpp/Sha1.cpp:35:10: fatal error: 'stdio.h' file not found
#include <stdio.h>
         ^~~~~~~~~
1 error generated.
.../kotlin-native/common/src/hash/cpp/Names.cpp:16:10: fatal error: 'cassert' file not found
#include <cassert>
         ^~~~~~~~~
1 error generated.
.../kotlin-native/common/src/hash/cpp/City.cpp:18:10: fatal error: 'string.h' file not found
#include <string.h>
         ^~~~~~~~~~
1 error generated.

> Task :Interop:Runtime:compileCallbacksSharedLibraryCallbacksC FAILED
.../kotlin-native/Interop/Runtime/src/callbacks/c/callbacks.c:4:10: fatal error: 'ffi.h' file not found
#include <ffi.h>
         ^~~~~~~
1 error generated.

Any other suggestions?

olonho commented 5 years ago

It seems dependencies aren't downloaded yet. What's missing is android_arm32 deps.

yuriry commented 5 years ago

@olonho In common subproject the following tasks fail with similar errors:

The following tasks succeed:

Where do dependencies for the failing tasks come from? Are they supposed to be downloaded by gradle or do I need to install them locally by hand?

yuriry commented 5 years ago

The task :Interop:Runtime:compileCallbacksSharedLibraryCallbacksC always fails

> Task :Interop:Runtime:compileCallbacksSharedLibraryCallbacksC FAILED
.../kotlin-native/Interop/Runtime/src/callbacks/c/callbacks.c:4:10: fatal error: 'ffi.h' file not found
#include <ffi.h>
         ^~~~~~~
1 error generated.

ffi.h is located in

/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/win/x64/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/win/ia32/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/freebsd/x64/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/freebsd/ia32/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/mac/x64/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/mac/ia32/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/linux/x64/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/linux/ia32/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/linux/arm/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/solaris/x64/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/config/solaris/ia32/ffi.h
/Applications/Xcode.app/Contents/Developer/usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/src/ffi.h
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/ffi/ffi.h

And after installing Command Line Tools, it is also in

/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/ffi/ffi.h

Could multiple ffi.h be the issue?

yuriry commented 5 years ago

I removed Command Line Tools, removed Xcode, installed Xcode (v 10.0) again, removed ~/.m2, ~/.gradle and ~/.konan, rebooted the laptop, ran ./gradlew dependencies:update without problems, but ./gradlew bundle still fails

> Task :Interop:Runtime:compileCallbacksSharedLibraryCallbacksC FAILED
.../kotlin-native/Interop/Runtime/src/callbacks/c/callbacks.c:4:10: fatal error: 'ffi.h' file not found
#include <ffi.h>
         ^~~~~~~
1 error generated.
yuriry commented 5 years ago

I noticed v1.0 was released which includes 973cc7b, so I synced to v1.0 tag and it seems to building fine (at least it is way past the previous point of failure)

yuriry commented 5 years ago

@olonho After checking out v1.0 tag, the build of Kotlin Native compiler succeeded. But then when I tried to build iOS version of my app, I got undefined symbols errors similar to this one.

Then I re-compiled and published locally serialization, atomicFu, kotlinx-io, couroutines, and ktor, making sure only locally built dependencies are used to build each of the libraries. After that I was able to build iOS version of my app.

Now, when I run my app, I get the error shown below. I'm not sure if this is because I built and published the libraries from incorrect branches, or if this a legitimate bug. Any thoughts?

at 0   MyLib                            0x0000000108b61ad6 kfun:kotlin.Exception.<init>(kotlin.String?)kotlin.Exception + 70
at 1   MyLib                            0x0000000108b61a46 kfun:kotlin.RuntimeException.<init>(kotlin.String?)kotlin.RuntimeException + 70
at 2   MyLib                            0x0000000108c01d76 kfun:kotlin.native.IncorrectDereferenceException.<init>(kotlin.String)kotlin.native.IncorrectDereferenceException + 70
at 3   MyLib                            0x0000000108c024bc ThrowIllegalObjectSharingException + 300
at 4   MyLib                            0x0000000108c1ac3d _ZNK16KRefSharedHolder14verifyRefOwnerEv + 77
at 5   MyLib                            0x0000000108c1ad0a -[KotlinObjectHolder ref] + 26
at 6   MyLib                            0x0000000108d005ba platform_darwin_kniBridge206 + 90
at 7   MyLib                            0x0000000108d00ca9 __platform_darwin_kniBridge205_block_invoke + 25
at 8   libdispatch.dylib                   0x000000010fc575d1 _dispatch_call_block_and_release + 12
at 9   libdispatch.dylib                   0x000000010fc5863e _dispatch_client_callout + 8
at 10  libdispatch.dylib                   0x000000010fc659d6 _dispatch_main_queue_callback_4CF + 1541
at 11  CoreFoundation                      0x000000010add87f9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
at 12  CoreFoundation                      0x000000010add2e86 __CFRunLoopRun + 2342
at 13  CoreFoundation                      0x000000010add2221 CFRunLoopRunSpecific + 625
at 14  GraphicsServices                    0x0000000114ee61dd GSEventRunModal + 62
at 15  UIKitCore                           0x0000000110bde115 UIApplicationMain + 140
at 16  libswiftUIKit.dylib                 0x000000010e97d92a $S5UIKit17UIApplicationMainys5Int32VAD_SpySpys4Int8VGGSgSSSgAJtF + 154
at 17  MyApp                          0x0000000104ba216f main + 255
at 18  libdyld.dylib                       0x000000010fcce551 start + 1

P.S. And there is no problem with Android app (as usual).

SvyatoslavScherbina commented 5 years ago

This stacktrace seems to be related to the problem you encountered before updating. It means that your program tries to use non-shared objects from other threads. In your case this is likely to be caused by creating RESTClient and calling getFoo from non-main thread.

yuriry commented 5 years ago

@SvyatoslavScherbina Thank you for the reply. This is interesting. May be my efforts to rebuild Kotlin Native compiler, the plugin and all the libraries were not necessary?

I didn't realize the RESTClient should be created on main thread, and getFoo should also be called on main thread. This behavior is different from what is happening on Android. In both iOS and Android I create RESTClient and call getFoo on background threads. Android works, iOS does not. If I'm not mistaken, this is related to this comment and the linked issue.

I changed iOS code to create RESTClient and call getFoo on main thread:

DispatchQueue.main.async {
    let client = RESTClient(host:host)
    client.getFoo(path:path, extraHeaders:headers) { (foo:Foo?, error:KotlinException?) -> KotlinUnit in
        processFoo(foo)  // <-- breakpoint 1
        return KotlinUnit()
    }
}

After the change I get deserialized foo instance and can access all its fields in the debugger. Everything is fine at breakpoint 1. The next problem is that processFoo() needs to be called on a background thread. If I call it as shown above, the call happens on the main thread and assserts inside processFoo() halt the application.

My next step was to call processFoo() on the background thread:

DispatchQueue.main.async {
    let client = RESTClient(host:host)
    client.getFoo(path:path, extraHeaders:headers) { (foo:Foo?, error:KotlinException?) -> KotlinUnit in
        DispatchQueue.global().async { // <-- breakpoint 2
            processFoo(foo)            // <-- breakpoint 3
        }
        return KotlinUnit()
    }
}

At breakpoint 2 foo is accessible in the debugger, but when I try to access foo at breakpoint 3, I get the following error in the lldb console:

(lldb) p foo
error: Execution was interrupted, reason: internal c++ exception breakpoint(-4)..
The process has been returned to the state before expression evaluation.

If I continue the debugger, I get the same stack with ThrowIllegalObjectSharingException as in the earlier comment.

Any advice on how to correctly call processFoo() on a background thread would be greatly appreciated.

yuriry commented 5 years ago

@SvyatoslavScherbina Is it possible that by the time the code on a background thread runs, the instance of Foo is already deallocated?

yuriry commented 5 years ago

@SvyatoslavScherbina As an experiment, if I commented out asserts inside of processFoo(), and the method processes Foo instance without problems. But this includes access to CoreData, and we don't want to access Core Data in production on the main thread. Is it possible to pass an instance of Foo from the main thread back to some background thread?

(Allowing creating RESTClient and calling getFoo() on background thread would be even better (similarly how it works on Android/JVM))

SvyatoslavScherbina commented 5 years ago

May be my efforts to rebuild Kotlin Native compiler, the plugin and all the libraries were not necessary?

These efforts were absolutely useful, since we have improved incorrect object sharing diagnostics in the version you updated to. That's why you get an exception now instead of runtime assertion.

Any advice on how to correctly call processFoo() on a background thread would be greatly appreciated.

It depends on how your data is organized. Kotlin/Native imposes strict restrictions on sharing objects among different threads. Does anything prevent you from parsing Foo on background thread just before calling processFoo? Does this parsing require anything except jsonText? If the answer is "no" in both cases, then I suppose I have simple solution for you.

yuriry commented 5 years ago

Foo is parsed by RESTClient using kotlinx.serialization library, and this is the implementation of Foo.parse. processFoo() takes a parsed Foo instance, maps it to an instance of different Core Data class and saves it. May be this is not an ideal cut point, but our application is pretty large and we'd like to migrate it incrementally to Kotlin Native. Our current goal is to create a REST API layer that we can share between iOS and Android.

RESTClient is making HTTP calls and returns us parsed data, such as Foo. The existing part of the application picks up parsed data and continues processing (mapping to Core Data and saving), and this further processing should happen on a background thread. I hope this description makes sense, but I can elaborate more if required.

SvyatoslavScherbina commented 5 years ago

Do I understand correctly that Foo is not mutated (i.e. fields values aren't changed) after parsing?

yuriry commented 5 years ago

Most data classes have only val fields. A few classes have var fields, but they are not modified after parsing. The classes with var fields are also used to construct requests, that's why the fields are var. But I think we can convert all of them to val fields with slight modification of the code that creates request objects.

SvyatoslavScherbina commented 5 years ago

But I think we can convert all of them to val fields

It is not necessary. Instead you can "freeze" Foo instance after parsing by calling .freeze() method on it. After this it becomes immutable (if you try to mutate it, then an exception will be thrown), but also can be properly used from any thread.

See documentation for more details.

yuriry commented 5 years ago

@SvyatoslavScherbina I hope this is right, at least it works - Foo is accessible on a background thread. Thank you so much for your help!

Common

internal expect fun <T> freeze(t: T): T

Android

internal actual fun <T> freeze(t: T): T = t

iOS

import kotlin.native.concurrent.*

internal actual fun <T> freeze(t: T): T = t.freeze()
SvyatoslavScherbina commented 5 years ago

Thank you for your efforts that made it possible for us to find this solution! Feel free to ask any questions if you encounter further issues with threads in Kotlin/Native.

yuriry commented 5 years ago

I've made these notes mostly for my own future reference. Linking them here in case someone needs to build dependencies locally. The steps are probably not optimal, I'll improve them in the future if time permits. Thanks again for your help!