JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.27k stars 1.18k forks source link

Compiler/linker error on iOS with composable lambdas. #2694

Closed mipastgt closed 1 year ago

mipastgt commented 1 year ago

I am currently trying to get Bonsai Core sources working in a multiplatform project with iOS which is currently not supported by Bonsai.

This works out of the box for desktop and Android (both JVM) but compilation/linking fails for iOS. It all boils down to the fact that the following combination of classes (stripped down from the originals) does not compile/link on iOS (and maybe other native targets too).

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.painter.Painter

typealias NodeComponentX<T> = @Composable BonsaiScopeX<T>.(NodeX<T>) -> Unit

sealed interface NodeX<T> {
    val iconComponent: NodeComponentX<T>
}

typealias NodeIconX<T> = @Composable (NodeX<T>) -> Painter?

@Immutable
data class BonsaiScopeX<T> internal constructor(
    internal val style: BonsaiStyleX<T>,
)

data class BonsaiStyleX<T>(
    val nodeCollapsedIcon: NodeIconX<T> = { null },
)

The build terminates with:

> Task :shared:linkPodDebugFrameworkIosX64
e: There are still 2 unbound symbols after generation of IR module <shared>:
Unbound public symbol IrConstructorPublicSymbolImpl: /BonsaiStyleX.<init>|-8323399005056659232[0]
Unbound public symbol IrSimpleFunctionPublicSymbolImpl: /BonsaiStyleX.copy|8877863784634607273[0]

This could happen if there are two libraries, where one library was compiled against the different version of the other library than the one currently used in the project. Please check that the project configuration is correct and has consistent versions of dependencies.

I made the following observations:

  1. This can be made to compile/link when I remove the two @Composable annotations (but then the code does not work anymore).
  2. It somehow also seems to depend on the project structure because in an older project, with a different structure, this does compile/link. I have verified the above error by just throwing the above code contained in a single file into the shared commonMain of https://github.com/JetBrains/compose-jb/tree/master/experimental/examples/falling-balls-mpp

Do you have an idea how to fix that or a usable workaround?

I tested this on macOS 12.6.3 Monterey, Kotlin 1.8.0, Compose 1.3.0

References: https://github.com/adrielcafe/bonsai https://github.com/adrielcafe/bonsai/issues/11

eymar commented 1 year ago

I can only confirm these issues exist. I noticed them myself recently. We plan to fix them. Unfortunately, I don't know a workaround, probably there is no WA.

It somehow also seems to depend on the project structure because in an older project, with a different structure, this does compile/link.

What was the structure that used to work for you?

mipastgt commented 1 year ago

I just verified that this problem is indeed related to this issue https://github.com/JetBrains/compose-jb/issues/2346

The workaround is to make every composable function internal. But that is a bit of a pain for the above code because it also uses composable lambdas as members in data structures and so all of these have to be made internal too and all functions which expose such data. So for me this can only be a temporary workaround.

The structure which still seems to work is the old structure of the expermental examples before they were refactored recently.

eymar commented 1 year ago

Oh, I didn't realise it's that same issue.

Since kotlin 1.8.0 you may try to add this annotation to your declarations instead of marking them internal https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native/-hidden-from-obj-c/

Anyway, it's not a convenient solution and we're working on adding that annotation automatically

mipastgt commented 1 year ago

That does not seem to be a valid workaround for me because, as far as I can see, this annotation cannot be applied to typealiases. (At least that's what the compiler is telling me.)

eymar commented 1 year ago

But is it really necessary to add it to the typealias? What if you add to the functions only and properties which use those typealiases?

I'll check it myself later.

mipastgt commented 1 year ago

I tried adding @HiddenFromObjC everywhere where it was applicable (and @file:OptIn(ExperimentalObjCRefinement::class)) but I then got the same error as shown above.

eymar commented 1 year ago

I haven't tried it yet. But just an idea that you could also try: We have a lot of public composable functions in the compose-ui, compose-foundation modules and we didn't add the annotation to them and they don't cause this issue.

What if the main.kt file moved to another module that depends on all your modules? The idea is that "main" module won't have any declarations except the app initialization.

eymar commented 1 year ago

@mipastgt while we work on a solution that would eliminate the need in any workarounds (it may take some time, because changes in kotlin involved), here is our imageviewer example https://github.com/JetBrains/compose-jb/pull/2705 where we removed internal visibility.

That PR implements what I mentioned earlier:

The idea is that "main" module won't have any declarations except the app initialization.

mipastgt commented 1 year ago

At the moment I cannot try it because my iOS setup is currently completely broken and I have to get some other work done before I can fix that.

Just to clarify this. The issue is only relevant for my own project sources, right? If I pull in composable functions from an external library this is not an issue?

eymar commented 1 year ago

Yes, the composables from an external library won't cause an issue

AlexGladkov commented 1 year ago

The same error but not for composables function

Showing Recent Issues Unbound public symbol IrConstructorPublicSymbolImpl: ru.alexgladkov.odyssey.compose.navigation.bottom_bar_navigation/TabInfo.|150164194066322410[0]

Unbound public symbol IrSimpleFunctionPublicSymbolImpl: ru.alexgladkov.odyssey.compose.navigation.bottom_bar_navigation/TabsNavModel.navConfiguration.|3814956042047583694[0]

Unbound public symbol IrSimpleFunctionPublicSymbolImpl: ru.alexgladkov.odyssey.compose.navigation.bottom_bar_navigation/TabItem.configuration.|1523816184942411441[0]

Unbound public symbol IrSimpleFunctionPublicSymbolImpl: ru.alexgladkov.odyssey.compose.navigation.bottom_bar_navigation/TabInfo.copy|7688116277759904276[0]

Error with some init, copy functions of public data classes. IDK how to fix this, because this classes must be public

jbruchanov commented 1 year ago

Evening guys,

I have same issue, is there any progress or official ticket for the issue ?

I've found that basically hiding anything what is @Composable and somehow living inside OOP helps. @HiddenFromObjC annotation helps, though as it has quite limited targeting it's not exactly strong workaround tool.

I'm wondering, is there any particular reason to expose anything @Composable to ios world anyway ? I doubt there is, if at all, any usage for it.

few code "dance" I use to workaround the issue

fun ContentRef(content: @Composable () -> Unit) = ContentRef(content, Unit)

class ContentRef internal constructor(
    internal val content: @Composable () -> Unit, 
    unit: Unit
) {

    @Composable
    internal fun render() = content()

    @HiddenFromObjC
    inline fun whatever(comp : @Composable () -> Unit) {
        //...
    }
}

@Composable
public fun render(contentRef: ContentRef) {
    contentRef.content()
}
eymar commented 1 year ago

The fix is available with Compose Multiplatform 1.4.0 when used with kotlin 1.8.20

okushnikov commented 4 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.