Skip to content

Commit 2dff621

Browse files
[pigeon] Prevents premature finalization from unused Dart instance for ProxyApis (#9231)
1. The Dart `InstanceManager.addHostCreatedInstance` is only ever used to respond to native calls that create a new instance right before the instance is returned in a host method call or passed in a flutter method call. So, `addHostCreatedInstance` now only adds a strong referenced instance and not the weak referenced instance. This will ensure the weak referenced instance is not created until it is being passed to a user immediately. 2. The Dart `InstanceManager.remove` now asserts that the strong referenced instance isn't removed before the weak referenced instance. This should prevent future scenarios where the user is holding a weak referenced instance while the native side no longer has any reference. Fixes flutter/flutter#168531 ## Pre-Review Checklist [^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 63843bb commit 2dff621

File tree

12 files changed

+169
-49
lines changed

12 files changed

+169
-49
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 25.3.2
2+
3+
* [dart] Fixes null pointer crashes/exceptions caused by premature finalization of Dart instances
4+
for ProxyApis.
5+
16
## 25.3.1
27

38
* [kotlin] Fixes Kotlin InstanceManager not properly removing callbacks from handler.

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

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,17 @@ class $dartInstanceManagerClassName {
110110
///
111111
/// Returns the randomly generated id of the [instance] added.
112112
int addDartCreatedInstance($proxyApiBaseClassName instance) {
113+
assert(getIdentifier(instance) == null);
114+
113115
final int identifier = _nextUniqueIdentifier();
114-
_addInstanceWithIdentifier(instance, identifier);
116+
_identifiers[instance] = identifier;
117+
_weakInstances[identifier] =
118+
WeakReference<$proxyApiBaseClassName>(instance);
119+
_finalizer.attach(instance, identifier, detach: instance);
120+
121+
final $proxyApiBaseClassName copy = instance.pigeon_copy();
122+
_identifiers[copy] = identifier;
123+
_strongInstances[identifier] = copy;
115124
return identifier;
116125
}
117126
@@ -143,9 +152,15 @@ class $dartInstanceManagerClassName {
143152
/// it was removed. Returns `null` if [identifier] was not associated with
144153
/// any strong reference.
145154
///
146-
/// This does not remove the weak referenced instance associated with
147-
/// [identifier]. This can be done with [removeWeakReference].
155+
/// Throws an `AssertionError` if the weak referenced instance associated with
156+
/// [identifier] is not removed first. This can be done with
157+
/// [removeWeakReference].
148158
T? remove<T extends $proxyApiBaseClassName>(int identifier) {
159+
final T? instance = _weakInstances[identifier]?.target as T?;
160+
assert(
161+
instance == null,
162+
'A strong instance with identifier \$identifier is being removed despite the weak reference still existing: \$instance',
163+
);
149164
return _strongInstances.remove(identifier) as T?;
150165
}
151166
@@ -191,24 +206,13 @@ class $dartInstanceManagerClassName {
191206
///
192207
/// Throws assertion error if the instance or its identifier has already been
193208
/// added.
194-
///
195-
/// Returns unique identifier of the [instance] added.
196209
void addHostCreatedInstance($proxyApiBaseClassName instance, int identifier) {
197-
_addInstanceWithIdentifier(instance, identifier);
198-
}
199-
200-
void _addInstanceWithIdentifier($proxyApiBaseClassName instance, int identifier) {
201210
assert(!containsIdentifier(identifier));
202211
assert(getIdentifier(instance) == null);
203212
assert(identifier >= 0);
204213
205214
_identifiers[instance] = identifier;
206-
_weakInstances[identifier] = WeakReference<$proxyApiBaseClassName>(instance);
207-
_finalizer.attach(instance, identifier, detach: instance);
208-
209-
final $proxyApiBaseClassName copy = instance.${classMemberNamePrefix}copy();
210-
_identifiers[copy] = identifier;
211-
_strongInstances[identifier] = copy;
215+
_strongInstances[identifier] = instance;
212216
}
213217
214218
/// Whether this manager contains the given [identifier].

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 = '25.3.1';
18+
const String pigeonVersion = '25.3.2';
1919

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

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ protocol ${instanceManagerFinalizerDelegateName(options)}: AnyObject {
3737
String instanceManagerFinalizerTemplate(InternalSwiftOptions options) => '''
3838
// Attaches to an object to receive a callback when the object is deallocated.
3939
internal final class ${_instanceManagerFinalizerName(options)} {
40-
private static let associatedObjectKey = malloc(1)!
40+
internal static let associatedObjectKey = malloc(1)!
4141
4242
private let identifier: Int64
4343
// Reference to the delegate is weak because the callback should be ignored if the
4444
// `InstanceManager` is deallocated.
45-
private weak var delegate: ${instanceManagerFinalizerDelegateName(options)}?
45+
internal weak var delegate: ${instanceManagerFinalizerDelegateName(options)}?
4646
4747
private init(identifier: Int64, delegate: ${instanceManagerFinalizerDelegateName(options)}) {
4848
self.identifier = identifier
@@ -57,7 +57,11 @@ internal final class ${_instanceManagerFinalizerName(options)} {
5757
}
5858
5959
static func detach(from instance: AnyObject) {
60-
objc_setAssociatedObject(instance, associatedObjectKey, nil, .OBJC_ASSOCIATION_ASSIGN)
60+
let finalizer = objc_getAssociatedObject(instance, associatedObjectKey) as? ${_instanceManagerFinalizerName(options)}
61+
if let finalizer = finalizer {
62+
finalizer.delegate = nil
63+
objc_setAssociatedObject(instance, associatedObjectKey, nil, .OBJC_ASSOCIATION_ASSIGN)
64+
}
6165
}
6266
6367
deinit {
@@ -219,6 +223,10 @@ final class ${swiftInstanceManagerClassName(options)} {
219223
/// The manager will be empty after this call returns.
220224
func removeAllObjects() throws {
221225
lockQueue.sync {
226+
let weakInstancesEnumerator = weakInstances.objectEnumerator()!
227+
while let instance = weakInstancesEnumerator.nextObject() {
228+
${_instanceManagerFinalizerName(options)}.detach(from: instance as AnyObject)
229+
}
222230
identifiers.removeAllObjects()
223231
weakInstances.removeAllObjects()
224232
strongInstances.removeAllObjects()

packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/proxy_api_tests.gen.dart

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,17 @@ class PigeonInstanceManager {
158158
///
159159
/// Returns the randomly generated id of the [instance] added.
160160
int addDartCreatedInstance(PigeonInternalProxyApiBaseClass instance) {
161+
assert(getIdentifier(instance) == null);
162+
161163
final int identifier = _nextUniqueIdentifier();
162-
_addInstanceWithIdentifier(instance, identifier);
164+
_identifiers[instance] = identifier;
165+
_weakInstances[identifier] =
166+
WeakReference<PigeonInternalProxyApiBaseClass>(instance);
167+
_finalizer.attach(instance, identifier, detach: instance);
168+
169+
final PigeonInternalProxyApiBaseClass copy = instance.pigeon_copy();
170+
_identifiers[copy] = identifier;
171+
_strongInstances[identifier] = copy;
163172
return identifier;
164173
}
165174

@@ -191,9 +200,15 @@ class PigeonInstanceManager {
191200
/// it was removed. Returns `null` if [identifier] was not associated with
192201
/// any strong reference.
193202
///
194-
/// This does not remove the weak referenced instance associated with
195-
/// [identifier]. This can be done with [removeWeakReference].
203+
/// Throws an `AssertionError` if the weak referenced instance associated with
204+
/// [identifier] is not removed first. This can be done with
205+
/// [removeWeakReference].
196206
T? remove<T extends PigeonInternalProxyApiBaseClass>(int identifier) {
207+
final T? instance = _weakInstances[identifier]?.target as T?;
208+
assert(
209+
instance == null,
210+
'A strong instance with identifier $identifier is being removed despite the weak reference still existing: $instance',
211+
);
197212
return _strongInstances.remove(identifier) as T?;
198213
}
199214

@@ -244,27 +259,14 @@ class PigeonInstanceManager {
244259
///
245260
/// Throws assertion error if the instance or its identifier has already been
246261
/// added.
247-
///
248-
/// Returns unique identifier of the [instance] added.
249262
void addHostCreatedInstance(
250263
PigeonInternalProxyApiBaseClass instance, int identifier) {
251-
_addInstanceWithIdentifier(instance, identifier);
252-
}
253-
254-
void _addInstanceWithIdentifier(
255-
PigeonInternalProxyApiBaseClass instance, int identifier) {
256264
assert(!containsIdentifier(identifier));
257265
assert(getIdentifier(instance) == null);
258266
assert(identifier >= 0);
259267

260268
_identifiers[instance] = identifier;
261-
_weakInstances[identifier] =
262-
WeakReference<PigeonInternalProxyApiBaseClass>(instance);
263-
_finalizer.attach(instance, identifier, detach: instance);
264-
265-
final PigeonInternalProxyApiBaseClass copy = instance.pigeon_copy();
266-
_identifiers[copy] = identifier;
267-
_strongInstances[identifier] = copy;
269+
_strongInstances[identifier] = instance;
268270
}
269271

270272
/// Whether this manager contains the given [identifier].

packages/pigeon/platform_tests/shared_test_plugin_code/pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ dependencies:
1919
integration_test:
2020
sdk: flutter
2121
mockito: ^5.4.4
22+
23+
dev_dependencies:
24+
leak_tracker: any

packages/pigeon/platform_tests/shared_test_plugin_code/test/instance_manager_test.dart

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// This file specifically tests the test PigeonInstanceManager generated by core_tests.
66

77
import 'package:flutter_test/flutter_test.dart';
8+
import 'package:leak_tracker/leak_tracker.dart';
89
import 'package:shared_test_plugin_code/src/generated/proxy_api_tests.gen.dart';
910

1011
void main() {
@@ -22,7 +23,7 @@ void main() {
2223
expect(instanceManager.getIdentifier(object), 0);
2324
expect(
2425
instanceManager.getInstanceWithWeakReference(0),
25-
object,
26+
isA<CopyableObject>(),
2627
);
2728
});
2829

@@ -106,7 +107,7 @@ void main() {
106107
expect(identical(object, copy), isFalse);
107108
});
108109

109-
test('removeStrongReference', () {
110+
test('remove', () {
110111
final PigeonInstanceManager instanceManager =
111112
PigeonInstanceManager(onWeakReferenceRemoved: (_) {});
112113

@@ -120,20 +121,16 @@ void main() {
120121
expect(instanceManager.containsIdentifier(0), isFalse);
121122
});
122123

123-
test('removeStrongReference removes only strong reference', () {
124+
test('remove throws AssertionError if weak reference still exists', () {
124125
final PigeonInstanceManager instanceManager =
125126
PigeonInstanceManager(onWeakReferenceRemoved: (_) {});
126127

127128
final CopyableObject object = CopyableObject(
128129
pigeon_instanceManager: instanceManager,
129130
);
130131

131-
instanceManager.addHostCreatedInstance(object, 0);
132-
expect(instanceManager.remove(0), isA<CopyableObject>());
133-
expect(
134-
instanceManager.getInstanceWithWeakReference(0),
135-
object,
136-
);
132+
instanceManager.addDartCreatedInstance(object);
133+
expect(() => instanceManager.remove(0), throwsAssertionError);
137134
});
138135

139136
test('getInstance can add a new weak reference', () {
@@ -153,6 +150,48 @@ void main() {
153150
)!;
154151
expect(identical(object, newWeakCopy), isFalse);
155152
});
153+
154+
test('addDartCreatedInstance should add finalizer to original object',
155+
() async {
156+
bool weakReferencedRemovedCalled = false;
157+
final PigeonInstanceManager instanceManager = PigeonInstanceManager(
158+
onWeakReferenceRemoved: (_) {
159+
weakReferencedRemovedCalled = true;
160+
},
161+
);
162+
163+
CopyableObject? object = CopyableObject(
164+
pigeon_instanceManager: instanceManager,
165+
);
166+
167+
instanceManager.addDartCreatedInstance(object);
168+
169+
object = null;
170+
await forceGC();
171+
172+
expect(weakReferencedRemovedCalled, isTrue);
173+
});
174+
175+
test('addHostCreatedInstance should not add finalizer to original object',
176+
() async {
177+
bool weakReferencedRemovedCalled = false;
178+
final PigeonInstanceManager instanceManager = PigeonInstanceManager(
179+
onWeakReferenceRemoved: (_) {
180+
weakReferencedRemovedCalled = true;
181+
},
182+
);
183+
184+
CopyableObject? object = CopyableObject(
185+
pigeon_instanceManager: instanceManager,
186+
);
187+
188+
instanceManager.addHostCreatedInstance(object, 0);
189+
190+
object = null;
191+
await forceGC();
192+
193+
expect(weakReferencedRemovedCalled, isFalse);
194+
});
156195
});
157196
}
158197

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ class InstanceManagerTest {
120120
assertTrue(instanceManager.containsInstance(instance))
121121
}
122122

123+
@Test
124+
fun clearPreventsFinalizationOfWeakInstances() {
125+
var finalizerRan = false
126+
val instanceManager: ProxyApiTestsPigeonInstanceManager =
127+
ProxyApiTestsPigeonInstanceManager.create(
128+
object : ProxyApiTestsPigeonInstanceManager.PigeonFinalizationListener {
129+
override fun onFinalize(identifier: Long) {
130+
finalizerRan = true
131+
}
132+
})
133+
134+
var testObject: Any? = Any()
135+
instanceManager.addDartCreatedInstance(testObject!!, 0)
136+
instanceManager.remove<Any?>(0)
137+
instanceManager.clear()
138+
139+
// To allow for object to be garbage collected.
140+
@Suppress("UNUSED_VALUE")
141+
testObject = null
142+
Runtime.getRuntime().gc()
143+
144+
// Changing this value triggers the callback.
145+
instanceManager.clearFinalizedWeakReferencesInterval = 1000
146+
instanceManager.stopFinalizationListener()
147+
148+
assertNull(instanceManager.getInstance<Any?>(0))
149+
assertFalse(finalizerRan)
150+
}
151+
123152
private fun createInstanceManager(): ProxyApiTestsPigeonInstanceManager {
124153
return ProxyApiTestsPigeonInstanceManager.create(
125154
object : ProxyApiTestsPigeonInstanceManager.PigeonFinalizationListener {

packages/pigeon/platform_tests/test_plugin/darwin/test_plugin/Sources/test_plugin/ProxyApiTests.gen.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ protocol ProxyApiTestsPigeonInternalFinalizerDelegate: AnyObject {
8282

8383
// Attaches to an object to receive a callback when the object is deallocated.
8484
internal final class ProxyApiTestsPigeonInternalFinalizer {
85-
private static let associatedObjectKey = malloc(1)!
85+
internal static let associatedObjectKey = malloc(1)!
8686

8787
private let identifier: Int64
8888
// Reference to the delegate is weak because the callback should be ignored if the
8989
// `InstanceManager` is deallocated.
90-
private weak var delegate: ProxyApiTestsPigeonInternalFinalizerDelegate?
90+
internal weak var delegate: ProxyApiTestsPigeonInternalFinalizerDelegate?
9191

9292
private init(identifier: Int64, delegate: ProxyApiTestsPigeonInternalFinalizerDelegate) {
9393
self.identifier = identifier
@@ -103,7 +103,13 @@ internal final class ProxyApiTestsPigeonInternalFinalizer {
103103
}
104104

105105
static func detach(from instance: AnyObject) {
106-
objc_setAssociatedObject(instance, associatedObjectKey, nil, .OBJC_ASSOCIATION_ASSIGN)
106+
let finalizer =
107+
objc_getAssociatedObject(instance, associatedObjectKey)
108+
as? ProxyApiTestsPigeonInternalFinalizer
109+
if let finalizer = finalizer {
110+
finalizer.delegate = nil
111+
objc_setAssociatedObject(instance, associatedObjectKey, nil, .OBJC_ASSOCIATION_ASSIGN)
112+
}
107113
}
108114

109115
deinit {
@@ -261,6 +267,10 @@ final class ProxyApiTestsPigeonInstanceManager {
261267
/// The manager will be empty after this call returns.
262268
func removeAllObjects() throws {
263269
lockQueue.sync {
270+
let weakInstancesEnumerator = weakInstances.objectEnumerator()!
271+
while let instance = weakInstancesEnumerator.nextObject() {
272+
ProxyApiTestsPigeonInternalFinalizer.detach(from: instance as AnyObject)
273+
}
264274
identifiers.removeAllObjects()
265275
weakInstances.removeAllObjects()
266276
strongInstances.removeAllObjects()

0 commit comments

Comments
 (0)