yohhoyの日記

技術的メモをしていきたい日記

C関数ポインタにObjective-Cデリゲートを渡す

関数ポインタ型をとるC言語インタフェースのライブラリに、Objective-C言語のデリゲート(Delegate)を指定する方法。ARC(Automatic Reference Counting)環境を想定。

// XxxLibライブラリ C言語/公開インタフェース

// コールバック関数ポインタ型
//   nValue: ライブラリから通知される値
//   pUserData: 登録時に設定する任意ポインタ値
typedef int (*CallbackFunc)(int nValue, void *pUserData);

// コールバック関数(関数ポインタ)を登録する
int XxxLib_RegisterCallback(CallbackFunc pFn, void *pUserData);

// 登録されたコールバック関数を呼び出す
int XxxLib_InvokeCallback();

Objective-Cレイヤでライブラリラッパー(XxxLibWrap)インタフェースを用意する。また、Cライブラリに登録したい関数ポインタに対応したデリゲート(XxxLibDelegate)プロトコルも宣言する。ライブラリラッパー層には、デリゲートを登録(registerCallback)する。

// XxxLibWrap.h: Objective-C言語/ラッパー層ヘッダ
#import <Foundation/Foundation.h>

@protocol XxxLibDelegate
@required
- (int)onCallback:(int)nValue;
@end

@interface XxxLib : NSObject
- (int)registerCallback:(id<XxxLibDelegate>)delegate;
- (int)invokeCallbak;
@end

コールバック関数にはC言語サンク関数(CallbackThunk)を登録しておき、そこからObjective-C言語デリゲートのメソッドへと転送する。このとき C/void*型 ⇔ Objective-C/id<XxxLibDelegate>型 の相互変換を行うために、型キャスト時に __bridge 修飾子が必要となる。

// XxxLibWrap.m: Objective-C言語/ラッパー層実装
#import <Foundation/Foundation.h>
#import "XxxLibWrap.h"
#import "XxxLib.h"

static int CallbackThunk(int nValue, void *pUserData) {
  // void*型ユーザデータからデリゲートid型を復元(ARCカウント操作なし)
  id<XxxLibDelegate> self_ = (__bridge id<XxxLibDelegate>)pUserData;
  return [self_ onCallback:nValue];
}

@implementation XxxLib
- (int)registerCallback:(id<XxxLibDelegate>)delegate {
  // デリゲートid型から汎用ポインタ(void*)型にキャスト(ARCカウント操作なし)
  return XxxLib_RegisterCallback(&CallbackThunk, (__bridge void*)delegate);
}
- (int)invokeCallbak {
  return XxxLib_InvokeCallback();
}
@end

注意:__bridge修飾キャストではid<XxxLibDelegate>型のARC参照カウントを操作しないため、Objective-C側でXxxLibDelegateプロトコルを採用したインタフェースのインスタンス存続期間管理に注意すること。つまり、コールバック関数が呼ばれうる期間は、プログラマの責任でObjective-Cインスタンスの生存を保証する。