Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[net10.0] [UIKit] Fix a GC race in UIBarButtonItem. #21572

Merged
merged 3 commits into from
Nov 6, 2024

Conversation

rolfbjarne
Copy link
Member

@rolfbjarne rolfbjarne commented Nov 4, 2024

Rewrite how we handle events/callbacks for UIBarButtonItem: there's no need for a special class to receive the click events, we can use the same UIBarButtonItem instance through a category.

This simplifies the code a lot, and should also fix this random exception:

 Failed to marshal the Objective-C object 0x2800fcc00 (type: UIKit_UIBarButtonItem_Callback). Could not find an existing managed instance for this object, nor was it possible to create a new managed instance (because the type 'UIKit.UIBarButtonItem+Callback' does not have a constructor that takes one NativeHandle argument).
  at ObjCRuntime.Runtime.MissingCtor(IntPtr, IntPtr, Type, Runtime.MissingCtorResolution, IntPtr, RuntimeMethodHandle) + 0x1dc
  at ObjCRuntime.Runtime.ConstructNSObject[T](IntPtr, Type, Runtime.MissingCtorResolution, IntPtr, RuntimeMethodHandle) + 0xfc
  at ObjCRuntime.Runtime.ConstructNSObject(IntPtr, IntPtr, Runtime.MissingCtorResolution) + 0x74
  at UIKit.UIBarButtonItem.get_Target() + 0x64
  at UIKit.UIBarButtonItem..ctor(UIBarButtonSystemItem, NSObject, Selector) + 0x94
  at UIKit.UIBarButtonItem..ctor(UIBarButtonSystemItem, EventHandler) + 0x90

This happens because we did something equivalent to this:

 public UIBarButtonItem (UIBarButtonSystemItem systemItem, EventHandler handler)
      : base (systemItem, new Callback (), actionSel)
 {
      instanceField = (Callback) Target;
 }

The problem is that the Target property on the native UIBarButtonItem (which is set from the second argument to the base constructor, the "new Callback ()" code) is defined with ArgumentSemantic.Assign, so it won't be retained. This means that if the GC runs between the creation of the native UIBarButtonItem, and fetching the Target property to assign it to the instance field later on in the managed constructor, the GC is free to collect the managed Callback instance, and then, when the code tries to fetch the Target property, the exception occurs because the managed instance has already been collected.

This was reported on Discord (thanks @tipa!): https://discord.com/channels/732297728826277939/732297808148824115/1301578382248509552

Also a few other fixes:

  • Enable nullability.
  • Use [DynamicDependencyAttribute] instead of [Preserve].
  • Remove watchOS logic.
  • Don't trim away the [Extension] attribute unless we're using a static registrar, because the dynamic registrar needs it.

Rewrite how we handle events/callbacks for UIBarButtonItem: there's no need
for a special class to receive the click events, we can use the same
UIBarButtonItem instance.

This simplifies the code a lot, and should also fix this random exception:

     Failed to marshal the Objective-C object 0x2800fcc00 (type: UIKit_UIBarButtonItem_Callback). Could not find an existing managed instance for this object, nor was it possible to create a new managed instance (because the type 'UIKit.UIBarButtonItem+Callback' does not have a constructor that takes one NativeHandle argument).
      at ObjCRuntime.Runtime.MissingCtor(IntPtr, IntPtr, Type, Runtime.MissingCtorResolution, IntPtr, RuntimeMethodHandle) + 0x1dc
      at ObjCRuntime.Runtime.ConstructNSObject[T](IntPtr, Type, Runtime.MissingCtorResolution, IntPtr, RuntimeMethodHandle) + 0xfc
      at ObjCRuntime.Runtime.ConstructNSObject(IntPtr, IntPtr, Runtime.MissingCtorResolution) + 0x74
      at UIKit.UIBarButtonItem.get_Target() + 0x64
      at UIKit.UIBarButtonItem..ctor(UIBarButtonSystemItem, NSObject, Selector) + 0x94
      at UIKit.UIBarButtonItem..ctor(UIBarButtonSystemItem, EventHandler) + 0x90

This happens because we did something equivalent to this:

	public UIBarButtonItem (UIBarButtonSystemItem systemItem, EventHandler handler)
		: this (systemItem, new Callback (), actionSel)
	{
		instanceField = (Callback) Target;
	}

The problem is that the Target property on the native UIBarButtonItem is
defined with ArgumentSemantic.Assign, so it won't be retained. This means that
if the GC runs between the creation of the native UIBarButtonItem, and
fetching the Target property to assign it to the instance field later on in
the managed constructor, the GC is free to collect the managed Callback
instance, and then, when the code tries to fetch the Target property, the
exception occurs.

This was reported on Discord (thanks @tipa!): https://discord.com/channels/732297728826277939/732297808148824115/1301578382248509552
The partial-static registrar is still also partially dynamic.
@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [CI Build] Tests on macOS M1 - Mac Monterey (12) passed 💻

All tests on macOS M1 - Mac Monterey (12) passed.

