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
21 changes: 21 additions & 0 deletions docs/src/reference/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,27 @@ settings:
entryPoint: com.example.MainKt.main
```

##### 'settings.cinterop'

`settings:native:cinterop` configures C/Objective-C interop for native targets.

| Attribute | Description | Default |
|----------------|-------------------------------------------|---------|
| `defs: list` | A list of `.def` files for cinterop generation. | (empty) |

By convention, Amper automatically discovers all `.def` files located in the `resources/cinterop` directory of a native fragment. The `defs` property can be used to include `.def` files from other locations.
Copy link
Collaborator

@joffrey-bion joffrey-bion Nov 14, 2025

Choose a reason for hiding this comment

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

I think we need some design here. Could you please describe the use cases for this approach? Why do we need to support explicit and implicit def files, respectively?

I'm not very fond of browsing the fs tree during model parsing in general (for perf reasons, because the auto-sync in the IDE should be super fast), so it's important to understand why we're doing this (cc @Jeffset, please feel free to jump in).

Also, from what I see in the interop docs, there seems to be more parameters that users should be able to customize when registering definition files. In Gradle it can look like this:

cinterops {
    val libcurl by creating {
        definitionFile.set(project.file("src/nativeInterop/cinterop/libcurl.def"))
        packageName("com.jetbrains.handson.http")
        compilerOpts("-I/path")
        includeDirs.allHeaders("path")
    }
}


Example:

```yaml
# Configure cinterop for a native module
settings:
native:
cinterop:
defs:
- src/native/cinterop/libfoo.def
```

### `settings.springBoot`

`settings:springBoot:` configures the Spring Boot framework (JVM platform only).
Expand Down
14 changes: 14 additions & 0 deletions examples/ktor-native-sample/module.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
product:
type: linux/app
platforms:
- linuxX64

settings:
kotlin:
serialization: json
native:
entryPoint: org.jetbrains.amper.ktor.main

dependencies:
- io.ktor:ktor-server-core:3.2.3
- io.ktor:ktor-server-cio:3.2.3
20 changes: 20 additions & 0 deletions examples/ktor-native-sample/src/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package org.jetbrains.amper.ktor

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.cio.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
embeddedServer(CIO, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}

fun Application.module() {
configureRouting()
}
17 changes: 17 additions & 0 deletions examples/ktor-native-sample/src/Routing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package org.jetbrains.amper.ktor

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}
7 changes: 7 additions & 0 deletions examples/native-cinterop/module.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
product:
type: linux/app
platforms: [ linuxX64 ]

settings:
native:
entryPoint: 'org.jetbrains.amper.samples.cinterop.main'
1 change: 1 addition & 0 deletions examples/native-cinterop/resources/cinterop/hello.def
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
headers = src/c/hello.c
5 changes: 5 additions & 0 deletions examples/native-cinterop/src/c/hello.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include <stdio.h>

void sayHello(const char* name) {
printf("Hello. %s from C!\n", name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.jetbrains.amper.samples.cinterop

import kotlinx.cinterop.*
import hello.*

@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
fun main() = memScoped {
println("Hello from Kotlin!")
sayHello("John Doe")
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,7 @@ class KotlinNativeCompiler(
logger.debug("konanc ${ShellQuoting.quoteArgumentsPosixShellWay(args)}")

withKotlinCompilerArgFile(args, tempRoot) { argFile ->
val konanLib = kotlinNativeHome / "konan" / "lib"

// We call konanc via java because the konanc command line doesn't support spaces in paths:
// https://youtrack.jetbrains.com/issue/KT-66952
// TODO in the future we'll switch to kotlin tooling api and remove this raw java exec anyway
val result = jdk.runJava(
workingDir = kotlinNativeHome,
mainClass = "org.jetbrains.kotlin.cli.utilities.MainKt",
classpath = listOf(
konanLib / "kotlin-native-compiler-embeddable.jar",
konanLib / "trove4j.jar",
),
programArgs = listOf("konanc", "@${argFile}"),
// JVM args partially copied from <kotlinNativeHome>/bin/run_konan
argsMode = ArgsMode.ArgFile(tempRoot = tempRoot),
jvmArgs = listOf(
"-ea",
"-XX:TieredStopAtLevel=1",
"-Dfile.encoding=UTF-8",
"-Dkonan.home=$kotlinNativeHome",
),
outputListener = LoggingProcessOutputListener(logger),
)

// TODO this is redundant with the java span of the external process run. Ideally, we
// should extract higher-level information from the raw output and use that in this span.
val result = runInProcess("konanc", listOf("@$argFile"), ArgsMode.ArgFile(tempRoot))
span.setProcessResultAttributes(result)

if (result.exitCode != 0) {
Expand All @@ -122,4 +97,60 @@ class KotlinNativeCompiler(
}
}
}

suspend fun cinterop(
args: List<String>,
module: AmperModule,
) {
spanBuilder("cinterop")
.setAmperModule(module)
.setListAttribute("args", args)
.setAttribute("version", kotlinVersion)
.use { span ->
logger.debug("cinterop ${ShellQuoting.quoteArgumentsPosixShellWay(args)}")

val result = runInProcess("cinterop", args, ArgsMode.CommandLine, module.source.moduleDir)
span.setProcessResultAttributes(result)

if (result.exitCode != 0) {
val errors = result.stderr
.lines()
.filter { it.startsWith("error: ") || it.startsWith("exception: ") }
.joinToString("\n")
val errorsPart = if (errors.isNotEmpty()) ":\n\n$errors" else ""
userReadableError("Kotlin native 'cinterop' failed$errorsPart")
}
}
}

private suspend fun runInProcess(
toolName: String,
programArgs: List<String>,
argsMode: ArgsMode,
workingDir: Path = kotlinNativeHome,
): org.jetbrains.amper.processes.ProcessResult {
val konanLib = kotlinNativeHome / "konan" / "lib"

// We call konanc via java because the konanc command line doesn't support spaces in paths:
// https://youtrack.jetbrains.com/issue/KT-66952
// TODO in the future we'll switch to kotlin tooling api and remove this raw java exec anyway
return jdk.runJava(
workingDir = workingDir,
mainClass = "org.jetbrains.kotlin.cli.utilities.MainKt",
classpath = listOf(
konanLib / "kotlin-native-compiler-embeddable.jar",
konanLib / "trove4j.jar",
),
programArgs = listOf(toolName) + programArgs,
// JVM args partially copied from <kotlinNativeHome>/bin/run_konan
argsMode = argsMode,
jvmArgs = listOf(
"-ea",
"-XX:TieredStopAtLevel=1",
"-Dfile.encoding=UTF-8",
"-Dkonan.home=$kotlinNativeHome",
),
outputListener = LoggingProcessOutputListener(logger),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package org.jetbrains.amper.tasks.native

import org.jetbrains.amper.cli.AmperProjectTempRoot
import org.jetbrains.amper.compilation.KotlinArtifactsDownloader
import org.jetbrains.amper.compilation.downloadNativeCompiler
import org.jetbrains.amper.compilation.serializableKotlinSettings
import org.jetbrains.amper.core.AmperUserCacheRoot
import org.jetbrains.amper.core.extract.cleanDirectory
import org.jetbrains.amper.engine.BuildTask
import org.jetbrains.amper.engine.TaskGraphExecutionContext
import org.jetbrains.amper.frontend.AmperModule
import org.jetbrains.amper.frontend.Platform
import org.jetbrains.amper.frontend.TaskName
import org.jetbrains.amper.frontend.isDescendantOf
import org.jetbrains.amper.incrementalcache.IncrementalCache
import org.jetbrains.amper.tasks.TaskOutputRoot
import org.jetbrains.amper.tasks.TaskResult
import org.jetbrains.amper.util.BuildType
import org.slf4j.LoggerFactory
import java.nio.file.Path

/**
* A task that runs the Kotlin/Native cinterop tool.
*/
internal class CinteropTask(
override val module: AmperModule,
override val platform: Platform,
private val userCacheRoot: AmperUserCacheRoot,
private val taskOutputRoot: TaskOutputRoot,
private val incrementalCache: IncrementalCache,
override val taskName: TaskName,
private val tempRoot: AmperProjectTempRoot,
override val isTest: Boolean,
override val buildType: BuildType,
private val defFile: Path,
private val kotlinArtifactsDownloader: KotlinArtifactsDownloader =
KotlinArtifactsDownloader(userCacheRoot, incrementalCache),
) : BuildTask {
init {
require(platform.isLeaf)
require(platform.isDescendantOf(Platform.NATIVE))
}

override suspend fun run(dependenciesResult: List<TaskResult>, executionContext: TaskGraphExecutionContext): TaskResult {
// For now, we assume a single fragment. This might need to be adjusted.
val fragment = module.fragments.first { it.platforms.contains(platform) && it.isTest == isTest }
val kotlinUserSettings = fragment.serializableKotlinSettings()

val configuration = mapOf(
"kotlin.version" to kotlinUserSettings.compilerVersion,
"def.file" to defFile.toString(),
)
val inputs = listOf(defFile)

val artifact = incrementalCache.execute(taskName.name, configuration, inputs) {
cleanDirectory(taskOutputRoot.path)

val outputKLib = taskOutputRoot.path.resolve(defFile.toFile().nameWithoutExtension + ".klib")

val nativeCompiler = downloadNativeCompiler(kotlinUserSettings.compilerVersion, userCacheRoot)
val args = listOf(
"-def", defFile.toString(),
"-o", outputKLib.toString(),
"-compiler-option", "-I.",
"-target", platform.nameForCompiler,
)

logger.info("Running cinterop for '${defFile.fileName}'...")
nativeCompiler.cinterop(args, module)

return@execute IncrementalCache.ExecutionResult(listOf(outputKLib))
}.outputs.singleOrNull()

return Result(
compiledKlib = artifact,
taskName = taskName,
)
}

class Result(
val compiledKlib: Path?,
val taskName: TaskName,
) : TaskResult

private val logger = LoggerFactory.getLogger(javaClass)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package org.jetbrains.amper.tasks.native

import kotlinx.serialization.json.Json
import org.jetbrains.amper.cli.AmperProjectTempRoot
import org.jetbrains.amper.tasks.native.CinteropTask
import org.jetbrains.amper.compilation.KotlinArtifactsDownloader
import org.jetbrains.amper.compilation.KotlinCompilationType
import org.jetbrains.amper.compilation.downloadCompilerPlugins
Expand Down Expand Up @@ -96,11 +97,15 @@ internal class NativeCompileKlibTask(

// todo native resources are what exactly?

val cinteropKlibs = dependenciesResult
.filterIsInstance<CinteropTask.Result>()
.mapNotNull { it.compiledKlib }

val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings()

logger.debug("native compile klib '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}")

val libraryPaths = compiledModuleDependencies + externalDependencies
val libraryPaths = compiledModuleDependencies + externalDependencies + cinteropKlibs

val additionalSources = additionalKotlinJavaSourceDirs.map { artifact ->
SourceRoot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ internal class NativeLinkTask(
val compiledKLibs = compileKLibDependencies.mapNotNull { it.compiledKlib }
val exportedKLibs = exportedKLibDependencies.mapNotNull { it.compiledKlib }

val cinteropKLibs = dependenciesResult
.filterIsInstance<CinteropTask.Result>()
.mapNotNull { it.compiledKlib }

val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings()

logger.debug("native link '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}")
Expand Down Expand Up @@ -167,7 +171,7 @@ internal class NativeLinkTask(
kotlinUserSettings = kotlinUserSettings,
compilerPlugins = compilerPlugins,
entryPoint = entryPoint,
libraryPaths = compiledKLibs + externalKLibs,
libraryPaths = compiledKLibs + externalKLibs + cinteropKLibs,
exportedLibraryPaths = exportedKLibs,
// no need to pass fragments nor sources, we only build from klibs
fragments = emptyList(),
Expand Down
Loading