Bug 34242 - Objective-C marshalling exception while scanning for Bluetooth low-energy peripherals
Summary: Objective-C marshalling exception while scanning for Bluetooth low-energy per...
Status: ASSIGNED
Alias: None
Product: iOS
Classification: Xamarin
Component: Xamarin.iOS.dll (show other bugs)
Version: XI 8.10
Hardware: Macintosh Mac OS
: --- normal
Target Milestone: Future Cycle (TBD)
Assignee: Rolf Bjarne Kvinge [MSFT]
URL:
Depends on:
Blocks:
 
Reported: 2015-09-24 11:11 UTC by Mark Pevec
Modified: 2017-02-01 21:34 UTC (History)
11 users (show)

See Also:
Tags:
Is this bug a regression?: ---
Last known good build:


Attachments
ViewController.cs for BLE test app (1.57 KB, application/octet-stream)
2015-09-25 16:08 UTC, Mark Pevec
Details
Test Case (11.55 KB, application/zip)
2015-10-06 10:59 UTC, John Miller [MSFT]
Details

Description Mark Pevec 2015-09-24 11:11:35 UTC
Hello,

We (Complete Innovations Inc, a Xamarin business licensee) have encountered an issue in our Xamarin.iOS app and were wondering if you have any suggestions to assist in debugging it.

This issue is an Objective-C marshalling exception while scanning for BLE devices (BLE = Bluetooth Low Energy, this issue is easier to reproduce when debugging on older iOS devices such as iPhone4 or iPhone5 while BLE scanning in an environment with many BLE peripherals nearby but can’t be reproduced when testing on the same device with a smaller number of BLE peripherals nearby, which leads me to believe it is tied to the number of BLE devices found while scanning).  The exception is an Objective-C marshalling error that seems like it could potentially be a bug in the Xamarin framework.  

The problem occurs when a CBPeripheral object (which is created during BLE scanning when a BLE peripheral is discovered) is created.   There is an associated CBPeriperhalDelegate object (to handle iOS runtime events related to the BLE peripheral) which is also created and it appears to me that there may be a race case where the iOS side object is occasionally created and receives an event before the Xamarin.iOS can create the corresponding .NET object.  Normally everything works fine, but after scanning enough devices quickly enough the possible race case may be encountered.   It occurs when we attempt to set or access certain CBPeripheral properties on the object returned in the CBPeripheralEventArgs of the event returned by CBCentralManager.DiscoveredPeripheral.

See my September 23 post to the following forum for the stack trace: https://forums.xamarin.com/discussion/20025/failed-to-marshal-the-objective-c-object?
Comment 1 Mark Pevec 2015-09-24 11:16:48 UTC
sorry that should be the CBDiscoveredPeripheralEventArgs, not CBPeripheralEventArgs
Comment 2 Sebastien Pouliot 2015-09-24 21:43:05 UTC
There's nothing specific for BLE (or bluetooth) in our bindings. However a race condition is always possible.

Can you provide us with a test case that can reproduce this ? Ideally we would be able to reproduce it (with the test case) but we might be able to get some clue from the code. Thanks!
Comment 3 Mark Pevec 2015-09-25 14:29:07 UTC
Though I'm able to reproduce the issue with our app, we can't provide that to you.  I can't reproduce the issue on a stripped down app and I was not able to reproduce the issue with a simple app that just does BLE scanning (it seems the issue requires some combination of memory allocation, GC, and BLE scanning).
Comment 4 Mark Pevec 2015-09-25 16:08:03 UTC
Created attachment 13077 [details]
ViewController.cs for BLE test app

BLE test app created by opening a new single-view iOS app template and modifying the ViewController.cs file
Comment 5 Mark Pevec 2015-09-25 16:09:32 UTC
I was just able to reproduce the problem on a simple BLE scanning test app (created by starting a new solution using the iOS single-view app template) and just modifying the ViewController.cs file as attached.  The environment had approx. 20 nearby BLE peripherals and it took 10 minutes of running the app and scanning before encountering the exception.
Comment 6 John Miller [MSFT] 2015-10-06 10:59:16 UTC
Created attachment 13202 [details]
Test Case

Attached a test project ready to go with the ViewController attached.
Comment 7 John Miller [MSFT] 2015-10-06 10:59:30 UTC
Updating to NEW per comment #6.
Comment 8 Rolf Bjarne Kvinge [MSFT] 2016-02-08 10:58:45 UTC
I can reproduce this.
Comment 9 Rolf Bjarne Kvinge [MSFT] 2016-02-08 11:58:37 UTC
This is what happens:

