Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## NEXT
## 26.0.2

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

## 26.0.1
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/src/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import 'generator.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '26.0.1';
const String pigeonVersion = '26.0.2';

/// Read all the content from [stdin] to a String.
String readStdin() {
Expand Down
15 changes: 14 additions & 1 deletion packages/pigeon/lib/src/kotlin/kotlin_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,15 @@ if (wrapped == null) {
});
indent.newln();

indent.format('''
fun logNewInstanceFailure(apiName: String, value: Any, exception: Throwable?) {
Log.w(
"${proxyApiCodecName(const InternalKotlinOptions(kotlinOut: ''))}",
"Failed to create new Dart proxy instance of \$apiName: \$value. \$exception"
)
}
''');

enumerate(sortedApis, (int index, AstProxyApi api) {
final String className =
api.kotlinOptions?.fullClassName ?? api.name;
Expand All @@ -1043,7 +1052,11 @@ if (wrapped == null) {

indent.format('''
${index > 0 ? ' else ' : ''}if (${versionCheck}value is $className) {
registrar.get$hostProxyApiPrefix${api.name}().${classMemberNamePrefix}newInstance(value) { }
registrar.get$hostProxyApiPrefix${api.name}().${classMemberNamePrefix}newInstance(value) {
if (it.isFailure) {
logNewInstanceFailure("${api.name}", value, it.exceptionOrNull())
}
}
Comment on lines +1055 to +1059

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While this logging is a great addition for debugging, the current implementation in the generator leads to duplicated code blocks in the generated writeValue method for each ProxyApi type. To improve the maintainability and readability of the generated code, consider refactoring this. You could generate a private helper method within the ProxyApiBaseCodec class to encapsulate the logging logic, and then call that helper from each if block.

For example, you could generate a method like this:

private fun logNewInstanceFailure(value: Any, apiName: String, result: Result<*>) {
  if (result.isFailure) {
    Log.w(
      "PigeonProxyApiBaseCodec",
      "Failed to create new Dart proxy instance of $apiName: $value. ${result.exceptionOrNull()}"
    )
  }
}

Then, the call site in the if block would be a single, clean line.

}''');
});
indent.newln();
Expand Down
53 changes: 42 additions & 11 deletions packages/pigeon/lib/src/kotlin/templates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,39 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
fun onFinalize(identifier: Long)
}

private val identifiers = java.util.WeakHashMap<Any, Long>()
private val weakInstances = HashMap<Long, java.lang.ref.WeakReference<Any>>()
// Extends WeakReference and overrides the `equals` and `hashCode` methods using identity rather
// than equality.
//
// Two `IdentityWeakReference`s are equal if either
// 1: `get()` returns the identical nonnull value for both references.
// 2: `get()` returns null for both references and the references are identical.
class IdentityWeakReference<T : Any> : java.lang.ref.WeakReference<T> {
private val savedHashCode: Int

constructor(instance: T) : this(instance, null)

constructor(instance: T, queue: java.lang.ref.ReferenceQueue<T>?) : super(instance, queue) {
savedHashCode = System.identityHashCode(instance)
}

override fun equals(other: Any?): Boolean {
val instance = get()
if (instance != null) {
return other is IdentityWeakReference<*> && other.get() === instance
}
return other === this
}

override fun hashCode(): Int {
return savedHashCode
}
Comment on lines +68 to +70

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The hashCode implementation for IdentityKey has a critical bug and a logical inconsistency.

  1. NPE Bug: If the WeakReference has been cleared by the garbage collector, instance.get() will return null. Calling .hashCode() on null will result in a NullPointerException. The equals method correctly handles this case, but hashCode does not.
  2. Logical Inconsistency: The purpose of IdentityKey is to enforce identity-based equality, similar to IdentityHashMap. Therefore, its hash code should also be based on identity. Using the instance's own hashCode() can lead to unnecessary hash collisions if different instances have the same value-based hash code, and it deviates from the principle of identity-based hashing.

The correct approach is to use System.identityHashCode(), which is safe for null inputs (it returns 0) and provides an identity-based hash code.

Suggested change
override fun hashCode(): Int {
return instance.get().hashCode()
}
override fun hashCode(): Int {
return System.identityHashCode(instance.get())
}

}

