[K/N] Make cinterop include unused Objective-C forward declarations
Previously, when an Objective-C library had an unused Objective-C forward declaration (`@class` or `@protocol`), cinterop tool didn't include it into the resulting klib at all. This led to a subtle bug (KT-64105). One Obj-C library has unused Obj-C forward declaration, and another one depends on the first and uses this forward declaration, e.g. as a function result type. When building the first cinterop klib, this forward declaration is not added to `includedForwardDeclarations` in the klib manifest (the compiler uses this property to decide whether to synthesize the corresponding class). When building the second cinterop klib, the forward declaration is not added to its manifest either, because it is located in the dependency (and therefore should've been included there). As a result, the forward declaration is included nowhere, and any attempt to use it in Kotlin fails, including calling the function from the second lib. This commit fixes this bug by including even unused Objective-C forward declarations, which is consistent with any other kind of declarations and seems more natural. ^KT-64105 Fixed
This commit is contained in:
committed by
Space Team
parent
59142b3051
commit
2e5a9b1416
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
// TARGET_BACKEND: NATIVE
|
||||
// MODULE: lib1
|
||||
// FILE: lib1.def
|
||||
language=Objective-C
|
||||
headers=lib1.h
|
||||
|
||||
// FILE: lib1.h
|
||||
@class Foo;
|
||||
@protocol Bar;
|
||||
struct Baz;
|
||||
|
||||
// MODULE: lib2(lib1)
|
||||
// FILE: lib2.def
|
||||
language=Objective-C
|
||||
headers=lib2.h
|
||||
|
||||
// FILE: lib2.h
|
||||
#import "../lib1/lib1.h"
|
||||
|
||||
Foo* createFoo() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
id<Bar> createBar() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct Baz* createBaz() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// MODULE: main(lib1,lib2)
|
||||
// FILE: main.kt
|
||||
import kotlinx.cinterop.CPointer
|
||||
import lib1.*
|
||||
import lib2.*
|
||||
import cnames.structs.Baz
|
||||
import objcnames.classes.Foo
|
||||
import objcnames.protocols.BarProtocol
|
||||
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
fun box(): String {
|
||||
val foo: Foo? = createFoo()
|
||||
if (foo !== null) return "FAIL 1"
|
||||
|
||||
val bar: BarProtocol? = createBar()
|
||||
if (bar !== null) return "FAIL 2"
|
||||
|
||||
val baz: CPointer<Baz>? = createBaz()
|
||||
if (baz !== null) return "FAIL 3"
|
||||
|
||||
return "OK"
|
||||
}
|
||||
+10
@@ -936,6 +936,11 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole
|
||||
|
||||
CXIdxEntity_ObjCClass -> if (cursor.kind != CXCursorKind.CXCursor_ObjCClassRef /* not a forward declaration */) {
|
||||
indexObjCClass(cursor)
|
||||
} else {
|
||||
// It is a class reference. To get the declaration cursor, we can use clang_getCursorReferenced.
|
||||
// If there is a real declaration besides this forward declaration, the function will automatically
|
||||
// resolve it.
|
||||
indexObjCClass(clang_getCursorReferenced(cursor))
|
||||
}
|
||||
|
||||
CXIdxEntity_ObjCCategory -> {
|
||||
@@ -946,6 +951,11 @@ public open class NativeIndexImpl(val library: NativeLibrary, val verbose: Boole
|
||||
|
||||
CXIdxEntity_ObjCProtocol -> if (cursor.kind != CXCursorKind.CXCursor_ObjCProtocolRef /* not a forward declaration */) {
|
||||
indexObjCProtocol(cursor)
|
||||
} else {
|
||||
// It is a protocol reference. To get the declaration cursor, we can use clang_getCursorReferenced.
|
||||
// If there is a real declaration besides this forward declaration, the function will automatically
|
||||
// resolve it.
|
||||
indexObjCProtocol(clang_getCursorReferenced(cursor))
|
||||
}
|
||||
|
||||
CXIdxEntity_ObjCProperty -> {
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2010-2024 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.native.interop.gen
|
||||
|
||||
import org.jetbrains.kotlin.konan.target.HostManager
|
||||
import org.jetbrains.kotlin.native.interop.indexer.IndexerResult
|
||||
import org.jetbrains.kotlin.native.interop.indexer.ObjCClassOrProtocol
|
||||
import org.jetbrains.kotlin.native.interop.indexer.StructDecl
|
||||
import org.junit.Assume
|
||||
import kotlin.test.*
|
||||
|
||||
class ForwardDeclarationsTests : InteropTestsBase() {
|
||||
|
||||
private fun StructDecl.getName() = spelling.removePrefix("struct ")
|
||||
|
||||
@Test
|
||||
fun `struct forward declarations`() {
|
||||
fun IndexerResult.assertHasOnlyForwardStructs(vararg names: String) {
|
||||
this.index.structs.forEach {
|
||||
assertNull(it.def, "${it.spelling} is not a forward declaration")
|
||||
}
|
||||
assertEquals(names.toSet(), this.index.structs.map { it.getName() }.toSet())
|
||||
}
|
||||
|
||||
val dir = "ForwardDeclarations/struct"
|
||||
val dependency = buildNativeIndex(dir, "dependency.def")
|
||||
val main = buildNativeIndex(dir, "main.def", mockImports(dependency))
|
||||
|
||||
dependency.assertHasOnlyForwardStructs("DependencyUsed", "DependencyUnused", "DependencyAndMain")
|
||||
|
||||
main.assertHasOnlyForwardStructs(
|
||||
"ImportedUsed", "IncludedUnused", "IncludedUsed",
|
||||
"MainUnused", "MainUsed", "DependencyAndMain"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `objc forward declarations`() {
|
||||
Assume.assumeTrue(HostManager.hostIsMac)
|
||||
|
||||
fun Collection<ObjCClassOrProtocol>.assertHasOnlyForward(vararg names: String) {
|
||||
this.forEach {
|
||||
assertTrue(it.isForwardDeclaration, "${it.name} is not a forward declaration")
|
||||
}
|
||||
assertEquals(names.toSet(), this.map { it.name }.toSet())
|
||||
}
|
||||
|
||||
val dir = "ForwardDeclarations/objc"
|
||||
val dependency = buildNativeIndex(dir, "dependency.def")
|
||||
val main = buildNativeIndex(dir, "main.def", mockImports(dependency))
|
||||
|
||||
dependency.index.objCClasses.assertHasOnlyForward(
|
||||
"DependencyClassUsed", "DependencyClassUnused", "DependencyAndMainClass"
|
||||
)
|
||||
dependency.index.objCProtocols.assertHasOnlyForward(
|
||||
"DependencyProtocolUsed", "DependencyProtocolUnused", "DependencyAndMainProtocol"
|
||||
)
|
||||
|
||||
main.index.objCClasses.assertHasOnlyForward(
|
||||
"ImportedClassUsed", "IncludedClassUsed", "IncludedClassUnused",
|
||||
"MainClassUsed", "MainClassUnused", "DependencyAndMainClass"
|
||||
)
|
||||
|
||||
main.index.objCProtocols.assertHasOnlyForward(
|
||||
"ImportedProtocolUsed", "IncludedProtocolUsed", "IncludedProtocolUnused",
|
||||
"MainProtocolUsed", "MainProtocolUnused", "DependencyAndMainProtocol"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `struct forward declarations with definitions`() {
|
||||
val dir = "ForwardDeclarations/structWithDefinition"
|
||||
val main = buildNativeIndex(dir, "main.def")
|
||||
val structs = main.index.structs
|
||||
|
||||
structs.forEach {
|
||||
assertNotNull(it.def, "${it.spelling} is forward declaration")
|
||||
}
|
||||
|
||||
assertEquals(setOf("Struct1", "Struct2", "Struct3", "Struct4"), structs.map { it.getName() }.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `objc forward declarations with definitions`() {
|
||||
Assume.assumeTrue(HostManager.hostIsMac)
|
||||
|
||||
fun Collection<ObjCClassOrProtocol>.assertHasOnlyNonForward(vararg names: String) {
|
||||
this.forEach {
|
||||
assertFalse(it.isForwardDeclaration, "${it.name} is a forward declaration")
|
||||
}
|
||||
assertEquals(names.toSet(), this.map { it.name }.toSet())
|
||||
}
|
||||
|
||||
val dir = "ForwardDeclarations/objcWithDefinition"
|
||||
val main = buildNativeIndex(dir, "main.def")
|
||||
|
||||
main.index.objCClasses.assertHasOnlyNonForward("Class1", "Class2", "Class3", "Class4")
|
||||
main.index.objCProtocols.assertHasOnlyNonForward("Protocol1", "Protocol2", "Protocol3", "Protocol4")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
headers = dependency.h
|
||||
headerFilter = dependency.h
|
||||
language = Objective-C
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
@class DependencyClassUsed;
|
||||
@class DependencyClassUnused;
|
||||
@class DependencyAndMainClass;
|
||||
|
||||
@protocol DependencyProtocolUsed;
|
||||
@protocol DependencyProtocolUnused;
|
||||
@protocol DependencyAndMainProtocol;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
@class ImportedClassUsed;
|
||||
@class ImportedClassUnused;
|
||||
|
||||
@protocol ImportedProtocolUsed;
|
||||
@protocol ImportedProtocolUnused;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
@class IncludedClassUsed, IncludedClassUnused;
|
||||
@protocol IncludedProtocolUsed, IncludedProtocolUnused;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
headers = main.h
|
||||
headerFilter = main.h included.h dependency.h
|
||||
language = Objective-C
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
#import "dependency.h"
|
||||
#import "imported.h"
|
||||
#import "included.h"
|
||||
|
||||
|
||||
@class MainClassUsed;
|
||||
@class MainClassUnused;
|
||||
@class DependencyAndMainClass;
|
||||
|
||||
@protocol MainProtocolUsed;
|
||||
@protocol MainProtocolUnused;
|
||||
@protocol DependencyAndMainProtocol;
|
||||
|
||||
void useDependency(DependencyClassUsed*, id<DependencyProtocolUsed>);
|
||||
void useImported(ImportedClassUsed*, id<ImportedProtocolUsed>);
|
||||
void useIncluded(IncludedClassUsed*, id<IncludedProtocolUsed>);
|
||||
void useMain(MainClassUsed*, id<MainProtocolUsed>);
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
@class Class3;
|
||||
@protocol Protocol3;
|
||||
|
||||
@interface Class4
|
||||
@end
|
||||
|
||||
@protocol Protocol4
|
||||
@end
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
headers = main.h
|
||||
language = Objective-C
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#import "included.h"
|
||||
|
||||
@class Class1, Class2;
|
||||
@class Class2;
|
||||
|
||||
@interface Class1
|
||||
@end
|
||||
|
||||
@interface Class2
|
||||
@end
|
||||
|
||||
@class Class2;
|
||||
|
||||
@protocol Protocol1;
|
||||
|
||||
@protocol Protocol1
|
||||
@end
|
||||
|
||||
@protocol Protocol2;
|
||||
|
||||
@protocol Protocol2
|
||||
@end
|
||||
|
||||
@interface Class3
|
||||
@end
|
||||
|
||||
@protocol Protocol3
|
||||
@end
|
||||
|
||||
@class Class4;
|
||||
@protocol Protocol4;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
headers = dependency.h
|
||||
headerFilter = dependency.h
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
struct DependencyUsed;
|
||||
struct DependencyUnused;
|
||||
|
||||
struct DependencyAndMain;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
struct ImportedUsed;
|
||||
struct ImportedUnused;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
struct IncludedUsed;
|
||||
struct IncludedUnused;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
headers = main.h
|
||||
headerFilter = main.h included.h dependency.h
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#include "dependency.h"
|
||||
#include "imported.h"
|
||||
#include "included.h"
|
||||
|
||||
struct MainUsed;
|
||||
struct MainUnused;
|
||||
struct DependencyAndMain;
|
||||
|
||||
void useDependency(struct DependencyUsed*);
|
||||
void useImported(struct ImportedUsed*);
|
||||
void useIncluded(struct IncludedUsed*);
|
||||
void useMain(struct MainUsed*);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
struct Struct3;
|
||||
|
||||
struct Struct4 {};
|
||||
+1
@@ -0,0 +1 @@
|
||||
headers = main.h
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#include "included.h"
|
||||
|
||||
struct Struct1;
|
||||
struct Struct1 {};
|
||||
|
||||
struct Struct2;
|
||||
struct Struct2 {};
|
||||
struct Struct2;
|
||||
|
||||
struct Struct3 {};
|
||||
|
||||
struct Struct4;
|
||||
+6
@@ -4874,6 +4874,12 @@ public class FirNativeCodegenBoxTestGenerated extends AbstractNativeCodegenBoxTe
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt63049.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("kt64105.kt")
|
||||
public void testKt64105() throws Exception {
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt64105.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("leakMemoryWithRunningThreadUnchecked.kt")
|
||||
public void testLeakMemoryWithRunningThreadUnchecked() throws Exception {
|
||||
|
||||
+6
@@ -4984,6 +4984,12 @@ public class FirNativeCodegenBoxTestNoPLGenerated extends AbstractNativeCodegenB
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt63049.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("kt64105.kt")
|
||||
public void testKt64105() throws Exception {
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt64105.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("leakMemoryWithRunningThreadUnchecked.kt")
|
||||
public void testLeakMemoryWithRunningThreadUnchecked() throws Exception {
|
||||
|
||||
+6
@@ -4764,6 +4764,12 @@ public class NativeCodegenBoxTestGenerated extends AbstractNativeCodegenBoxTest
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt63049.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("kt64105.kt")
|
||||
public void testKt64105() throws Exception {
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt64105.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("leakMemoryWithRunningThreadUnchecked.kt")
|
||||
public void testLeakMemoryWithRunningThreadUnchecked() throws Exception {
|
||||
|
||||
+6
@@ -4875,6 +4875,12 @@ public class NativeCodegenBoxTestNoPLGenerated extends AbstractNativeCodegenBoxT
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt63049.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("kt64105.kt")
|
||||
public void testKt64105() throws Exception {
|
||||
runTest("compiler/testData/codegen/box/cinterop/kt64105.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("leakMemoryWithRunningThreadUnchecked.kt")
|
||||
public void testLeakMemoryWithRunningThreadUnchecked() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user