KMM with iOS Arm64 Binary Framework (.xcframework) and cinterop
Asked Answered
N

1

7

I'm following the guide here to try and use an iOS framework without CocoaPods in a new KMM project:

https://kotlinlang.org/docs/kmm-add-dependencies.html#without-cocoapods

I have an existing, working .xcframework that I added to the project under shared/src. I added a MyKit.def file in src/nativeInterop/cinterop/ and updated the build.gradle.kts file in same shared dir:

MyKit.def looks like

language = Objective-C
modules = MyKit
package = MyKit

build.gradle.kts

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
    kotlin("multiplatform")
    id("com.android.library")
}

kotlin {
    android()

    val iosTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
            ::iosArm64
        else
            ::iosX64

    iosTarget("ios") {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        val androidMain by getting
        val androidTest by getting {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.13.2")
            }
        }
        val iosMain by getting
        val iosTest by getting
    }
    iosArm64() {
        compilations.getByName("main") {
            val MyKit by cinterops.creating {
                // Path to .def file
                defFile("src/nativeInterop/cinterop/MyKit.def")
                compilerOpts("-framework", "MyKit", "-F/src/MyKit.framework")
            }
        }

        binaries.all {
            // Linker options required to link to the library -- the framework binary is located under src/MyKit.framework/ios-arm64/MyKit.framework/
            linkerOpts("-framework", "MyKit", "-F/src/MyKit.framework/ios-arm64/MyKit.framework/")
        }
    }
}

android {
    compileSdkVersion(31)
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdkVersion(26)
        targetSdkVersion(31)
    }
}

After adding import MyKit.* to my MainActivity I get unknown reference errors.

Are iOS binary frameworks supported? They have a . separator in the file name so maybe that's an issue. Problem with my -F paths?? I'm not clear on whether the path should be all the way to the dir with Headers and the binary itself or just to the Framework root. TIA

Novice answered 14/9, 2021 at 12:10 Comment(8)
Hello, I got to questions to ask. 1. Have you tried with the absolute path to the directory where the framework is? I mean the path you send as -F option. 2. Have you performed the cinteropMyKitIosArm64 task before using the import in your iosArm64Main source set? – Prizewinner
Artyom - I have not tried #1 but I will. I don't understand your second question? After adding to or modifying the build.gradle.kts I'm running Gradle sync and then running the app from Android Studio on a device, so I would assume the iosArm64 step has been run. I did not see any instructions to run a cinterop task. Thanks! – Novice
The second question was if you tried to sync and build before checking the code. As you've done it, now I hope the absolute path will help 😁. – Prizewinner
i have the same problem. Did you solved ? – Unequal
@Unequal no I was not able to solve it – Novice
Hello again, @mutable2112! If the problem is still relevant to you, I would like to suggest two things to check. 1. Right now its hard to understand whether your framework has .xcframework or .framework extension. If your cinterop block contains the correct extension, then just forget about this question. 2. What are the results of the cinteropMyKitIosArm64 in your case? In general, it should create a .klib file located at $projectbuild/classes/kotlin/native/main/cinterop/. if its empty that mean the cinterop tool did not found the appropriate headers to generate bindings. – Prizewinner
If its true, I would recommend checking if the framework structure is not too different from the standard MyKit.framework/module.modulemap and MyKit.framework/Headers/MyKitHeadersAsNamedInTheModulemap. – Prizewinner
@ArtyomDegtyarev I created local xcframework using xcode and added dependency to Android studio and was able to create .klib library in $projectbuild/classes/kotlin/native/main/cinterop/. But couldn't import my library either in xcode nor in iosMain source set. – Fermat
U
7

Assuming you have the default KMM project structure with also the framework and the def file as follows:

- androidApp
- iosApp
  - MyFramework.framework
- shared
  - src
    - nativeInterop
      - cinterop
        - MyFramework.def

This is what you need to add in your build.gradle file:

    val myFrameworkDefFilePath = "src/nativeInterop/cinterop/MyFramework.def"
    val myFrameworkCompilerLinkerOpts = listOf("-framework", "MyFramework", "-F${projectDir}/../iosApp/")
    iosArm64 {
        compilations.getByName("main") {
            val MyFramework by cinterops.creating {
                // Path to .def file
                defFile(myFrameworkDefFilePath)

                compilerOpts(myFrameworkCompilerLinkerOpts)
            }
        }

        binaries.all {
            // Tell the linker where the framework is located.
            linkerOpts(myFrameworkCompilerLinkerOpts)
        }
    }

Unfortunately, as stated here, relative paths don't work correctly as compiler arguments, so what you can do is use projectDir value and from that point to where your framework is located.

Note: you need to point to the parent folder of where the framework is (in the example, I'm pointing to iosApp/, which is the parent of MyFramework.framework).

Another note: if you have an .framework, afaik, you need to replicate this code for iosX64, iosArm64, iosSimulatorArm64, whatever, to make the imports work correctly in iosMain.
If you have an .xcframework the path needs to point to the correct variant inside the .xcframework, e.g. ios-arm64_armv7, ios-arm64_i386_x86_64-simulator, whatever.

Upandcoming answered 25/10, 2022 at 16:42 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.