Skip to content

Instantly share code, notes, and snippets.

@sureshg
Forked from aSemy/kotlin_native_c_compile.md
Created December 18, 2024 01:43
Show Gist options
  • Save sureshg/5c2615700867530e63c1ad254e835e35 to your computer and use it in GitHub Desktop.
Save sureshg/5c2615700867530e63c1ad254e835e35 to your computer and use it in GitHub Desktop.
Compile C libraries for Kotlin/Native

Kotlin/Native requires that C libraries are compiled with a specific verison of GCC.

You can use the GCC used by Kotlin/Native itself. These Gradle tasks will help with this.

Example usage

// build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.konan.target.HostManager

plugins {
  kotlin("multiplatform") version "1.9.22"
}

kotlin {
  linuxArm64()
  linuxX64()
  mingwX64()
  macosArm64()
  macosX64()
}

val zphysicsSrcPrep by tasks.registering {
  // download zphysics source to src/main/cpp/ & src/main/cppHeaders/
}

val konanClangCompileJolt by tasks.registering(RunKonanClangTask::class) {
  dependsOn(zphysicsSrcPrep)

  kotlinTarget = HostManager.host

  sourceFiles.from(
      layout.projectDirectory
          .dir("src/main/cpp/")
          .asFileTree
          .matching {
            include("**/*.cpp")
            exclude("**/*_Tests*")
          }
  )

  includeDirs.from(layout.projectDirectory.dir("src/main/cppHeaders/"))

  arguments.addAll(
      "-std=c++17",
      "-fno-sanitize=undefined",
      "-D" + "JPH_CROSS_PLATFORM_DETERMINISTIC",
      "-D" + "JPH_ENABLE_ASSERTS",
  )

  runKonan = runKonanFile()
}


val konanLinkJolt by tasks.registering(RunKonanLinkTask::class) {
  group = project.name

  libName = "jolt"
  objectFiles.from(konanClangCompileJolt)
  runKonan = runKonanFile()
}

Code

RunKonanClangTask

import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.api.tasks.PathSensitivity.NAME_ONLY
import org.gradle.api.tasks.PathSensitivity.RELATIVE
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.util.parseSpaceSeparatedArgs
import javax.inject.Inject
import kotlin.io.path.copyTo

/**
 * Compile C/C++ source files using the
 * [`run_konan`](https://github.com/JetBrains/kotlin/blob/v1.9.0/kotlin-native/HACKING.md#running-clang-the-same-way-kotlinnative-compiler-does)
 * utility.
 */
@CacheableTask
abstract class RunKonanClangTask @Inject constructor(
    private val exec: ExecOperations,
    private val fs: FileSystemOperations,
    private val objects: ObjectFactory,
) : DefaultTask() {

  /** Destination of compiled `.o` object files */
  @get:OutputDirectory
  val outputDir: Provider<Directory>
    get() = objects.directoryProperty().fileValue(temporaryDir.resolve("output"))

  /** C and C++ source files to compile to object files */
  @get:InputFiles
  @get:PathSensitive(RELATIVE)
  abstract val sourceFiles: ConfigurableFileCollection

  /** Directories that include `.h` header files */
  @get:InputFiles
  @get:PathSensitive(RELATIVE)
  abstract val includeDirs: ConfigurableFileCollection

  /** Path to the (platform specific) `run_konan` utility */
  @get:InputFile
  @get:PathSensitive(NAME_ONLY)
  abstract val runKonan: RegularFileProperty

  /** Kotlin target platform, e.g. `mingw_x64` */
  @get:Input
  abstract val kotlinTarget: Property<KonanTarget>

  @get:Input
  @get:Optional
  abstract val arguments: ListProperty<String>

  @get:Internal
  val workingDir: DirectoryProperty =
      objects.directoryProperty().convention(
          // workaround for https://github.com/gradle/gradle/issues/23708
          objects.directoryProperty().fileValue(temporaryDir)
      )

  @TaskAction
  fun compile() {
    val workingDir = workingDir.asFile.get()
    val kotlinTarget = kotlinTarget.get()


    // prepare output dir
    val compileDir = workingDir.resolve("compile")

    fs.delete { delete(compileDir) }
    compileDir.mkdirs()


    // prepare args file
    val includeDirsArgs = includeDirs
        .filter { it.isDirectory }
        .joinToString("\n") { headersDir ->
          /* language=text */ """
            |--include-directory ${headersDir.invariantSeparatorsPath}
          """.trimMargin()
        }
    val arguments = arguments.getOrElse(emptyList()).joinToString("\n")

    val sourceFilePaths = sourceFiles
        .asFileTree
        .files
        .filter { it.isFile && it.extension in listOf("cpp", "c") }
        .joinToString("\n") { it.invariantSeparatorsPath }

    compileDir.resolve("args").writeText(/*language=text*/ """
          |$includeDirsArgs
          |
          |$arguments
          |
          |-c $sourceFilePaths
        """.trimMargin()
    )

    // compile files
    val result = exec.execCapture {
      executable(runKonan.asFile.get())
      args(parseSpaceSeparatedArgs(
          "clang clang $kotlinTarget @args"
      ))
      workingDir(workingDir)
    }

    val outputLog = workingDir.resolve("compileResult.log")
    result.logFile.copyTo(outputLog.toPath(), overwrite = true)
    logger.lifecycle("compilation output log: file://${outputLog}")

    result.assertNormalExitValue()

    // move compiled files to output directory
    fs.sync {
      from(compileDir) {
        include("**/*.o")
      }
      into(outputDir)
    }
  }
}

RunKonanLinkTask

import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.util.parseSpaceSeparatedArgs
import javax.inject.Inject
import kotlin.io.path.copyTo

/**
 * Link compiled C/C++ source files into a static lib using the
 * [`run_konan`](https://github.com/JetBrains/kotlin/blob/v1.9.0/kotlin-native/HACKING.md#running-clang-the-same-way-kotlinnative-compiler-does)
 * utility.
 */
@CacheableTask
abstract class RunKonanLinkTask @Inject constructor(
    private val exec: ExecOperations,
    private val fs: FileSystemOperations,
    objects: ObjectFactory,
) : DefaultTask() {

  /** The linked file */
  @get:OutputFile
  val compiledLib: Provider<RegularFile>
    get() = workingDir.file(libFileName)

  /** All `.o` object files that will be linked */
  @get:InputFiles
  @get:PathSensitive(PathSensitivity.NAME_ONLY)
  abstract val objectFiles: ConfigurableFileCollection

  @get:Input
  @get:Optional
  abstract val arguments: ListProperty<String>

  /** Path to the (platform specific) `run_konan` utility */
  @get:InputFile
  @get:PathSensitive(PathSensitivity.NAME_ONLY)
  abstract val runKonan: RegularFileProperty

  @get:Input
  abstract val libName: Property<String>

  private val libFileName: Provider<String>
    get() = libName.map { "lib${it}.a" }

  @get:Internal
  val workingDir: DirectoryProperty =
      objects.directoryProperty().convention(
          // workaround for https://github.com/gradle/gradle/issues/23708
          objects.directoryProperty().fileValue(temporaryDir)
      )

  @TaskAction
  fun compile() {
    val workingDir = workingDir.asFile.get()
    val libFileName = libFileName.get()

    // prepare output dir
    fs.delete { delete(workingDir) }
    workingDir.mkdirs()

    // prepare args file
    val sourceFilePaths = objectFiles
        .asFileTree
        .matching { include("**/*.o") }
        .joinToString("\n") { it.invariantSeparatorsPath }

    val arguments = arguments.getOrElse(emptyList()).joinToString("\n")

    workingDir.resolve("args").writeText(/*language=text*/ """
          |-rv
          |$libFileName
          |
          |$arguments
          |
          |$sourceFilePaths
        """.trimMargin()
    )

    // compile files
    val result = exec.execCapture {
      executable(runKonan.asFile.get())
      args(parseSpaceSeparatedArgs(
          "llvm llvm-ar @args"
      ))
      workingDir(workingDir)
    }

    val outputLog = workingDir.resolve("linkResult.log")
    result.logFile.copyTo(outputLog.toPath(), overwrite = true)
    logger.lifecycle("linking output log: file://${outputLog}")

    result.assertNormalExitValue()
  }
}

execCapture

import org.gradle.process.ExecOperations
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.outputStream

fun ExecOperations.execCapture(
    configure: ExecSpec.() -> Unit,
): ExecCaptureResult {

  val logFile = Files.createTempFile("gradle_exec", "log")

  val result = logFile.outputStream().use { os ->
    exec {
      isIgnoreExitValue = true
      standardOutput = os
      errorOutput = os
      configure()
    }
  }

  return ExecCaptureResult(
      logFile = logFile,
      result = result
  )
}

class ExecCaptureResult(
    val logFile: Path,
    private val result: ExecResult,
) : ExecResult by result {
  val exitCodeSuccess: Boolean get() = result.exitValue == 0
}

Project.runKonanFile()

/** The `run_konan` or `run_konan.bat` file used to compile/link C/C++ sources. */
fun Project.runKonanFile(): Provider<RegularFile> {
  return layout.file(
      provider {
        val allRunKonanFiles = DependencyDirectories.localKonanDir.walk()
            .filter { it.isFile && it.nameWithoutExtension == "run_konan" }

        val currentOsName = HostManager.simpleOsName()
        val currentArch = HostManager.hostArch()
        val currentKotlinVersion = kotlinToolingVersion

        val runKonanFile = allRunKonanFiles
            .lastOrNull { runKonan ->
              val path = runKonan.invariantSeparatorsPath
              path.contains(currentOsName, ignoreCase = true)
                  && path.contains(currentArch, ignoreCase = true)
                  && path.contains(currentKotlinVersion.toString(), ignoreCase = true)
            }

        if (runKonanFile == null) {
          val allOptions = allRunKonanFiles.joinToString("\n") { "  - $it" }
          error("couldn't find run_konan or run_konan.bat for $currentOsName/$currentArch/$currentKotlinVersion - all options:\n$allOptions}")
        }

        logger.lifecycle("[project ${project.path}] using ${runKonanFile.invariantSeparatorsPath} for Konan compilation")
        runKonanFile
      }
  )
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment