Search code examples
gradlekotlin-multiplatformkotlin-nativesource-setscinterop

Rust and Kotlin/Native interop using gradle


Recently, I have been trying to create a simple Kotlin/Native program that can use a Rust defined function.

I am using the Kotlin Multiplatform plugin in Gradle to try to acieve this. I have created a shared library from the Rust program. I have a C header that declares this Rust function. The def file does its thing.

However, I am not able to understand how this Gradle plugin works. The official docs has very less documentation on linking with shared libraries.

I am compiling this program only for native LinuxX64 target (to keep things simple).

I am doing the gradle processes on terminal without any IDE support.

I am using the official multiplatform template from here: GitHub.

The gradle process doesnt create new folders in my case. If I use this applyDefaultHierarchyTemplate() function, it gives some Circular dependencies error even though there is only 1 target.

Without that function it works somewhat, but its unable to find the nativeInterop folder(which is a default folder for storing def files) and the cinterop process fails to find anything.

Some insight into this would be very helpful.


Solution

  • I have found the answer to all my questions. The final source code is here: GitHub

    In this project, I wanted to support only the linux 64 bit target. Here's what I got to know:

    There were some source conflicts due to various targets. I removed all of them and just used the linuxX64() source set which is one of the predefined sets.

    Most of the issues were due to the usage of non-standard source sets (I'd have to redefine everything in that case).

    All the headers and defs are kept in the nativeInterop folder. The cinterop block picks these up and generates the bindings.

    However, in my case, nativeInterop doesn't seem to be a default folder(I had to configure some things manually to get it working), but regardless, it works pretty well.

    A point to note is that, you need to configure a linker option named "rpath" for the executable to detect the shared library at runtime. This rpath variable is the absolute path of the shared library.

    Use the runReleaseExecutableLinuxX64 task to run the final executable.

    Here's the kotlin section of build.gradle.kts.

    kotlin {
        //Defines the folders where the .def and headers are kept
        val nativeLinuxResDir = "src/nativeInterop/cinterop"
        //Defines the folders where native libraries are kept
        val nativeLinuxLibsDir = "$nativeLinuxResDir/libs"
    
        //Add Linux 64 bit target
        linuxX64() {
            binaries {
                //Produces a native linux executable file
                executable {
                    //The entry point of the program
                    entryPoint = "main"
    
                    //Configure Runtime Path of the executable
                    linkerOpts = mutableListOf(
                        "-rpath=${project.projectDir}/$nativeLinuxLibsDir",
                        "-ldemo"
                    )
                }
            }
    
            /* All the headers and .def files should be kept in src/nativeInterop/cinterop/
             * All shared and static libs should be kept in src/nativeInterop/cinterop/libs/
             * This is done to maintain good practices.
             */
    
            //Settings for Main compilation process
            compilations.getByName("main") {
                cinterops {
                    //Create a Cinterop process
                    val demo by creating {
                        //Sets the .def file
                        definitionFile.set(project.file("$nativeLinuxResDir/demo.def"))
    
                        //Sets the package name where the generated bindings will be kept
                        packageName("demo")
    
                        //Directory to find headers
                        includeDirs(nativeLinuxResDir)
                    }
                }
            }
    
            //Settings for Test compilation process
            compilations.getByName("test") {
                cinterops {
                    //Create a Cinterop process
                    val demo by creating {
                        //Sets the .def file
                        definitionFile.set(project.file("$nativeLinuxResDir/demo.def"))
    
                        //Sets the package name where the generated bindings will be kept
                        packageName("demo")
    
                        //Directory to find headers
                        includeDirs(nativeLinuxResDir)
                    }
                }
            }
        }
        sourceSets {
            //Dependencies for Linux 64 bit target
            linuxX64Main.dependencies {
            }
        }
    }
    

    Hope the answer helps.