This blog post is about producing better .NET bindings for using Objective-C libraries on Xamarin.iOS and Xamarin.Mac. Read the series introduction to get a better idea of why this is important and how it can save you time and headaches.
What can go wrong ?
It’s not immediately obvious, but Objective-C object initialization (init...
methods) differ quite a bit from .NET constructors. Those differences have an important impact on bindings.
In Objective-C:
init...
can returnnil
, in fact they generally do so if initialization fails;init...
methods are inherited, the base typesÂinit...
method will be used;- Documentation (at least Apple’s) is rarely clear on wether you can (or can’t) call the default
init
to create an instance. This might work, returnnil
, throw or crash.
In .NET:
- Constructors cannot return
null
. If the instance cannot be created, it will throw an exception; - Constructors are not inherited, they must be re-defined in each type (and call the base type). This makes it clear if a constructor is available or not, except for one case…
- A default (no parameters)
public
constructor is added to (non-static) types unless you define one with less visibility (e.g.private
).
Mixing those two different feature sets can create some weird cases. E.g.
- If
init...
returnsnull
then a constructor will create a managed instance with anÂIntPtr.Zero
handle. That instance won’t be usable, except as anull
value; - You can end up with a default constructor for Objective-C types that should not be created, e.g. abstract, singletons… but they can partially and unpredictably work (hard to spot);
- You might be missing some constructors because you overlooked the base class
init...
. That might make some types impossible to create from a .NET application;
What can we check for ?
A test can reflect each NSObject
-derived type from a binding assembly, find its default constructor, create an instance and validate that it’s Handle
property is valid (not IntPtr.Zero
). That would spot:
- Types that are abstract (and should not be created);
- Types that are singleton (and should be used differently);
- Types that do not support being created using the default “
- (id)init
” signature.
In addition, the same test can also check that the ToString
method is implemented correctly. The default NSObject.ToString()
implementation calls the description
selector. That call might crash due to uninitialized native members—even if the instance is fine.
Finally, by calling the Dispose()
method, the test also ensures that the type can be released correctly. If all of this can be done without failure then the type is likely consumable.
Why is this important ?
Default constructors will be shown in your IDE code completion and binding consumers will assume it’s a valid way to create an object instance. If it results in an invalid/unusable instance—or worse, a crash—then it makes the binding much more complex to use (e.g. remember what constructors are valid). In other words, any public API should be safe to call.
The ToString
method is used by the debugger to show you a description of an object (e.g. in the watch pad). If calling ToString
 causes an application to crash, then debugging your application will cause it to crash. This can be hard to diagnose (and often irreproducible) because it will be the debugger that will cause this, depending on it’s state (e.g. watches), not the code that’s being executed.
The Dispose
check does not often fail. When it does, it generally means that creating an instance has special rules (hopefully documented) or that something else is invalid (somehow a bad instance was created but has not failed the previous checks). One should be aware of such special requirements before using the type. You might be able to document it, or even hide it from the binding consumers.
How to fix issues ?
In most cases, the test will fail (or crash) because a handle is invalid. The library’s documentation (or source code, when available) will often tell you if there are other ways to create an instance of that particular type. In general, the fix will be to add the [DisableDefaultCtor]
attribute to such types in your bindings. For example:
[BaseType (typeof (CCActionInterval))] [DisableDefaultCtor] // Objective-C exception thrown. Name: NSInternalInconsistencyException Reason: IntervalActionInit: Init not supported. Use InitWithDuration interface CCSequence { // ... }
There might be cases where creating an instance only works if some other action has been done previously (e.g. initialization). You can add such steps in your fixture constructor.
[TestFixture] public class BindingCtorTest : ApiCtorInitTest { public BindingCtorTest () { // fictional example where binding won't work without // some specific initialization code MyLibrary.SharedConfiguration.Server = "127.0.0.1"; } }
Creating some types might also require something external to the library (e.g. specific hardware or an API key). In such cases, you might need to skip the type by overriding the fixture’s Skip(Type)
method and returning true
for this type.
[TestFixture] public class BindingCtorTest : ApiCtorInitTest { public BindingCtorTest () { // fictional example where binding won't work without // an API key MyLibrary.ApiKey = "foo"; } protected override bool Skip (Type type) { // fictional example where a specific type won't work without // some some hardware device, you can skip those if (type.FullName == "MyLibrary.MyDevice") return true; return base.Skip (type); } }
If ToString
fails, but the instance is valid, then you might want to add a manual override for it (e.g. inside Extra.cs
), providing some useful data about the state or simply returning it’s fully qualified name.
public partial class MyTypeCrashingOnDescription { // when `description` fails we prefer returning something useful (not null) // so the debugging session won't be interupted by a native crash public override string ToString () { return GetType ().FullName; } }
If an instance cannot be disposed, then you should override the fixture’s Dispose(NSObject,Type)
method and keep a reference to it. That will allow the test execution to continue.
[TestFixture] public class BindingCtorTest : ApiCtorInitTest { static List do_not_dispose = new List (); protected override void Dispose (NSObject obj, Type type) { switch (type.FullName) { // this crash the application after test completed their execution so we keep it alive case "MyLibrary.MyTypeThatCannotBeDisposed": do_not_dispose.Add (obj); break; default: base.Dispose (obj, type); break; } } }
Most of the above (except adding [DisableDefaultCtor])
 are uncommon, but useful to know. For example, the fixture implementation to test Cocos2d did not require any overrides (besides specifying the Assembly
to test).
What’s missing ?
There’s no easy way to test for missing constructors, e.g. the ones available in base classes that are not inherited. That would require static analysis (not introspection) in order to be useful. Keep an eye on types where you add [DisableDefaultCtor]
. If they do not have any Constructor
methods, then they might be expected to use the base type init...
methods. Quite a few constructors were added to Cocos2d.
Finally, to make it easier to review untested types—those without a default constructor—you can set the LogUntestedTypes
property to true
and re-execute the tests.
[TestFixture] public class BindingCtorTest : ApiCtorInitTest { public BindingCtorTest () { // Useful to know which types are being skipped for lack of a default ctor LogUntestedTypes = true; } }
Read the rest of the series: