Skip to content

Instantly share code, notes, and snippets.

@aSemy
Last active July 30, 2024 22:06
Show Gist options
  • Save aSemy/0ec58b89f79702ea9822f198ccef0dc3 to your computer and use it in GitHub Desktop.
Save aSemy/0ec58b89f79702ea9822f198ccef0dc3 to your computer and use it in GitHub Desktop.
Kotlin/Native: Compile C++ code to

Kotlin/Native has a utility, run_konan, that can be used to compile C/C++ code.

I've written two Gradle tasks, RunKonanClangTask and RunKonanClangTask, that can be used to compile JoltC. A lot of credit goes to https://github.com/michal-z for writing C wrappers for Jolt!

This Gist is very scrappy, and is missing a lot of information.

  • Kotlin 1.9.0
  • I've only tested it on Windows.
  • The Kotlin target must be set manually - is it possible to make this automatic?
  • While the .konan dir can be retrieved from a KGP util function, the path to konan_run file must be set manually (e.g. kotlin-native-prebuilt-windows-x86_64-1.9.0) - is it possible to make this automatic?
  • I had to remove #define private public and amend the Jolt source code to make everything public, otherwise compilation failed in sstreams
  • I moved JoltPhysicsC.h into a JoltC/ dir, and updated #include "JoltPhysicsC.h" to #include <JoltC/JoltPhysicsC.h>

Project layout

├── buildSrc/
│   ├── src/main/kotlin/
│   │   ├── RunKonanClangTask.kt
│   │   ├── RunKonanLinkTask.kt
│   │   └── utils.kt
│   ├── build.gradle.kts
│   └── settings.gradle.kts
├── src/main/cpp/
│   ├── Jolt/ (Jolt C++ sources)
│   └── JoltC/ (from zphysics)
├── zphysics.def
├── build.gradle.kts
└── settings.gradle.kts

Example usage

// build.gradle.kts

import org.jetbrains.kotlin.konan.util.DependencyDirectories.localKonanDir

plugins {
  kotlin("multiplatform")
}

description = "Kotlin/Native C-interop bindings for Jolt Physics, via zig-gamedev zphysics"

kotlin {
  targets.withType<KotlinNativeTarget>().configureEach {
    compilations.getByName("main") {
      cinterops {
        register("zphysics") {
          defFileProperty.set(file("zphysics.def"))
        }
      }
    }
    binaries {
      staticLib()
    }
  }
}

//region lots of utilities for downloading JoltC
// ...
//endregion

val konanClangCompile by tasks.registering(RunKonanClangTask::class) {
  group = project.name

  kotlinTarget.set(KonanTarget.MINGW_X64)

  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.set(localKonanDir.resolve("kotlin-native-prebuilt-windows-x86_64-1.9.0/bin/run_konan.bat"))
}

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

  libName.set("zphysics")
  objectFiles.from(konanClangCompile)
  runKonan.set(localKonanDir.resolve("kotlin-native-prebuilt-windows-x86_64-1.9.0/bin/run_konan.bat"))
}

RunKonanClangTask

import execCapture
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

/**
 * 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.
 */
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
  abstract val workingDir: DirectoryProperty

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

    // prepare output dirs
    val sourcesDir = workingDir.resolve("sources")
    val headersDir = workingDir.resolve("headers")
    val compileDir = workingDir.resolve("compile")

    fs.sync {
      from(sourceFiles)
      into(sourcesDir)
    }
    fs.sync {
      from(includeDirs)
      into(headersDir)
    }
    fs.delete { delete(compileDir) }
    compileDir.mkdirs()

    // prepare args file
    val sourceFilePaths = sourcesDir.walk()
        .filter { it.extension in listOf("cpp", "c") }
        .joinToString("\n") { it.invariantSeparatorsPath }

    compileDir.resolve("args").writeText(/*language=text*/ """
          |--include-directory ${headersDir.invariantSeparatorsPath}
          |${arguments.getOrElse(emptyList()).joinToString("\n")}
          |-c $sourceFilePaths
        """.trimMargin()
    )

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

    // verify output
    val outputLog = workingDir.resolve("compileResult.log").apply { writeText(compileResult.output) }
    logger.lifecycle("compilation output log: file://${outputLog.invariantSeparatorsPath}")
    compileResult.assertNormalExitValue()

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

RunKonanLinkTask

import execCapture
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.model.ObjectFactory
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

/**
 * Link compiled 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.
 */
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

  /** 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 }

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

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

    val outputLog = workingDir.resolve("linkResult.log").apply { writeText(linkResult.output) }
    logger.lifecycle("compilation output log: file://${outputLog.invariantSeparatorsPath}")

    linkResult.assertNormalExitValue()
  }
}

Utils

import org.gradle.process.ExecOperations
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
import java.io.ByteArrayOutputStream

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

  val (result, output) = ByteArrayOutputStream().use { os ->
    exec {
      isIgnoreExitValue = true
      standardOutput = os
      errorOutput = os
      configure()
    } to os.toString()
  }

  return if (result.exitValue != 0) {
    ExecCaptureResult.Error(output, result)
  } else {
    ExecCaptureResult.Success(output, result)
  }
}


sealed class ExecCaptureResult(
    val output: String,
    private val result: ExecResult,
) : ExecResult by result {
  class Success(output: String, result: ExecResult) : ExecCaptureResult(output, result)
  class Error(output: String, result: ExecResult) : ExecCaptureResult(output, result)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment