[Swift Export] KT-63516

Add a sir-compiler-bridge module that adds Kotlin and C bridges
for SIR. As SIR is still WIP, the module accepts `BridgeRequest`
instances instead of SIR elements.
This commit is contained in:
Sergey Bogolepov
2023-11-16 15:58:43 +02:00
committed by Space Team
parent 7c15e3f229
commit fe9ab0a1fc
36 changed files with 701 additions and 0 deletions
@@ -10,6 +10,7 @@ sourceSets {
dependencies {
testApi(projectTests(":native:swift:sir-analysis-api"))
testApi(projectTests(":native:swift:sir-compiler-bridge"))
testImplementation(projectTests(":generators:test-generator"))
testRuntimeOnly(projectTests(":analysis:analysis-test-framework"))
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.generators.tests.native.swift.sir.analysis.api
import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5
import org.jetbrains.kotlin.sir.analysisapi.AbstractKotlinSirContextTest
import org.jetbrains.kotlin.sir.bridge.AbstractKotlinSirBridgeTest
fun main() {
@@ -22,5 +23,15 @@ fun main() {
model("", pattern = "^([^_](.+)).kt$", recursive = false)
}
}
testGroup(
"native/swift/sir-compiler-bridge/tests-gen/",
"native/swift/sir-compiler-bridge/testData"
) {
testClass<AbstractKotlinSirBridgeTest>(
suiteTestClassName = "SirCompilerBridgeTestGenerated"
) {
model("", extension = null, recursive = false)
}
}
}
}
@@ -0,0 +1,18 @@
# Swift IR to Kotlin compiler bridges
This module is responsible for the generation of "glue" code between Swift Export and Kotlin compiler.
Swift does not "know" how to call Kotlin directly. But it has a C/C++/Objective-C interop, and we can leverage it for Swift Export.
We do the following:
1. Generate a Kotlin wrapper around the original Kotlin function with a primitive C-like ABI.
2. Generate a C header with C declarations of these wrappers.
3. Call these C functions from Swift functions.
That's it! This approach also has the following advantages:
1. All nuances of Kotlin compiler and Kotlin/Native runtime are hidden behind wrappers with a simple ABI.
So Swift Export (mostly) does not need to care about how Kotlin/Native works under the hood.
2. The contract between Swift Export and Kotlin compiler is well-defined in the form of a set of compiler intrinsics.
For the sake of simplicity, Kotlin wrappers are generated as Kotlin sources,
but later we can switch to direct generation of Kotlin Backend IR.
@@ -0,0 +1,31 @@
plugins {
kotlin("jvm")
}
description = "SIR to Kotlin bindings generator"
dependencies {
compileOnly(kotlinStdlib())
testApi(platform(libs.junit.bom))
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.junit.jupiter.api)
testImplementation(projectTests(":compiler:tests-common"))
testImplementation(projectTests(":compiler:tests-common-new"))
}
sourceSets {
"main" { projectDefault() }
"test" {
projectDefault()
generatedTestDir()
}
}
projectTest(jUnitMode = JUnitMode.JUnit5) {
workingDir = rootDir
useJUnitPlatform { }
}
testsJar()
@@ -0,0 +1,108 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.sir.bridge
import org.jetbrains.kotlin.sir.bridge.impl.BridgeGeneratorImpl
import org.jetbrains.kotlin.sir.bridge.impl.CBridgePrinter
import org.jetbrains.kotlin.sir.bridge.impl.KotlinBridgePrinter
/**
* Description of a Kotlin function for which we are creating the bridge.
*
* TODO: Replace with SIR once KT-63266 is merged.
*
* @param fqName Fully qualified name of the function we are bridging.
* @param bridgeName C name of the bridge
*/
class BridgeRequest(
val fqName: List<String>,
val bridgeName: String,
val parameters: List<Parameter>,
val returnType: Type,
) {
class Parameter(
val name: String,
val type: Type,
)
sealed interface Type {
data object Boolean : Type
data object Byte : Type
data object Short : Type
data object Int : Type
data object Long : Type
data object UByte : Type
data object UShort : Type
data object UInt : Type
data object ULong : Type
}
}
/**
* A C-like wrapper around some Kotlin function.
* Abstracts away all nuances of Kotlin compiler ABI, making it possible
* to call Kotlin code from other environments.
*/
class FunctionBridge(
val kotlinFunctionBridge: KotlinFunctionBridge,
val cDeclarationBridge: CFunctionBridge,
)
/**
* C part of [FunctionBridgeImpl].
*
* @param lines with function declaration.
* @param headerDependencies required headers.
*/
class CFunctionBridge(
val lines: List<String>,
val headerDependencies: List<String>,
)
/**
* Kotlin part of [FunctionBridgeImpl].
*
* @param lines definition of Kotlin bridge.
* @param packageDependencies required packages to make sources compile.
*/
class KotlinFunctionBridge(
val lines: List<String>,
val packageDependencies: List<String>,
)
/**
* Generates [FunctionBridge] that binds SIR function to its Kotlin origin.
*/
interface BridgeGenerator {
fun generate(request: BridgeRequest): FunctionBridge
}
/**
* A common interface for classes that serialize [FunctionBridge] in some form.
*/
interface BridgePrinter {
/**
* Populate printer with an additional [bridge].
*/
fun add(bridge: FunctionBridge)
/**
* Outputs the aggregated result.
*/
fun print(): Sequence<String>
}
fun createBridgeGenerator(): BridgeGenerator =
BridgeGeneratorImpl()
fun createCBridgePrinter(): BridgePrinter =
CBridgePrinter()
fun createKotlinBridgePrinter(): BridgePrinter =
KotlinBridgePrinter()
@@ -0,0 +1,140 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.sir.bridge.impl
import org.jetbrains.kotlin.sir.bridge.*
private const val exportAnnotationFqName = "kotlin.native.internal.ExportForCppRuntime"
private const val stdintHeader = "stdint.h"
internal class BridgeGeneratorImpl : BridgeGenerator {
override fun generate(request: BridgeRequest): FunctionBridge {
val (kotlinReturnType, cReturnType) = bridgeType(request.returnType)
val parameterBridges = request.parameters.map { bridgeParameter(it) }
val cDeclaration = createCDeclaration(request.bridgeName, cReturnType, parameterBridges.map { it.c })
val kotlinBridge = createKotlinBridge(request.bridgeName, request.fqName, kotlinReturnType, parameterBridges.map { it.kotlin })
return FunctionBridge(
KotlinFunctionBridge(kotlinBridge, listOf(exportAnnotationFqName)),
CFunctionBridge(cDeclaration, listOf(stdintHeader))
)
}
}
private fun createKotlinBridge(
bridgeName: String,
functionFqName: List<String>,
returnType: KotlinType,
parameterBridges: List<KotlinBridgeParameter>
): List<String> {
val declaration = createKotlinDeclarationSignature(bridgeName, returnType, parameterBridges)
val annotation = "@${exportAnnotationFqName.substringAfterLast('.')}(\"${bridgeName}\")"
val resultName = "result"
val callSite = createCallSite(functionFqName, parameterBridges.map { it.name }, resultName)
return """
$annotation
$declaration {
$callSite
return $resultName
}
""".trimIndent().lines()
}
private fun createCallSite(functionFqName: List<String>, parameterNames: List<String>, resultName: String): String {
val functionCall = "${functionFqName.joinToString(separator = ".")}(${parameterNames.joinToString(", ")})"
return "val $resultName = $functionCall"
}
private fun createKotlinDeclarationSignature(bridgeName: String, returnType: KotlinType, parameters: List<KotlinBridgeParameter>): String {
return "public fun $bridgeName(${
parameters.joinToString(
separator = ", ",
transform = { "${it.name}: ${it.type.repr}" }
)
}): ${returnType.repr}"
}
private fun createCDeclaration(bridgeName: String, returnType: CType, parameters: List<CBridgeParameter>): List<String> {
val cParameters = parameters.joinToString(separator = ", ", transform = { "${it.type.repr} ${it.name}" })
val declaration = "${returnType.repr} $bridgeName($cParameters);"
return listOf(declaration)
}
private fun bridgeType(type: BridgeRequest.Type): Pair<KotlinType, CType> {
return when (type) {
BridgeRequest.Type.Boolean -> (KotlinType.Boolean to CType.Bool)
BridgeRequest.Type.Byte -> (KotlinType.Byte to CType.Int8)
BridgeRequest.Type.Int -> (KotlinType.Int to CType.Int32)
BridgeRequest.Type.Long -> (KotlinType.Long to CType.Int64)
BridgeRequest.Type.Short -> (KotlinType.Short to CType.Int16)
BridgeRequest.Type.UByte -> (KotlinType.UByte to CType.UInt8)
BridgeRequest.Type.UInt -> (KotlinType.UInt to CType.UInt32)
BridgeRequest.Type.ULong -> (KotlinType.ULong to CType.UInt64)
BridgeRequest.Type.UShort -> (KotlinType.UShort to CType.UInt16)
}
}
private fun bridgeParameter(request: BridgeRequest.Parameter): BridgeParameter {
val bridgeParameterName = createBridgeParameterName(request.name)
val (kotlinType, cType) = bridgeType(request.type)
return BridgeParameter(
KotlinBridgeParameter(bridgeParameterName, kotlinType),
CBridgeParameter(bridgeParameterName, cType)
)
}
fun createBridgeParameterName(kotlinName: String): String {
// TODO: Post-process because C has stricter naming conventions.
return kotlinName
}
internal data class BridgeParameter(
val kotlin: KotlinBridgeParameter,
val c: CBridgeParameter,
)
internal data class CBridgeParameter(
val name: String,
val type: CType,
)
enum class CType(val repr: String) {
Bool("_Bool"),
Int8("int8_t"),
Int16("int16_t"),
Int32("int32_t"),
Int64("int64_t"),
UInt8("uint8_t"),
UInt16("uint16_t"),
UInt32("uint32_t"),
UInt64("uint64_t"),
}
internal data class KotlinBridgeParameter(
val name: String,
val type: KotlinType,
)
internal enum class KotlinType(val repr: String) {
Boolean("Boolean"),
Byte("Byte"),
Short("Short"),
Int("Int"),
Long("Long"),
UByte("UByte"),
UShort("UShort"),
UInt("UInt"),
ULong("ULong"),
}
@@ -0,0 +1,34 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.sir.bridge.impl
import org.jetbrains.kotlin.sir.bridge.BridgePrinter
import org.jetbrains.kotlin.sir.bridge.FunctionBridge
internal class CBridgePrinter : BridgePrinter {
private val includes = mutableSetOf<String>()
private val functions = mutableListOf<List<String>>()
override fun add(bridge: FunctionBridge) {
functions += bridge.cDeclarationBridge.lines
includes += bridge.cDeclarationBridge.headerDependencies
}
override fun print(): Sequence<String> = sequence {
if (includes.isNotEmpty()) {
includes.forEach {
yield("#include <$it>")
}
yield("")
}
functions.forEach { functionLines ->
yieldAll(functionLines)
yield("")
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.sir.bridge.impl
import org.jetbrains.kotlin.sir.bridge.BridgePrinter
import org.jetbrains.kotlin.sir.bridge.FunctionBridge
internal class KotlinBridgePrinter : BridgePrinter {
private val imports = mutableSetOf<String>()
private val functions = mutableListOf<List<String>>()
override fun add(bridge: FunctionBridge) {
functions += bridge.kotlinFunctionBridge.lines
imports += bridge.kotlinFunctionBridge.packageDependencies
}
override fun print(): Sequence<String> = sequence {
if (imports.isNotEmpty()) {
imports.forEach {
yield("import $it")
}
yield("")
}
functions.forEach { functionLines ->
yieldAll(functionLines)
yield("")
}
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.sir.bridge
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.test.services.JUnit5Assertions
import org.jetbrains.kotlin.test.util.KtTestUtil
import java.io.File
import java.util.*
abstract class AbstractKotlinSirBridgeTest {
protected fun runTest(@TestDataFile testPath: String) {
val testPathFull = File(KtTestUtil.getHomeDirectory()).resolve(testPath)
val expectedKotlinSrc = testPathFull.resolve("expected.kt")
val expectedCHeader = testPathFull.resolve("expected.h")
val requests = parseRequestsFromTestDir(testPathFull)
val generator = createBridgeGenerator()
val kotlinBridgePrinter = createKotlinBridgePrinter()
val cBridgePrinter = createCBridgePrinter()
requests.forEach { request ->
val bridge = generator.generate(request)
kotlinBridgePrinter.add(bridge)
cBridgePrinter.add(bridge)
}
val actualKotlinSrc = kotlinBridgePrinter.print().joinToString(separator = lineSeparator)
val actualHeader = cBridgePrinter.print().joinToString(separator = lineSeparator)
JUnit5Assertions.assertEqualsToFile(expectedCHeader, actualHeader)
JUnit5Assertions.assertEqualsToFile(expectedKotlinSrc, actualKotlinSrc)
}
}
private val lineSeparator: String = System.getProperty("line.separator")
private fun parseRequestsFromTestDir(testDir: File): List<BridgeRequest> =
testDir.listFiles()
?.filter { it.extension == "properties" && it.name.startsWith("request") }
?.map { readRequestFromFile(it) }
?.sortedBy { it.bridgeName }
?: emptyList()
private fun parseType(typeName: String): BridgeRequest.Type {
return when (typeName.lowercase()) {
"boolean" -> BridgeRequest.Type.Boolean
"byte" -> BridgeRequest.Type.Byte
"short" -> BridgeRequest.Type.Short
"int" -> BridgeRequest.Type.Int
"long" -> BridgeRequest.Type.Long
"ubyte" -> BridgeRequest.Type.UByte
"ushort" -> BridgeRequest.Type.UShort
"uint" -> BridgeRequest.Type.UInt
"ulong" -> BridgeRequest.Type.ULong
else -> error("Unknown type: $typeName")
}
}
private fun readRequestFromFile(file: File): BridgeRequest {
val properties = Properties()
file.bufferedReader().use(properties::load)
val fqName = properties.getProperty("fqName").split('.')
val bridgeName = properties.getProperty("bridgeName")
val returnType = parseType(properties.getProperty("returnType"))
val parameters = properties.getProperty("parameters").let { parametersString ->
when {
parametersString.isNullOrEmpty() -> emptyList()
else -> parametersString.split(Regex("\\s+"))
}.map {
BridgeRequest.Parameter(
name = it.substringBefore(':'),
type = parseType(it.substringAfter(':'))
)
}
}
return BridgeRequest(fqName, bridgeName, parameters, returnType)
}
@@ -0,0 +1,6 @@
#include <stdint.h>
int32_t a_b_bar_bridge(int32_t param1, int64_t param2);
_Bool a_b_foo_bridge(int32_t param1, int64_t param2);
@@ -0,0 +1,13 @@
import kotlin.native.internal.ExportForCppRuntime
@ExportForCppRuntime("a_b_bar_bridge")
public fun a_b_bar_bridge(param1: Int, param2: Long): Int {
val result = a.b.bar(param1, param2)
return result
}
@ExportForCppRuntime("a_b_foo_bridge")
public fun a_b_foo_bridge(param1: Int, param2: Long): Boolean {
val result = a.b.foo(param1, param2)
return result
}
@@ -0,0 +1,4 @@
fqName=a.b.foo
bridgeName=a_b_foo_bridge
parameters=param1:int param2:long
returnType=boolean
@@ -0,0 +1,4 @@
fqName=a.b.bar
bridgeName=a_b_bar_bridge
parameters=param1:int param2:long
returnType=int
@@ -0,0 +1,7 @@
#include <stdint.h>
int32_t a(int8_t p0, int16_t p1, int32_t p2, int64_t p3);
uint32_t b(uint8_t p0, uint16_t p1, uint32_t p2, uint64_t p3);
_Bool c(_Bool p0);
@@ -0,0 +1,20 @@
import kotlin.native.internal.ExportForCppRuntime
@ExportForCppRuntime("a")
public fun a(p0: Byte, p1: Short, p2: Int, p3: Long): Int {
val result = pkg.a(p0, p1, p2, p3)
return result
}
@ExportForCppRuntime("b")
public fun b(p0: UByte, p1: UShort, p2: UInt, p3: ULong): UInt {
val result = pkg.b(p0, p1, p2, p3)
return result
}
@ExportForCppRuntime("c")
public fun c(p0: Boolean): Boolean {
val result = pkg.c(p0)
return result
}
@@ -0,0 +1,4 @@
fqName=pkg.a
bridgeName=a
returnType=int
parameters=p0:byte p1:short p2:int p3:long
@@ -0,0 +1,4 @@
fqName=pkg.b
bridgeName=b
returnType=uint
parameters=p0:ubyte p1:ushort p2:uint p3:ulong
@@ -0,0 +1,4 @@
fqName=pkg.c
bridgeName=c
returnType=boolean
parameters=p0:boolean
@@ -0,0 +1,11 @@
#include <stdint.h>
_Bool a_bridge();
int16_t b_bridge();
int32_t c_bridge();
int64_t d_bridge();
int8_t e_bridge();
@@ -0,0 +1,31 @@
import kotlin.native.internal.ExportForCppRuntime
@ExportForCppRuntime("a_bridge")
public fun a_bridge(): Boolean {
val result = a()
return result
}
@ExportForCppRuntime("b_bridge")
public fun b_bridge(): Short {
val result = b()
return result
}
@ExportForCppRuntime("c_bridge")
public fun c_bridge(): Int {
val result = c()
return result
}
@ExportForCppRuntime("d_bridge")
public fun d_bridge(): Long {
val result = d()
return result
}
@ExportForCppRuntime("e_bridge")
public fun e_bridge(): Byte {
val result = e()
return result
}
@@ -0,0 +1,3 @@
fqName=a
bridgeName=a_bridge
returnType=boolean
@@ -0,0 +1,3 @@
fqName=b
bridgeName=b_bridge
returnType=short
@@ -0,0 +1,3 @@
fqName=c
bridgeName=c_bridge
returnType=int
@@ -0,0 +1,3 @@
fqName=d
bridgeName=d_bridge
returnType=long
@@ -0,0 +1,3 @@
fqName=e
bridgeName=e_bridge
returnType=byte
@@ -0,0 +1,3 @@
#include <stdint.h>
int32_t a_b_foo_bridge(int32_t param1, int64_t param2);
@@ -0,0 +1,7 @@
import kotlin.native.internal.ExportForCppRuntime
@ExportForCppRuntime("a_b_foo_bridge")
public fun a_b_foo_bridge(param1: Int, param2: Long): Int {
val result = a.b.foo(param1, param2)
return result
}
@@ -0,0 +1,4 @@
fqName=a.b.foo
bridgeName=a_b_foo_bridge
parameters=param1:int param2:long
returnType=int
@@ -0,0 +1,9 @@
#include <stdint.h>
uint16_t b_bridge();
uint32_t c_bridge();
uint64_t d_bridge();
uint8_t e_bridge();
@@ -0,0 +1,25 @@
import kotlin.native.internal.ExportForCppRuntime
@ExportForCppRuntime("b_bridge")
public fun b_bridge(): UShort {
val result = b()
return result
}
@ExportForCppRuntime("c_bridge")
public fun c_bridge(): UInt {
val result = c()
return result
}
@ExportForCppRuntime("d_bridge")
public fun d_bridge(): ULong {
val result = d()
return result
}
@ExportForCppRuntime("e_bridge")
public fun e_bridge(): UByte {
val result = e()
return result
}
@@ -0,0 +1,3 @@
fqName=b
bridgeName=b_bridge
returnType=ushort
@@ -0,0 +1,3 @@
fqName=c
bridgeName=c_bridge
returnType=uint
@@ -0,0 +1,3 @@
fqName=d
bridgeName=d_bridge
returnType=ulong
@@ -0,0 +1,3 @@
fqName=e
bridgeName=e_bridge
returnType=ubyte
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.sir.bridge;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.native.swift.sir.analysis.api.GenerateSirAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("native/swift/sir-compiler-bridge/testData")
@TestDataPath("$PROJECT_ROOT")
public class SirCompilerBridgeTestGenerated extends AbstractKotlinSirBridgeTest {
@Test
public void testAllFilesPresentInTestData() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("native/swift/sir-compiler-bridge/testData"), Pattern.compile("^([^\\.]+)$"), null, false);
}
@Test
@TestMetadata("multiple_requests")
public void testMultiple_requests() throws Exception {
runTest("native/swift/sir-compiler-bridge/testData/multiple_requests/");
}
@Test
@TestMetadata("smoke0")
public void testSmoke0() throws Exception {
runTest("native/swift/sir-compiler-bridge/testData/smoke0/");
}
@Test
@TestMetadata("primitive_types")
public void testPrimitive_types() throws Exception {
runTest("native/swift/sir-compiler-bridge/testData/primitive_types/");
}
@Test
@TestMetadata("unsigned_primitive_types")
public void testUnsigned_primitive_types() throws Exception {
runTest("native/swift/sir-compiler-bridge/testData/unsigned_primitive_types/");
}
@Test
@TestMetadata("primitive_parameters")
public void testPrimitive_parameters() throws Exception {
runTest("native/swift/sir-compiler-bridge/testData/primitive_parameters/");
}
}
+1
View File
@@ -401,6 +401,7 @@ include ":plugins:compose-compiler-plugin:compiler",
include ":native:swift:sir",
":native:swift:sir-passes",
":native:swift:sir-analysis-api",
":native:swift:sir-compiler-bridge",
":generators:sir-analysis-api-generator"
void intellij(String imlPath) {