https://kotlinlang.org logo
Title
a

Alexandre Gressier

08/21/2022, 1:11 PM
Greetings! I am trying to call some functions of a Swift only library from Kotlin. I am aware that the only way to do this is to define some wrappers available in Objective-C and then compile it via Kotlin/Native. I started experimenting by creating a static library via Xcode, defining a Gradle task that runs
xcodebuild -workspace MyLibInterop.xcworkspace -scheme MyLibInterop -sdk $sdk -configuration Release build SYMROOT=$PWD/build OBJROOT=$PWD/build
, and finally creating a cinterop configuration via Gradle and a .def file. It works quite well. My setup is very similar to the one explained here: https://medium.com/kodein-koders/create-a-kotlin-multiplatform-library-with-swift-1a818b2dc1b0 (the ChachaPoly one). However I encounter a problem as soon as I start to add dependencies to this static library: I don’t know how to tell cinterop where to look for the dependency frameworks used by the static library. I used Cocoapods (without Gradle) to define the dependencies. The previous
xcodebuild
command then outputs several .framework directories that represent the dependencies in addition to the existing .h and .a files. My exact issue is ``ld: warning: Could not find or use auto-linked framework 'Amplify'` which is one of the .framework directories when running the task
shared:linkPodDebugFrameworkIosArm64
. I suppose that I have to define a recursive framework search path within my Kotlin cinterop configuration (i.e., the .def file). I came across the
-F
and
-framework
linker & compiler options but used them without any success. Thank you in anticipation for any kind of help you will be able to provide me with. I can provide any info or piece of code you need.
r

Rick Clephas

08/22/2022, 6:37 AM
How/where did you define the
-F
and
-framework
options?
a

Alexandre Gressier

08/22/2022, 9:44 AM
Thank you for your quick answer. I tried several configurations, including writing
-F
search paths under
compilerOpts()
in the Gradle build script using the static library, and
-framework
flags under
linkerOpts
in my .def file.
To provide you with a bit more context, here is the Podfile of the static library:
platform :ios, '13.0'

target 'AmplifyAuthInterop' do
  use_frameworks!
  pod 'AmplifyPlugins/AWSCognitoAuthPlugin'
end
Here is the content of the build directory (at depth 2) after running `xcodebuild`:
build
├── AWSAuthCore
│   ├── AWSAuthCore.framework
├── AWSCognitoIdentityProvider
│   ├── AWSCognitoIdentityProvider.framework
├── AWSCognitoIdentityProviderASF
│   ├── AWSCognitoIdentityProviderASF.framework
├── AWSCore
│   ├── AWSCore.framework
├── AWSMobileClient
│   ├── AWSMobileClient.framework
├── AWSPluginsCore
│   ├── AWSPluginsCore.framework
├── Amplify
│   ├── Amplify.framework
├── AmplifyAuthInterop.swiftmodule
│   ├── Project
│   ├── arm64-apple-ios.abi.json
│   ├── arm64-apple-ios.swiftdoc
│   └── arm64-apple-ios.swiftmodule
├── AmplifyPlugins
│   ├── AmplifyPlugins.framework
├── Pods_AmplifyAuthInterop.framework
│   ├── Headers
│   ├── Info.plist
│   ├── Modules
│   └── Pods_AmplifyAuthInterop
├── include
│   └── AmplifyAuthInterop
└── libAmplifyAuthInterop.a
I am not sure if
-F
works recursively, so I wrote the following:
kotlin {
    ios {
        val platform = when (name) {
            "iosX64" -> "iphonesimulator"
            "iosArm64" -> "iphoneos"
            else -> error("Unsupported target $name")
        }
        compilations.getByName("main") {
            cinterops.create("AmplifyAuthInterop") {
                project(":shared:interop:AmplifyAuthInterop").let {
                    tasks[interopProcessingTaskName].dependsOn(
                        it.getTasksByName("build${platform.capitalize()}", false).first().path
                    )
                    packageName = "interop.amplifyauth"
                    includeDirs.headerFilterOnly("${it.buildDir}/Release-$platform/include")
                    compilerOpts(
                        listOf(
                            "AWSAuthCore",
                            "AWSCognitoIdentityProvider",
                            "AWSCognitoIdentityProviderASF",
                            "AWSCore",
                            "AWSMobileClient",
                            "AWSPluginsCore",
                            "Amplify",
                            "AmplifyPlugins",
                        ).map { framework ->
                            "-F${it.buildDir}/Release-$platform/$framework"
                        } + "-F${it.buildDir}/Release-$platform"
                    )
                    extraOpts("-libraryPath", "${it.buildDir}/Release-$platform")
                }
            }
        }
    }
And I ensured that the framework search paths exist, of course.
r

Rick Clephas

08/22/2022, 11:20 AM
Could you try to specify the search paths in the
.def
file? I vaguely remember something about on of these options not being supported from the Gradle task.
Scratch that. Please try to also set the search paths in the linker options:
kotlin {
    iosX64 {
         binaries.all {
            linkerOpts("-F$frameworkFolder")
        }
    }
}
a

Alexandre Gressier

08/22/2022, 3:23 PM
Thank you for putting me on the right path Rick! I was able to compile and run my project successfully! I ended up not using this
binaries
block but I'll leave the following notes for future reference: I'm almost certain that you are referring to
warning: -linker-option(s)/-linkerOpts option is not supported by cinterop. Please add linker options to .def file or binary compilation instead.
. I suppose it would be fine in the
binaries
block though, as opposed to the
cinterop
one. I ended up writing most of my configuration in my .def file instead of the Gradle build script. I also discovered that I only need to use and
-F
for the imports I write in Swift (i.e., only
Amplify
). Here it is:
package = interop.amplifyauth
language = Objective-C

# Headers
headers = AmplifyAuthInterop/AmplifyAuthInterop-Swift.h
headerFilter = AmplifyAuthInterop/**

# Libraries
staticLibraries = libAmplifyAuthInterop.a
libraryPaths.ios_arm64 = /Users/alex/Projects/MyProject/shared/interop/AmplifyAuthInterop/build/Release-iphoneos
libraryPaths.ios_x64 = /Users/alex/Projects/MyProject/shared/interop/AmplifyAuthInterop/build/Release-iphonesimulator

# Linker
linkerOpts = -L/usr/lib/swift
linkerOpts.ios_arm64 = -iphoneos_version_min 16.0.0 \
    -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos/ \
    -F/Users/alex/Projects/MyProject/shared/interop/AmplifyAuthInterop/build/Release-iphoneos/Amplify

linkerOpts.ios_x64 = -ios_simulator_version_min 16.0.0 \
    -L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator/ \
    -F/Users/alex/Projects/MyProject/shared/interop/AmplifyAuthInterop/build/Release-iphonesimulator/Amplify
And here is the Gradle build script:
kotlin {
    ios {
        val platform = when (name) {
            "iosX64" -> "iphonesimulator"
            "iosArm64" -> "iphoneos"
            else -> error("Unsupported target $name")
        }
        compilations.getByName("main") {
            cinterops.create("AmplifyAuthInterop") {
                project(":shared:interop:AmplifyAuthInterop").let {
                    tasks[interopProcessingTaskName].dependsOn(
                        it.getTasksByName("build${platform.capitalize()}", false).first().path
                    )
                    includeDirs.headerFilterOnly("${it.buildDir}/Release-$platform/include")
                }
            }
        }
    }
I initially forgot to add a Copy Files build phase to copy
Amplify.framework
in the Frameworks directory of my app. After that it ran successfully. There are still nasty absolute paths in my .def file but I don't mind for the time being. Thank you again for your precious help.
r

Rick Clephas

08/22/2022, 3:48 PM
Nice. Yeah that’s indeed the warning I was referring to. Regarding the absolute paths, you could create a tasks that generates the correct def file. Another note about the cinterop: it runs in the final Kotlin module. E.g. it will run in the client project if you distribute your code as a library.