March 25, 2013

Producing Better Bindings #3: Selectors

By

This blog post is about producing better bindings of Objective-C libraries for Xamarin.iOS and Xamarin.Mac. Read the series introduction to get a better idea why this is important and how it can save you time and headaches.

What can go wrong ?

Binding selectors is largely done using the [Export("")] attribute. As such, it shares a lot of similarities with the [Field] attribute—including the main cause of issues: typos.

In fact, the all time favorite issue is likely the infamous missing colon at the selector’s end (when parameters are used). For example, the following Objective-C method declaration:

-(void) step: (ccTime) dt;

should be translated to:

[Export ("step:")] // without the ':' this method won't work
void Step (float deltaTime);

However, it is also a common error to forget the [Static] attribute when a selector starts with a plus sign (+) character. The Objective-C static method declaration:

+(id) actionWithDuration:(ccTime)duration angle:(float)angle;

should be translated to:

interface CCRotateBy {
	[Static] // easy to forget, unusable without it!
	[Export ("actionWithDuration:angle:")]
	CCRotateBy Create (float duration, float deltaAngle);
	// ...
}

A less common issue is wrong builds. Your bindings might include selectors that are not part of the native library binary (e.g. missing source files, wrong defines…) even if they are documented and present in the header files.

Finally, some library updates might introduce breaking changes, including removed API entry points. If you only bind new APIs, you might be shipping old, missing, APIs without realizing it.

What can we check for ?

Quite a few things, including:

  • Static selectors: One test case checks every type, inheriting from NSObject, inside your assembly and tries every [Export] on static methods. If respondsToSelector: returns false then an error is raised.
  • Instance selectors: A similar test case does the same for every instance method and tries all [Export] selectors. If instancesRespondToSelector: returns false then an error is raised.
  • Missing setters: In general, we can only test what’s present. If something is missing then we have no way to test it. In this case we can stretch the limits to try to find some missing property setters. If a property has a getter Foo then we check if a selector exists with the default selector name, i.e. “setFoo:“. If this selector exists and the property does not have a setter then an error is raised.

Why is this important ?

The binding consumer expects public APIs to exist (i.e. not throw a missing selector exception) in the bound library. However, without testing every method/property bound you cannot be sure if they really exist or not outside your .NET assembly. You can avoid many headaches for yourself and other binding consumers in exchange for a few extra ‘:‘.

How to fix issues ?

Most cases are fixed by verifying the selector name. If it exists as-is then check if you’re not missing a [Static] attribute (or if you have one where none is needed). Again, the library’s documentation and/or header files should have all the information you need to fix them.

One quite important exception is the Objective-C Protocol References. The protocols are similar to .NET interfaces,  but not quite. The major difference is that many members are not, in Objective-C, required to be implemented. However, to create your bindings you might have to bind them inside a base class, without being sure every subclasses will provide them—not ideal, but the alternative would be worse.

To let the test run with those special cases, you can override the fixture‘s Skip(Type,string) method. E.g. for cocos2d test fixture I needed to consider two special cases:

	[TestFixture]
	public class BindingSelectorTest : ApiSelectorTest {
		protected override bool Skip (Type type, string selectorName)
		{
			switch (selectorName) {
			// CCRGBAProtocol
			case "doesOpacityModifyRGB":
			case "opacityModifyRGB:":
				switch (type.Name) {
				case "CCAtlasNode":
				case "CCLabelBMFont":
				case "CCLayerColor":
				case "CCMenu":
				case "CCMenuItemLabel":
				case "CCMenuItemSprite":
				case "CCMenuItemToggle":
				case "CCSprite":
					return true;
				}
				break;
			// CCTargetedTouchDelegate
			case "ccTouchMoved:withEvent:":
			case "ccTouchEnded:withEvent:":
			case "ccTouchCancelled:withEvent:":
			case "ccTouchesBegan:withEvent:":
			case "ccTouchesMoved:withEvent:":
			case "ccTouchesEnded:withEvent:":
			case "ccTouchesCancelled:withEvent:":
				return type.Name == "CCLayer";
			}
			return base.Skip (type, selectorName);
		}
	}

Another less common issue is that some types might act as a proxy to another type. In such cases, respondToSelector for foo might return false but calling it will work. That behaviour is rarely documented and it’s often best to write some traditional unit tests for such cases. The same override can be used to exclude this case too.

What’s missing ?

With the notable exception of most missing setters, the test fixture cannot detect missing selectors: only the [Export("")] selectors are validated.

Read the rest of the series:

Discuss this post on the Xamarin forums

TwitterFacebookGoogle+LinkedInEmail