Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
*.iml
*.ipr

# Generated files
# Generated files
bin/
gen/

# Gradle files
# Gradle files
.gradle
.gradle/
build/

# Kotlin
.kotlin/

local.properties

Pods
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ cklib {

## Examples

You can find a [tutorial](https://hackernoon.com/how-to-extend-a-kmm-shared-module-with-cc-code) with a [GitHub Sample](https://github.com/ttypic/kmm-embedded-c) to get a brief understanding of how the library works.
[`sample/`](sample) is a small sample that packages [Monocypher](https://monocypher.org), compiles it with CKlib, binds it with cinterop, and exposes a passphrase-encrypted note API.

You can also find a [tutorial](https://hackernoon.com/how-to-extend-a-kmm-shared-module-with-cc-code) with a [GitHub Sample](https://github.com/ttypic/kmm-embedded-c) to get a brief understanding of how the library works.

Additionally you can see multiple examples of C Klib in use here:
1. [zstd-kmp](https://github.com/square/zstd-kmp) - Packages [ztsd](https://github.com/facebook/zstd), a fast real-time compression algorithm.
Expand Down
5 changes: 2 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ plugins {
alias(libs.plugins.kotlin.jvm) apply false
}

val GROUP: String by project

val CKLIB_VERSION: String by project
val GROUP = property("GROUP") as String
val CKLIB_VERSION = property("CKLIB_VERSION") as String

allprojects {
group = GROUP
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kotlin = "2.0.0"
kotlin = "2.4.10"

[libraries]
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
6 changes: 2 additions & 4 deletions plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("java-gradle-plugin")
kotlin("jvm")
Expand All @@ -9,7 +7,7 @@ plugins {
}

kotlin {
jvmToolchain(11)
jvmToolchain(17)
}

dependencies {
Expand Down Expand Up @@ -42,7 +40,7 @@ repositories {
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
val releaseSigningEnabled =
project.properties["RELEASE_SIGNING_ENABLED"]?.toString()?.equals("false", ignoreCase = true) != true
findProperty("RELEASE_SIGNING_ENABLED")?.toString()?.equals("false", ignoreCase = true) != true
if (releaseSigningEnabled) signAllPublications()
pomFromGradleProperties()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import org.gradle.work.DisableCachingByDefault
import java.io.File
import javax.inject.Inject

@DisableCachingByDefault(because = "Invokes host clang and llvm-link with absolute toolchain paths")
open class CompileToBitcode @Inject constructor(
srcRoot: File,
@Input val compileName: String,
Expand Down Expand Up @@ -56,9 +58,11 @@ open class CompileToBitcode @Inject constructor(

// Source files and headers are registered as inputs by the `inputFiles` and `headers` properties.
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
var srcDirs: FileCollection = project.files(srcRoot.resolve("cpp"))

@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
var headersDirs: FileCollection = srcDirs + project.files(srcRoot.resolve("headers"))

@Input
Expand All @@ -73,7 +77,7 @@ open class CompileToBitcode @Inject constructor(
@get:Internal
internal val targetDir: File
get() {
return project.buildDir.resolve("$PLUGIN_NAME/$compileName/$target")
return project.layout.buildDirectory.get().asFile.resolve("$PLUGIN_NAME/$compileName/$target")
}

@get:OutputDirectory
Expand Down Expand Up @@ -133,6 +137,7 @@ open class CompileToBitcode @Inject constructor(

@get:SkipWhenEmpty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
val inputFiles: Iterable<File>
get() {
return srcDirs.flatMap { srcDir ->
Expand All @@ -147,6 +152,7 @@ open class CompileToBitcode @Inject constructor(
private fun bitcodeFileForInputFile(file: File) = outputFileForInputFile(file, "bc")

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
protected val headers: Iterable<File>
get() {
// Not using clang's -M* flags because there's a problem with our current include system:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,44 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {
) {
val kmpExt = project.kmpExt

val allBitcode = project.tasks.register("all${name.snakeCaseToCamelCase().capitalize()}") {
val allBitcode = project.tasks.register("all${name.snakeCaseToCamelCase().capitalized()}") {
it.group = GROUP_NAME
it.description = "Compiles '$name' to bitcode for all targets"
}.get()

val allTaskProviders = kmpExt.kotlinNativeTargets.map { knTarget ->

val compileKotlinTask = project.tasks.getByPath("compileKotlin${knTarget.name.capitalize()}")
val compileKotlinTask = project.tasks.getByPath("compileKotlin${knTarget.name.capitalized()}")

val taskName = "${knTarget.name}${name.snakeCaseToCamelCase().capitalize()}"
val taskName = "${knTarget.name}${name.snakeCaseToCamelCase().capitalized()}"

val taskProvider = project.tasks.register(
taskName,
CompileToBitcode::class.java,
srcDir, name, knTarget.konanTarget.name, { project.platformManager.isEnabled(knTarget.konanTarget) }
)

//tasks.getByName("compileKotlin${targetName.capitalize()}").dependsOn("${it.second}Quickjs")
compileKotlinTask.dependsOn(taskName)

compileKotlinTask
.dependsOn(taskName)
val bitcodeFile = project.layout.buildDirectory
.file("${CompileToBitcodePlugin.PLUGIN_NAME}/$name/${knTarget.konanTarget.name}/$name.bc")
val nativeLibraryArgs = bitcodeFile.map { listOf("-native-library", it.asFile.absolutePath) }

taskProvider.configure { compileToBitcodeTask ->
compileToBitcodeTask.group = GROUP_NAME
compileToBitcodeTask.description = "Compiles '$name' to bitcode for ${knTarget.name}"

compilations.forEach { compilation ->
val knCompilation = knTarget.compilations.getByName(compilation)
compilations.forEach { compilation ->
@Suppress("DEPRECATION")
knTarget.compilations.getByName(compilation)
.compilerOptions.options.freeCompilerArgs.addAll(nativeLibraryArgs)
}

knCompilation.kotlinOptions.freeCompilerArgs +=
listOf("-native-library", compileToBitcodeTask.outFile.absolutePath)
knTarget.binaries.configureEach { binary ->
binary.linkTaskProvider.configure { linkTask ->
linkTask.toolOptions.freeCompilerArgs.addAll(nativeLibraryArgs)
}
}

taskProvider.configure { compileToBitcodeTask ->
compileToBitcodeTask.group = GROUP_NAME
compileToBitcodeTask.description = "Compiles '$name' to bitcode for ${knTarget.name}"
compileToBitcodeTask.configurationBlock()
}

Expand All @@ -70,7 +75,10 @@ open class CompileToBitcodeExtension @Inject constructor(val project: Project) {

companion object {
private fun String.snakeCaseToCamelCase() =
split('_').joinToString(separator = "") { it.capitalize() }
split('_').joinToString(separator = "") { it.capitalized() }

private fun String.capitalized(): String =
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }

const val GROUP_NAME = CompileToBitcodePlugin.PLUGIN_NAME
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class PlatformManager(dist: Distribution, konanHome:String) {
private val blindDelegate:Any

init {
val child = URLClassLoader(arrayOf(File(kotlinNativeJar).toURL()), this.javaClass.classLoader)
val child = URLClassLoader(arrayOf(File(kotlinNativeJar).toURI().toURL()), this.javaClass.classLoader)
pmClass = Class.forName("org.jetbrains.kotlin.konan.target.PlatformManager", true, child)
blindDelegate = pmClass.declaredConstructors.find {
it.parameters.size == 2 && it.parameters[0].type == Distribution::class.java
Expand Down
61 changes: 61 additions & 0 deletions sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# C Klib sample: passphrase-encrypted notes with Monocypher

A sample for C Klib that packages the C library [Monocypher](https://monocypher.org). It's a simple passphrase-encrypted note implementation.

```kotlin
val sealed: ByteArray = SecureNote.seal("hunter2", "meet me at the docks at midnight")
val note: String? = SecureNote.open("hunter2", sealed) // null if the passphrase is wrong
```

C Klib compiles C to LLVM bitcode; cinterop reads the same headers and generates the Kotlin
declarations; the Kotlin/Native compiler links the two together.

## Running the sample

From the repository root, using the root wrapper:
```bash
./gradlew -p sample build # compile every declared target
./gradlew -p sample macosArm64Test # the host-runnable test suite
./gradlew -p sample allMonocypher # just the bitcode, for every target
```

`sample/settings.gradle.kts` uses `pluginManagement { includeBuild("..") }`, so the plugin is built
from this working tree rather than downloaded. No `version` on the plugin id, and no
`include(":sample")` in the root build.

> **Note:** The first run downloads an LLVM toolchain into `~/.cklib` (about 1.6 GB) and Kotlin/Native into
`~/.konan`.

## Notes

* **`srcDirs` defaults to `srcRoot/cpp`, even for `Language.C`.** `headersDirs` defaults to
`srcDirs + srcRoot/headers`. If you want your C in a directory not named `cpp`, override both:

```kotlin
create("monocypher", srcDir = file("src/monocypher")) {
language = CompileToBitcode.Language.C
srcDirs = files("src/monocypher/c")
headersDirs = files("src/monocypher/headers")
}
```

* **`mingwX64`** is deliberately not declared: `secure_random.c` has no Windows branch, and adding one
means `BCryptGenRandom` plus linking `bcrypt.lib`.

* **`Language.C` compiles with `-std=gnu11 -O3 -Wall -Wextra -Werror`, hardcoded.** The only escape
hatch is `compilerArgs`, which is appended after those flags, so `-Wno-error=<specific>` works.
This sample needs **no suppressions at all**: Monocypher 4.0.3 and `secure_random.c` both build
clean. Vendored code that is not warning-clean will stop the build dead, so check before you commit
to a library.

* **Shared `nativeMain` + cinterop needs `kotlin.mpp.enableCInteropCommonization=true`.** Without it,
per-target tasks like `compileKotlinMacosArm64` and `macosArm64Test` work fine, but
`compileNativeMainKotlinMetadata` — which `build` runs — fails with `Unresolved reference
'cinterop'` for every binding. It is in `gradle.properties`.

* **`config.kotlinVersion` must match the Kotlin plugin version.** CKlib resolves the toolchain at
`~/.konan/kotlin-native-prebuilt-<os>-<arch>-<kotlinVersion>` and fails with a bare
`InvocationTargetException` if that directory is missing. On a machine with no Kotlin/Native
installed yet, the bitcode task can therefore fail before anything has had a chance to download it;
running any Kotlin/Native task first (`./gradlew -p sample cinteropMonocypherMacosArm64`) fetches
the distribution and unblocks it.
66 changes: 66 additions & 0 deletions sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026 Touchlab
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

import co.touchlab.cklib.gradle.CompileToBitcode

plugins {
kotlin("multiplatform") version "2.4.10"
id("co.touchlab.cklib")
}

kotlin {
val nativeTargets = listOf(
macosArm64(),
macosX64(),
iosArm64(),
iosSimulatorArm64(),
linuxX64(),
)

nativeTargets.forEach { target ->
target.compilations.getByName("main").cinterops.create("monocypher") {
// cinterop only needs the headers. The compiled code arrives as bitcode from cklib,
// which is why the .def declares no staticLibraries or libraryPaths.
includeDirs(project.file("src/monocypher/headers"))
}
}

sourceSets {
getByName("commonTest") {
dependencies {
implementation(kotlin("test"))
}
}
}
}

cklib {
config.kotlinVersion = "2.4.10"

create("monocypher", srcDir = file("src/monocypher")) {
language = CompileToBitcode.Language.C

// Both default to a `cpp` subdirectory (headersDirs additionally to `headers`) even for
// Language.C, so point them at the real layout instead of naming a C folder "cpp".
srcDirs = files("src/monocypher/c")
headersDirs = files("src/monocypher/headers")

// No compilerArgs. Language.C compiles with a hardcoded
// `-std=gnu11 -O3 -Wall -Wextra -Werror`, and both Monocypher 4.0.3 and secure_random.c
// build clean under it, so no -Wno-error= escape hatch is needed.
// compilerArgs.addAll(
// listOf(
// )
// )
}
}
8 changes: 8 additions & 0 deletions sample/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kotlin.code.style=official
org.gradle.jvmargs=-Xmx3g

# Required because SecureNote.kt lives in the shared `nativeMain` source set and references cinterop
# declarations. Per-target compilation (`compileKotlinMacosArm64`) works without this, but
# `compileNativeMainKotlinMetadata`, which type-checks the shared source set and runs as part of
# `build`, fails with "Unresolved reference 'cinterop'" unless commonization is on.
kotlin.mpp.enableCInteropCommonization=true
Binary file added sample/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
9 changes: 9 additions & 0 deletions sample/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gradle-wrapper.properties, but no gradlew binary for the sample

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading