diff --git a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt index ac4f52cab89..a9f490e081d 100644 --- a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt +++ b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt @@ -896,6 +896,7 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean return ObjCMethod( selector, encoding, parameters, returnType, + isVariadic = clang_Cursor_isVariadic(cursor) != 0, isClass = isClass, nsConsumesSelf = hasAttribute(cursor, NS_CONSUMES_SELF), nsReturnsRetained = hasAttribute(cursor, NS_RETURNS_RETAINED), diff --git a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt index b406bc9abe5..2652ad42784 100644 --- a/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt +++ b/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt @@ -174,7 +174,7 @@ sealed class ObjCClassOrProtocol(val name: String) : ObjCContainer(), TypeDeclar data class ObjCMethod( val selector: String, val encoding: String, val parameters: List, private val returnType: Type, - val isClass: Boolean, val nsConsumesSelf: Boolean, val nsReturnsRetained: Boolean, + val isVariadic: Boolean, val isClass: Boolean, val nsConsumesSelf: Boolean, val nsReturnsRetained: Boolean, val isOptional: Boolean, val isInit: Boolean, val isExplicitlyDesignatedInitializer: Boolean ) { diff --git a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/KotlinCodeModel.kt b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/KotlinCodeModel.kt index f6574704878..4056e145272 100644 --- a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/KotlinCodeModel.kt +++ b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/KotlinCodeModel.kt @@ -321,6 +321,17 @@ abstract class KotlinFile( } -data class KotlinParameter(val name: String, val type: KotlinType) { - fun render(scope: KotlinScope) = "${name.asSimpleName()}: ${type.render(scope)}" +data class KotlinParameter( + val name: String, + val type: KotlinType, + val isVararg: Boolean, + val annotations: List +) { + fun render(scope: KotlinScope) = buildString { + annotations.forEach { append("$it ") } + if (isVararg) append("vararg ") + append(name.asSimpleName()) + append(": ") + append(type.render(scope)) + } } diff --git a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt index 20109992f8f..43be0772a28 100644 --- a/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt +++ b/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/ObjCStubs.kt @@ -25,29 +25,29 @@ private fun ObjCMethod.getKotlinParameterNames(forConstructorOrFactory: Boolean val result = mutableListOf() + fun String.mangled(): String { + var mangled = this + while (mangled in result) { + mangled = "_$mangled" + } + return mangled + } + // The names of all parameters except first must depend only on the selector: this.parameters.forEachIndexed { index, _ -> if (index > 0) { - var name = selectorParts[index] - if (name.isEmpty()) { - name = "_$index" - } - - while (name in result) { - name = "_$name" - } - - result.add(name) + val name = selectorParts[index].takeIf { it.isNotEmpty() } ?: "_$index" + result.add(name.mangled()) } } this.parameters.firstOrNull()?.let { - var name = this.getFirstKotlinParameterNameCandidate(forConstructorOrFactory) + val name = this.getFirstKotlinParameterNameCandidate(forConstructorOrFactory) + result.add(0, name.mangled()) + } - while (name in result) { - name = "_$name" - } - result.add(0, name) + if (this.isVariadic) { + result.add("args".mangled()) } return result @@ -65,6 +65,32 @@ private fun ObjCMethod.getFirstKotlinParameterNameCandidate(forConstructorOrFact return this.parameters.first().name?.takeIf { it.isNotEmpty() } ?: "_0" } +private fun ObjCMethod.getKotlinParameters( + stubGenerator: StubGenerator, + forConstructorOrFactory: Boolean +): List { + val names = getKotlinParameterNames(forConstructorOrFactory) // TODO: consider refactoring. + val result = mutableListOf() + + this.parameters.mapIndexedTo(result) { index, it -> + val kotlinType = stubGenerator.mirror(it.type).argType + val name = names[index] + val annotations = if (it.nsConsumed) listOf("@CCall.Consumed") else emptyList() + KotlinParameter(name, kotlinType, isVararg = false, annotations = annotations) + } + + if (this.isVariadic) { + result += KotlinParameter( + names.last(), + KotlinTypes.any.makeNullable(), + isVararg = true, + annotations = emptyList() + ) + } + + return result +} + class ObjCMethodStub(private val stubGenerator: StubGenerator, val method: ObjCMethod, private val container: ObjCContainer, @@ -80,17 +106,15 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, // TODO: generate only constructor/factory in this case. val kotlinScope = stubGenerator.kotlinFile - val newParameterNames = method.getKotlinParameterNames(forConstructorOrFactory = true) - val parameters = kotlinParameters.zip(newParameterNames) { parameter, newName -> - KotlinParameter(newName, parameter.type) - }.renderParameters(kotlinScope) + val parameters = method.getKotlinParameters(stubGenerator, forConstructorOrFactory = true) + .renderParameters(kotlinScope) when (container) { is ObjCClass -> { result.add(0, deprecatedInit( container.kotlinClassName(method.isClass), - kotlinParameters.map { it.name }, + kotlinMethodParameters.map { it.name }, factory = false ) ) @@ -113,7 +137,7 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, result.add(0, deprecatedInit( className, - kotlinParameters.map { it.name }, + kotlinMethodParameters.map { it.name }, factory = true ) ) @@ -145,29 +169,19 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, ) } - private val kotlinParameters: List + private val kotlinMethodParameters: List private val kotlinReturnType: String private val header: String internal val objCMethodAnnotations: List private val isStret: Boolean init { - kotlinParameters = mutableListOf() + kotlinMethodParameters = method.getKotlinParameters(stubGenerator, forConstructorOrFactory = false) val returnType = method.getReturnType(container.classOrProtocol) isStret = returnType.isStret(stubGenerator.configuration.target) - val kotlinParameterNames = method.getKotlinParameterNames() - - method.parameters.forEachIndexed { index, it -> - val name = kotlinParameterNames[index] - - val kotlinType = stubGenerator.mirror(it.type).argType - val annotatedName = if (it.nsConsumed) "@CCall.Consumed $name" else name - kotlinParameters.add(KotlinParameter(annotatedName, kotlinType)) - } - this.kotlinReturnType = if (returnType.unwrapTypedefs() is VoidType) { KotlinTypes.unit } else { @@ -176,7 +190,7 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, objCMethodAnnotations = buildObjCMethodAnnotations("@ObjCMethod") - val joinedKotlinParameters = kotlinParameters.renderParameters(stubGenerator.kotlinFile) + val joinedKotlinParameters = kotlinMethodParameters.renderParameters(stubGenerator.kotlinFile) this.header = buildString { if (container !is ObjCProtocol) append("external ") @@ -229,7 +243,7 @@ class ObjCMethodStub(private val stubGenerator: StubGenerator, private fun deprecatedInit(className: String, initParameterNames: List, factory: Boolean): String { val replacement = if (factory) "$className.create" else className val replacementKind = if (factory) "factory method" else "constructor" - val replaceWith = "$replacement(${initParameterNames.joinToString()})" + val replaceWith = "$replacement(${initParameterNames.joinToString { it.asSimpleName() }})" return deprecated("Use $replacementKind instead", replaceWith) } diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt index c8a770b521e..1a9f0c5c1ac 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/cgen/CBridgeGen.kt @@ -57,7 +57,8 @@ internal interface KotlinStubs { private class KotlinToCCallBuilder( val irBuilder: IrBuilderWithScope, - val stubs: KotlinStubs + val stubs: KotlinStubs, + val isObjCMethod: Boolean ) { val cBridgeName = stubs.getUniqueCName("knbridge") @@ -108,7 +109,7 @@ private fun KotlinToCCallBuilder.buildKotlinBridgeCall(transformCall: (IrCall) - internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWithScope, isInvoke: Boolean): IrExpression { require(expression.dispatchReceiver == null) - val callBuilder = KotlinToCCallBuilder(builder, this) + val callBuilder = KotlinToCCallBuilder(builder, this, isObjCMethod = false) val callee = expression.symbol.owner as IrSimpleFunction @@ -138,27 +139,10 @@ internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWit targetPtrParameter = null targetFunctionName = this.getUniqueCName("target") - (0 until expression.valueArgumentsCount).forEach { index -> - val parameter = callee.valueParameters[index] - val argument = expression.getValueArgument(index) - if (parameter.isVararg) { - require(index == expression.valueArgumentsCount - 1) - require(argument is IrVararg?) - argument?.elements.orEmpty().forEach { - when (it) { - is IrExpression -> callBuilder.addArgument(it, it.type, variadic = true, parameter = null) - - is IrSpreadElement -> - reportError(it, "spread operator is not supported for variadic C functions") - - else -> error(it) - } - } - callBuilder.cFunctionBuilder.variadic = true - } else { - callBuilder.addArgument(argument!!, parameter.type, variadic = false, parameter = parameter) - } + val arguments = (0 until expression.valueArgumentsCount).map { + expression.getValueArgument(it) } + callBuilder.addArguments(arguments, callee) } val returnValuePassing = if (isInvoke) { @@ -184,6 +168,30 @@ internal fun KotlinStubs.generateCCall(expression: IrCall, builder: IrBuilderWit return result } +private fun KotlinToCCallBuilder.addArguments(arguments: List, callee: IrFunction) { + arguments.forEachIndexed { index, argument -> + val parameter = callee.valueParameters[index] + if (parameter.isVararg) { + require(index == arguments.lastIndex) + require(argument is IrVararg?) + argument?.elements.orEmpty().forEach { + when (it) { + is IrExpression -> addArgument(it, it.type, variadic = true, parameter = null) + + is IrSpreadElement -> + stubs.reportError(it, "spread operator is not supported for variadic " + + if (isObjCMethod) "Objective-C methods" else "C functions") + + else -> error(it) + } + } + cFunctionBuilder.variadic = true + } else { + addArgument(argument!!, parameter.type, variadic = false, parameter = parameter) + } + } +} + private fun KotlinToCCallBuilder.emitCBridge() { val cLines = mutableListOf() @@ -210,10 +218,10 @@ internal fun KotlinStubs.generateObjCCall( call: IrFunctionAccessExpression, superQualifier: IrClassSymbol?, receiver: IrExpression, - arguments: List + arguments: List ): IrExpression { - val callBuilder = KotlinToCCallBuilder(builder, this) + val callBuilder = KotlinToCCallBuilder(builder, this, isObjCMethod = true) val superClass = superQualifier?.let { builder.getObjCClass(symbols, it) } ?: builder.irNullNativePtr(symbols) @@ -257,15 +265,7 @@ internal fun KotlinStubs.generateObjCCall( callBuilder.cCallBuilder.arguments += "@selector($selector)" callBuilder.cFunctionBuilder.addParameter(CTypes.voidPtr) - (0 until method.valueParameters.size).forEach { - val parameter = method.valueParameters[it] - callBuilder.addArgument( - arguments[it], - type = parameter.type, - variadic = false, - parameter = parameter - ) - } + callBuilder.addArguments(arguments, method) val returnValuePassing = mapReturnType(method.returnType, TypeLocation.FunctionCallResult(call), signature = method) @@ -327,6 +327,14 @@ private fun CCallbackBuilder.addParameter(it: IrValueParameter, functionParamete TypeLocation.FunctionPointerParameter(cFunctionBuilder.numberOfParameters, location) } + if (functionParameter.isVararg) { + stubs.reportError(typeLocation.element, if (isObjCMethod) { + "overriding variadic Objective-C methods is not supported" + } else { + "variadic function pointers are not supported" + }) + } + val valuePassing = stubs.mapType( it.type, retained = it.isConsumed(), @@ -548,8 +556,14 @@ private fun KotlinToCCallBuilder.mapParameter( classifier == symbols.interopCValues || // Note: this should not be accepted, but is required for compatibility classifier == symbols.interopCValuesRef -> CValuesRefArgumentPassing - classifier == symbols.string && (variadic || parameter?.isCStringParameter() == true) -> + classifier == symbols.string && (variadic || parameter?.isCStringParameter() == true) -> { + if (variadic && isObjCMethod) { + stubs.reportError(argument, "Passing String as variadic Objective-C argument is ambiguous; " + + "cast it to NSString or pass with '.cstr' as C string") + // TODO: consider reporting a warning for C functions. + } CStringArgumentPassing() + } classifier == symbols.string && parameter?.isWCStringParameter() == true -> WCStringArgumentPassing() @@ -1177,7 +1191,7 @@ private class ObjCBlockPointerValuePassing( } private fun IrBuilderWithScope.callBlock(blockPtr: IrExpression, arguments: List): IrExpression { - val callBuilder = KotlinToCCallBuilder(this, stubs) + val callBuilder = KotlinToCCallBuilder(this, stubs, isObjCMethod = false) val rawBlockPointerParameter = callBuilder.passThroughBridge(blockPtr, blockPtr.type, CTypes.id) val blockVariableName = "block" diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt index e8acb9d9c0b..8c3b2873823 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/InteropLowering.kt @@ -596,7 +596,7 @@ internal class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfo initMethodInfo, superQualifier = expression.symbol.owner.constructedClass.symbol, receiver = builder.getRawPtr(builder.irGet(constructedClass.thisReceiver!!)), - arguments = initMethod.valueParameters.map { expression.getValueArgument(it.index)!! }, + arguments = initMethod.valueParameters.map { expression.getValueArgument(it.index) }, call = expression, method = initMethod ) @@ -629,7 +629,7 @@ internal class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfo info: ObjCMethodInfo, superQualifier: IrClassSymbol?, receiver: IrExpression, - arguments: List, + arguments: List, call: IrFunctionAccessExpression, method: IrSimpleFunction ): IrExpression = generateWithStubs(call) { @@ -655,7 +655,7 @@ internal class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfo val initMethod = callee.getObjCInitMethod() if (initMethod != null) { - val arguments = descriptor.valueParameters.map { expression.getValueArgument(it)!! } + val arguments = descriptor.valueParameters.map { expression.getValueArgument(it) } assert(expression.extensionReceiver == null) assert(expression.dispatchReceiver == null) @@ -670,7 +670,7 @@ internal class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfo descriptor.getObjCFactoryInitMethodInfo()?.let { initMethodInfo -> val arguments = (0 until expression.valueArgumentsCount) - .map { index -> expression.getValueArgument(index)!! } + .map { index -> expression.getValueArgument(index) } return builder.at(expression).run { val classPtr = getRawPtr(expression.extensionReceiver!!) @@ -688,7 +688,7 @@ internal class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfo builder.scope.scopeOwner.annotations.hasAnnotation(FqName("kotlin.native.internal.ExportForCppRuntime")) if (!useKotlinDispatch) { - val arguments = descriptor.valueParameters.map { expression.getValueArgument(it)!! } + val arguments = descriptor.valueParameters.map { expression.getValueArgument(it) } assert(expression.dispatchReceiver == null || expression.extensionReceiver == null) if (expression.superQualifier?.isObjCMetaClass() == true) { @@ -773,7 +773,7 @@ internal class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfo private fun IrBuilderWithScope.callAllocAndInit( classPtr: IrExpression, initMethodInfo: ObjCMethodInfo, - arguments: List, + arguments: List, call: IrFunctionAccessExpression, initMethod: IrSimpleFunction ): IrExpression = irBlock(startOffset, endOffset) { diff --git a/backend.native/tests/interop/objc/smoke.h b/backend.native/tests/interop/objc/smoke.h index a176feb3728..ea1bb140f17 100644 --- a/backend.native/tests/interop/objc/smoke.h +++ b/backend.native/tests/interop/objc/smoke.h @@ -144,4 +144,14 @@ NSObject* createNSObject() { @property (class) void (^nullBlock)(void); @property (class) void (^notNullBlock)(void); +@end; + +@interface TestVarargs : NSObject +-(instancetype _Nonnull)initWithFormat:(NSString*)format, ...; ++(instancetype _Nonnull)testVarargsWithFormat:(NSString*)format, ...; +@property NSString* formatted; +@end; + +@interface TestVarargs (TestVarargsExtension) +-(instancetype _Nonnull)initWithFormat:(NSString*)format, ...; @end; \ No newline at end of file diff --git a/backend.native/tests/interop/objc/smoke.kt b/backend.native/tests/interop/objc/smoke.kt index f4f78650b56..14689136e6a 100644 --- a/backend.native/tests/interop/objc/smoke.kt +++ b/backend.native/tests/interop/objc/smoke.kt @@ -21,6 +21,7 @@ fun run() { testExceptions() testBlocks() testCustomRetain() + testVarargs() assertEquals(2, ForwardDeclaredEnum.TWO.value) @@ -256,6 +257,32 @@ fun testCustomRetain() { assertFalse(unexpectedDeallocation) } +fun testVarargs() { + assertEquals( + "a b -1", + TestVarargs.testVarargsWithFormat( + "%@ %s %d", + "a" as NSString, "b".cstr, (-1).toByte() + ).formatted + ) + + assertEquals( + "2 3 9223372036854775807", + TestVarargs( + "%d %d %lld", + 2.toShort(), 3, Long.MAX_VALUE + ).formatted + ) + + assertEquals( + "0.1 0.2 1 0", + TestVarargs.create( + "%.1f %.1lf %d %d", + 0.1.toFloat(), 0.2, true, false + ).formatted + ) +} + private class MyException : Throwable() fun nsArrayOf(vararg elements: Any): NSArray = NSMutableArray().apply { diff --git a/backend.native/tests/interop/objc/smoke.m b/backend.native/tests/interop/objc/smoke.m index 4a0485800d6..9bbcc8d3a34 100644 --- a/backend.native/tests/interop/objc/smoke.m +++ b/backend.native/tests/interop/objc/smoke.m @@ -119,4 +119,29 @@ static CustomRetainMethodsImpl* retainedCustomRetainMethodsImpl; return ^{}; } +@end; + +@implementation TestVarargs +-(instancetype _Nonnull)initWithFormat:(NSString*)format, ... { + self = [super init]; + + va_list args; + va_start(args, format); + self.formatted = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + return self; +} + ++(instancetype _Nonnull)testVarargsWithFormat:(NSString*)format, ... { + TestVarargs* result = [[TestVarargs alloc] init]; + + va_list args; + va_start(args, format); + result.formatted = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + return result; +} + @end; \ No newline at end of file