Swiftのライブラリの仕組みメモ


この辺の知識は大事だけど、使ってないと忘れるので完全に自分用のメモとして残しておきます。(昔書いてて、下書きにあったやつです)

# ライブラリとは何か

## ライブラリには複数の種類がある

### 作成された言語による違い

見た目ではわからないが、作成された言語によって微妙に違う

* Swift
* Objective-C
* C

たとえば、Swiftのframeworkでは、`.swiftmodule` というPublicなAPIを含んだバイナリファイルがあり、Objective-C/Cは、`.modulemap` というmodule map言語で記述されているテキストファイルがある

#### swiftmodule

Swiftファイルを以下のようにコンパイルすると`.swiftmodule`と`.swiftdoc` ができる。

```sh
$ swiftc -emit-module <...>.swift <...>.swift -module-name <...>
```

#### modulemap

`.modulemap` を作成して、

### CPUアーキテクチャによる違い

見た目ではわからないが、対象CPUによって微妙に違う。

* Intel
* ARM

それぞれ32bit、64bitがある。

それぞれ必要になるので、Universal Binaryを作る必要がある。
作るコマンドは以下。
（ちなみにこの方法だとSimulator用と実機用をframeworkに入れておくとApp Storeに提出できないので、Simulator用を削除する必要がある。Carthage利用時は、`carthage copy-frameworks` でそれを解決している）

```sh
$ xcrun lipo -create <...> <...> -output <...>
```

詳細に書くとフレームワークのBuild Phaseで以下のようなRunScriptを記述する

```sh
#!/bin/sh

UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal

# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

# Step 1. Build Device and Simulator versions
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${PROJECT_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

# Step 2. Copy the framework structure (from iphoneos build) to the universal folder
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}/"

# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule"
fi

# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework/${PROJECT_NAME}"

# Step 5. Convenience step to copy the framework to the project's directory
cp -R "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework" "${PROJECT_DIR}"

# Step 6. Convenience step to open the project's directory in Finder
open "${PROJECT_DIR}"
```

もしくは、XCFramework形式で作る。（この方法は複数のアーキテクチャ/プラットフォームの`.framework`を1つにまとめた`.xcframework`というディレクトリを作る）

```sh
$ xcodebuild archive -scheme MyFramework-iOS \ 
-destination "iphoneos/platform=iOS" \ 
-archivePath build/iphoneos \ 
SKIP_INSTALL=NO 
$ xcodebuild archive -scheme MyFramework-iOS \ 
-destination "iphoneos/platform=iOS Simulator" \ 
-archivePath build/iphonesimulator \ 
SKIP_INSTALL=NO 

$ xcodebuild -create-xcframework \ 
-framework build/iphoneos.xcarchive/Products/@rpath/MyFramework.framework \ 
-framework build/iphonesimulator.xcarchive/Products/@rpath/MyFramework.framework \ 
-output MyFramework.xcframework
```

どんなアーキテクチャが含まれているかの確認コマンド

```sh
$ xcrun lipo -info <...> 
```

### 拡張子

見た目で違うのでわかりやすい。

#### .framework (ダイナミックフレームワーク、スタティックフレームワーク)

それぞれモジュールが含まれるものと含まれないものがある。(インポート時に必要なもので、PublicなApiの宣言をしているだけ)

フレームワークとは、バンドルを持つもの（バンドルを持たないものはライブラリと呼ばれる）。
バンドルとは、コード以外のものを含むもの。便利に使うための一定のルールに基づいたディレクトリ構造な形式のもの。

.frameworkのディレクトリ構造は以下のようになっています。