private val identifiers = java.util.WeakHashMap<IdentityWeakReference<Any>, Long>()
private val weakInstances = HashMap<Long, IdentityWeakReference<Any>>()
private val strongInstances = HashMap<Long, Any>()
private val referenceQueue = java.lang.ref.ReferenceQueue<Any>()
private val weakReferencesToIdentifiers = HashMap<java.lang.ref.WeakReference<Any>, Long>()
private val weakReferencesToIdentifiers = HashMap<IdentityWeakReference<Any>, Long>()
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
private val releaseAllFinalizedInstancesRunnable = Runnable {
this.releaseAllFinalizedInstances()
Expand Down Expand Up @@ -112,9 +140,12 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
*/
fun getIdentifierForStrongReference(instance: Any?): Long? {
logWarningIfFinalizationListenerHasStopped()
val identifier = identifiers[instance]
if (instance == null) {
return null
}
val identifier = identifiers[IdentityWeakReference(instance)]
if (identifier != null) {
strongInstances[identifier] = instance!!
strongInstances[identifier] = instance
}
return identifier
}
Expand Down Expand Up @@ -151,14 +182,14 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
/** Retrieves the instance associated with identifier, if present, otherwise `null`. */
fun <T> getInstance(identifier: Long): T? {
logWarningIfFinalizationListenerHasStopped()
val instance = weakInstances[identifier] as java.lang.ref.WeakReference<T>?
val instance = weakInstances[identifier] as IdentityWeakReference<T>?
return instance?.get()
}

/** Returns whether this manager contains the given `instance`. */
fun containsInstance(instance: Any?): Boolean {
logWarningIfFinalizationListenerHasStopped()
return identifiers.containsKey(instance)
return instance != null && identifiers.containsKey(IdentityWeakReference(instance))
}

/**
Expand Down Expand Up @@ -199,8 +230,8 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
if (hasFinalizationListenerStopped()) {
return
}
var reference: java.lang.ref.WeakReference<Any>?
while ((referenceQueue.poll() as java.lang.ref.WeakReference<Any>?).also { reference = it } != null) {
var reference: IdentityWeakReference<Any>?
while ((referenceQueue.poll() as IdentityWeakReference<Any>?).also { reference = it } != null) {
val identifier = weakReferencesToIdentifiers.remove(reference)
if (identifier != null) {
weakInstances.remove(identifier)
Expand All @@ -216,8 +247,8 @@ class ${kotlinInstanceManagerClassName(options)}(private val finalizationListene
require(!weakInstances.containsKey(identifier)) {
"Identifier has already been added: \$identifier"
}
val weakReference = java.lang.ref.WeakReference(instance, referenceQueue)
identifiers[instance] = identifier
val weakReference = IdentityWeakReference(instance, referenceQueue)
identifiers[weakReference] = identifier
weakInstances[identifier] = weakReference
weakReferencesToIdentifiers[weakReference] = identifier
strongInstances[identifier] = instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,39 @@ class ProxyApiTestsPigeonInstanceManager(
fun onFinalize(identifier: Long)
}

private val identifiers = java.util.WeakHashMap<Any, Long>()
private val weakInstances = HashMap<Long, java.lang.ref.WeakReference<Any>>()
// Extends WeakReference and overrides the `equals` and `hashCode` methods using identity rather
// than equality.
//
// Two `IdentityWeakReference`s are equal if either
// 1: `get()` returns the identical nonnull value for both references.
// 2: `get()` returns null for both references and the references are identical.
class IdentityWeakReference<T : Any> : java.lang.ref.WeakReference<T> {
private val savedHashCode: Int

constructor(instance: T) : this(instance, null)

constructor(instance: T, queue: java.lang.ref.ReferenceQueue<T>?) : super(instance, queue) {
savedHashCode = System.identityHashCode(instance)
}

override fun equals(other: Any?): Boolean {
val instance = get()
if (instance != null) {
return other is IdentityWeakReference<*> && other.get() === instance
}
return other === this
}

override fun hashCode(): Int {
return savedHashCode
}
}

private val identifiers = java.util.WeakHashMap<IdentityWeakReference<Any>, Long>()
private val weakInstances = HashMap<Long, IdentityWeakReference<Any>>()
private val strongInstances = HashMap<Long, Any>()
private val referenceQueue = java.lang.ref.ReferenceQueue<Any>()
private val weakReferencesToIdentifiers = HashMap<java.lang.ref.WeakReference<Any>, Long>()
private val weakReferencesToIdentifiers = HashMap<IdentityWeakReference<Any>, Long>()
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
private val releaseAllFinalizedInstancesRunnable = Runnable {
this.releaseAllFinalizedInstances()
Expand Down Expand Up @@ -144,9 +172,12 @@ class ProxyApiTestsPigeonInstanceManager(
*/
fun getIdentifierForStrongReference(instance: Any?): Long? {
logWarningIfFinalizationListenerHasStopped()
val identifier = identifiers[instance]
if (instance == null) {
return null
}
val identifier = identifiers[IdentityWeakReference(instance)]
if (identifier != null) {
strongInstances[identifier] = instance!!
strongInstances[identifier] = instance
}
return identifier
}
Expand Down Expand Up @@ -185,14 +216,14 @@ class ProxyApiTestsPigeonInstanceManager(
/** Retrieves the instance associated with identifier, if present, otherwise `null`. */
fun <T> getInstance(identifier: Long): T? {
logWarningIfFinalizationListenerHasStopped()
val instance = weakInstances[identifier] as java.lang.ref.WeakReference<T>?
val instance = weakInstances[identifier] as IdentityWeakReference<T>?
return instance?.get()
}

/** Returns whether this manager contains the given `instance`. */
fun containsInstance(instance: Any?): Boolean {
logWarningIfFinalizationListenerHasStopped()
return identifiers.containsKey(instance)
return instance != null && identifiers.containsKey(IdentityWeakReference(instance))
}

/**
Expand Down Expand Up @@ -233,9 +264,8 @@ class ProxyApiTestsPigeonInstanceManager(
if (hasFinalizationListenerStopped()) {
return
}
var reference: java.lang.ref.WeakReference<Any>?
while ((referenceQueue.poll() as java.lang.ref.WeakReference<Any>?).also { reference = it } !=
null) {
var reference: IdentityWeakReference<Any>?
while ((referenceQueue.poll() as IdentityWeakReference<Any>?).also { reference = it } != null) {
val identifier = weakReferencesToIdentifiers.remove(reference)
if (identifier != null) {
weakInstances.remove(identifier)
Expand All @@ -251,8 +281,8 @@ class ProxyApiTestsPigeonInstanceManager(
require(!weakInstances.containsKey(identifier)) {
"Identifier has already been added: $identifier"
}
val weakReference = java.lang.ref.WeakReference(instance, referenceQueue)
identifiers[instance] = identifier
val weakReference = IdentityWeakReference(instance, referenceQueue)
identifiers[weakReference] = identifier
weakInstances[identifier] = weakReference
weakReferencesToIdentifiers[weakReference] = identifier
strongInstances[identifier] = instance
Expand Down Expand Up @@ -460,14 +490,36 @@ private class ProxyApiTestsPigeonProxyApiBaseCodec(
return
}

fun logNewInstanceFailure(apiName: String, value: Any, exception: Throwable?) {
Log.w(
"PigeonProxyApiBaseCodec",
"Failed to create new Dart proxy instance of $apiName: $value. $exception")
}

if (value is ProxyApiTestClass) {
registrar.getPigeonApiProxyApiTestClass().pigeon_newInstance(value) {}
registrar.getPigeonApiProxyApiTestClass().pigeon_newInstance(value) {
if (it.isFailure) {
logNewInstanceFailure("ProxyApiTestClass", value, it.exceptionOrNull())
}
}
} else if (value is com.example.test_plugin.ProxyApiSuperClass) {
registrar.getPigeonApiProxyApiSuperClass().pigeon_newInstance(value) {}
registrar.getPigeonApiProxyApiSuperClass().pigeon_newInstance(value) {
if (it.isFailure) {
logNewInstanceFailure("ProxyApiSuperClass", value, it.exceptionOrNull())
}
}
} else if (value is ProxyApiInterface) {
registrar.getPigeonApiProxyApiInterface().pigeon_newInstance(value) {}
registrar.getPigeonApiProxyApiInterface().pigeon_newInstance(value) {
if (it.isFailure) {
logNewInstanceFailure("ProxyApiInterface", value, it.exceptionOrNull())
}
}
} else if (android.os.Build.VERSION.SDK_INT >= 25 && value is ClassWithApiRequirement) {
registrar.getPigeonApiClassWithApiRequirement().pigeon_newInstance(value) {}
registrar.getPigeonApiClassWithApiRequirement().pigeon_newInstance(value) {
if (it.isFailure) {
logNewInstanceFailure("ClassWithApiRequirement", value, it.exceptionOrNull())
}
}
}

when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,97 @@ class InstanceManagerTest {
assertFalse(finalizerRan)
}

@Test
fun containsInstanceAndGetIdentifierForStrongReferenceUseIdentityComparison() {
val instanceManager: ProxyApiTestsPigeonInstanceManager = createInstanceManager()
instanceManager.stopFinalizationListener()

// Create two objects that are equal.
val testString = "aString"
val testObject1 = TestDataClass(testString)
val testObject2 = TestDataClass(testString)
assertEquals(testObject1, testObject2)

val identifier1 = instanceManager.addHostCreatedInstance(testObject1)
assertFalse(instanceManager.containsInstance(testObject2))
assertNull(instanceManager.getIdentifierForStrongReference(testObject2))

val identifier2 = instanceManager.addHostCreatedInstance(testObject2)
assertTrue(instanceManager.containsInstance(testObject1))
assertTrue(instanceManager.containsInstance(testObject2))
assertEquals(identifier1, instanceManager.getIdentifierForStrongReference(testObject1))
assertEquals(identifier2, instanceManager.getIdentifierForStrongReference(testObject2))
}

@Test
fun addingTwoDartCreatedInstancesThatAreEqual() {
val instanceManager: ProxyApiTestsPigeonInstanceManager = createInstanceManager()
instanceManager.stopFinalizationListener()

// Create two objects that are equal.
val testString = "aString"
val testObject1 = TestDataClass(testString)
val testObject2 = TestDataClass(testString)
assertEquals(testObject1, testObject2)

instanceManager.addDartCreatedInstance(testObject1, 0)
instanceManager.addDartCreatedInstance(testObject2, 1)

assertEquals(testObject1, instanceManager.getInstance(0))
assertEquals(testObject2, instanceManager.getInstance(1))
assertEquals(0L, instanceManager.getIdentifierForStrongReference(testObject1))
assertEquals(1L, instanceManager.getIdentifierForStrongReference(testObject2))
}

@Test
fun identityWeakReferencesAreEqualWithSameInstance() {
val testObject = Any()

assertEquals(
ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject),
ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject))
}

@Test
fun identityWeakReferenceRemainsEqualAfterGetReturnsNull() {
var testObject: Any? = Any()

val reference = ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject!!)

// To allow for object to be garbage collected.
@Suppress("UNUSED_VALUE")
testObject = null
Runtime.getRuntime().gc()

assertNull(reference.get())
assertEquals(reference, reference)
}

@Test
fun identityWeakReferencesAreNotEqualAfterGetReturnsNull() {
var testObject1: Any? = Any()
var testObject2: Any? = Any()

val reference1 = ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject1!!)
val reference2 = ProxyApiTestsPigeonInstanceManager.IdentityWeakReference(testObject2!!)

// To allow for object to be garbage collected.
@Suppress("UNUSED_VALUE")
testObject1 = null
testObject2 = null
Runtime.getRuntime().gc()

assertNull(reference1.get())
assertNull(reference2.get())
assertFalse(reference1 == reference2)
}

private fun createInstanceManager(): ProxyApiTestsPigeonInstanceManager {
return ProxyApiTestsPigeonInstanceManager.create(
object : ProxyApiTestsPigeonInstanceManager.PigeonFinalizationListener {
override fun onFinalize(identifier: Long) {}
})
}

data class TestDataClass(val value: String)
}
2 changes: 1 addition & 1 deletion packages/pigeon/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: pigeon
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22
version: 26.0.1 # This must match the version in lib/src/generator_tools.dart
version: 26.0.2 # This must match the version in lib/src/generator_tools.dart

environment:
sdk: ^3.7.0
Expand Down