Skip to content

Commit 79c5239

Browse files
[pigeon] Fixes support for Kotlin/Java classes that override equals and hashCode for ProxyApis (#10039)
ProxyApis for classes that override `equals()/hashcode` in Kotlin/Java were breaking the `InstanceManager`. The Kotlin `InstanceManager` would: 1. Return true for `containsInstance`. 2. Then remove the strong reference to the original object in `getIdentifierForStrongReference`. 3. Erroneously trigger the call to Dart to remove the strong reference of an instance. This changes the the use of `WeakReference` to a new `IdentityWeakReference` that uses identity to check for equality. Java/Kotlin has an `IdentityHashMap` and a `WeakHashMap`, but not an `IdentityWeakHashMap`. So this is a quick workaround to create an equivalent. Alternatively, `getIdentifierForStrongReference` could be updated to check `containsInstance` first, but this would leave small timeframes where objects would only have a weak reference. And it seems possible that Kotlin GC could cleanup at any moment. Potential fix for flutter/flutter#174134 since the class they were creating a ProxyApi for was a data class: https://github.com/JavesonYehudi/proxy_bug_report/blob/main/android/src/main/kotlin/com/example/proxy_bug_report/auth/Credentials.kt ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent d062181 commit 79c5239

File tree

7 files changed

+216
-31
lines changed

7 files changed

+216
-31
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
## NEXT
1+
## 26.0.2
22

3+
* [kotlin] Fixes support for classes that override equals and hashCode for ProxyApis.
4+
* [kotlin] Adds error message log when a new Dart proxy instance fails to be created.
35
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
46

57
## 26.0.1

packages/pigeon/lib/src/generator_tools.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import 'generator.dart';
1515
/// The current version of pigeon.
1616
///
1717
/// This must match the version in pubspec.yaml.
18-
const String pigeonVersion = '26.0.1';
18+
const String pigeonVersion = '26.0.2';
1919

2020
/// Read all the content from [stdin] to a String.
2121
String readStdin() {

packages/pigeon/lib/src/kotlin/kotlin_generator.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,15 @@ if (wrapped == null) {
10311031
});
10321032
indent.newln();
10331033

1034+
indent.format('''
1035+
fun logNewInstanceFailure(apiName: String, value: Any, exception: Throwable?) {
1036+
Log.w(
1037+
"${proxyApiCodecName(const InternalKotlinOptions(kotlinOut: ''))}",
1038+
"Failed to create new Dart proxy instance of \$apiName: \$value. \$exception"
1039+
)
1040+
}
1041+
''');
1042+
10341043
enumerate(sortedApis, (int index, AstProxyApi api) {
10351044
final String className =
10361045
api.kotlinOptions?.fullClassName ?? api.name;
@@ -1043,7 +1052,11 @@ if (wrapped == null) {
10431052

10441053
indent.format('''
10451054
${index > 0 ? ' else ' : ''}if (${versionCheck}value is $className) {
1046-
registrar.get$hostProxyApiPrefix${api.name}().${classMemberNamePrefix}newInstance(value) { }
1055+
registrar.get$hostProxyApiPrefix${api.name}().${classMemberNamePrefix}newInstance(value) {
1056+
if (it.isFailure) {
1057+
logNewInstanceFailure("${api.name}", value, it.exceptionOrNull())
1058+
}
1059+
}
10471060
}''');
10481061
});
10491062
indent.newln();

packages/pigeon/lib/src/kotlin/templates.dart

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,39 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
4242
fun onFinalize(identifier: Long)
4343
}
4444
45-
private val identifiers = java.util.WeakHashMap<Any, Long>()
46-
private val weakInstances = HashMap<Long, java.lang.ref.WeakReference<Any>>()
45+
// Extends WeakReference and overrides the `equals` and `hashCode` methods using identity rather
46+
// than equality.
47+
//
48+
// Two `IdentityWeakReference`s are equal if either
49+
// 1: `get()` returns the identical nonnull value for both references.
50+
// 2: `get()` returns null for both references and the references are identical.
51+
class IdentityWeakReference<T : Any> : java.lang.ref.WeakReference<T> {
52+
private val savedHashCode: Int
53+
54+
constructor(instance: T) : this(instance, null)
55+
56+
constructor(instance: T, queue: java.lang.ref.ReferenceQueue<T>?) : super(instance, queue) {
57+
savedHashCode = System.identityHashCode(instance)
58+
}
59+
60+
override fun equals(other: Any?): Boolean {
61+
val instance = get()
62+
if (instance != null) {
63+
return other is IdentityWeakReference<*> && other.get() === instance
64+
}
65+
return other === this
66+
}
67+
68+
override fun hashCode(): Int {
69+
return savedHashCode
70+
}
71+
}
72+
73+
private val identifiers = java.util.WeakHashMap<IdentityWeakReference<Any>, Long>()
74+
private val weakInstances = HashMap<Long, IdentityWeakReference<Any>>()
4775
private val strongInstances = HashMap<Long, Any>()
4876
private val referenceQueue = java.lang.ref.ReferenceQueue<Any>()
49-
private val weakReferencesToIdentifiers = HashMap<java.lang.ref.WeakReference<Any>, Long>()
77+
private val weakReferencesToIdentifiers = HashMap<IdentityWeakReference<Any>, Long>()
5078
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
5179
private val releaseAllFinalizedInstancesRunnable = Runnable {
5280
this.releaseAllFinalizedInstances()
@@ -112,9 +140,12 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
112140
*/
113141
fun getIdentifierForStrongReference(instance: Any?): Long? {
114142
logWarningIfFinalizationListenerHasStopped()
115-
val identifier = identifiers[instance]
143+
if (instance == null) {
144+
return null
145+
}
146+
val identifier = identifiers[IdentityWeakReference(instance)]
116147
if (identifier != null) {
117-
strongInstances[identifier] = instance!!
148+
strongInstances[identifier] = instance
118149
}
119150
return identifier
120151
}
@@ -151,14 +182,14 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
151182
/** Retrieves the instance associated with identifier, if present, otherwise `null`. */
152183
fun <T> getInstance(identifier: Long): T? {
153184
logWarningIfFinalizationListenerHasStopped()
154-
val instance = weakInstances[identifier] as java.lang.ref.WeakReference<T>?
185+
val instance = weakInstances[identifier] as IdentityWeakReference<T>?
155186
return instance?.get()
156187
}
157188
158189
/** Returns whether this manager contains the given `instance`. */
159190
fun containsInstance(instance: Any?): Boolean {
160191
logWarningIfFinalizationListenerHasStopped()
161-
return identifiers.containsKey(instance)
192+
return instance != null && identifiers.containsKey(IdentityWeakReference(instance))
162193
}
163194
164195
/**
@@ -199,8 +230,8 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
199230
if (hasFinalizationListenerStopped()) {
200231
return
201232
}
202-
var reference: java.lang.ref.WeakReference<Any>?
203-
while ((referenceQueue.poll() as java.lang.ref.WeakReference<Any>?).also { reference = it } != null) {
233+
var reference: IdentityWeakReference<Any>?
234+
while ((referenceQueue.poll() as IdentityWeakReference<Any>?).also { reference = it } != null) {
204235
val identifier = weakReferencesToIdentifiers.remove(reference)
205236
if (identifier != null) {
206237
weakInstances.remove(identifier)
@@ -216,8 +247,8 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
216247
require(!weakInstances.containsKey(identifier)) {
217248
"Identifier has already been added: \$identifier"
218249
}
219-
val weakReference = java.lang.ref.WeakReference(instance, referenceQueue)
220-
identifiers[instance] = identifier
250+
val weakReference = IdentityWeakReference(instance, referenceQueue)
251+
identifiers[weakReference] = identifier
221252
weakInstances[identifier] = weakReference
222253
weakReferencesToIdentifiers[weakReference] = identifier
223254
strongInstances[identifier] = instance

packages/pigeon/platform_tests/test_plugin/android/src/main/kotlin/com/example/test_plugin/ProxyApiTests.gen.kt

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,39 @@ class ProxyApiTestsPigeonInstanceManager(
7575
fun onFinalize(identifier: Long)
7676
}
7777

78-
private val identifiers = java.util.WeakHashMap<Any, Long>()
79-
private val weakInstances = HashMap<Long, java.lang.ref.WeakReference<Any>>()
78+
// Extends WeakReference and overrides the `equals` and `hashCode` methods using identity rather
79+
// than equality.
80+
//
81+
// Two `IdentityWeakReference`s are equal if either
82+
// 1: `get()` returns the identical nonnull value for both references.
83+
// 2: `get()` returns null for both references and the references are identical.
84+
class IdentityWeakReference<T : Any> : java.lang.ref.WeakReference<T> {
85+
private val savedHashCode: Int
86+
87+
constructor(instance: T) : this(instance, null)
88+
89+
constructor(instance: T, queue: java.lang.ref.ReferenceQueue<T>?) : super(instance, queue) {
90+
savedHashCode = System.identityHashCode(instance)
91+
}
92+
93+
override fun equals(other: Any?): Boolean {
94+
val instance = get()
95+
if (instance != null) {
96+
return other is IdentityWeakReference<*> && other.get() === instance
97+
}
98+
return other === this
99+
}
100+
101+
override fun hashCode(): Int {
102+
return savedHashCode
103+
}
104+
}
105+
106+
private val identifiers = java.util.WeakHashMap<IdentityWeakReference<Any>, Long>()
107+
private val weakInstances = HashMap<Long, IdentityWeakReference<Any>>()
80108
private val strongInstances = HashMap<Long, Any>()
81109
private val referenceQueue = java.lang.ref.ReferenceQueue<Any>()
82-
private val weakReferencesToIdentifiers = HashMap<java.lang.ref.WeakReference<Any>, Long>()
110+
private val weakReferencesToIdentifiers = HashMap<IdentityWeakReference<Any>, Long>()
83111
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
84112
private val releaseAllFinalizedInstancesRunnable = Runnable {
85113
this.releaseAllFinalizedInstances()
@@ -144,9 +172,12 @@ class ProxyApiTestsPigeonInstanceManager(
144172
*/
145173
fun getIdentifierForStrongReference(instance: Any?): Long? {
146174
logWarningIfFinalizationListenerHasStopped()
147-
val identifier = identifiers[instance]
175+
if (instance == null) {
176+
return null
177+
}
178+
val identifier = identifiers[IdentityWeakReference(instance)]
148179
if (identifier != null) {
149-
strongInstances[identifier] = instance!!
180+
strongInstances[identifier] = instance
150181
}
151182
return identifier
152183
}
@@ -185,14 +216,14 @@ class ProxyApiTestsPigeonInstanceManager(
185216
/** Retrieves the instance associated with identifier, if present, otherwise `null`. */
186217
fun <T> getInstance(identifier: Long): T? {
187218
logWarningIfFinalizationListenerHasStopped()
188-
val instance = weakInstances[identifier] as java.lang.ref.WeakReference<T>?
219+
val instance = weakInstances[identifier] as IdentityWeakReference<T>?
189220
return instance?.get()
190221
}
191222

192223
/** Returns whether this manager contains the given `instance`. */
193224
fun containsInstance(instance: Any?): Boolean {
194225
logWarningIfFinalizationListenerHasStopped()
195-
return identifiers.containsKey(instance)
226+
return instance != null && identifiers.containsKey(IdentityWeakReference(instance))
196227
}
197228

198229
/**
@@ -233,9 +264,8 @@ class ProxyApiTestsPigeonInstanceManager(
233264
if (hasFinalizationListenerStopped()) {
234265
return
235266
}
236-
var reference: java.lang.ref.WeakReference<Any>?
237-
while ((referenceQueue.poll() as java.lang.ref.WeakReference<Any>?).also { reference = it } !=
238-
null) {
267+
var reference: IdentityWeakReference<Any>?
268+
while ((referenceQueue.poll() as IdentityWeakReference<Any>?).also { reference = it } != null) {
239269
val identifier = weakReferencesToIdentifiers.remove(reference)
240270
if (identifier != null) {
241271
weakInstances.remove(identifier)
@@ -251,8 +281,8 @@ class ProxyApiTestsPigeonInstanceManager(
251281
require(!weakInstances.containsKey(identifier)) {
252282
"Identifier has already been added: $identifier"
253283
}
254-
val weakReference = java.lang.ref.WeakReference(instance, referenceQueue)
255-
identifiers[instance] = identifier
284+
val weakReference = IdentityWeakReference(instance, referenceQueue)
285+
identifiers[weakReference] = identifier
256286
weakInstances[identifier] = weakReference
257287
weakReferencesToIdentifiers[weakReference] = identifier
258288
strongInstances[identifier] = instance
@@ -460,14 +490,36 @@ private class ProxyApiTestsPigeonProxyApiBaseCodec(
460490
return
461491
}
462492

493+
fun logNewInstanceFailure(apiName: String, value: Any, exception: Throwable?) {
494+
Log.w(
495+
"PigeonProxyApiBaseCodec",
496+
"Failed to create new Dart proxy instance of $apiName: $value. $exception")
497+
}
498+
463499
if (value is ProxyApiTestClass) {
464-
registrar.getPigeonApiProxyApiTestClass().pigeon_newInstance(value) {}
500+
registrar.getPigeonApiProxyApiTestClass().pigeon_newInstance(value) {
501+
if (it.isFailure) {
502+
logNewInstanceFailure("ProxyApiTestClass", value, it.exceptionOrNull())
503+
}
504+
}
465505
} else if (value is com.example.test_plugin.ProxyApiSuperClass) {
466-
registrar.getPigeonApiProxyApiSuperClass().pigeon_newInstance(value) {}
506+
registrar.getPigeonApiProxyApiSuperClass().pigeon_newInstance(value) {
507+
if (it.isFailure) {
508+
logNewInstanceFailure("ProxyApiSuperClass", value, it.exceptionOrNull())
509+
}
510+
}
467511
} else if (value is ProxyApiInterface) {
468-
registrar.getPigeonApiProxyApiInterface().pigeon_newInstance(value) {}
512+
registrar.getPigeonApiProxyApiInterface().pigeon_newInstance(value) {
513+
if (it.isFailure) {
514+
logNewInstanceFailure("ProxyApiInterface", value, it.exceptionOrNull())
515+
}
516+
}
469517
} else if (android.os.Build.VERSION.SDK_INT >= 25 && value is ClassWithApiRequirement) {
470-
registrar.getPigeonApiClassWithApiRequirement().pigeon_newInstance(value) {}
518+
registrar.getPigeonApiClassWithApiRequirement().pigeon_newInstance(value) {
519+
if (it.isFailure) {
520+
logNewInstanceFailure("ClassWithApiRequirement", value, it.exceptionOrNull())
521+
}
522+
}
471523
}
472524

473525
when {

packages/pigeon/platform_tests/test_plugin/android/src/test/kotlin/com/example/test_plugin/InstanceManagerTest.kt

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,97 @@ class InstanceManagerTest {
149149
assertFalse(finalizerRan)
150150
}
151151

152+
@Test
153+
fun containsInstanceAndGetIdentifierForStrongReferenceUseIdentityComparison() {
154+
val instanceManager: ProxyApiTestsPigeonInstanceManager = createInstanceManager()
155+
instanceManager.stopFinalizationListener()
156+
157+
// Create two objects that are equal.
158+
val testString = "aString"
159+
val testObject1 = TestDataClass(testString)
160+
val testObject2 = TestDataClass(testString)
161+
assertEquals(testObject1, testObject2)
162+
163+
val identifier1 = instanceManager.addHostCreatedInstance(testObject1)
164+
assertFalse(instanceManager.containsInstance(testObject2))
165+
assertNull(instanceManager.getIdentifierForStrongReference(testObject2))
166+
167+
val identifier2 = instanceManager.addHostCreatedInstance(testObject2)
168+
assertTrue(instanceManager.containsInstance(testObject1))
169+
assertTrue(instanceManager.containsInstance(testObject2))
170+
assertEquals(identifier1, instanceManager.getIdentifierForStrongReference(testObject1))
171+
assertEquals(identifier2, instanceManager.getIdentifierForStrongReference(testObject2))
172+
}
173+
174+
@Test
175+
fun addingTwoDartCreatedInstancesThatAreEqual() {
176+
val instanceManager: ProxyApiTestsPigeonInstanceManager = createInstanceManager()
177+
instanceManager.stopFinalizationListener()
178+
179+
// Create two objects that are equal.
180+
val testString = "aString"
181+
val testObject1 = TestDataClass(testString)
182+
val testObject2 = TestDataClass(testString)
183+
assertEquals(testObject1, testObject2)
184+
185+
instanceManager.addDartCreatedInstance(testObject1, 0)
186+
instanceManager.addDartCreatedInstance(testObject2, 1)
187+
188+
assertEquals(testObject1, instanceManager.getInstance(0))
189+
assertEquals(testObject2, instanceManager.getInstance(1))
190+
assertEquals(0L, instanceManager.getIdentifierForStrongReference(testObject1))
191+
assertEquals(1L, instanceManager.getIdentifierForStrongReference(testObject2))
192+
}
193+
194+
@Test
195+
fun identityWeakReferencesAreEqualWithSameInstance() {
196+
val testObject = Any()
197+
198+
assertEquals(
199+
ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject),
200+
ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject))
201+
}
202+
203+
@Test
204+
fun identityWeakReferenceRemainsEqualAfterGetReturnsNull() {
205+
var testObject: Any? = Any()
206+
207+
val reference = ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject!!)
208+
209+
// To allow for object to be garbage collected.
210+
@Suppress("UNUSED_VALUE")
211+
testObject = null
212+
Runtime.getRuntime().gc()
213+
214+
assertNull(reference.get())
215+
assertEquals(reference, reference)
216+
}
217+
218+
@Test
219+
fun identityWeakReferencesAreNotEqualAfterGetReturnsNull() {
220+
var testObject1: Any? = Any()
221+
var testObject2: Any? = Any()
222+
223+
val reference1 = ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject1!!)
224+
val reference2 = ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject2!!)
225+
226+
// To allow for object to be garbage collected.
227+
@Suppress("UNUSED_VALUE")
228+
testObject1 = null
229+
testObject2 = null
230+
Runtime.getRuntime().gc()
231+
232+
assertNull(reference1.get())
233+
assertNull(reference2.get())
234+
assertFalse(reference1 == reference2)
235+
}
236+
152237
private fun createInstanceManager(): ProxyApiTestsPigeonInstanceManager {
153238
return ProxyApiTestsPigeonInstanceManager.create(
154239
object : ProxyApiTestsPigeonInstanceManager.PigeonFinalizationListener {
155240
override fun onFinalize(identifier: Long) {}
156241
})
157242
}
243+
244+
data class TestDataClass(val value: String)
158245
}

packages/pigeon/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: pigeon
22
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
33
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22
5-
version: 26.0.1 # This must match the version in lib/src/generator_tools.dart
5+
version: 26.0.2 # This must match the version in lib/src/generator_tools.dart
66

77
environment:
88
sdk: ^3.7.0

0 commit comments

Comments
 (0)