```sh
MyFramework.framework
├── Headers
│   ├── MyFramework-Swift.h
│   └── MyFramework.h
├── Info.plist
├── Modules
│   ├── MyFramework.swiftmodule
│   │   ├── Project
│   │   │   ├── arm64-apple-ios.swiftsourceinfo
│   │   │   ├── arm64.swiftsourceinfo
│   │   │   ├── x86_64-apple-ios-simulator.swiftsourceinfo
│   │   │   └── x86_64.swiftsourceinfo
│   │   ├── arm64-apple-ios.swiftdoc
│   │   ├── arm64-apple-ios.swiftmodule
│   │   ├── arm64.swiftdoc
│   │   ├── arm64.swiftmodule
│   │   ├── x86_64-apple-ios-simulator.swiftdoc
│   │   ├── x86_64-apple-ios-simulator.swiftmodule
│   │   ├── x86_64.swiftdoc
│   │   └── x86_64.swiftmodule
│   └── module.modulemap
└── MyFramework
```

#### .dylib 

* ダイナミックライブラリのこと

#### .a 

スタティックライブラリのこと

# リンクとインポート

## リンクとは何か

コンパイルによって生成されたオブジェクトファイルをリンカによって結合する処理をリンクと呼ぶ。全てのシンボルはこのときに解決される。

### 2種類

* スタティックリンク

リンクの際にターゲットを結合して一つの実行ファイルにする。ビルド時にシンボル（クラスやメソッドなど）はすべて解決される。
リンクしたときに問題がなければ、実行時にも問題が起きにくい。
複数のライブラリをリンクするときにそれぞれで同じライブラリを使っているとシンボルの衝突が起きやすい。

* ダイナミックリンク

ビルド時はシンボルの参照だけが設定される。シンボルの解決は実行時にされる。
リンクが成功してても、実行時に問題になることがある。
当然、複数のライブラリからそれぞれ同じライブラリを使うことは可能。

#### 見分け方

fileコマンドを使う

```
$ file MyFramework.framework/MyFramework
MyFramework.framework/MyFramework: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64]
MyFramework.framework/MyFramework (for architecture x86_64):	Mach-O 64-bit dynamically linked shared library x86_64
MyFramework.framework/MyFramework (for architecture arm64):	Mach-O 64-bit dynamically linked shared library arm64
```

### フレームワークのリンクの仕方

`FRAMEWORK_SEARCH_PATHS`にフレームワークのディレクトリを指定する。
Auto Linkingが無効の場合は、`Other Swift Flags` = `OTHER_LDFLAGS` に `-framework MyFramework` のように指定する。

#### Automatic Linking

Swiftは自動でリンク機能が働く。インポート=リンクになる。

たとえば、`Other Swift Flags`に `-Xfrontend -disable-autolink-framework -Xfrontend MyFramework` とすると無効化することができる。

`Ohter Linker Flags` に `-framework MyFramework` を記述すると明示的にリンクされる。

### ライブラリのリンクの仕方

`LIBRARY_SEARCH_PATHS` にライブラリのディレクトリを指定する。
`OTHER_LDFLAGS` に `-l<ライブラリ名>` を指定する。(例： libssl.aの場合は、`-lssl`)


## インポートとは何か

外部ライブラリが公開しているシンボル（クラスやメソッドなど）を自分のコードで利用可能にするための言語機能で、Swiftの場合は、コンパイル時点で解決される

Swiftのframeworkで以下のように`swiftmodule`があれば、インポートができる。


```
MyFramework.framework
└── Modules
    └── MyFramework.swiftmodule
        ├── arm64-apple-ios.swiftmodule
        ├── arm64.swiftmodule
        ├── x86_64-apple-ios-simulator.swiftmodule
        └── x86_64.swiftmodule
```

Swift以外は、`module.modulemap`ファイルがある。

### モジュールとは

Frameworkにバンドルする必要がなくインポート時に必要なもので、PublicなApiの宣言。

モジュールがないフレームワークの場合（ObjectiveーC/C言語製）は、ヘッダファイルをmodulemapでモジュールに変換する必要がある。
Swiftの場合は、コンパイラが自動生成する。

Module Mapとは、Bridging Headerの上位互換。（汎用性が高い）
Bridging Headerはアプリケーションターゲットでしか使えない（独自フレームワークでは使えない）。
Bridging Headerはグローバルに作用するので、必要なところだけでimportすることができない。