1. OnDiscoveredPeripheral is called, a CBDiscoveredPeripheralEventArgs instance containing a CBPeripheral instance is passed in.
2. OnDiscoveredPeripheral adds an event handler to the CBPeripheral instance. This will create an instance of an internal delegate (_CBPeripheralDelegate) that handles the event, and store this object as a field on the CBPeripheral instance. In this process the CBPeripheral instance is "marked dirty", which means that the GC will only free it if native code does not reference the instance anymore.
3. Native code creates a weak reference to the CBPeripheral instance.
4. The GC runs, and sees that native code does not retain the CBPeripheral instance (there's only a weak reference, which is invisible to the GC), and thus the CBPeripheral instance is scheduled for collection. By extension the _CBPeripheralDelegate instance is also scheduled for collection, and subsequently collected. Apparently only the _CBPeripheralDelegate instance is collected, and not the CBPeripheral instance (the GC is allowed to not collect everything; this is not a bug).
5. Native code reads the weak reference to the CBPeripheral instance, and calls OnDiscoveredPeripheral again.
6. OnDiscoveredPeripheral tries to add an event handler again, but runs into the ObjC marshalling exception because the internal delegate instance has been collected.

The workaround is to keep the CBPeripheral instances you care about visible to the GC in managed code (put in them in a list for instance). This way step 4 above doesn't happen.
Comment 10 Rolf Bjarne Kvinge [MSFT] 2016-02-08 12:19:55 UTC
Complete test solution: https://github.com/rolfbjarne/TestApp/tree/bug34242

Unfortunately I'm not sure we can fix this (or if there is something to fix: after all you're attaching an event to an object that iOS may or may not keep alive; if you want consistent behavior you'll have to make sure the CBPeripheral instance doesn't go away anyway - i.e. the workaround I mentioned is the actual fix).

One idea is to make sure the delegate isn't collected by the GC before the CBPeripheral instance, but that could introduce cycles (if the delegate contains a reference to the CBPeripheral instance, none would ever be collected).

I'll have to think a bit more about this.
Comment 11 Christian Giambalvo 2016-03-16 12:34:00 UTC
Hi,

we are hitting the exact same problem except that it happens on the CBCentralManager.Disconnected event.

We are keeping the CBPeripheral object as a property of another object which is within a list. So GC shouldn't be a problem.
Is there a binding problem? While reading the stacktrace it looks like the the code tries to create an instance of the missing delegate but failes because of the missing ctor.

This is the stacktrace:

critical:   at <unknown> <0xffffffff>
critical:   at (wrapper managed-to-native) ObjCRuntime.Messaging.IntPtr_objc_msgSend (intptr,intptr) <0xffffffff>
critical:   at ObjCRuntime.Class.GetClassForObject (intptr) [0x00000] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/ObjCRuntime/Class.cs:135
critical:   at ObjCRuntime.Runtime.MissingCtor (intptr,intptr,System.Type,ObjCRuntime.Runtime/MissingCtorResolution) [0x00000] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/ObjCRuntime/Runtime.cs:742
critical:   at ObjCRuntime.Runtime.ConstructNSObject<T_REF> (intptr,System.Type,ObjCRuntime.Runtime/MissingCtorResolution) <0x0006f>
critical:   at ObjCRuntime.Runtime.ConstructNSObject (intptr,intptr,ObjCRuntime.Runtime/MissingCtorResolution) [0x00013] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/ObjCRuntime/Runtime.cs:771
critical:   at ObjCRuntime.Runtime.GetNSObject (intptr,ObjCRuntime.Runtime/MissingCtorResolution,bool) [0x00022] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/ObjCRuntime/Runtime.cs:869
critical:   at ObjCRuntime.Runtime.GetNSObject (intptr) [0x00000] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/ObjCRuntime/Runtime.cs:857
critical:   at CoreBluetooth.CBPeripheral.get_WeakDelegate () [0x0000b] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/build/ios/native/CoreBluetooth/CBPeripheral.g.cs:347
critical:   at CoreBluetooth.CBPeripheral.get_Delegate () [0x00000] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/build/ios/native/CoreBluetooth/CBPeripheral.g.cs:230
critical:   at CoreBluetooth.CBPeripheral.EnsureCBPeripheralDelegate () <0x0001b>
critical:   at CoreBluetooth.CBPeripheral.remove_DiscoveredService (System.EventHandler`1<Foundation.NSErrorEventArgs>) [0x00000] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/build/ios/native/CoreBluetooth/CBPeripheral.g.cs:586
critical:   at TestApp.iOSConnectionManager.DisconnectedPeripheral (object,CoreBluetooth.CBPeripheralErrorEventArgs)
critical:   at CoreBluetooth.CBCentralManager/_CBCentralManagerDelegate.DisconnectedPeripheral (CoreBluetooth.CBCentralManager,CoreBluetooth.CBPeripheral,Foundation.NSError) [0x00015] in /Users/builder/data/lanes/2966/58ba2bc3/source/maccore/src/build/ios/native/CoreBluetooth/CBCentralManager.g.cs:484
critical:   at (wrapper runtime-invoke) object.runtime_invoke_dynamic (intptr,intptr,intptr,intptr) <0xffffffff>



=== Xamarin Studio ===

Version 5.10.3 (build 26)
Installation UUID: 06eda8fd-7ac2-4db4-bef5-16cfc968f3b6
Runtime:
	Mono 4.2.3 (explicit/832de4b)
	GTK+ 2.24.23 (Raleigh theme)

	Package version: 402030004

=== Xamarin.Profiler ===

Version: 0.14.0.0
Location: /Applications/Xamarin Profiler.app/Contents/MacOS/Xamarin Profiler

=== Xamarin.Android ===

Version: 6.0.2.1 (Business Edition)
Android SDK: /Users/dev/android/android-sdks
	Supported Android versions:
		2.3 (API level 10)
		4.2 (API level 17)
		4.3 (API level 18)
		4.4 (API level 19)
		5.0 (API level 21)
		5.1 (API level 22)
		6.0 (API level 23)

SDK Tools Version: 24.4
SDK Platform Tools Version: 23.0.1
SDK Build Tools Version: 23.0.2

Java SDK: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
java version "1.6.0_65"
Java(TM) SE Runtime Environment (build 1.6.0_65-b14-466.1-11M4716)
Java HotSpot(TM) 64-Bit Server VM (build 20.65-b04-466.1, mixed mode)

Android Designer EPL code available here:
https://github.com/xamarin/AndroidDesigner.EPL

=== Xamarin Android Player ===

Not Installed

=== Apple Developer Tools ===

Xcode 7.2.1 (9548.1)
Build 7C1002

=== Xamarin.Mac ===

Not Installed

=== Xamarin.iOS ===

Version: 9.4.2.27 (Business Edition)
Hash: 58ba2bc
Branch: master
Build date: 2016-03-03 09:05:19-0500

=== Build Information ===

Release ID: 510030026
Git revision: ac9b7fcba9ee92ac30c8eb90f20c2228ce033efa
Build date: 2016-03-01 18:02:09-05
Xamarin addins: 633fde3bf405e3c402a51980976c431c204cf4f6
Build lane: monodevelop-lion-cycle6-c6sr2

=== Operating System ===

Mac OS X 10.10.5
Darwin wstuchgi01 14.5.0 Darwin Kernel Version 14.5.0
    Tue Sep  1 21:23:09 PDT 2015
    root:xnu-2782.50.1~1/RELEASE_X86_64 x86_64
Comment 12 Rolf Bjarne Kvinge [MSFT] 2016-03-16 12:43:09 UTC
@Christian, can you show me your CoreBluetooth code and the code where you keep the CBPeripheral instance alive?
Comment 13 Christian Giambalvo 2016-03-16 13:17:58 UTC
Unfortunately i cannot as the codebase is quite big and the project is not made public till now.

I give you a summary about the call flow so you can get an idea.

1. Upon CBCentralManager.DiscoveredPeripheral an internal object is created, let's call this object "DeviceObject". The DeviceObject contains an "object" auto property to which the CBPeripheral object is assigned to and after setting all properties the DeviceObject is stored in a collection of type Dictionary<string, DeviceObject>, where 'string' is the unique identifier. 

2. We initiate a connection to that assigned CBPeripheral by calling CBCentralManager.ConnectPeripheral.

3. On CBCentralManager.ConnectedPeripheral we add an event handler to CBPeripheral.DiscoveredService and call CBPeripheral.DiscoverServices. 

4. On CBCentralManager.DiscoveredService we add an event handler to CBPeripheral.DiscoveredCharacteristic and call CBPeripheral.DiscoverCharacteristics. 

5. After doing what every we wanted to do with the discovered characteristics we call CBCentralManager.CancelPeripheralConnection.

6. On CBCentralManager.DisconnectedPeripheral we remove the assigned event handlers and at this point the app sometimes crashes with the stacktrace from previous post.

In all steps the DeviceObject and the assigned CBPeripheral object are still alive and valid, except for the Delegate being destroyed for some reason.

We had a hard time making the whole codebase failure proof as we often get invalid objects passed into the callbacks. 
Sometimes the CBPeripheralEventArgs.CBPeripheral or CBPeripheral.Identifier properties are null so we have a lot of null checks now. 
We also observe the iOS BT stack crashing if we put to much load on it. Meaning that while testing with just a few peripherals the stack stays stable but a soon as we test with > 80 Peripherals (just scanning all of them and connecting/writing/disconnecting one by one) the stack crashes in 1 out of 4 tries. 
This is really frustrating especially that we cannot recover from stack crashes from within the application and the user has to turn the bluetooth off and on by himself.

I hope you can get something valuable out of the provided informations.
Comment 14 Christian Giambalvo 2016-03-16 13:25:27 UTC
Btw: 

As a workaround i don't remove the event handlers in CBCentralManager.DisconnectedPeripheral anymore. This is not a solution but at least the app doesn't crash anymore (till now :D ).
Comment 15 Rolf Bjarne Kvinge [MSFT] 2016-03-18 12:38:02 UTC
@Christian, from your description it looks like you're doing everything right. I can try and figure out what's actually going on, but I'll need a test project so that I can reproduce it myself.
Comment 16 Ken Sykora 2017-02-01 21:34:56 UTC
I was able to reproduce this issue. Tracking the CBPeripheral objects in a static list as I encounter them and never removing them from the list solved the issue.

Anxiously awaiting fix so I can remove an intentional memory leak from my application.

Note You need to log in before you can comment on or make changes to this bug.