Skip to content

BINDINGS

Vincent Dondain edited this page Aug 27, 2019 · 9 revisions

Table of Contents

Bindings

Apple frameworks are implemented in a few languages, the most common ones expose their API using:

  • C [1]
  • Objective-C
  • JavaScript [2]

So far Apple has not shipped a swift-only framework.

[1] some implementations are in C++ but the exposed APIs are in C.
[2] TVMLJS is tvOS only and not bound (mainly for backward compatibility with private SDK).

Workflow and release process

  1. Input: API(s) to be bound
    1.1. Missing API : part of the currently stable SDK
    1.2. New API : part of a future, unstable SDK (potentially under NDA)
  2. Binding
    2.1. Manual bindings
    2.2. Objective Sharpie
  3. Compiling
    3.1. For Objective-C bindings (only) compile the binding definitions to generate code
    3.2. Compile manual and generated code together
  4. Testing

The addition of new bindings is triggered by two events.

The simplest one is a bug report/request for a missing API.

In this case we can refer to a stable and public API, i.e. an API that is generally documented and part of the current SDK shipping with Xcode.

Simple (single API) can be added to xamarin-macios/master or a personal branch with a pull-request. Without extra steps they will become available in the next cycle releases.

The other case happens when Apple releases a new SDK (e.g. Xcode 8.0 added the iOS 10.0 SDK). In this case we must use a branch because:

  • We're likely to have other releases (cycles or service releases) before the final SDK releases.
  • API won't be stable between the beta releases of the SDK.
  • The App Store will reject submissions using the new toolchain and API.

Minor versions (e.g. iOS 10.1) tends to be small but major versions (like iOS 10.0) are generally huge and it takes months to catch up with Apple.

Tools

Generator

btouch and bmac (both sharing the code in generator*.cs) are the tools that generate C# final code from the interface definitions.

So code is compiled twice, once to created a temporary assembly (dll) with the contract. The generator processes this file and then a second compilation gives us a working assembly.

Tests

See https://github.com/xamarin/xamarin-macios/blob/master/tests/README.md

Objective-C Bindings

Our documentation about Objective-C bindings can be found here:

But there are some scenarios where we have have to do some manual work in order to create a nicer API.

Naming Guidelines

When contributing to xamarin-macios project we follow the Mono Coding Guidelines but when something is not covered by it we fallback to Framework Design Guidelines however, the Mono Coding Guidelines take precedent if there is a conflict between them.

There is one catch when it comes to Naming Guidelines for name capitalization rules. We keep the Apple's Framework prefix as is (uppercase), take the following example:

NFCISO15693ReaderSession

Let us break this intro three parts NFC-ISO-15693ReaderSession

  1. NFC: Apple's Framework Prefix, it stays uppercase NFC.
  2. ISO: Three letter acronym, this follows the Capitalization Rules for Identifiers so it is turned into Iso.
  3. 15693ReaderSession: It follows the Capitalization Rules for Identifiers 15693ReaderSession.

So the name we expect is: NFCIso15693ReaderSession, we can use Basetype.Name in order to map the .NET class name with the native one.

[BaseType (typeof (NFCReaderSession), Name = "NFCISO15693ReaderSession")]
interface NFCIso15693ReaderSession {
}

Breaking Changes

Often you will be wondering if a change you're making is a breaking change and if you are not, you should!

Here is a good document regrouping all the breaking change rules.

Protocols breaking changes

Adding [Abstract] to a new or existing protocol member is a breaking change (because users implementing the interface will suddenly have to use this method).

required and optional are encoded in the Objective-C “metadata" but not enforced by the compiler. Therefore, in Objective-C, making a protocol member required will only result in compiler warnings, not compiler errors. That’s why Apple can add new @required methods or even move an @optional to become @required. What it means is that they want you to add it – but they still must check if it exists (and deal with missing case) since older code (your app from last SDK) still needs to run fine.

We could call these protocol members that are moving from optional to required: "fake required".

Therefore the "binding rule" is the following:

Add #if XAMCORE_X_X to [Abstract] for all the new required methods of an existing protocol (add that info in xtro *.pending) and don’t [Abstract] an old optional method that’s moving to required (add that info in xtro *.ignore, fake required).