# XcodeでDynamic Frameworkを追加するとどうなるか

以下の追加をした場合

<img width="802" alt="スクリーンショット 2020-07-25 0.16.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10494/9d69f46a-2cfa-adc9-af20-a755baf803f7.png">

`MySample.xcodeproj/project.pbxproj` で、二つの重要なことが起きる。

一つは、Build Phaseに `Embed Frameworks`という名前で対象のフレームワークをアプリケーションのFrameworksディレクトリにコピーする処理が追加されたこと

```diff
+/* Begin PBXCopyFilesBuildPhase section */
+               9F9ACFCD24CB318B003C2EBA /* Embed Frameworks */ = {
+                       isa = PBXCopyFilesBuildPhase;
+                       buildActionMask = 2147483647;
+                       dstPath = "";
+                       dstSubfolderSpec = 10;
+                       files = (
+                               9F9ACFCC24CB318B003C2EBA /* MyFramework.framework in Embed Frameworks */,
+                       );
+                       name = "Embed Frameworks";
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
+/* End PBXCopyFilesBuildPhase section */
```

<img width="864" alt="スクリーンショット 2020-07-25 0.26.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10494/6e415239-feca-90a2-9002-59f786581ca1.png">

もう一つは、Build Settingsに `FRAMEWORK_SEARCH_PATHS`が追加されこと

```diff
                        buildSettings = {
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                                CODE_SIGN_STYLE = Automatic;
+                               FRAMEWORK_SEARCH_PATHS = (
+                                       "$(inherited)",
+                                       "$(PROJECT_DIR)",
+                               );
                                INFOPLIST_FILE = MySample/Info.plist;
                                LD_RUNPATH_SEARCH_PATHS = (
                                        "$(inherited)",
```

<img width="766" alt="スクリーンショット 2020-07-25 0.27.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10494/56d82cf7-1da5-0d4c-1cd4-c89bc466fc9b.png">

## Frameworksディレクトリにコピーする処理とは

Build Phaseに `Embed Frameworks`を削除してみるとビルドは成功するが実行時に以下のエラーが出る。

```sh
dyld: Library not loaded: @rpath/MyFramework.framework/MyFramework
  Referenced from: /Users/ko2ic/Library/Developer/CoreSimulator/Devices/1137B367-CB15-4A40-8D09-214A3FBFFF5A/data/Containers/Bundle/Application/FB55EB6E-0F63-4FE8-A7F3-95F68CFD2405/MySample.app/MySample
  Reason: image not found
```

`dyld` は動的リンカのことで、それが失敗しているということ。

## FRAMEWORK_SEARCH_PATHSとは

フレームワークでモジュールを持つ場合にモジュールのあるフレームワークのパスを指定する場所。

ちなみにフレームワークでモジュールを持たない場合（やライブラリをインポートしたい場合）は、`SWIFT_INCLUDE_PATHS`でmodulemapのある親ディレクトリを指定する。


## ダイナミックフレークワークで利用されるpath

* @executable_path

例
`~/Library/Developer/CoreSimulator/Devices/xxxxxx/data/Containers/Bundle/Application/zzzzzzz/MySample.app/MySample`

アプリ実行時のアプリ自体のファイルパス

* @loader_path

ローダ（dyld）が読み込もうとしているライブラリのパス

* @rpath

Xcodeでは、Runpath Search Paths (LD_RUNPATH_SEARCH_PATH) で設定する。

リンカが検索するパス。動的ライブラリを見つけるためにパスのリストを順に検索する。

# 参考

https://wincent.com/wiki/%40executable_path%2C_%40load_path_and_%40rpath
https://gist.github.com/cromandini/1a9c4aeab27ca84f5d79#file-universal-framework-sh
https://speakerdeck.com/kishikawakatsumi/swiftniokeruinpototorinkufalseshi-zu-miwotan-ru