-
Notifications
You must be signed in to change notification settings - Fork 29.2k
Description
Steps to reproduce
Hi, I am dealing with a recurring crash with Pigeon's new Proxy APIs when creating multiple instances of an object that takes another PigeonApi object in its constructor.
I've been able to create a reproducible example app here: https://github.com/jointhejourney/flutter-proxy-api-crash
The reproducible example will have a button that you can trigger the crash. it will create a bunch of proxy API instances in paralell, and after some time (usually 5~10s) the app will crash.
Note: I can consistently reproduce this on my iPhone 15 Pro. If the app doesn't crash for you, just increase the parallelSimulationsCount
count from 500 to 2000.
Here's how to reproduce the crash:
Using Pigeon's new @ProxyApi()
, set up the following apis:
@ProxyApi(swiftOptions: SwiftProxyApiOptions(import: 'Foundation'))
abstract class NSAttributedString extends NSObject {
NSAttributedString({String? string});
}
@ProxyApi(swiftOptions: SwiftProxyApiOptions(import: 'Foundation'))
abstract class NSMutableAttributedString extends NSAttributedString {
NSMutableAttributedString(NSAttributedString attributedString);
}
@ProxyApi()
abstract class NSAttributedStringGenerator extends NSObject {
@static
late final NSAttributedStringGenerator instance;
NSAttributedString? generate();
}
On the native iOS side, just set up simple delegates for it:
class NSAttributedStringGenerator {
func generate() -> NSAttributedString {
return NSAttributedString(string: "Hello, World!")
}
}
class ProxiesApiDelegate: ProxiesPigeonProxyApiDelegate {
func pigeonApiNSAttributedString(_ registrar: ProxiesPigeonProxyApiRegistrar)
-> PigeonApiNSAttributedString
{
return PigeonApiNSAttributedString(
pigeonRegistrar: registrar, delegate: NSAttributedStringProxyApiDelegate())
}
func pigeonApiNSMutableAttributedString(_ registrar: ProxiesPigeonProxyApiRegistrar)
-> PigeonApiNSMutableAttributedString
{
return PigeonApiNSMutableAttributedString(
pigeonRegistrar: registrar, delegate: NSMutableAttributedStringProxyApiDelegate())
}
func pigeonApiNSAttributedStringGenerator(_ registrar: ProxiesPigeonProxyApiRegistrar)
-> PigeonApiNSAttributedStringGenerator
{
return PigeonApiNSAttributedStringGenerator(
pigeonRegistrar: registrar, delegate: NSAttributedStringGeneratorProxyApiDelegate())
}
}
class NSAttributedStringProxyApiDelegate: PigeonApiDelegateNSAttributedString {
func pigeonDefaultConstructor(pigeonApi: PigeonApiNSAttributedString, string: String?) throws
-> NSAttributedString
{
return NSAttributedString(string: string ?? "")
}
}
class NSMutableAttributedStringProxyApiDelegate: PigeonApiDelegateNSMutableAttributedString {
func pigeonDefaultConstructor(
pigeonApi: PigeonApiNSMutableAttributedString, attributedString: NSAttributedString
) throws -> NSMutableAttributedString {
return NSMutableAttributedString(attributedString: attributedString)
}
}
class NSAttributedStringGeneratorProxyApiDelegate: PigeonApiDelegateNSAttributedStringGenerator {
func instance(pigeonApi: PigeonApiNSAttributedStringGenerator) throws
-> NSAttributedStringGenerator
{
return NSAttributedStringGenerator()
}
func generate(
pigeonApi: PigeonApiNSAttributedStringGenerator, pigeonInstance: NSAttributedStringGenerator
) throws -> NSAttributedString? {
return pigeonInstance.generate()
}
}
Finally, simulate the crash by creating hundreds of instances in parallel. After a few seconds, it will crash.
Future<void> simulateCrash() async {
final generator = await NSAttributedStringGenerator.instance.generate();
if (generator == null) {
throw Exception('Generator is null');
}
final attributedString = NSMutableAttributedString(
attributedString: generator,
);
print('attributedString: $attributedString');
}
void runCrashLoop() async {
final parallelSimulations = List.generate(
500,
(_) => simulateCrash(),
);
await Future.wait(parallelSimulations);
// Schedule the next iteration without blocking the main thread
Future.microtask(_runCrashLoop);
}
runCrashLoop();
Expected results
It should not crash.
Actual results
Crash with:
Failed to find instance with identifier: 74050
Could not cast value of type 'NSNull' (0x1f176a748) to 'NSAttributedString' (0x1f176a358).
warning: could not execute support code to read Objective-C class data in the process. This may reduce the quality of type information available.
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x18dc45380)
An abort signal terminated the process. Such crashes often happen because of an uncaught exception or unrecoverable error or calling the abort() function.
The crash occurs on the dev.flutter.pigeon.pigeon_crash_example.NSMutableAttributedString.pigeon_defaultConstructor
channel, during this casting:
let attributedStringArg = args[1] as! NSAttributedString
I'll confess I've lost a few nights troubleshooting this, but hopefully all the info above can help identify a fix. 😅
I've noticed that the WeakReference
's finalizer for the NSAttributedString returned by the .generate() function call is getting called shortly after the addHostCreatedInstance() -> _addInstanceWithIdentifier()
functions are called.
So by the time the code for the constructor of NSMutableAttributedString() is called, the strong reference in the native side doesn't exist anymore, hence the "Failed to find instance with identifier" error and subsequent crash.
Interestingly, if you run the code above sequentially, I am unable to reproduce the crash. I was only able to find this bug because our app can use quite a bit of memory, and Dart is triggering the garbage collector between these calls... something like this:
Future<void> simulateCrash() async {
// Native send the NSAttributedString to Dart during it's write() codec operations
// Dart adds the WeakReference to the weak references array
// Dart immediately garbage collects the WeakReference (finalizer is getting called)
// Dart notifies native side to remove the strong reference to the NSAttributedString object
// Native removes the reference
// However, Dart has kept the instance ID in the _identifiers expando
final generator = await NSAttributedStringGenerator.instance.generate();
// Native finally responds with the NSAttributedString object, but now it's not stored in Pigeon's instance manager any longer
// generator will not be null because the native side sends the actual object as a result of the generate() call, not the identifier
if (generator == null) {
throw Exception('Generator is null');
}
// Dart notifies native side a new NSMutableAttributedString instance was created, with the NSMutableAttributedString object as the first arg and the ID of the NSAttributedString instance that is still in the _identifiers Expando
// Native tries to find the NSAttributedString object in it's strong references, doesn't find it, and tries to cast NSNull to NSAttributedString which causes the crash
final attributedString = NSMutableAttributedString(
attributedString: generator,
);
}
Code sample
Code sample
Please see the reproducible app here: https://github.com/jointhejourney/flutter-proxy-api-crash
Logs
Logs
Failed to find instance with identifier: 74050
Could not cast value of type 'NSNull' (0x1f176a748) to 'NSAttributedString' (0x1f176a358).
warning: could not execute support code to read Objective-C class data in the process. This may reduce the quality of type information available.
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x18dc45380)
An abort signal terminated the process. Such crashes often happen because of an uncaught exception or unrecoverable error or calling the abort() function.
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.29.1, on macOS 15.1 24B83 darwin-arm64, locale en-US) [314ms]
• Flutter version 3.29.1 on channel stable at /Users/gabriel/fvm/versions/3.29.1
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 09de023485 (10 weeks ago), 2025-02-28 13:44:05 -0800
• Engine revision 871f65ac1b
• Dart version 3.7.0
• DevTools version 2.42.2
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [2.1s]
• Android SDK at /Users/gabriel/Library/Android/sdk
• Platform android-35, build-tools 35.0.0
• ANDROID_HOME = /Users/gabriel/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.1) [600ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16B40
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [11ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2024.2) [11ms]
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
[✓] VS Code (version 1.99.0) [10ms]
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.108.0
[✓] Connected device (4 available) [7.0s]
• iPhone (mobile) • 0000 • ios • iOS 18.4.1 22E252
[✓] Network resources [157ms]
• All expected network resources are available.
• No issues found!