Support variadic Objective-C methods

This commit is contained in:
Svyatoslav Scherbina
2019-04-12 17:17:47 +03:00
committed by SvyatoslavScherbina
parent 79e36a31cf
commit e437339d9a
9 changed files with 181 additions and 79 deletions
@@ -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),
@@ -174,7 +174,7 @@ sealed class ObjCClassOrProtocol(val name: String) : ObjCContainer(), TypeDeclar
data class ObjCMethod(
val selector: String, val encoding: String, val parameters: List<Parameter>, 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
) {
@@ -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<String>
) {
fun render(scope: KotlinScope) = buildString {
annotations.forEach { append("$it ") }
if (isVararg) append("vararg ")
append(name.asSimpleName())
append(": ")
append(type.render(scope))
}
}
@@ -25,29 +25,29 @@ private fun ObjCMethod.getKotlinParameterNames(forConstructorOrFactory: Boolean
val result = mutableListOf<String>()
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<KotlinParameter> {
val names = getKotlinParameterNames(forConstructorOrFactory) // TODO: consider refactoring.
val result = mutableListOf<KotlinParameter>()
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<KotlinParameter>
private val kotlinMethodParameters: List<KotlinParameter>
private val kotlinReturnType: String
private val header: String
internal val objCMethodAnnotations: List<String>
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<String>, 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)
}
@@ -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<IrExpression?>, 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<String>()
@@ -210,10 +218,10 @@ internal fun KotlinStubs.generateObjCCall(
call: IrFunctionAccessExpression,
superQualifier: IrClassSymbol?,
receiver: IrExpression,
arguments: List<IrExpression>
arguments: List<IrExpression?>
): 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>): 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"
@@ -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<IrExpression>,
arguments: List<IrExpression?>,
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<IrExpression>,
arguments: List<IrExpression?>,
call: IrFunctionAccessExpression,
initMethod: IrSimpleFunction
): IrExpression = irBlock(startOffset, endOffset) {
+10
View File
@@ -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;
@@ -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 {
+25
View File
@@ -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;