SwiftからCFBundleを呼び出す

CFBundleで構成されたSDKをSwiftから呼び出す際のメモ。
やり方を知ってしまえば簡単だけど、あまり実例を説明したものが見つけられず、設定方法に困ったので記事にした。

# TLDR
CFBundleをSwiftから呼び出すにはObjective-Cのラッパーを介して呼び出す。

* Swift <-> ObjCラッパー <-> CFBundle(C API)

詳細はAppleのサイト[Loading Non-Cocoa Bundles with CFBundle](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingCode/Tasks/LoadingBundles.html#//apple_ref/doc/uid/20001273-99770)に記載してある。

# 開発環境
macOS Catalina: 10.15.7 (19H114)
XCode: Version 12.3 (12C33)

## 0. CFBundleの定義（前提）
CFBundleのヘッダーでは以下のように関数の型が定義されており、"MyCFBundle.bundle"に実装されているものとする。

```c:MyCFBundle.h
#ifndef __MYCFBUNDLE_H__
#define __MYCFBUNDLE_H__

#import <CoreFoundation/CoreFoundation.h>
#ifdef  __cplusplus
extern "C" {
#endif  // _cplusplus
    long DoSomething(long);
    void DoSomethingElse(void);
#ifdef  __cplusplus
}
#endif  // _cplusplus

typedef long (*DoSomethingPtr)(long);
typedef void (*DoSomethingElsePtr)(void);

#endif // __MYCFBUNDLE_H__
```

## 1. CFBundleをXCodeプロジェクトに登録
プロジェクト名のところで右クリックして"New Groups..."で"PlugIns"グループを作り、この中にCFBundleをドロップする。ドロップするとプロジェクト設定の"Bundle Phases"-"Copy Bundle Resources"に自動登録されるが、これはCFBundleを`Contents/Resources`にコピーする設定。`Contents/PlugIns`に入れる場合は一旦、"Copy Bundle Resources"にあるCFBundleを"-"で削除して、"Bundle Phases"-"Embed Libraries"で"+"ボタンを押してCFBundleを追加する。追加する際、Destinationを"Plugins"とする。
Appleの例では"MyCFBundle.bundle"を`/Contents/PlugIns/MyCFBundle.bundle`に配置する前提となっている。

## 2. Objective-Cラッパー作成
これを呼び出すObjective-Cのラッパーは、以下のようにCFBundleRefと関数ポインタを保持するMyBundleWrapperを定義する。また、Objective-Cのメソッドを宣言する。

```objc:MyBundleWrapperMM.h
#import <CoreFoundation/CoreFoundation.h>
#include "MyCFBundle.h" 
@interface MyBundleWrapper : NSObject
{
    // CFBundle内の関数ポインタを保持する
    DoSomethingPtr doSomething;
    DoSomethingElsePtr doSomethingElse;
 
    // CFBundleの参照
    CFBundleRef cfBundle;
}
// Objective-Cのメソッド宣言
- (long)doSomething:(long)arg;
- (void)doSomethingElse; 
@end
```
Objective-Cラッパーの実装は以下の通り。

```objc:MyBundleWrapper.mm
#import "MyBundleWrapperMM.h"
@implementation MyBundleWrapper
 
- (id)init
{
    NSString *bundlePath;
    NSURL *bundleURL;
 
    self = [super init];
 
    // CFBundleのインストール先がPlugInsの場合は、builtInPlugInsPathを使ってCFBundleを見つける。
    // CFBundleをContents/Resourcesに登録する場合は、resourcePathを使ってCFBundleを見つける。
    bundlePath = [[[NSBundle mainBundle] builtInPlugInsPath]
                    stringByAppendingPathComponent:@"MyCFBundle.bundle"];
    bundleURL = [NSURL fileURLWithPath:bundlePath];
    cfBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)bundleURL);
 
    return self;
}
 
- (void)dealloc
{
    CFRelease(cfBundle);
}
 
- (long)doSomething:(long)arg
{
    if(!doSomething) //最初はNULLなので関数ポインタの取得が必要
    {
        // CFBundleの関数ポインタを取得
        doSomething = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("DoSomething"));
    }
    // CFBundleの関数を呼び出し。
    return doSomething(arg);
}
 
- (void)doSomethingElse
{
    if(!doSomethingElse)
    {
        doSomethingElse = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("DoSomethingElse"));
    }
    doSomethingElse();
}
@end
```

## 3. ブリッジヘッダの作成
SwiftからObjective-Cのコードを呼び出すため、ブリッジヘッダを定義する。`プロジェクト名-Bridging-Header.h`というヘッダファイルを作成し、そこでObjective-Cのヘッダをimportする。プロジェクト名が仮に"MySwiftProject"とすると以下のようになる。

```swift:MySwiftProject-Bridging-Header.h
#import "MyBundleWrapperMM.h"
```

## 4. Swiftからの呼び出し
あとは普通にSwiftからMyBundleWrapperが呼び出せる。

```swift:AppDelegate.swift
import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    ...
    let myBundle = MyBundleWrapper()
    ...
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        ...
        let param:Int = 0
        let result = myBundle.doSomething(param)
        ...
        myBundle.doSomethingElse()
        ...
    }
    ...
}
...
```

## 5. 署名問題対応
SDKが提供するbundleやdylibにSDK提供元で署名されているものが含まれている場合は `code signature in (xxx) not valid for use in process using Library Validation: mapping process and mapped file (non-platform) have different Team IDs` のようなエラーとなる。これを回避するには[com.apple.security.cs.disable-library-validation](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation)を有効にする必要がある。(これが個人的に解決までに時間がかかってしまった問題。)

### 5-1. Hardend Runtimeの有効化
XCodeのプロジェクト設定の`Build Settings`タブで、`Enable Hardened Runtime`(強化されたランタイム機能)を有効(Yes)にする。すると、`Signing & Capabilities`タブに`Hardend Runtime`というグループが表示されるようになる。

### 5-2. Library Validationの無効化
`Signing & Capabilities`タブに`Hardend Runtime`にある、`Disable Library Validation`にチェックを入れて、Library Validationを無効化する。

# まとめ
CFBundleをSwiftから呼び出すにはObjective-Cのラッパーを介して呼び出せばよい。署名問題は設定方法さえわかればすぐ解決できる。呼び出しではポインタや文字列などCの型とSwiftの型を相互変換することになるが、場合によってはその変換をObjCのラッパー部分に持たせた方が良いかもしれない。