Hi, I am having some difficulties with different t...
# kotlin-native
t
Hi, I am having some difficulties with different threads in Kotlin/Native. I am creating a delegate class that I pass to some Objective C code. That delegate implements a function (https://developer.apple.com/documentation/storekit/skproductsrequestdelegate/1506070-productsrequest?language=objc) that is called from a different thread than the main thread. Because of that I cannot update some mutable state from the delegate. How can I switch to the main thread in that function and update my mutable state?
k
you've got a catch 22 here because writing to the DB from the main thread is not a good idea
t
That’s not really an issue here. I just would like to know how I can pass the result back to the main thread
t
@Kris Wong I’ve already read the article, it is really interesting and shows how to execute some work using a worker. However, I can’t seem to find anything in the article that shows how to execute something on the main thread. I am not using workers, it’s just that Apple calls this function from a different thread.
k
You need to understand how to handle multiple threads in Kotlin Native (I wrote that post and the second one that follows). As for calling back to the main thread, use GCD in iOS to post back to the main thread.
However, what mutable state? You’re trying to update the db? Is that mutable? I’m confused.
k
it seems like you should be able to use coroutines but I am having difficulty finding any examples that are specific to this use case
coroutines only run on the main thread with K/N
k
I wouldn’t get into coroutines yet. You can’t pass data around that way in native. They’re restricted.
k
ah
i guess there are bindings for GCD, then?
k
Copy code
internal actual fun <B> backToFront(b: () -> B, job: (B) -> Unit) {
    dispatch_async_f(dispatch_get_main_queue(), DetachedObjectGraph {
        JobAndThing(job.freeze(), b())
    }.asCPointer(), staticCFunction { it: COpaquePointer? ->
        initRuntimeIfNeeded()
        val result = DetachedObjectGraph<Any>(it).attach() as JobAndThing<B>
        result.job(result.thing)
    })
}
I wouldn’t necessarily handle it the way I did there, but
dispatch_async_f(dispatch_get_main_queue()
is where you can get started
k
depending on ownership, this may be all you need?
Copy code
dispatch_async(dispatch_get_main_queue()) {
            println("Hello world")
        }
k
You can pass pointers to things around with
StableRef
, you just need to be super careful that you’re in the right thread when you reference them again
That will get you back to the main thread, but if you’ve got mutable state, that may be tricky
k
based on his original question it seems like the main thread may have ownership of the mutable state
t
Thanks for the details! I am aware that coroutines won’t really work in Kotlin/Native. For now most of the code is on the main thread which is not really an issue for me, as libraries like Ktor do not block. This is the first time I need to work with different threads in KN, so it is still a bit new for me. If I were to use
dispatch_async_f
, is it correct that I can not access
request
and
didReceiveResponse
from my example? Would I need to use
DetachedObjectGraph
to use it?
Or would I need to create my own state object, freeze it, and then pass that to the main thread?
k
They’re both objc objects, so you may need to freeze them, but freezing won’t affect “them”, just the wrapper header that is specific to Kotlin (you may not need to freeze them. I’m not sure). Freezing an interop target doesn’t make them immutable because Objc has no concept of freeze. So, anyway, freeze them, then jump to the main thread and call them. They should be available
t
However, what mutable state? You’re trying to update the db? Is that mutable? I’m confused.
I might have used the wrong term here. What I meant is that I would like to pass the result from the
productsRequest
to the main thread. Then on the main thread I can update the UI and database.
@kpgalligan You are right about the freezing, that is not needed. This works fine:
Copy code
override fun productsRequest(request: SKProductsRequest, didReceiveResponse: SKProductsResponse) {
    initRuntimeIfNeeded()

    dispatch_async(NSOperationQueue.mainQueue) {
        // On main thread and can use request and didReceiveResponse without exceptions.
    }
}
So if I understand correctly this is only possible because request and didReceiveResponse are ObjC objects. If they were Kotlin objects, you would need to freeze them or detach/attach them?
k
that would depend on which thread they were created
assuming that is deterministic
k
You might need to freeze them anyway. I'd need to poke around some
If you pass data around threads outside of KN’s control, you need to be extra careful about the KN runtime. The runtime is managing a reference to SKProductsRequest & co. The lack of an exception may not mean the lack of an issue
t
Ok, thank you both very much for the help. It is a bit more clear to me now. Also thanks to Kevin for the articles, they are really good!
👍 1
Hey, the solution with
dispatch_async
worked fine for me yesterday, but now it is crashing with a EXC_BAD_ACCESS. See attached screenshot. Do you have any idea what could be wrong? Really appreciate your help, sorry if I ask too many questions!
I cannot find a real stacktrace somewhere in Xcode, this is all it shows
Freezing `request`/`didReceiveResponse` before dispatch_async does not appear to fix it
k
Need more code context. Post more of that file. EXE_BAD_ACCESS (probably) means you’re trying to access something deallocted.
request/didReceiveResponse
are 2 things that may be problematic, but it could also be the parent class itself
t
Thanks, the RequestDelegate class is created below and not referenced anywhere else:
Copy code
val productRequest = SKProductsRequest(productIds.toSet().freeze())
productRequest.delegate = RequestDelegate(cont, database)
// Send the request to the App Store.
productRequest.start()
it could also be the parent class itself
If you mean the RequestDelegate class, could it be that I need to keep a reference to it myself as well?
k
“reference” is often implicit. I would guess it likely that the
RequestDelegate
instance is touched by multiple threads, especially since you’re dispatching to the main thread from inside one of its methods.
Copy code
val productRequest = SKProductsRequest(productIds.toSet().freeze())
productRequest.delegate = RequestDelegate(cont, database).freeze()
// Send the request to the App Store.
productRequest.start()
I would start with freezing the delegate
t
Wouldn’t that freeze cont and database as well?
k
Yep. If you need them to be unfrozen yet available to other threads then you need alternate plans/architecture
2 rules. 1 thread at a time or frozen.
t
Well, cont is a Kotlin Coroutines continuation so I am not sure if it would be possible to freeze it. Here is a bit more could of what I am trying to do:
I could leave the database out of the delegate class and put that in for example line 17. But I still need to get the response from the delegate back to the getProductDetails function. Currently I am using a Coroutines continutation for that but I guess I need to find another way.
k
What’s the db?
For the cont…
Put that in a
StableRef
, and only unpack it when back in the main thread
t
Haven’t heart of StableRef before, looks interesting. I would need to freeze the RequestDelegate too, right?
k
The StableRef allows you to freeze
RequestDelegate
without freezing the continuation, but only if you’re super careful about only touching the continuation in the main thread (or whatever thread you originally had the continuation in).
If “database” is Sqldelight, anything from there out of the box should be freezable. I wrote (most) of that driver. If you have a wrapper class with some state, that may be a different story
t
Thanks, the StableRef is something I need to definitely keep in mind. However, it looks like the crash is not related to the cont. I made a more simplified version, which still crashes. If I understand correctly, the RequestDelegate should have a strong reference here, so that cannot be the issue. Thank you very much for the help so far!
message has been deleted
k
It’ll be easier to help diagnose this if you can post a full sample to run (zip or git project). Not sure by looking at that what’s wrong
t
I managed to get it working. What I did was freeze the lambda as last parameter in dispatch_async. I am not sure why that fixes it. If that should not be needed I can try to create a sample project to look into this further.
k
No, that absolutely makes sense. Most of my concurrency is handled through libraries at this point, so I kind of forgot that you also need to freeze the lambda
t
Ah ok, thanks for all your help! I have one more question about the freezing of the lambda: what exactly does it do for a no arg lambda like here? I can not think of anything that could be “frozen”. Does it still work the same as a non-frozen lambda?
I tried to search for some documentation about freezing lambdas, but could not really find anything.
k
The lambda itself is a form of state. It’s a function pointer. If I had more time I’d try to draw a diagram, but it would be better represented as an animation. When you call dispatch, in this case, you’re in a background thread:
Copy code
dispatch_async(NSOperationQueue.mainQueue) { }
The
{ }
creates an object instance that represents the operation. That is created and “exists” in the background thread. Inside of
dispatch_async
, that operation is scheduled to be run on the main thread. The main thread may loop through to that operation while the caller is exiting it’s context.
While all of that is happening, in KN, every object has a reference count. When that hits 0, the memory is reclaimed (or scheduled to be, but don’t worry about that).
The critical thing, mentioned in the blog posts, is that non-frozen memory does that ref counting locally. Not atomically. Frozen does it atomic. What can happen is one thread thinks the ref count is at 0 and deallocs it, and the other thread tries to use it. Boom.
The
{ }
is represented by an object, just like everything else (https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md). If not frozen, it can have the same concurrency issues as everything else. The trick is, if you capture state in a lambda, and you freeze the lambda, you’ll freeze the captured state as well.
t
Wow, thanks for the explanation! I now understand exactly what it does. You are really good at describing how KN works.
👍 1
Just finishing up the last parts of my project. Almost ready to publish the app, previously only Android, to iOS TestFlight. Kotlin Native is really amazing and saves so much time porting an Android app to iOS.
k
What kind of app?
t
It's an app for controlling smart lights like Philips Hue. The app is called Hue Essentials
Was able to share most code with iOS including the view model. Only need to write the UI and implement some iOS specific code
k
That’s pretty interesting. I actually have hue lights. Mostly use echo to turn them on/off, but will check out the app when it launches. If you’re doing iOS testing and want testers, let me know.
👍 1
t
That's amazing, those lights are really fun, aren't they? I will let you know when the iOS version is available for testing. Initially I never thought I would develop for iOS, but Android users just kept asking for it. Because the Android app is written in Kotlin, I went with Kotlin/Native, which turned out pretty good.
Hey Kevin, if you are still interested in my app, let me know. It is on the App Store now and there is a TestFlight program, too.