Xamarin.iOS の Objective-C セレクター

Objective-C 言語は "セレクター" に基づいています。 セレクターは、オブジェクトまたは "クラス" に送信できるメッセージです。 Xamarin.iOS は、インスタンス セレクターをインスタンス メソッドに、クラス セレクターを静的メソッドにマップします。

通常の C 関数 (および C++ メンバー関数など) とは異なり、代わりに P/Invoke を使用してセレクターを直接呼び出すことはできません。代わりに、セレクターは Objective-C のクラスやインスタンスに対して、objc_msgSend 関数を使用して送信できます。

Objective-C のメッセージの詳細については、Apple の「オブジェクトの操作」ガイドを参照してください。

次のことを行いたいと仮定します。NSStringsizeWithFont:forWidth:lineBreakMode: セレクターを呼び出す。 宣言は以下のとおりです (Apple のドキュメントから)。

- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode

この API には以下の特徴があります。

  • Unified API での戻り値の型は CGSize です。
  • font パラメーターは、NSObject から派生した UIFont (および間接的に派生している型) であり、System.IntPtr にマップされます。
  • CGFloat である width パラメーターは nfloat にマップされます。
  • UILineBreakMode である lineBreakMode パラメーターは、Xamarin.iOS で既に UILineBreakMode 列挙型としてバインドされています。

すべてをまとめると、objc_msgSend 宣言は以下のとおりとする必要があります。

CGSize objc_msgSend(
    IntPtr target,
    IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode
);

これを以下のように宣言します。

[DllImport (Constants.ObjectiveCLibrary, EntryPoint="objc_msgSend")]
static extern CGSize cgsize_objc_msgSend_IntPtr_float_int (
    IntPtr target,
    IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode
);

このメソッドを呼び出すには、以下のようなコードを使用します。

NSString target = ...
Selector selector = new Selector ("sizeWithFont:forWidth:lineBreakMode:");
UIFont font = ...
nfloat width = ...
UILineBreakMode mode = ...

CGSize size = cgsize_objc_msgSend_IntPtr_float_int(
    target.Handle,
    selector.Handle,
    font == null ? IntPtr.Zero : font.Handle,
    width,
    mode
);

戻り値が (Unified API への切り替え前に使用されていた古い SizeF のように) 8 byte 未満のサイズの構造体であった場合、上記のコードはシミュレーター上では実行できてもデバイス上ではクラッシュしていました。 8 ビット未満のサイズの値を返すセレクターを呼び出すには、以下のように objc_msgSend_stret 関数を宣言します。

[DllImport (MonoTouch.Constants.ObjectiveCLibrary, EntryPoint="objc_msgSend_stret")]
static extern void cgsize_objc_msgSend_stret_IntPtr_float_int (
    out CGSize retval,
    IntPtr target,
    IntPtr selector,
    IntPtr font,
    nfloat width,
    UILineBreakMode mode
);

このメソッドを呼び出すには、以下のようなコードを使用します。

NSString      target = ...
Selector    selector = new Selector ("sizeWithFont:forWidth:lineBreakMode:");
UIFont          font = ...
nfloat          width = ...
UILineBreakMode mode = ...

CGSize size;

if (Runtime.Arch == Arch.SIMULATOR)
    size = cgsize_objc_msgSend_IntPtr_float_int(
        target.Handle,
        selector.Handle,
        font == null ? IntPtr.Zero : font.Handle,
        width,
        mode
    );
else
    cgsize_objc_msgSend_stret_IntPtr_float_int(
        out size,
        target.Handle, selector.Handle,
        font == null ? IntPtr.Zero: font.Handle,
        width,
        mode
    );

セレクターの呼び出し

セレクターの呼び出しには、以下の 3 つの手順があります。

  1. セレクター ターゲットを取得する。
  2. セレクター名を取得する。
  3. 適切な引数を使用して objc_msgSend を呼び出す。

セレクター ターゲット

セレクター ターゲットは、オブジェクト インスタンスまたは Objective-C クラスです。 ターゲットがインスタンスであり、バインドされた Xamarin.iOS 型に由来する場合は、ObjCRuntime.INativeObject.Handle プロパティを使用します。

ターゲットがクラスの場合は、ObjCRuntime.Class を使用してクラス インスタンスへの参照を取得した後、Class.Handle プロパティを使用します。

セレクター名

セレクター名は、Apple のドキュメントに記載されています。 たとえば、NSString には sizeWithFont: および sizeWithFont:forWidth:lineBreakMode: セレクターが含まれています。 途中のコロンと末尾のコロンはセレクター名の一部であり、省略できません。

セレクター名を取得したら、それに対する ObjCRuntime.Selector インスタンスを作成できます。

objc_msgSend の呼び出し

objc_msgSend はメッセージ (セレクター) をオブジェクトに送信します。 この関数ファミリは、セレクター ターゲット (インスタンスまたはクラスのハンドル)、セレクター自体、およびセレクターに必要なすべての引数という少なくとも 2 つの必須引数を受け取ります。 インスタンス引数とセレクター引数は System.IntPtr で、残りのすべての引数はセレクターが想定する型と一致する必要があります (たとえば、int に対しては nintNSObject から派生する型に対しては System.IntPtr)。 を使用するObjective-C 型インスタンスの IntPtr を取得するための NSObject.Handle プロパティ。

以下のように objc_msgSend 関数は複数存在します。

  • 構造体を返すセレクターには objc_msgSend_stret を使用します。 ARM では、これには列挙型でも C の組み込み型 (charshortintlongfloatdouble) のいずれでもないすべての戻り値の型が含まれます。 x86 (シミュレーター) では、サイズが 8 byte (CGSize は 8 byte であり、シミュレーターでは objc_msgSend_stret を使用しません) を超えるすべての構造体に対して、このメソッドを使用する必要があります。
  • x86 でのみ浮動小数点値を返すセレクターに objc_msgSend_fpret を使用します。 この関数は ARM で使用する必要はありません。代わりに objc_msgSend を使用します。
  • メインの objc_msgSend 関数は、他のセレクターすべてに対して使用されます。

どの objc_msgSend 関数を呼び出す必要があるかを決定したら (シミュレーターとデバイスでそれぞれ異なるメソッドが必要になる場合があります)、通常の [DllImport] メソッドを使用して、後で呼び出すための関数を宣言できます。

事前に作成された objc_msgSend 宣言のセットは ObjCRuntime.Messaging 内にあります。

シミュレーターとデバイス上の異なる呼び出し

前述のように、Objective-C には 3 種類の objc_msgSend メソッドがあります。1 つは通常の呼び出し用、1 つは浮動小数点値を返す呼び出し用 (x86 のみ)、1 つは構造体値を返す呼び出し用です。 最後のものには ObjCRuntime.Messaging 内で _stret というサフィックスが付いています。

特定の構造体 (以下で説明する規則を参照) を返すメソッドを呼び出す場合、以下のように、戻り値があるメソッドは最初のパラメーターを out 値として呼び出す必要があります。

// The following returns a PointF structure:
PointF ret;
Messaging.PointF_objc_msgSend_stret_PointF_IntPtr (out ret, this.Handle, selConvertPointFromWindow.Handle, point, window.Handle);

いつ _stret_ メソッドを使用するべきかの規則は、x86 と ARM で異なります。 バインディングをシミュレーターとデバイスの両方で機能させたい場合は、次のようなコードを追加します。

if (Runtime.Arch == Arch.DEVICE)
{
    PointF ret;
    Messaging.PointF_objc_msgSend_stret_PointF_IntPtr (out ret, myHandle, selector.Handle);
    return ret;
}
else
{
    return Messaging.PointF_objc_msgSend_PointF_IntPtr (myHandle, selector.Handle);
}

objc_msgSend_stret メソッドの使用

ARM 用にビルドする場合は、列挙型でもなく、列挙型のための基本データ型 (intbyteshortlongdoublefloat) のいずれでもない値の型に対してはすべて objc_msgSend_stret を使用します。

x86 用にビルドする場合は、列挙型でもなく、列挙型のための基本データ型 (intbyteshortlongdoublefloat) のいずれでもなく、かつネイティブ サイズが 8 byte より大きい値の型に対してはすべて objc_msgSend_stret を使用します。

独自のシグネチャの作成

必要であれば、こちらの gist を使用して独自のシグネチャを作成できます。