From 29c46d01fb05733c2c918e1e557feb60841f8581 Mon Sep 17 00:00:00 2001 From: Leo Date: Tue, 28 Oct 2025 00:29:09 -0300 Subject: [PATCH 1/8] cinterop support --- docs/src/reference/module.md | 21 +++++ examples/ktor-native-sample/module.yaml | 14 +++ .../ktor-native-sample/src/Application.kt | 20 +++++ examples/ktor-native-sample/src/Routing.kt | 17 ++++ examples/native-cinterop/module.yaml | 7 ++ .../resources/cinterop/hello.def | 1 + examples/native-cinterop/src/c/hello.c | 5 ++ .../jetbrains/amper/samples/cinterop/main.kt | 10 +++ .../amper/compilation/KotlinNativeCompiler.kt | 83 +++++++++++------ .../amper/tasks/native/CinteropTask.kt | 90 +++++++++++++++++++ .../tasks/native/NativeCompileKlibTask.kt | 7 +- .../amper/tasks/native/NativeLinkTask.kt | 6 +- .../amper/tasks/native/taskBuilderNative.kt | 29 ++++++ .../amper/frontend/schema/settings.kt | 8 ++ .../amper/frontend/tree/CinteropDiscovery.kt | 77 ++++++++++++++++ .../aomBuilder/CinteropDiscoveryTest.kt | 89 ++++++++++++++++++ 16 files changed, 456 insertions(+), 28 deletions(-) create mode 100644 examples/ktor-native-sample/module.yaml create mode 100644 examples/ktor-native-sample/src/Application.kt create mode 100644 examples/ktor-native-sample/src/Routing.kt create mode 100644 examples/native-cinterop/module.yaml create mode 100644 examples/native-cinterop/resources/cinterop/hello.def create mode 100644 examples/native-cinterop/src/c/hello.c create mode 100644 examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt create mode 100644 sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt create mode 100644 sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt create mode 100644 sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt diff --git a/docs/src/reference/module.md b/docs/src/reference/module.md index f64aa693e..d53c1db18 100644 --- a/docs/src/reference/module.md +++ b/docs/src/reference/module.md @@ -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. + +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). diff --git a/examples/ktor-native-sample/module.yaml b/examples/ktor-native-sample/module.yaml new file mode 100644 index 000000000..92c701876 --- /dev/null +++ b/examples/ktor-native-sample/module.yaml @@ -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 diff --git a/examples/ktor-native-sample/src/Application.kt b/examples/ktor-native-sample/src/Application.kt new file mode 100644 index 000000000..7c9bf18c4 --- /dev/null +++ b/examples/ktor-native-sample/src/Application.kt @@ -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() +} diff --git a/examples/ktor-native-sample/src/Routing.kt b/examples/ktor-native-sample/src/Routing.kt new file mode 100644 index 000000000..af6d52948 --- /dev/null +++ b/examples/ktor-native-sample/src/Routing.kt @@ -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!") + } + } +} diff --git a/examples/native-cinterop/module.yaml b/examples/native-cinterop/module.yaml new file mode 100644 index 000000000..37f3f1fea --- /dev/null +++ b/examples/native-cinterop/module.yaml @@ -0,0 +1,7 @@ +product: + type: linux/app + platforms: [ linuxX64 ] + +settings: + native: + entryPoint: 'org.jetbrains.amper.samples.cinterop.main' diff --git a/examples/native-cinterop/resources/cinterop/hello.def b/examples/native-cinterop/resources/cinterop/hello.def new file mode 100644 index 000000000..bf8ee733a --- /dev/null +++ b/examples/native-cinterop/resources/cinterop/hello.def @@ -0,0 +1 @@ +headers = src/c/hello.c diff --git a/examples/native-cinterop/src/c/hello.c b/examples/native-cinterop/src/c/hello.c new file mode 100644 index 000000000..277413f7c --- /dev/null +++ b/examples/native-cinterop/src/c/hello.c @@ -0,0 +1,5 @@ +#include + +void sayHello(const char* name) { + printf("Hello. %s from C!\n", name); +} diff --git a/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt new file mode 100644 index 000000000..3f41b5465 --- /dev/null +++ b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt @@ -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") +} \ No newline at end of file diff --git a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt index 93f8237f1..62492900a 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt @@ -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 /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) { @@ -122,4 +97,60 @@ class KotlinNativeCompiler( } } } + + suspend fun cinterop( + args: List, + 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, + 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 /bin/run_konan + argsMode = argsMode, + jvmArgs = listOf( + "-ea", + "-XX:TieredStopAtLevel=1", + "-Dfile.encoding=UTF-8", + "-Dkonan.home=$kotlinNativeHome", + ), + outputListener = LoggingProcessOutputListener(logger), + ) + } } diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt new file mode 100644 index 000000000..1ac7d2954 --- /dev/null +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt @@ -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, 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) +} \ No newline at end of file diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt index 0c15f7cd4..011121d2d 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt @@ -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 @@ -96,11 +97,15 @@ internal class NativeCompileKlibTask( // todo native resources are what exactly? + val cinteropKlibs = dependenciesResult + .filterIsInstance() + .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( diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt index 9e13c0672..6f6b021ab 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt @@ -105,6 +105,10 @@ internal class NativeLinkTask( val compiledKLibs = compileKLibDependencies.mapNotNull { it.compiledKlib } val exportedKLibs = exportedKLibDependencies.mapNotNull { it.compiledKlib } + val cinteropKLibs = dependenciesResult + .filterIsInstance() + .mapNotNull { it.compiledKlib } + val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings() logger.debug("native link '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}") @@ -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(), diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt index 815d26260..746338c7a 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt @@ -5,9 +5,12 @@ package org.jetbrains.amper.tasks.native import org.jetbrains.amper.compilation.KotlinCompilationType +import org.jetbrains.amper.compilation.singleLeafFragment +import org.jetbrains.amper.compilation.singleLeafFragment import org.jetbrains.amper.dependency.resolution.ResolutionScope 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.tasks.CommonTaskType import org.jetbrains.amper.tasks.PlatformTaskType @@ -37,6 +40,29 @@ fun ProjectTasksBuilder.setupNativeTasks() { .alsoBuildTypes() .alsoTests() .withEach { + val fragment = module.fragments + .filter { it.platforms.contains(platform) && it.isTest == isTest } + .singleLeafFragment() + + val cinteropTasks = fragment.settings.native?.cinterop?.defs.orEmpty().map { defFile -> + // Create a unique task name for each def file. + val taskNameSuffix = defFile.substring(defFile.lastIndexOf('/') + 1) + val cinteropTaskName = NativeTaskType.Cinterop.getTaskName(module, platform, isTest, buildType) + .let { TaskName(it.name + "-" + taskNameSuffix) } + CinteropTask( + module = module, + platform = platform, + userCacheRoot = context.userCacheRoot, + taskOutputRoot = context.getTaskOutputPath(cinteropTaskName), + incrementalCache = executeOnChangedInputs, + taskName = cinteropTaskName, + tempRoot = context.projectTempRoot, + isTest = isTest, + buildType = buildType, + defFile = module.source.moduleDir.resolve(defFile), + ).also { tasks.registerTask(it) } + } + val compileKLibTaskName = NativeTaskType.CompileKLib.getTaskName(module, platform, isTest, buildType) tasks.registerTask( task = NativeCompileKlibTask( @@ -53,6 +79,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { ), dependsOn = buildList { add(CommonTaskType.Dependencies.getTaskName(module, platform, isTest)) + cinteropTasks.forEach { add(it.taskName) } if (isTest) { // todo (AB) : Check if this is required for test KLib compilation add(NativeTaskType.CompileKLib.getTaskName(module, platform, isTest = false, buildType)) @@ -92,6 +119,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { ), dependsOn = buildList { add(compileKLibTaskName) + cinteropTasks.forEach { add(it.taskName) } add(CommonTaskType.Dependencies.getTaskName(module, platform, isTest)) if (isTest) { add(NativeTaskType.CompileKLib.getTaskName(module, platform, isTest = false, buildType)) @@ -194,4 +222,5 @@ private fun getNativeLinkTaskDetails( enum class NativeTaskType(override val prefix: String) : PlatformTaskType { CompileKLib("compile"), Link("link"), + Cinterop("cinterop"), } diff --git a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt index d370069d0..3092d21e0 100644 --- a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt +++ b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt @@ -254,6 +254,14 @@ class NativeSettings : SchemaNode() { // TODO other options from NativeApplicationPart @SchemaDoc("The fully-qualified name of the application's entry point function") var entryPoint by nullableValue() + + @SchemaDoc("C/Objective-C interop settings for native targets") + val cinterop: CinteropSettings by nested() +} + +class CinteropSettings : SchemaNode() { + @SchemaDoc("A list of .def files for cinterop generation.") + var defs: List by value(emptyList()) } class KtorSettings: SchemaNode() { diff --git a/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt b/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt new file mode 100644 index 000000000..71a188f48 --- /dev/null +++ b/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt @@ -0,0 +1,77 @@ +package org.jetbrains.amper.frontend.tree + +import org.jetbrains.amper.frontend.aomBuilder.BuildCtx +import org.jetbrains.amper.frontend.api.DefaultTrace +import org.jetbrains.amper.frontend.contexts.DefaultCtxs +import org.jetbrains.amper.frontend.messages.extractPsiElementOrNull +import org.jetbrains.amper.frontend.types.SchemaType +import java.io.IOException +import kotlin.io.path.relativeTo + +context(buildCtx: BuildCtx) +internal fun MapLikeValue<*>.discoverCinteropDefs(): Merged { + val discoveryAppender = CinteropDiscoveryAppender() + val withDiscoveredDefs = discoveryAppender.transform(this) as MapLikeValue<*> + return buildCtx.treeMerger.mergeTrees(listOf(withDiscoveredDefs)) +} + +private class CinteropDiscoveryAppender() : TreeTransformer() { + + override fun visitMapValue(value: MapLikeValue): TransformResult> { + val transformResult = super.visitMapValue(value) + val transformedValue = when (transformResult) { + is Changed -> transformResult.value + NotChanged -> value + Removed -> return transformResult + } + + val moduleDir = transformedValue.trace.extractPsiElementOrNull()?.containingFile?.virtualFile?.parent ?: return transformResult + val cinteropDir = moduleDir.findChild("resources")?.findChild("cinterop") + if (cinteropDir == null || !cinteropDir.exists() || !cinteropDir.isDirectory) { + return transformResult + } + + val discoveredDefFiles = try { + cinteropDir.children.filter { it.name.endsWith(".def") } + } catch (e: IOException) { + return transformResult + } + + if (discoveredDefFiles.isEmpty()) { + return transformResult + } + + val modulePath = moduleDir.toNioPath() + val discoveredDefPaths = discoveredDefFiles.map { + it.toNioPath().relativeTo(modulePath).toString().replace('\\', '/') + } + + val cinteropProperty = transformedValue.children.find { it.key == "cinterop" } + val cinteropNode = cinteropProperty?.value as? MapLikeValue<*> + val defsProperty = cinteropNode?.children?.find { it.key == "defs" } + val defsNode = defsProperty?.value as? ListValue<*> + val existingDefValues = defsNode?.children + ?.mapNotNull { it.asScalar?.value?.toString() } + .orEmpty() + + val allDefPaths = (existingDefValues + discoveredDefPaths).distinct() + + val newDefNodes = allDefPaths.map { path -> + ScalarValue(path, SchemaType.StringType, DefaultTrace, DefaultCtxs) + } + + val newDefsList = ListValue(newDefNodes, SchemaType.ListType(SchemaType.StringType), DefaultTrace, DefaultCtxs) + val newDefsMapProperty = MapLikeValue.Property("defs", DefaultTrace, newDefsList, defsProperty?.pType) + + val otherCinteropProperties = cinteropNode?.children?.filter { it.key != "defs" }.orEmpty() + val newCinteropChildren = otherCinteropProperties + newDefsMapProperty + + val newCinteropNode = (cinteropNode ?: Owned(emptyList(), SchemaType.MapType(SchemaType.StringType), DefaultTrace, DefaultCtxs)) + .copy(children = newCinteropChildren) + val newCinteropMapProperty = MapLikeValue.Property("cinterop", DefaultTrace, newCinteropNode, cinteropProperty?.pType) + + val finalChildren = transformedValue.children.filter { it.key != "cinterop" } + newCinteropMapProperty + + return Changed(transformedValue.copy(children = finalChildren)) + } +} \ No newline at end of file diff --git a/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt b/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt new file mode 100644 index 000000000..6037c32e4 --- /dev/null +++ b/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt @@ -0,0 +1,89 @@ +package org.jetbrains.amper.frontend.aomBuilder + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.jetbrains.amper.frontend.FrontendPathResolver +import org.jetbrains.amper.frontend.schema.NativeSettings +import org.jetbrains.amper.frontend.schema.RootSchema +import org.jetbrains.amper.frontend.tree.appendDefaultValues +import org.jetbrains.amper.frontend.tree.discoverCinteropDefs +import org.jetbrains.amper.frontend.tree.reading.readTree +import org.jetbrains.amper.frontend.tree.refineTree +import org.jetbrains.amper.frontend.util.assertIs +import org.junit.Test + +class CinteropDiscoveryTest : BasePlatformTestCase() { + + private fun runTest( + moduleYamlContent: String, + setup: (FrontendPathResolver) -> Unit, + assertions: (NativeSettings) -> Unit + ) { + val pathResolver = FrontendPathResolver(project) + setup(pathResolver) + val moduleFile = myFixture.tempDirFixture.createFile("my-module/module.yaml", moduleYamlContent) + + val buildCtx = BuildCtx(pathResolver, project) + val tree = with(buildCtx) { + readTree(moduleFile, schema.types.getType()) + .appendDefaultValues() + .discoverCinteropDefs() + } + val refinedTree = tree.refineTree(setOf("linuxX64"), buildCtx.schema.contexts) + val root = buildCtx.createSchemaNode(refinedTree) + + assertNotNull(root) + val nativeSettings = root!!.product.fragments.single().settings.assertIs() + assertions(nativeSettings) + } + + @Test + fun `cinterop defs are discovered automatically`() { + runTest( + moduleYamlContent = """ + product: + type: lib + platforms: [linuxX64] + settings@linuxX64: + native: {} + """.trimIndent(), + setup = { + myFixture.tempDirFixture.findOrCreateDir("my-module/resources/cinterop") + myFixture.tempDirFixture.createFile("my-module/resources/cinterop/mydef.def") + }, + assertions = { nativeSettings -> + val cinterop = nativeSettings.cinterop + assertNotNull(cinterop) + assertEquals(1, cinterop.defs.size) + assertEquals("resources/cinterop/mydef.def", cinterop.defs.single().value) + } + ) + } + + @Test + fun `discovered and explicit defs are merged and deduplicated`() { + runTest( + moduleYamlContent = """ + product: + type: lib + platforms: [linuxX64] + settings@linuxX64: + native: + cinterop: + defs: + - resources/cinterop/discovered.def # Duplicate + - explicit.def + """.trimIndent(), + setup = { + myFixture.tempDirFixture.findOrCreateDir("my-module/resources/cinterop") + myFixture.tempDirFixture.createFile("my-module/resources/cinterop/discovered.def") + }, + assertions = { nativeSettings -> + val cinterop = nativeSettings.cinterop + assertNotNull(cinterop) + assertEquals(2, cinterop.defs.size) + assertTrue(cinterop.defs.any { it.value == "resources/cinterop/discovered.def" }) + assertTrue(cinterop.defs.any { it.value == "explicit.def" }) + } + ) + } +} \ No newline at end of file From 6e0a88d56406039f2f7843af232afd9bd4417e18 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 19 Jan 2026 23:05:55 -0300 Subject: [PATCH 2/8] cinterop support --- examples/native-cinterop/module.yaml | 3 + .../resources/cinterop/hello.def | 1 + .../jetbrains/amper/samples/cinterop/main.kt | 8 +- .../amper/tasks/native/CinteropTask.kt | 43 ++++++--- .../amper/tasks/native/NativeLinkTask.kt | 4 + .../amper/tasks/native/taskBuilderNative.kt | 17 ++-- .../amper/frontend/schema/settings.kt | 17 +++- .../amper/frontend/tree/CinteropDiscovery.kt | 89 ++++++++++++------- .../aomBuilder/CinteropDiscoveryTest.kt | 60 ++++++++++--- 9 files changed, 173 insertions(+), 69 deletions(-) diff --git a/examples/native-cinterop/module.yaml b/examples/native-cinterop/module.yaml index 37f3f1fea..f2524f60d 100644 --- a/examples/native-cinterop/module.yaml +++ b/examples/native-cinterop/module.yaml @@ -5,3 +5,6 @@ product: settings: native: entryPoint: 'org.jetbrains.amper.samples.cinterop.main' + cinterop: + hello: + defFile: 'resources/cinterop/hello.def' \ No newline at end of file diff --git a/examples/native-cinterop/resources/cinterop/hello.def b/examples/native-cinterop/resources/cinterop/hello.def index bf8ee733a..24166684b 100644 --- a/examples/native-cinterop/resources/cinterop/hello.def +++ b/examples/native-cinterop/resources/cinterop/hello.def @@ -1 +1,2 @@ headers = src/c/hello.c +package = org.jetbrains.amper.samples.cinterop.hello \ No newline at end of file diff --git a/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt index 3f41b5465..79667fc6d 100644 --- a/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt +++ b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt @@ -1,10 +1,10 @@ package org.jetbrains.amper.samples.cinterop import kotlinx.cinterop.* -import hello.* +import org.jetbrains.amper.samples.cinterop.hello.* // Import the generated bindings -@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) -fun main() = memScoped { - println("Hello from Kotlin!") +@kotlinx.cinterop.ExperimentalForeignApi +fun main() { + println("Hello, John Doe from Kotlin!") sayHello("John Doe") } \ No newline at end of file diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt index 1ac7d2954..3cb399a8d 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt @@ -17,6 +17,7 @@ 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.jdk.provisioning.JdkProvider import org.jetbrains.amper.tasks.TaskOutputRoot import org.jetbrains.amper.tasks.TaskResult import org.jetbrains.amper.util.BuildType @@ -37,6 +38,9 @@ internal class CinteropTask( override val isTest: Boolean, override val buildType: BuildType, private val defFile: Path, + private val packageName: String?, + private val compilerOpts: List, + private val linkerOpts: List, private val kotlinArtifactsDownloader: KotlinArtifactsDownloader = KotlinArtifactsDownloader(userCacheRoot, incrementalCache), ) : BuildTask { @@ -53,27 +57,46 @@ internal class CinteropTask( val configuration = mapOf( "kotlin.version" to kotlinUserSettings.compilerVersion, "def.file" to defFile.toString(), - ) - val inputs = listOf(defFile) + "package.name" to packageName, + "compiler.opts" to compilerOpts.joinToString(" "), + "linker.opts" to linkerOpts.joinToString(" "), + ).filterValues { it != null } as Map + val inputs = listOf(defFile) + linkerOpts.map { module.source.moduleDir.resolve(it) } 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, - ) + val nativeCompiler = + downloadNativeCompiler(kotlinUserSettings.compilerVersion, userCacheRoot, JdkProvider(userCacheRoot)) + val args = buildList { + add("-def") + add(defFile.toString()) + add("-o") + add(outputKLib.toString()) + add("-target") + add(platform.nameForCompiler) + if (packageName != null) { + add("-pkg") + add(packageName) + } + add("-compiler-option") + add("-I.") + compilerOpts.forEach { + add(it) + } + linkerOpts.forEach { + add("-linker-option") + add(it) + } + } logger.info("Running cinterop for '${defFile.fileName}'...") nativeCompiler.cinterop(args, module) return@execute IncrementalCache.ExecutionResult(listOf(outputKLib)) - }.outputs.singleOrNull() + }.outputFiles.singleOrNull() return Result( compiledKlib = artifact, diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt index 6f6b021ab..95cde90eb 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt @@ -109,6 +109,10 @@ internal class NativeLinkTask( .filterIsInstance() .mapNotNull { it.compiledKlib } + println("Linking native binary for ${fragments.identificationPhrase()} with compiled klibs:\n" + + compiledKLibs.sorted().joinToString("\n").prependIndent(" ") + ) + val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings() logger.debug("native link '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}") diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt index 746338c7a..c89d8f17c 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt @@ -6,7 +6,6 @@ package org.jetbrains.amper.tasks.native import org.jetbrains.amper.compilation.KotlinCompilationType import org.jetbrains.amper.compilation.singleLeafFragment -import org.jetbrains.amper.compilation.singleLeafFragment import org.jetbrains.amper.dependency.resolution.ResolutionScope import org.jetbrains.amper.frontend.AmperModule import org.jetbrains.amper.frontend.Platform @@ -44,22 +43,24 @@ fun ProjectTasksBuilder.setupNativeTasks() { .filter { it.platforms.contains(platform) && it.isTest == isTest } .singleLeafFragment() - val cinteropTasks = fragment.settings.native?.cinterop?.defs.orEmpty().map { defFile -> - // Create a unique task name for each def file. - val taskNameSuffix = defFile.substring(defFile.lastIndexOf('/') + 1) + val cinteropTasks = fragment.settings.native?.cinterop?.map { (moduleName, cinteropModule) -> + val defFile = cinteropModule.defFile ?: "resources/cinterop/$moduleName.def" val cinteropTaskName = NativeTaskType.Cinterop.getTaskName(module, platform, isTest, buildType) - .let { TaskName(it.name + "-" + taskNameSuffix) } + .let { TaskName(it.name + "-" + moduleName) } CinteropTask( module = module, platform = platform, userCacheRoot = context.userCacheRoot, taskOutputRoot = context.getTaskOutputPath(cinteropTaskName), - incrementalCache = executeOnChangedInputs, + incrementalCache = incrementalCache, taskName = cinteropTaskName, tempRoot = context.projectTempRoot, isTest = isTest, buildType = buildType, defFile = module.source.moduleDir.resolve(defFile), + packageName = cinteropModule.packageName, + compilerOpts = cinteropModule.compilerOpts, + linkerOpts = cinteropModule.linkerOpts, ).also { tasks.registerTask(it) } } @@ -79,7 +80,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { ), dependsOn = buildList { add(CommonTaskType.Dependencies.getTaskName(module, platform, isTest)) - cinteropTasks.forEach { add(it.taskName) } + cinteropTasks?.forEach { add(it.taskName) } if (isTest) { // todo (AB) : Check if this is required for test KLib compilation add(NativeTaskType.CompileKLib.getTaskName(module, platform, isTest = false, buildType)) @@ -119,7 +120,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { ), dependsOn = buildList { add(compileKLibTaskName) - cinteropTasks.forEach { add(it.taskName) } + cinteropTasks?.forEach { add(it.taskName) } add(CommonTaskType.Dependencies.getTaskName(module, platform, isTest)) if (isTest) { add(NativeTaskType.CompileKLib.getTaskName(module, platform, isTest = false, buildType)) diff --git a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt index 3092d21e0..a77a6c4a3 100644 --- a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt +++ b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt @@ -256,12 +256,21 @@ class NativeSettings : SchemaNode() { var entryPoint by nullableValue() @SchemaDoc("C/Objective-C interop settings for native targets") - val cinterop: CinteropSettings by nested() + val cinterop: Map by value(default = emptyMap()) } -class CinteropSettings : SchemaNode() { - @SchemaDoc("A list of .def files for cinterop generation.") - var defs: List by value(emptyList()) +class CinteropModule : SchemaNode() { + @SchemaDoc("Path to the .def file for cinterop generation.") + var defFile by nullableValue() + + @SchemaDoc("The package name for the generated bindings.") + var packageName by nullableValue() + + @SchemaDoc("Options to be passed to the C compiler.") + var compilerOpts: List by value(default = emptyList()) + + @SchemaDoc("Options to be passed to the linker, and C/C++ source files to be compiled.") + var linkerOpts: List by value(default = emptyList()) } class KtorSettings: SchemaNode() { diff --git a/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt b/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt index 71a188f48..4385a36f3 100644 --- a/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt +++ b/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt @@ -2,10 +2,11 @@ package org.jetbrains.amper.frontend.tree import org.jetbrains.amper.frontend.aomBuilder.BuildCtx import org.jetbrains.amper.frontend.api.DefaultTrace -import org.jetbrains.amper.frontend.contexts.DefaultCtxs +import org.jetbrains.amper.frontend.contexts.DefaultContext import org.jetbrains.amper.frontend.messages.extractPsiElementOrNull import org.jetbrains.amper.frontend.types.SchemaType import java.io.IOException +import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.relativeTo context(buildCtx: BuildCtx) @@ -15,7 +16,7 @@ internal fun MapLikeValue<*>.discoverCinteropDefs(): Merged { return buildCtx.treeMerger.mergeTrees(listOf(withDiscoveredDefs)) } -private class CinteropDiscoveryAppender() : TreeTransformer() { +private class CinteropDiscoveryAppender : TreeTransformer() { override fun visitMapValue(value: MapLikeValue): TransformResult> { val transformResult = super.visitMapValue(value) @@ -25,49 +26,73 @@ private class CinteropDiscoveryAppender() : TreeTransformer() { Removed -> return transformResult } - val moduleDir = transformedValue.trace.extractPsiElementOrNull()?.containingFile?.virtualFile?.parent ?: return transformResult + val moduleDir = transformedValue.trace.extractPsiElementOrNull()?.containingFile?.virtualFile?.parent + ?: return transformResult val cinteropDir = moduleDir.findChild("resources")?.findChild("cinterop") - if (cinteropDir == null || !cinteropDir.exists() || !cinteropDir.isDirectory) { - return transformResult - } - - val discoveredDefFiles = try { - cinteropDir.children.filter { it.name.endsWith(".def") } - } catch (e: IOException) { - return transformResult - } - if (discoveredDefFiles.isEmpty()) { - return transformResult + val discoveredDefFiles = if (cinteropDir != null && cinteropDir.exists() && cinteropDir.isDirectory) { + try { + cinteropDir.children.filter { it.name.endsWith(".def") } + } catch (e: IOException) { + // Log error? + emptyList() + } + } else { + emptyList() } val modulePath = moduleDir.toNioPath() - val discoveredDefPaths = discoveredDefFiles.map { - it.toNioPath().relativeTo(modulePath).toString().replace('\\', '/') + val discoveredModules = discoveredDefFiles.associate { + val moduleName = it.nameWithoutExtension + val defPath = it.toNioPath().relativeTo(modulePath).invariantSeparatorsPathString + val defFileProperty = MapLikeValue.Property( + "defFile", + DefaultTrace, + ScalarValue(defPath, SchemaType.StringType, DefaultTrace, listOf(DefaultContext.ReactivelySet)), + null + ) + val moduleNode = Owned( + children = listOf(defFileProperty), + type = SchemaType.MapType(SchemaType.StringType), + trace = DefaultTrace, + contexts = listOf(DefaultContext.ReactivelySet) + ) + moduleName to moduleNode } val cinteropProperty = transformedValue.children.find { it.key == "cinterop" } - val cinteropNode = cinteropProperty?.value as? MapLikeValue<*> - val defsProperty = cinteropNode?.children?.find { it.key == "defs" } - val defsNode = defsProperty?.value as? ListValue<*> - val existingDefValues = defsNode?.children - ?.mapNotNull { it.asScalar?.value?.toString() } - .orEmpty() + val explicitCinteropNode = cinteropProperty?.value as? MapLikeValue - val allDefPaths = (existingDefValues + discoveredDefPaths).distinct() + // Get explicitly defined modules + val explicitModules = explicitCinteropNode?.children + ?.filter { it.value is MapLikeValue<*> } + ?.associate { it.key to it.value as MapLikeValue } + .orEmpty() - val newDefNodes = allDefPaths.map { path -> - ScalarValue(path, SchemaType.StringType, DefaultTrace, DefaultCtxs) + // Merge discovered and explicit modules + val allModuleNames = discoveredModules.keys + explicitModules.keys + val mergedModuleProperties = allModuleNames.map { moduleName -> + val discovered = discoveredModules[moduleName] + val explicit = explicitModules[moduleName] + val mergedNode = when { + discovered != null && explicit != null -> { + // Merge properties: explicit wins + val discoveredProps = discovered.children.associateBy { it.key } + val explicitProps = explicit.children.associateBy { it.key } + val allProps = (discoveredProps + explicitProps).values.toList() + discovered.copy(children = allProps) + } + else -> discovered ?: explicit!! + } + MapLikeValue.Property(moduleName, DefaultTrace, mergedNode, null) } - val newDefsList = ListValue(newDefNodes, SchemaType.ListType(SchemaType.StringType), DefaultTrace, DefaultCtxs) - val newDefsMapProperty = MapLikeValue.Property("defs", DefaultTrace, newDefsList, defsProperty?.pType) - - val otherCinteropProperties = cinteropNode?.children?.filter { it.key != "defs" }.orEmpty() - val newCinteropChildren = otherCinteropProperties + newDefsMapProperty + if (mergedModuleProperties.isEmpty()) { + return transformResult + } - val newCinteropNode = (cinteropNode ?: Owned(emptyList(), SchemaType.MapType(SchemaType.StringType), DefaultTrace, DefaultCtxs)) - .copy(children = newCinteropChildren) + val newCinteropNode = (explicitCinteropNode ?: Owned(emptyList(), SchemaType.MapType(SchemaType.StringType), DefaultTrace, emptyList())) + .copy(children = mergedModuleProperties) val newCinteropMapProperty = MapLikeValue.Property("cinterop", DefaultTrace, newCinteropNode, cinteropProperty?.pType) val finalChildren = transformedValue.children.filter { it.key != "cinterop" } + newCinteropMapProperty diff --git a/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt b/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt index 6037c32e4..9d04097df 100644 --- a/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt +++ b/sources/frontend/schema/test/org/jetbrains/amper/frontend/aomBuilder/CinteropDiscoveryTest.kt @@ -15,7 +15,7 @@ class CinteropDiscoveryTest : BasePlatformTestCase() { private fun runTest( moduleYamlContent: String, - setup: (FrontendPathResolver) -> Unit, + setup: (FrontendPathResolver) -> Unit = {}, assertions: (NativeSettings) -> Unit ) { val pathResolver = FrontendPathResolver(project) @@ -37,7 +37,7 @@ class CinteropDiscoveryTest : BasePlatformTestCase() { } @Test - fun `cinterop defs are discovered automatically`() { + fun `cinterop module is discovered automatically`() { runTest( moduleYamlContent = """ product: @@ -53,14 +53,15 @@ class CinteropDiscoveryTest : BasePlatformTestCase() { assertions = { nativeSettings -> val cinterop = nativeSettings.cinterop assertNotNull(cinterop) - assertEquals(1, cinterop.defs.size) - assertEquals("resources/cinterop/mydef.def", cinterop.defs.single().value) + val module = cinterop.modules["mydef"] + assertNotNull(module) + assertEquals("resources/cinterop/mydef.def", module!!.defFile) } ) } @Test - fun `discovered and explicit defs are merged and deduplicated`() { + fun `discovered and explicit modules are merged`() { runTest( moduleYamlContent = """ product: @@ -69,9 +70,10 @@ class CinteropDiscoveryTest : BasePlatformTestCase() { settings@linuxX64: native: cinterop: - defs: - - resources/cinterop/discovered.def # Duplicate - - explicit.def + discovered: + linkerOpts: [ "src/c/discovered.c" ] + explicit: + defFile: "explicit.def" """.trimIndent(), setup = { myFixture.tempDirFixture.findOrCreateDir("my-module/resources/cinterop") @@ -80,9 +82,45 @@ class CinteropDiscoveryTest : BasePlatformTestCase() { assertions = { nativeSettings -> val cinterop = nativeSettings.cinterop assertNotNull(cinterop) - assertEquals(2, cinterop.defs.size) - assertTrue(cinterop.defs.any { it.value == "resources/cinterop/discovered.def" }) - assertTrue(cinterop.defs.any { it.value == "explicit.def" }) + assertEquals(2, cinterop.modules.size) + + val discoveredModule = cinterop.modules["discovered"] + assertNotNull(discoveredModule) + assertEquals("resources/cinterop/discovered.def", discoveredModule!!.defFile) + assertEquals(listOf("src/c/discovered.c"), discoveredModule.linkerOpts) + + val explicitModule = cinterop.modules["explicit"] + assertNotNull(explicitModule) + assertEquals("explicit.def", explicitModule!!.defFile) + } + ) + } + + @Test + fun `non-conventional module is configured correctly`() { + runTest( + moduleYamlContent = """ + product: + type: lib + platforms: [linuxX64] + settings@linuxX64: + native: + cinterop: + custom: + defFile: "custom/path/custom.def" + packageName: "com.example.custom" + compilerOpts: [ "-I/custom/include" ] + linkerOpts: [ "custom/path/custom.c" ] + """.trimIndent(), + assertions = { nativeSettings -> + val cinterop = nativeSettings.cinterop + assertNotNull(cinterop) + val module = cinterop.modules["custom"] + assertNotNull(module) + assertEquals("custom/path/custom.def", module!!.defFile) + assertEquals("com.example.custom", module.packageName) + assertEquals(listOf("-I/custom/include"), module.compilerOpts) + assertEquals(listOf("custom/path/custom.c"), module.linkerOpts) } ) } From 93991781ba1fdd2fb1820d1a8589d2d0befe0416 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 25 Jan 2026 18:05:10 -0300 Subject: [PATCH 3/8] cinterop support --- .../src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt | 3 ++- .../src/org/jetbrains/amper/frontend/schema/settings.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt index c89d8f17c..8e98d4cc9 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt @@ -19,6 +19,7 @@ import org.jetbrains.amper.tasks.getModuleDependencies import org.jetbrains.amper.tasks.ios.IosTaskType import org.jetbrains.amper.tasks.ios.ManageXCodeProjectTask import org.jetbrains.amper.util.BuildType +import kotlin.io.path.Path private fun isIosApp(platform: Platform, module: AmperModule) = platform.isDescendantOf(Platform.IOS) && module.type.isApplication() @@ -44,7 +45,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { .singleLeafFragment() val cinteropTasks = fragment.settings.native?.cinterop?.map { (moduleName, cinteropModule) -> - val defFile = cinteropModule.defFile ?: "resources/cinterop/$moduleName.def" + val defFile = cinteropModule.defFile ?: Path("resources/cinterop/$moduleName.def") val cinteropTaskName = NativeTaskType.Cinterop.getTaskName(module, platform, isTest, buildType) .let { TaskName(it.name + "-" + moduleName) } CinteropTask( diff --git a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt index a77a6c4a3..83481fd36 100644 --- a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt +++ b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt @@ -261,7 +261,7 @@ class NativeSettings : SchemaNode() { class CinteropModule : SchemaNode() { @SchemaDoc("Path to the .def file for cinterop generation.") - var defFile by nullableValue() + var defFile by nullableValue() @SchemaDoc("The package name for the generated bindings.") var packageName by nullableValue() From 013497104bdedfb9a0d20aa17e20d969064a34ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Douglas=20Padilha?= Date: Sun, 25 Jan 2026 20:35:18 -0300 Subject: [PATCH 4/8] Update sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt Co-authored-by: Joffrey Bion --- .../org/jetbrains/amper/compilation/KotlinNativeCompiler.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt index 62492900a..3aff77c7c 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt @@ -123,9 +123,9 @@ class KotlinNativeCompiler( } } - private suspend fun runInProcess( - toolName: String, - programArgs: List, + private suspend fun runNativeCommand( + commandName: String, + commandArgs: List, argsMode: ArgsMode, workingDir: Path = kotlinNativeHome, ): org.jetbrains.amper.processes.ProcessResult { From 0319a44e8421d0da2211e53cb68f3928112a7820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Douglas=20Padilha?= Date: Sun, 25 Jan 2026 21:09:45 -0300 Subject: [PATCH 5/8] Update sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt Co-authored-by: Joffrey Bion --- .../src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt index 95cde90eb..27bc33ceb 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeLinkTask.kt @@ -109,13 +109,10 @@ internal class NativeLinkTask( .filterIsInstance() .mapNotNull { it.compiledKlib } - println("Linking native binary for ${fragments.identificationPhrase()} with compiled klibs:\n" + - compiledKLibs.sorted().joinToString("\n").prependIndent(" ") - ) - val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings() - logger.debug("native link '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}") + logger.debug("Linking native binary for ${fragments.identificationPhrase()} with compiled klibs:\n" + + compiledKLibs.sorted().joinToString("\n").prependIndent(" ")) val entryPoints = if (module.type.isApplication()) { fragments.mapNotNull { it.settings.native?.entryPoint }.distinct() From 99ec6dd3aefdcf53b63293f5f1f07e5a04d91ca6 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 25 Jan 2026 22:05:04 -0300 Subject: [PATCH 6/8] cinterop support --- .../amper/compilation/KotlinNativeCompiler.kt | 16 ++++++++-------- .../jetbrains/amper/tasks/native/CinteropTask.kt | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt index 3aff77c7c..eaea4bcb8 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt @@ -17,6 +17,7 @@ import org.jetbrains.amper.jdk.provisioning.Jdk import org.jetbrains.amper.jdk.provisioning.JdkProvider import org.jetbrains.amper.processes.ArgsMode import org.jetbrains.amper.processes.LoggingProcessOutputListener +import org.jetbrains.amper.processes.ProcessResult import org.jetbrains.amper.processes.runJava import org.jetbrains.amper.telemetry.setListAttribute import org.jetbrains.amper.telemetry.use @@ -83,7 +84,7 @@ class KotlinNativeCompiler( logger.debug("konanc ${ShellQuoting.quoteArgumentsPosixShellWay(args)}") withKotlinCompilerArgFile(args, tempRoot) { argFile -> - val result = runInProcess("konanc", listOf("@$argFile"), ArgsMode.ArgFile(tempRoot)) + val result = runNativeCommand("konanc", listOf("@$argFile"), ArgsMode.ArgFile(tempRoot)) span.setProcessResultAttributes(result) if (result.exitCode != 0) { @@ -109,7 +110,7 @@ class KotlinNativeCompiler( .use { span -> logger.debug("cinterop ${ShellQuoting.quoteArgumentsPosixShellWay(args)}") - val result = runInProcess("cinterop", args, ArgsMode.CommandLine, module.source.moduleDir) + val result = runNativeCommand("cinterop", args, ArgsMode.CommandLine) span.setProcessResultAttributes(result) if (result.exitCode != 0) { @@ -126,24 +127,23 @@ class KotlinNativeCompiler( private suspend fun runNativeCommand( commandName: String, commandArgs: List, - argsMode: ArgsMode, - workingDir: Path = kotlinNativeHome, - ): org.jetbrains.amper.processes.ProcessResult { + argsMode: ArgsMode + ): 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, + workingDir = kotlinNativeHome, 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 /bin/run_konan + programArgs = listOf(commandName) + commandArgs, argsMode = argsMode, + // JVM args partially copied from /bin/run_konan jvmArgs = listOf( "-ea", "-XX:TieredStopAtLevel=1", diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt index 3cb399a8d..8c3809b4f 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt @@ -82,7 +82,7 @@ internal class CinteropTask( add(packageName) } add("-compiler-option") - add("-I.") + add("-I${module.source.moduleDir}") compilerOpts.forEach { add(it) } From 70f900150fe228015fdbffe2d568a0e8d2cc0c93 Mon Sep 17 00:00:00 2001 From: Leo Date: Sat, 31 Jan 2026 00:27:06 -0300 Subject: [PATCH 7/8] cinterop support --- examples/native-cinterop/module.yaml | 3 +- .../resources/cinterop/hello.def | 3 +- .../jetbrains/amper/samples/cinterop/main.kt | 1 - .../amper/compilation/KotlinNativeCompiler.kt | 9 +- .../amper/tasks/native/CinteropTask.kt | 20 ++-- .../tasks/native/NativeCompileKlibTask.kt | 8 +- .../amper/tasks/native/taskBuilderNative.kt | 7 +- .../amper/frontend/schema/settings.kt | 12 +-- .../amper/frontend/tree/CinteropDiscovery.kt | 102 ------------------ 9 files changed, 33 insertions(+), 132 deletions(-) delete mode 100644 sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt diff --git a/examples/native-cinterop/module.yaml b/examples/native-cinterop/module.yaml index f2524f60d..ccca892f5 100644 --- a/examples/native-cinterop/module.yaml +++ b/examples/native-cinterop/module.yaml @@ -7,4 +7,5 @@ settings: entryPoint: 'org.jetbrains.amper.samples.cinterop.main' cinterop: hello: - defFile: 'resources/cinterop/hello.def' \ No newline at end of file + defFile: 'resources/cinterop/hello.def' + packageName: 'org.jetbrains.amper.samples.cinterop.hello' \ No newline at end of file diff --git a/examples/native-cinterop/resources/cinterop/hello.def b/examples/native-cinterop/resources/cinterop/hello.def index 24166684b..38e696dcc 100644 --- a/examples/native-cinterop/resources/cinterop/hello.def +++ b/examples/native-cinterop/resources/cinterop/hello.def @@ -1,2 +1 @@ -headers = src/c/hello.c -package = org.jetbrains.amper.samples.cinterop.hello \ No newline at end of file +headers = src/c/hello.c \ No newline at end of file diff --git a/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt index 79667fc6d..93c88f368 100644 --- a/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt +++ b/examples/native-cinterop/src/kotlin/org/jetbrains/amper/samples/cinterop/main.kt @@ -3,7 +3,6 @@ package org.jetbrains.amper.samples.cinterop import kotlinx.cinterop.* import org.jetbrains.amper.samples.cinterop.hello.* // Import the generated bindings -@kotlinx.cinterop.ExperimentalForeignApi fun main() { println("Hello, John Doe from Kotlin!") sayHello("John Doe") diff --git a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt index 887e83941..8a246a516 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/compilation/KotlinNativeCompiler.kt @@ -73,7 +73,7 @@ class KotlinNativeCompiler( logger.debug("konanc ${ShellQuoting.quoteArgumentsPosixShellWay(args)}") withKotlinCompilerArgFile(args, tempRoot) { argFile -> - val result = runNativeCommand("konanc", listOf("@$argFile"), ArgsMode.ArgFile(tempRoot)) + val result = processRunner.runNativeCommand("konanc", listOf("@$argFile"), ArgsMode.ArgFile(tempRoot)) // 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. span.setProcessResultAttributes(result) @@ -91,6 +91,7 @@ class KotlinNativeCompiler( } suspend fun cinterop( + processRunner: ProcessRunner, args: List, module: AmperModule, ) { @@ -101,7 +102,7 @@ class KotlinNativeCompiler( .use { span -> logger.debug("cinterop ${ShellQuoting.quoteArgumentsPosixShellWay(args)}") - val result = runNativeCommand("cinterop", args, ArgsMode.CommandLine) + val result = processRunner.runNativeCommand("cinterop", args, ArgsMode.CommandLine) span.setProcessResultAttributes(result) if (result.exitCode != 0) { @@ -115,7 +116,7 @@ class KotlinNativeCompiler( } } - private suspend fun runNativeCommand( + private suspend fun ProcessRunner.runNativeCommand( commandName: String, commandArgs: List, argsMode: ArgsMode @@ -125,7 +126,7 @@ class KotlinNativeCompiler( // 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 processRunner.runJava( + return runJava( jdk = jdk, workingDir = kotlinNativeHome, mainClass = "org.jetbrains.kotlin.cli.utilities.MainKt", diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt index 8c3809b4f..a7eaed912 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/CinteropTask.kt @@ -4,8 +4,7 @@ package org.jetbrains.amper.tasks.native -import org.jetbrains.amper.cli.AmperProjectTempRoot -import org.jetbrains.amper.compilation.KotlinArtifactsDownloader +import org.jetbrains.amper.ProcessRunner import org.jetbrains.amper.compilation.downloadNativeCompiler import org.jetbrains.amper.compilation.serializableKotlinSettings import org.jetbrains.amper.core.AmperUserCacheRoot @@ -20,6 +19,7 @@ import org.jetbrains.amper.incrementalcache.IncrementalCache import org.jetbrains.amper.jdk.provisioning.JdkProvider import org.jetbrains.amper.tasks.TaskOutputRoot import org.jetbrains.amper.tasks.TaskResult +import org.jetbrains.amper.tasks.artifacts.ArtifactTaskBase import org.jetbrains.amper.util.BuildType import org.slf4j.LoggerFactory import java.nio.file.Path @@ -34,16 +34,15 @@ internal class CinteropTask( 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 packageName: String?, private val compilerOpts: List, private val linkerOpts: List, - private val kotlinArtifactsDownloader: KotlinArtifactsDownloader = - KotlinArtifactsDownloader(userCacheRoot, incrementalCache), -) : BuildTask { + private val jdkProvider: JdkProvider, + private val processRunner: ProcessRunner, +): ArtifactTaskBase(), BuildTask { init { require(platform.isLeaf) require(platform.isDescendantOf(Platform.NATIVE)) @@ -59,8 +58,8 @@ internal class CinteropTask( "def.file" to defFile.toString(), "package.name" to packageName, "compiler.opts" to compilerOpts.joinToString(" "), - "linker.opts" to linkerOpts.joinToString(" "), - ).filterValues { it != null } as Map + "linker.opts" to linkerOpts.joinToString(" ") + ).mapNotNull { (k, v) -> v?.let { k to it } }.toMap() val inputs = listOf(defFile) + linkerOpts.map { module.source.moduleDir.resolve(it) } val artifact = incrementalCache.execute(taskName.name, configuration, inputs) { @@ -68,8 +67,7 @@ internal class CinteropTask( val outputKLib = taskOutputRoot.path.resolve(defFile.toFile().nameWithoutExtension + ".klib") - val nativeCompiler = - downloadNativeCompiler(kotlinUserSettings.compilerVersion, userCacheRoot, JdkProvider(userCacheRoot)) + val nativeCompiler = downloadNativeCompiler(kotlinUserSettings.compilerVersion, userCacheRoot, jdkProvider) val args = buildList { add("-def") add(defFile.toString()) @@ -93,7 +91,7 @@ internal class CinteropTask( } logger.info("Running cinterop for '${defFile.fileName}'...") - nativeCompiler.cinterop(args, module) + nativeCompiler.cinterop(processRunner, args, module) return@execute IncrementalCache.ExecutionResult(listOf(outputKLib)) }.outputFiles.singleOrNull() diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt index 2b0c5260a..7d3402718 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/NativeCompileKlibTask.kt @@ -4,10 +4,10 @@ package org.jetbrains.amper.tasks.native +import com.intellij.util.applyIf import kotlinx.serialization.json.Json import org.jetbrains.amper.ProcessRunner 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 @@ -103,7 +103,11 @@ internal class NativeCompileKlibTask( .filterIsInstance() .mapNotNull { it.compiledKlib } - val kotlinUserSettings = fragments.singleLeafFragment().serializableKotlinSettings() + val kotlinUserSettings = fragments.singleLeafFragment() + .serializableKotlinSettings() + .applyIf(cinteropKlibs.isNotEmpty()) { + copy(optIns = optIns + "kotlinx.cinterop.ExperimentalForeignApi") + } logger.debug("native compile klib '${module.userReadableName}' -- ${fragments.joinToString(" ") { it.name }}") diff --git a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt index ca0752236..5af0072d6 100644 --- a/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt +++ b/sources/amper-cli/src/org/jetbrains/amper/tasks/native/taskBuilderNative.kt @@ -46,7 +46,7 @@ fun ProjectTasksBuilder.setupNativeTasks() { .singleLeafFragment() val cinteropTasks = fragment.settings.native?.cinterop?.map { (moduleName, cinteropModule) -> - val defFile = cinteropModule.defFile ?: Path("resources/cinterop/$moduleName.def") + val defFile = cinteropModule.defFile val cinteropTaskName = NativeTaskType.Cinterop.getTaskName(module, platform, isTest, buildType) .let { TaskName(it.name + "-" + moduleName) } CinteropTask( @@ -54,15 +54,16 @@ fun ProjectTasksBuilder.setupNativeTasks() { platform = platform, userCacheRoot = context.userCacheRoot, taskOutputRoot = context.getTaskOutputPath(cinteropTaskName), - incrementalCache = incrementalCache, + incrementalCache = context.incrementalCache, taskName = cinteropTaskName, - tempRoot = context.projectTempRoot, isTest = isTest, buildType = buildType, + jdkProvider = context.jdkProvider, defFile = module.source.moduleDir.resolve(defFile), packageName = cinteropModule.packageName, compilerOpts = cinteropModule.compilerOpts, linkerOpts = cinteropModule.linkerOpts, + processRunner = context.processRunner ).also { tasks.registerTask(it) } } diff --git a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt index 0000f5de4..6d76e2fbf 100644 --- a/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt +++ b/sources/frontend-api/src/org/jetbrains/amper/frontend/schema/settings.kt @@ -255,7 +255,7 @@ class NativeSettings : SchemaNode() { // TODO other options from NativeApplicationPart @SchemaDoc("The fully-qualified name of the application's entry point function") - var entryPoint by nullableValue() + val entryPoint by nullableValue() @SchemaDoc("C/Objective-C interop settings for native targets") val cinterop: Map by value(default = emptyMap()) @@ -263,16 +263,16 @@ class NativeSettings : SchemaNode() { class CinteropModule : SchemaNode() { @SchemaDoc("Path to the .def file for cinterop generation.") - var defFile by nullableValue() + val defFile by value() @SchemaDoc("The package name for the generated bindings.") - var packageName by nullableValue() + val packageName by nullableValue() @SchemaDoc("Options to be passed to the C compiler.") - var compilerOpts: List by value(default = emptyList()) + val compilerOpts: List by value(default = emptyList()) @SchemaDoc("Options to be passed to the linker, and C/C++ source files to be compiled.") - var linkerOpts: List by value(default = emptyList()) + val linkerOpts: List by value(default = emptyList()) } class KtorSettings: SchemaNode() { @@ -302,7 +302,7 @@ class SpringBootSettings: SchemaNode() { } class LombokSettings: SchemaNode() { - + @Shorthand @SchemaDoc("Enables Lombok") val enabled by value(default = false) diff --git a/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt b/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt deleted file mode 100644 index 4385a36f3..000000000 --- a/sources/frontend/schema/src/org/jetbrains/amper/frontend/tree/CinteropDiscovery.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.jetbrains.amper.frontend.tree - -import org.jetbrains.amper.frontend.aomBuilder.BuildCtx -import org.jetbrains.amper.frontend.api.DefaultTrace -import org.jetbrains.amper.frontend.contexts.DefaultContext -import org.jetbrains.amper.frontend.messages.extractPsiElementOrNull -import org.jetbrains.amper.frontend.types.SchemaType -import java.io.IOException -import kotlin.io.path.invariantSeparatorsPathString -import kotlin.io.path.relativeTo - -context(buildCtx: BuildCtx) -internal fun MapLikeValue<*>.discoverCinteropDefs(): Merged { - val discoveryAppender = CinteropDiscoveryAppender() - val withDiscoveredDefs = discoveryAppender.transform(this) as MapLikeValue<*> - return buildCtx.treeMerger.mergeTrees(listOf(withDiscoveredDefs)) -} - -private class CinteropDiscoveryAppender : TreeTransformer() { - - override fun visitMapValue(value: MapLikeValue): TransformResult> { - val transformResult = super.visitMapValue(value) - val transformedValue = when (transformResult) { - is Changed -> transformResult.value - NotChanged -> value - Removed -> return transformResult - } - - val moduleDir = transformedValue.trace.extractPsiElementOrNull()?.containingFile?.virtualFile?.parent - ?: return transformResult - val cinteropDir = moduleDir.findChild("resources")?.findChild("cinterop") - - val discoveredDefFiles = if (cinteropDir != null && cinteropDir.exists() && cinteropDir.isDirectory) { - try { - cinteropDir.children.filter { it.name.endsWith(".def") } - } catch (e: IOException) { - // Log error? - emptyList() - } - } else { - emptyList() - } - - val modulePath = moduleDir.toNioPath() - val discoveredModules = discoveredDefFiles.associate { - val moduleName = it.nameWithoutExtension - val defPath = it.toNioPath().relativeTo(modulePath).invariantSeparatorsPathString - val defFileProperty = MapLikeValue.Property( - "defFile", - DefaultTrace, - ScalarValue(defPath, SchemaType.StringType, DefaultTrace, listOf(DefaultContext.ReactivelySet)), - null - ) - val moduleNode = Owned( - children = listOf(defFileProperty), - type = SchemaType.MapType(SchemaType.StringType), - trace = DefaultTrace, - contexts = listOf(DefaultContext.ReactivelySet) - ) - moduleName to moduleNode - } - - val cinteropProperty = transformedValue.children.find { it.key == "cinterop" } - val explicitCinteropNode = cinteropProperty?.value as? MapLikeValue - - // Get explicitly defined modules - val explicitModules = explicitCinteropNode?.children - ?.filter { it.value is MapLikeValue<*> } - ?.associate { it.key to it.value as MapLikeValue } - .orEmpty() - - // Merge discovered and explicit modules - val allModuleNames = discoveredModules.keys + explicitModules.keys - val mergedModuleProperties = allModuleNames.map { moduleName -> - val discovered = discoveredModules[moduleName] - val explicit = explicitModules[moduleName] - val mergedNode = when { - discovered != null && explicit != null -> { - // Merge properties: explicit wins - val discoveredProps = discovered.children.associateBy { it.key } - val explicitProps = explicit.children.associateBy { it.key } - val allProps = (discoveredProps + explicitProps).values.toList() - discovered.copy(children = allProps) - } - else -> discovered ?: explicit!! - } - MapLikeValue.Property(moduleName, DefaultTrace, mergedNode, null) - } - - if (mergedModuleProperties.isEmpty()) { - return transformResult - } - - val newCinteropNode = (explicitCinteropNode ?: Owned(emptyList(), SchemaType.MapType(SchemaType.StringType), DefaultTrace, emptyList())) - .copy(children = mergedModuleProperties) - val newCinteropMapProperty = MapLikeValue.Property("cinterop", DefaultTrace, newCinteropNode, cinteropProperty?.pType) - - val finalChildren = transformedValue.children.filter { it.key != "cinterop" } + newCinteropMapProperty - - return Changed(transformedValue.copy(children = finalChildren)) - } -} \ No newline at end of file From 8a1a7dd03519ea30e43ed7048417dafa6f49e486 Mon Sep 17 00:00:00 2001 From: Leo Date: Sat, 31 Jan 2026 00:41:56 -0300 Subject: [PATCH 8/8] cinterop support --- docs/src/reference/module.md | 27 +++++++--- .../src/user-guide/advanced/native-interop.md | 51 +++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 docs/src/user-guide/advanced/native-interop.md diff --git a/docs/src/reference/module.md b/docs/src/reference/module.md index 06cabec65..236dd8175 100644 --- a/docs/src/reference/module.md +++ b/docs/src/reference/module.md @@ -668,15 +668,18 @@ settings: entryPoint: com.example.MainKt.main ``` -##### 'settings.cinterop' +##### 'settings.native.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) | +Each cinterop module is defined by a name and supports the following attributes: -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. +| Attribute | Description | +|----------------|-------------------------------------------| +| `defFile` | Path to the Kotlin/Native definition (`.def`) file. | +| `packageName` | The Kotlin package name for the generated bindings. | +| `compilerOpts` | A list of flags to be passed to the C compiler (e.g., `-I/path/to/includes`). | +| `linkerOpts` | A list of C/C++ source files (`.c`, `.cpp`) to be compiled and/or flags to be passed to the linker (e.g., `-lm`). | Example: @@ -685,8 +688,18 @@ Example: settings: native: cinterop: - defs: - - src/native/cinterop/libfoo.def + hello: + defFile: cinterop/hello.def + packageName: com.example.hello + linkerOpts: + - cinterop/hello.c + libfoo: + defFile: external/libfoo/api.def + packageName: com.example.libfoo + compilerOpts: + - -I/usr/local/include + linkerOpts: + - -lfoo ``` ### `settings.springBoot` diff --git a/docs/src/user-guide/advanced/native-interop.md b/docs/src/user-guide/advanced/native-interop.md new file mode 100644 index 000000000..214263b9f --- /dev/null +++ b/docs/src/user-guide/advanced/native-interop.md @@ -0,0 +1,51 @@ +# Native Interoperability with C + +Amper provides a powerful and flexible way to interoperate with C and Objective-C code in your native projects using the Kotlin/Native `cinterop` tool. This guide explains how to configure and use this feature. + +## How it Works + +The `cinterop` process in Amper allows you to create Kotlin bindings for C libraries by defining modules in your `module.yaml` file. +Each module references a `.def` file that describes the C API and can include additional compiler and linker options. + +## Configuration + +The `cinterop` settings are configured under `settings.native.cinterop` in your `module.yaml`. + +```yaml +settings: + native: + cinterop: + : + defFile: + packageName: + compilerOpts: + - + linkerOpts: + - + - +``` + +### API Details + +| Option | Description | +| :--- | :--- | +| `defFile` | Path to the Kotlin/Native definition (`.def`) file. **Optional** if the module is discovered by convention. | +| `packageName` | The Kotlin package name for the generated bindings. | +| `compilerOpts`| A list of flags to be passed to the C compiler (e.g., `-I/path/to/includes`). | +| `linkerOpts` | A list of C/C++ source files (`.c`, `.cpp`) to be compiled and/or flags to be passed to the linker (e.g., `-lm`). | + +### Example + +To create a cinterop module, define it in your module.yaml with the path to your .def file: + +**`module.yaml`:** +```yaml +settings: + native: + cinterop: + # Define a completely new 'bar' module + bar: + defFile: external/libbar/api.def + packageName: com.example.bar + linkerOpts: + - external/libbar/bar.c \ No newline at end of file