Kotlin multiplatform/native interoperability with Objective-C framework - kotlin

I'm trying to call Swift/Objective-C code from Kotlin in a multiplatform project. There are no problems with calls to platform code. But when I'm trying to call some library (or framework, not sure how it is properly called as I'm not an iOS dev) it fails. Docs states that it is possible to call Objective-C code and Swift if it is properly exported:
Kotlin/Native provides bidirectional interoperability with Objective-C. Objective-C frameworks and libraries can be used in Kotlin code if properly imported to the build (system frameworks are imported by default). See e.g. "Using cinterop" in Gradle plugin documentation. A Swift library can be used in Kotlin code if its API is exported to Objective-C with #objc. Pure Swift modules are not yet supported.
But it does not say anything about how can I import them properly. It only point to gradle plugin description that describes old version of gradle plugin. So it does not work for me. Finally I figured out something might be the way to import Objective-C code:
fromPreset(presets.iosX64, 'ios') {
compilations.main.outputKinds('FRAMEWORK')
compilations.main {
cinterops {
firebase {
def pods = '${System.getProperty("user.home")}/Projects/kmpp/iosApp/Pods/'
includeDirs '${pods}Firebase/CoreOnly/Sources',
'${pods}FirebaseAnalytics/Frameworks/FirebaseAnalytics.framework/Headers'
}
}
}
}
Build runs without failures, but it does not import anything. What am I doing wrong? Is it possible to import such a lib at all?
UPD:
here I found an example of usage cinterop tool like this:
cd samples/gitchurn
../../dist/bin/cinterop -def src/main/c_interop/libgit2.def \
-compilerOpts -I/usr/local/include -o libgit2
It looks like cinterop tool should be in /dist/bin/ folder in my projects but there is no such folder. Where do I get cinterop tool ?

I ended up with this cinterops section in build.gradle
fromPreset(presets.iosX64, 'ios') {
// This preset is for iPhone emulator
// Switch here to presets.iosArm64 (or iosArm32) to build library for iPhone device
compilations.main {
outputKinds('FRAMEWORK')
cinterops {
firebase {
defFile "$projectDir/src/iosMain/cinterop/firebase.def"
includeDirs {
allHeaders "$projectDir/../iosApp/Pods/FirebaseCore/Firebase/Core/Public",
"$projectDir/../iosApp/Pods/FirebaseDatabase/Firebase/Database/Public"
}
compilerOpts "-F$projectDir/../iosApp/Pods/Firebase -F$projectDir/../iosApp/Pods/FirebaseCore -F$projectDir/../iosApp/Pods/FirebaseDatabase"
linkerOpts "-F$projectDir/../iosApp/Pods/Firebase -F$projectDir/../iosApp/Pods/FirebaseCore -F$projectDir/../iosApp/Pods/FirebaseDatabase"
}
}
}
}
end this .def file:
language = Objective-C
headers = FirebaseCore.h FirebaseDatabase.h
What's going on here? Cocopods frameworks are placed in Pods directory in your Xcode project. Navigating this folder a bit, you'll find what you need. I'm not sure if there is some standard but firebase place main header file in Public folder. And it contains references to other header files it needs... So you specify these file names in your .def file in the headers section.
Next, you need to specify where to look for these files and others referenced by them. You can do it in the .def file in includeDirs or in build.gradle file. I prefer to use the build.gradle file as it can use variables. So you specify the path to these Public folders. (This is enough for kotlin to see library API, but in order to be able to run the app you need to compile and link this library...)
Then the compiler and linker need to know where library/framework is. So you specify path to root folder of the framework in compilerOpts and linkerOpts prefixing them with -F if it is a framework or -L if it is a library.

It looks like you are going to use a cocoapods library. Currently the Gradle plugin has no support for cocapods out of the box. But may be a dependency on your library can be configured "manully". Could you please share a link to your project?

Related

The same Gradle Kotlin `copy.from` api use `it` when called from build file (kt) and `this` when called from Pluign (kt)

I hope someone can explain this to me.
I was moving some code from a Gradle build file into a Gradle plugin.
Below are two code snippes calling the same from function (based on Ideas indexing)
I noticed that there are some strange differences between how the apis can be used in those two contexts.
I know that Gradle is adding some extra syntax suger around the build files which is why I need to manually cast the task in the Plugin.kt file, but I cannot find anything that explains why from in context of the Build file has this as context where in the plugin the function uses it to access the into function.
It is not just Idea that reports this, running Gradle also shows that it must be like this.
I assume this is something special to Kotlin's way of handling the Action interface in different contexts:
kts file (No wrapping class)
kt file (with class)
Here are the two samples
hostedStaticFiles is gradle configuration that will be used to configure web frontend from a separate build.
build.gradle.kts
tasks.getByName<ProcessResources>("processResources") {
this.from(hostedStaticFiles) {
this#from.into("static") // <-- Note use of this here
}
}
Plugin.kt
project.tasks.getByName("processResources").let<Task, ProcessResources> {
if (it !is ProcessResources) {
throw IllegalStateException("The processResources task in Project is not of type ${ProcessResources::class.java}")
}
it
}.apply {
dependsOn(hostedStaticFiles)
this#apply.from(hostedStaticFiles) { it -> // <-- Note use of it here and below
it.into("static")
}
}
dependencies {
hostedStaticFiles(project("client"))
}
I hobe someone can point me to an explanation or preferably documentation on why this behaves this way :)
Gradle version 7.4.1
###################
After getting the answer from #Joffrey I updated my buildSrc/build.gradle.kts with the below plugin configuration and it all started working as expected.
plugins {
`java-gradle-plugin`
`kotlin-dsl`
}
Gradle uses the HasImplicitReceiver annotation on some function types (like Action), so you can use this instead of it. It leverages Kotlin's SAM-with-receiver compiler plugin.
In Kotlin build scripts (.gradle.kts files) you benefit from this automatically because the Kotlin compiler used to compile your scripts is already properly configured. However, in custom plugin projects, you are in control of the build and you need to apply the kotlin-dsl plugin yourself. As mentioned in the documentation, it does a few things for you, including:
Configures the Kotlin compiler with the same settings that are used for Kotlin DSL scripts, ensuring consistency between your build logic and those scripts.

add google ml-kit dependecy outside the app-level gradle file

I'm following Googles instructions on how to implement "Google ML Kit" into my application. It says
Before you begin
In your project-level build.gradle file, make sure to include Google's Maven repository in both your buildscript and allprojects sections.
Add the dependencies for the ML Kit Android libraries to your module's app-level gradle file, which is usually app/build.gradle:
dependencies {
// If you want to use the base sdk
implementation 'com.google.mlkit:pose-detection:17.0.1-beta6'
// If you want to use the accurate sdk
implementation 'com.google.mlkit:pose-detection-accurate:17.0.1-beta6'
}
However I need to use ML-Kit in a library outside my app. It's basically a java library that gets used from within my app. It also has a gradle file. However when I add the dependency there, I cannot import any ml-kit classes.
Here is the gradle file of my java library:
apply plugin: 'java-library'
dependencies {
api 'com.annimon:stream:1.2.2'
api 'org.javatuples:javatuples:1.2'
api 'com.google.guava:guava:30.1.1-jre'
// JSON
implementation 'org.json:json:20210307'
implementation 'androidx.annotation:annotation:1.2.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.4'
implementation 'com.google.mlkit:pose-detection:17.0.1-beta6'
}
sourceCompatibility = "8"
targetCompatibility = "8"
gradle sync is still successful, but upon importing any class inside any other class of my java library, I get an import error:
import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions;
--> cannot resolve symbol mlkit
Any idea what I could do about that?
Meanwhile I found the problem: The above gradle file belongs to a pure java library that is integrated into android later. ML-Kit apparantly cannot be used in Java projects. It only works if I integrate ML-Kit in pure android gradle projects

Skeleton for Kotlin/native multiproject with dll library for backend and frontend app using it

I'm trying to create a multiproject using Kotlin/native and gradle in IDEA that consists of:
A backend subproject library. I want to use this library in frontend Kotlin app and also produce a native DLL that can be later used in other software. I doubt I'll need any platform specific behavior -- the most I'll interact with the system is read, write and watch a file for changes.
A frontend jvm app in Kotlin using this library as required dependency. To be more precise I'm going to write a glfw app in Kotlin that will use this lib, but that's a detail you don't have to bother with.
I want to be able to:
build DLL on it's own
build an app that depends on library and rebuilds if needed when the lib changed.
I made a hyperlink trip over gradle docs, JetBrains examples and repos but I don't quite understand how to make a multiproject like that. Can someone provide a minimal working example of such a Hello World project?
Right now this works for me in the initial stage of the project:
Use gradle init with basic type and kotlin DSL to generate a wrapper project
Add 2 modules with New > Module > Gradle > Kotlin/Multiplatform and Kotlin/JVM. That should add include("...") entries to root settings.gradle.kts At this point those modules will be empty with a single build.gradle.kts scripts
Root build.gradle.kts can be deleted - both library and app use their own
In the Multiplatform library setup at least one target for example like that:
// rootProject/library/build.gradle.kts
// ...
kotlin {
jvm() // used by jvm app
sourceSets { /*...*/ }
}
Now there should be no gradle errors and IDEA will detect the project properly - refresh gradle configuration (CTRL+SHIFT+O)
Create source directories for each module (because the modules rn): IDEA should hint the names of corresponding source sets (src/<target>/<kotlin|resources> etc.)
So now just to link the app and library together in the the app's buildscript add the implementation(project(":library")) dependency
Of course don't forget to configure the library target for example using linuxX64("native") { binaries { sharedLib {/*...*/ } } } block in kotlin plugin when trying to generate a DLL
Now it should mostly work. The structure of a project I'm working on rn looks like that:

Cocoapod dependency in iosMain of a Kotlin Multiplatform project, cocoapod unresolved reference

I am trying to create a wrapper around AWS Amplify to use for my project. I have a module inside my shared ( common ) module called Amplify. Here I integrated cocoapods as instructed in the official docs. But when I try to import anything from the iosMain I keep getting Unresolved reference: cocoapods.
My project structure is as follows
Common module (shared)
|- root module
|- other features
|- amplify wrapper module
In the root module I have the
kotlin {
ios {
binaries {
framework {
baseName = "Framework"
linkerOpts.add("-lsqlite3")
export(project(":common:main"))
}
}
}
And the setup for the cocoapods in the amplify module
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("multiplatform-setup") // this is from buildSrc, it adds the multiplatform plugin
id("android-setup")
id("org.jetbrains.kotlin.native.cocoapods")
kotlin("plugin.serialization") version "1.5.10"
}
version = "1.0"
kotlin {
sourceSets {
named("commonMain") {
dependencies {
}
}
}
cocoapods {
summary = "Amplify wrapper for KMP project"
homepage = "Link to a Kotlin/Native module homepage"
frameworkName = "AmplifyKMP"
pod("Amplify")
pod("AmplifyPlugins/AWSCognitoAuthPlugin")
pod("AmplifyPlugins/AWSPinpointAnalyticsPlugin")
}
}
My idea is that I'd be able to expose my wrapper from the commonMain code, that would call actual implementations from the Amplify Android and Amplify IOS libraries. My first problem is the cocoapods being unresolved and secondly all of the examples from the official docs and github all have cocoapods in the main module (root module in my case) from where they export the framework, I am not sure my approach is even feasible.
The first problem is easy. You need to add kotlin("native.cocoapods") to your plugins section.
On the second, can a sub-module import pods with cinterop and make them available to a module that depends on them? I haven't tried it. In theory the cocoapods plugin should be able to just import pods definitions to kotlin. However, the cocoapods Kotlin gradle plugin (aka kotlin("native.cocoapods")) will configure ios targets to create a framework. That might create issues with the dependency config.
In the amplify module I don't see that you're defining any iOS targets, so you'll probably need to do that, but that config will be altered by kotlin("native.cocoapods"). You'll likely need to step in and modify that yourself. You can step in and do that in gradle, but I would prepare to spend some time tweaking that.
https://github.com/touchlab/KaMPKit/blob/main/shared/build.gradle.kts#L114

How to add dependencies to a kotlin library?

I am trying to build a kotlin library for discord bots, which can be found at https://github.com/TheDrone7/discord-kt , published to jcenter (bintray link - https://bintray.com/thedrone7/discordKt/discord-kt). The library has a few dependencies of it's own as well.
When I add my own library to my test app, the library's dependencies were not installed and I started getting some errors. Is there a way to specify the library's dependencies so that they get automatically installed when a user uses my library?
EDIT: -
So basically my test app's build.gradle.kts file's dependencies section is given below
dependencies {
// Use the Kotlin JDK 8 standard library.
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.theDrone:discordKt:0.0.1a")
// Use the Kotlin test library.
testImplementation("org.jetbrains.kotlin:kotlin-test")
// Use the Kotlin JUnit integration.
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
And my library is dependent on the following packages: -
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0
org.java-websocket:Java-WebSocket:1.4.0
com.beust:klaxon:5.0.5
org.slf4j:slf4j-jdk14:1.7.26
now when I run my test app, it shows gives error that there is no class named WebSocketClient which is a part of the org.java-websocket:Java-WebSocket:1.4.0 package and is also the base of my entire library.
When I add the listed packages to my test app's dependencies, it works perfectly fine. So is there a way that I could define in my library that the apps using it will also automatically depend on the packages my library depends on?
You declared the Java-WebSocket library as a dependency of your library using the implementation configuration.
This configuration means: I need that for my code to work, but it's an implementation detail and it's thus not part of my public API, so users of my library won't have access to it in their compile classpath.
So Gradle, when it generates the pom.xml file for your library, adds Java-WebSocket as a runtime dependency, and not as a compile dependency.
Read the java-library plugin documentation, which explains all of that in details. Once you have understood it, use api instead of implementation in your library's build.gradle.kts file for the dependencies that are part of your API, and should thus be compile dependencies and not runtime dependencies:
api("org.java-websocket:Java-WebSocket:1.4.0")