Pipeline on Agent
Hash: d918f28d8e43f83ce0ec5fbac82aa1d92d2d2f77 [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [CI Build] Tests on macOS X64 - Mac Sonoma (14) passed 💻

All tests on macOS X64 - Mac Sonoma (14) passed.

Pipeline on Agent
Hash: d918f28d8e43f83ce0ec5fbac82aa1d92d2d2f77 [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [CI Build] Tests on macOS M1 - Mac Ventura (13) passed 💻

All tests on macOS M1 - Mac Ventura (13) passed.

Pipeline on Agent
Hash: d918f28d8e43f83ce0ec5fbac82aa1d92d2d2f77 [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

✅ API diff for current PR / commit

.NET (No breaking changes)

✅ API diff vs stable

.NET (No breaking changes)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: d918f28d8e43f83ce0ec5fbac82aa1d92d2d2f77 [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [CI Build] Windows Integration Tests passed 💻

All Windows Integration Tests passed.

Pipeline on Agent
Hash: d918f28d8e43f83ce0ec5fbac82aa1d92d2d2f77 [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2
Copy link
Collaborator

🚀 [CI Build] Test results 🚀

Test results

✅ All tests passed on VSTS: test results.

🎉 All 101 tests passed 🎉

Tests counts

✅ cecil: All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (iOS): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (MacCatalyst): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (macOS): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (Multiple platforms): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (tvOS): All 1 tests passed. [attempt 4] Html Report (VSDrops) Download
✅ framework: All 2 tests passed. Html Report (VSDrops) Download
✅ fsharp: All 4 tests passed. Html Report (VSDrops) Download
✅ generator: All 3 tests passed. Html Report (VSDrops) Download
✅ interdependent-binding-projects: All 4 tests passed. Html Report (VSDrops) Download
✅ introspection: All 4 tests passed. Html Report (VSDrops) Download
✅ linker: All 40 tests passed. Html Report (VSDrops) Download
✅ monotouch (iOS): All 7 tests passed. Html Report (VSDrops) Download
✅ monotouch (MacCatalyst): All 8 tests passed. Html Report (VSDrops) Download
✅ monotouch (macOS): All 9 tests passed. Html Report (VSDrops) Download
✅ monotouch (tvOS): All 7 tests passed. Html Report (VSDrops) Download
✅ msbuild: All 2 tests passed. Html Report (VSDrops) Download
✅ xcframework: All 4 tests passed. Html Report (VSDrops) Download
✅ xtro: All 1 tests passed. Html Report (VSDrops) Download

Pipeline on Agent
Hash: d918f28d8e43f83ce0ec5fbac82aa1d92d2d2f77 [PR build]

@rolfbjarne rolfbjarne marked this pull request as ready for review November 5, 2024 11:20
@rolfbjarne rolfbjarne requested a review from dalexsoto as a code owner November 5, 2024 11:20
@rolfbjarne rolfbjarne merged commit 8a9efb9 into net10.0 Nov 6, 2024
17 checks passed
@rolfbjarne rolfbjarne deleted the dev/rolf/net10.0-barbuttonitem branch November 6, 2024 09:28
@tipa
Copy link

tipa commented Nov 10, 2024

@rolfbjarne is there by any chance a simple workaround that could resolve the crash until .net10 is released? I don't know if it's because of Xcode 16 and the new tooling, or because I recently refactored my ViewController initialization logic or because I am using a new way to log my crash reports, but I am seeing this crash more frequently in the last few days.
It also appears not to be super-random, I see that it crashed twice for the same user within a timeframe of 30 minutes (on an iPad with iOS 17.5.1).

@rolfbjarne
Copy link
Member Author

@tipa I haven't tried this, but subclassing UIBarButtonItem and doing something like this might work:

public class MyUIBarButtonItem : UIBarButtonItem {
	const string MySelector = "myUIBarButtonItemCallback:";
	static Selector actionSel = new Selector (MySelector);

	public MyUIBarButtonItem (UIImage image, UIBarButtonItemStyle style, EventHandler handler)
		: base (image, style, this, actionSel))
	{

	}

	[Export (MySelector)]
	void Call (NSObject sender)
	{
		OnClicked (sender);
	}

	EventHandler? my_clicked;
	internal void OnClicked (NSObject sender)
	{
		if (my_clicked is not null)
			my_clicked (sender, EventArgs.Empty);
	}

	public event EventHandler MyClicked {
		add {
			if (my_clicked is null) {
				Target = this;
				this.Action = actionSel;
				MarkDirty ();
			}

			my_clicked += value;
		}

		remove {
			my_clicked -= value;
		}
	}
}

@tipa
Copy link

tipa commented Nov 13, 2024

@rolfbjarne thanks for that workaround, could have thought of that myself... :D
Your code almost worked out of the box, just couldn't pass this to the base class constructor and the handler parameter wasn't used, this seems to work:

public MyUIBarButtonItem(UIBarButtonSystemItem style, EventHandler handler) : base(style, null, actionSel)
{
    Target = this;
    clicked += handler;
}

Should I also call MarkDirty, like in the source code?
https://github.com/xamarin/xamarin-macios/blob/e773f04cd09e5a41013cfe8a25af830b23c18ca5/src/UIKit/UIBarButtonItem.cs#L41-L47

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants