Access C/C++ lib from a multiplatform kotlin project
Asked Answered
A

1

8

For the first time, I'm using Android Studio to build a multiplatform project. I have created an Android app module that uses the multiplatform lib on Android. I have also used XCode to build an iOS app that uses the multiplatform lib on iOS. Everything works fine and I'm able to use the expect fun that is implemented by different actual fun for Android and iOS.

I have also created a library in C++ that exposes a C interface.

#ifndef PINGLIB_LIBRARY_H
#define PINGLIB_LIBRARY_H

#ifdef __cplusplus
extern "C" {
#endif

typedef struct {
    long long elapsed;
} PingInfo;

typedef void (*PingCallback)(PingInfo pingInfo);
typedef struct
{
    PingCallback pingUpdate;
} PingObserver;

void* ping(const char * url, const PingCallback *pingCallback);
void subscribe(void* pingOperation);
void unsubscribe(void* pingOperation);

#ifdef __cplusplus
}
#endif

#endif //PINGLIB_LIBRARY_H

I use CLion to build the C++ code. I have created a .def file that I use to build the library using cinterop.

package = com.exercise.pinglib
headers = PingLibrary.h
linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu
compilerOpts = -std=c99 -I/Users/username/myproject/ping/ping/header
staticLibraries = libping.a
libraryPaths = /opt/local/lib /Users/username/myproject/ping/cmake-build-debug

libping.a is the library created building the C++ code. It is created in the folder /Users/username/myproject/ping/cmake-build-debug

When I run the command cinterop -def ping.def -o ping, it creates the klib file and a folder containing a manifest.properties file a natives subfolder containing a cstubs.bc file and a kotlin subfolder with a .kt file.

ping.klib
-ping-build
    manifest.properties
    -natives
        cstubs.bc
    -kotlin
        -com
            -exercise
                -pinglib
                    pinglib.kt

How can I use the library created by cinterop in my kotlin-multiplatform project?

I have found different ways to import it but I did not find any complete description of how to do it. Here they say that I can use something like:

    implementation files("ping.klib")

I did it for the iOS project but I still don't know how to access the kotlin classes neither on Android nor on iOS.

This is my build.gradle

apply plugin: 'com.android.library'
apply plugin: 'kotlin-multiplatform'

android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 15
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
kotlin {
    targets {
        final def iOSTarget = System.getenv('SDK_NAME')?.startsWith('iphoneos') ? presets.iosArm64 : presets.iosX64
        fromPreset(iOSTarget, 'ios') {
            binaries {
                framework('shared')
            }
        }
        fromPreset(presets.android, 'android')
    }
    sourceSets {
        // for common code
        commonMain.dependencies {
            api 'org.jetbrains.kotlin:kotlin-stdlib-common'
        }
        androidMain.dependencies {
            api 'org.jetbrains.kotlin:kotlin-stdlib'
        }
        iosMain.dependencies {
            implementation files("ping.klib")
        }
    }
}
configurations {
    compileClasspath
}

task packForXCode(type: Sync) {
    final File frameworkDir = new File(buildDir, "xcode-frameworks")
    final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
    final def framework = kotlin.targets.ios.binaries.getFramework("shared", mode)
    inputs.property "mode", mode
    dependsOn framework.linkTask
    from { framework.outputFile.parentFile }
    into frameworkDir
    doLast {
        new File(frameworkDir, 'gradlew').with {
            text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
            setExecutable(true)
        }
    }
}
tasks.build.dependsOn packForXCode

EDIT I have changed the question because, initially, I thought that cinterop was not creating the klib library but it was just a mistake: I was looking in the ping-build folder but the file is outside that folder. So I resolved half of the question.

EDIT2 I have added the build.script

Aileen answered 15/3, 2020 at 10:58 Comment(2)
Hello, @kingston! Can you extend the question with an example of cinterop call? At first sight, it seems like you have set the -Xnopack=true here, so you got a ping-build, but no ping.klib.Flasket
@ArtyomDegtyarev I do not add that flag so I guess that by default it is false. I added the command in the question.Aileen
F
2

I'm glad to see that everything is fine with the KLIB, now about the library use.
First of all, I have to mention that this library can be utilized only by Kotlin/Native compiler, meaning it will be available for some targets(see list here). Then, if you're going to include C library use into an MPP project, it is always better to produce bindings via the Gradle script. It can be done inside of a target, see this doc for example. For your iOS target it should be like:

kotlin {
    iosX64 {  // Replace with a target you need.
        compilations.getByName("main") {
            val ping by cinterops.creating {
                defFile(project.file("ping.def"))
                packageName("c.ping")
            }
        }
    }
}

This snippet will add cinterop task to your Gradle, and provide module to include like import c.ping.* inside of the corresponding Kotlin files.

Flasket answered 23/3, 2020 at 11:30 Comment(7)
do you know any general reasons why the corresponding Kotlin files wouldn't be able to see the c.ping.* (as in my case) ? I'm using target androidNativeArm64 in kotlin { ... } block of the shared Gradle file.Humane
to Hi, @Pixel! In general, there are two possible problems - the cinterop tool did not create correct bindings, or the IDE cannot add them as a dependency, or resolve symbols. For the first problem, please check if the bindings are correct. They should be located in the build/classes/kotlin/androidNativeArm64/main/ file, after the cinteropMainAndroidNativeArm64 task execution. If the KLIB is there, you should be able to check its contents using klib contents <KLIB_NAME> CLI command. The klib executable should be located at the ~/.konan/kotlin-native-<version>/bin/klib.Flasket
Please perform those steps and reach me with the results.Flasket
hey there, thanks for replying. Yes the .klib is there. and klib contents gives package c.mylib { @CCall(id = "knifunptr_c_mylib0_hello") external fun hello(): Int }. But I'm not sure about your last point. I can't seem to find any .kexe files in the ~/.konan directory or its subfolders, e.g. I did find . -name "*.kexe" and nothing showed up ! So I guess that is the problem? Would you know how I might go about fixing that? (ps my full steps can be read in my SO question here: #66851818)Humane
Sorry, I meant to say that the klib utility can be found in this folder: ~/.konan/kotlin-native-<version>/bin/klib. If it works correctly without specifying the full path - nice!Flasket
So, the problem is that you have this external fun hello() in the KLIB, but cannot use it from the Kotlin code. Does it mean the code importing this function is not building, or the problem is about the red lines in the IDE?Flasket
I'll take a look. I don't think it's the red lines because the project builds and runs on the Android device (and there's this known bug: #54838725)Humane

© 2022 - 2024 — McMap. All rights reserved.