Availability attributes

General rules

  • Any API in tvOS without an attribute is implicitly 9,0. Similarly, any watchOS API without an attribute is implicitly 2,0. Therefore we try not to include TV (9,0), Watch (2,0) in our binding files because it's redundant.

Messages

When adding Deprecated or Obsoleted availability attributes to APIs, it's common to add a message explaining what should be used in replacement.

Here are the rules of thumb when adding such messages:

Rule #1

Never put OS version information in the message.

E.g: Soft deprecated in iOS 9, use XXX instead..

It's the analyzer's job to show the OS information and adding it in the message would be a duplicate.

Here's what the analyzer would show to the user if you used the above example as your message:

'XXX' was deprecated in iOS 9. Soft deprecated in iOS 9, use XXX instead.

Rule #2

Always put methods and properties between ' ' (apostrophe, the character below the quotation mark ("), not the grave accent (`)).

E.g: [Deprecated (PlatformName.iOS, 10, 0, message: "Use 'MyClass.MyMethod' instead.")].

This helps emphasise what the user should use as a replacement of the deprecated API. It's also what the analyzer uses to show the deprecated/obsoleted method to the user.

E.g: 'XXX' was deprecated in iOS 9. Use 'MyClass.MyMethod' instead..

Rule #3

Use proper sentences and punctuation.

  • All sentences must start with a capital letter and end with a dot.
  • It must be readable as a full sentence. E.g: "the 'XXX' property". It's okay to say: "Use 'XXX'" if you're talking about a class or a method.

E.g: Instead of "use 'XXX' property instead" it must me "Use the 'XXX' property instead**.**".

Rule #4

Don't use availability keywords in availability message.

The analyzer will specify if the API availability, no need to say it in the message.

E.g: Deprecated method, please use...

Binding Protocols with optional property members

There are some cases where you will find some optional property members inside a protocol, this needs some special care because of how our protocol support relies on C# extension methods (for optional members). Example:

@protocol MyProtocol <NSObject>

@required
@property (nonatomic, assign) BOOL editingEnabled;

@optional
@property (nonatomic, assign) BOOL zoomEnabled;

@end

In the above scenario our C# binding would look like

[Protocol]
public interface MyProtocol {

	[Abstract] // We use abstract for @required members
	[Export ("editingEnabled")]
	bool EditingEnabled { get; set; }
	
	// Optional property members need to ve converted into getter and setter methods
	[Export ("zoomEnabled")]
	bool GetZoomEnabled ();
	
	[Export ("setZoomEnabled:")]
	void SetZoomEnabled (bool zoomEnabled);
}

Our optional member zoomEnabled breaks into a getter and a setter method, each with their own Export attribute. Note that the selector in the setter has been modified to match the Objective-C pattern of setter selector which is prepend the set word, upper case the first letter of the selector and append a : (colon).

Static members in Objective-C categories

We have to be careful with Objective-C categories that contains static members like:

@interface CNGroup (Predicates)

+ (NSPredicate *)predicateForGroupsWithIdentifiers:(NSArray<NSString*> *)identifiers;

+ (NSPredicate *)predicateForSubgroupsInGroupWithIdentifier:(NSString *)parentGroupIdentifier NS_AVAILABLE(10_11, NA);

+ (NSPredicate *)predicateForGroupsInContainerWithIdentifier:(NSString *)containerIdentifier;

@end

These members must be inlined directly in the interface definition instead of creating their own category interface definition. So we avoid creating ugly API such as: (null as CNContact).GetPredicateForContacts ("Foo");

When to use [Model] Attribute

Mostly [Model] attribute is applied to Protocols that are Delegates or DataSources i.e. UITableViewDelegate or UITableViewDataSource.

Private to Public APIs

It sometimes happens that Apple turn private APIs into public ones.
You can detect such a change if, when adding new APIs for a given existing type, you either see an older availability attribute than the Xcode you're currently binding for or do not see an availability attribute in the header file. In other words, in the header file, Apple did not bother precising the availability of the given API because it was actually already there, simply hidden and it is gonna use the general availability attribute of the type.

Now, it is important to understand that Apple headers are not always right about that. Sometimes Apple engineers think the API was inluded in all the OS versions covered by the type's availability attribute and it was not... So do not trust the headers but trust our tests instead (;

While you could test on the older platform the API says it exists in, it's recommended to just use the latest availability attribute. This is safer, customers can still use the API on older OS if they want, and it's not a breaking change to update that in the future based on feedback.

Different Objective-C selectors who lead to identical C# signatures

Objective-C tends to have verbose method signatures.

Therefore, you might have cases where 2 methods are different in Objective-C but identical once translated to C#.

E.g:

initVectorNoiseWithSmoothness:name:textureDimensions:channelEncoding:

and

initCellularNoiseWithFrequency:name:textureDimensions:channelEncoding:

Those two selectors have the exact same arguments but note how the wording at the beginning differs: initVectorNoise vs initCellularNoise.

Once translated to C# you would get:

IntPtr Constructor (float smoothness, string name, Vector2i textureDimensions, MDLTextureChannelEncoding channelEncoding);

and

IntPtr Constructor (float frequency, string name, Vector2i textureDimensions, MDLTextureChannelEncoding channelEncoding);

The two signatures here are identical and we cannot do that in C#.

Option 1 - Different signature names

The simplest option, which unfortunately wouldn't work for a constructor, would be to have two different names for the C# signatures where you'd make VectorNoise and CellularNoise appear.
Note: it's not always the best option since by design C# tend to differentiate signatures based on the number/type of arguments rather than the name of the method.

Option 2 - Introduce an enum

The preferred option is often to introduce an enum which will help determine which Objective-C selector we must call.

Given our previous example you'd do something like this:

In the bindings file:

[Internal]
[Export ("initVectorNoiseWithSmoothness:name:textureDimensions:channelEncoding:")]
IntPtr InitVectorNoiseWithSmoothness (float smoothness, string name, Vector2i textureDimensions, MDLTextureChannelEncoding channelEncoding);

[Internal]
[Export ("initCellularNoiseWithFrequency:name:textureDimensions:channelEncoding:")]
IntPtr InitCellularNoiseWithFrequency (float frequency, string name, Vector2i textureDimensions, MDLTextureChannelEncoding channelEncoding);

Here we're making those two methods internal, note that we do not use Constructor for the signatures anymore making the C# methods distinct.

In the code behind file:

public enum  MDLNoiseTextureType {
	Vector,
	Cellular,
}

We introduce an enum that will let the user tell which is the desired behavior.
Note: we're reusing the words Vector and Cellular from the Objective-C selectors.

public partial class MDLNoiseTexture {
	// 1. Attributes a default behavior so users don't always need to use MDLNoiseTextureType
	public MDLNoiseTexture (float input, string name, Vector2i textureDimensions, MDLTextureChannelEncoding channelEncoding) : this (input, name, textureDimensions, channelEncoding, MDLNoiseTextureType.Vector)
	{
	}

	// 2. The new public constructor that uses the enum
	// Note: call "base (NSObjectFlag.Empty)" to avoid calling the default "init" method
	public MDLNoiseTexture (float input, string name, Vector2i textureDimensions, MDLTextureChannelEncoding channelEncoding, MDLNoiseTextureType type) : base (NSObjectFlag.Empty)
	{
		switch (type) {
		// 3. Select which internal method to call based on the enum value
		case MDLNoiseTextureType.Vector:
			// 4. Call the internal method and set the Handle
			// Use InitializeHandle () which instead of setting the "Handle" field, because InitializeHandle will show a better exception message if the init call fails and returns null.
			InitializeHandle (InitVectorNoiseWithSmoothness (input, name, textureDimensions, channelEncoding), "initVectorNoiseWithSmoothness:name:textureDimensions:channelEncoding:")
			break;
		case MDLNoiseTextureType.Cellular:
			InitializeHandle (InitCellularNoiseWithFrequency (input, name, textureDimensions, channelEncoding), "initCellularNoiseWithFrequency:name:textureDimensions:channelEncoding:")
			break;
		default:
			// 5. Require the enum
			throw new ArgumentException ("The 'MDLNoiseTextureType type' argument needs a value.");
		}
	}
}
Clone this wiki locally