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 tokonan_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 insstreams
- I moved
JoltPhysicsC.h
into aJoltC/
dir, and updated#include "JoltPhysicsC.h"
to#include <JoltC/JoltPhysicsC.h>
├── 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
// 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"))
}
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)
}
}
}
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()
}
}
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)
}