LoginSignup
66
41

More than 3 years have passed since last update.

Xcode の力を借りない Swift の Dynamic Library の作成とビルドを試す

Posted at

この記事は ドワンゴ Advent Calendar 2019 の 5 日目の記事でしたが、遅刻しました :pray:

去年の今頃はサーバサイドエンジニアとして GraphQL サーバおよびクライアントの設計・開発等 に携わっていましたが、今年の6月から iOS エンジニアとして働きはじめました。雑多な技術系のメモは普段は scrapbox に載せています。

今秋、チームのメンバー数名と iOSDC Japan 2019 に参加し、Swiftにおけるインポートとリンクの仕組みを探る を拝聴したことで今までふんわりしていたライブラリやインポート周りへの知識を深める機運が生まれたため、今回は周辺知識を改めて整理しつつ、最終的には得た知識で持って、Xcode を利用せずに手作業でライブラリの作成とビルド, 実行までを試してみよう!という記事を書いていきます。
モジュールやインポート、ライブラリについて理解しつつ、普段 Xcode に任せている裏側の部分について、実際に手作業で行ってみることで実際にソースコードが実行可能になるまでの流れを掴む、のがゴールです。

この記事内で話すこと

  • 周辺知識の整理
    • モジュールとは?
    • インポートとは?
    • ライブラリとは?
  • 手動で行う Swift の Dynamic Library の作成,インポート及びビルドの手順

モジュールとは?

モジュールとは、ソースコードを外部に公開する場合の基本単位であり、その実態とも言えるものとして、公開べきシンボル (クラスや関数等) を定義したモジュールファイルがあります。異なるソースファイル間でインタフェースを共有するための仕組みという点では、C 言語ファミリーにおけるヘッダーファイルと似ています。実際、Objective-C におけるモジュールは、ヘッダーファイルの上位に位置する概念と言えます。モジュールという概念自体は Swift、Objective-C のどちらにも共通して存在していますが、利用されるモジュールファイルのフォーマットは異なっていて、主に Swift (swiftc) の世界で利用される .swiftmodule ファイルと、主に Objective-C (clang) の世界で利用される .modulemap ファイルの二種類が存在しています。1

swiftmodule

Swiftには、ヘッダーファイルという概念はなく、モジュールがコードの配布, 共有の基本単位です。モジュールファイルである .swiftmodule ファイルは swiftc から出力できます。2
ある swift コードについてモジュールファイルを吐き出したい場合は、-emit-module オプションを付与して下記のように swiftc を実行します。この時、モジュール名を明示したい場合は -module-name も付与します。

swiftc -module-name <module name> -emit-module <swift file>

この実行結果として、下記のような2つのファイルが得られます。出力されるファイルはどちらもバイナリ形式であり、開発者が直接読み書きするようなことは想定されていません。.swiftmodule が Public な API の宣言を含んでいる一方で、.swiftdoc ファイルはその名前から想像できる通り、doc コメントの情報を含んでいるようです。

  • <module name>.swiftdoc
  • <module name>.swiftmodule

Clang module

Objective-C 等の C 言語ファミリーのコンパイルには、Xcode では clang が利用されます。冒頭で話した通り、C 言語ファミリーでは、異なるファイル間のインタフェースの共有にはもともとヘッダーファイルが利用されていました。しかし、ヘッダーファイルの include は、基本的には単なる文字列置換であるため、様々な問題を引き起こします。例えば、以下のような問題です。3

  • 共通のヘッダが複数箇所で参照されていても、参照元毎にヘッダの内容が置換され、各々を処理する必要があるためコンパイルの効率が悪い
  • 呼び出し順序によってヘッダ内の定義がコンフリクトするために include guard を設ける等の対策が必要になってしまう
  • どの言語 (C or C++ or Objective-C) のヘッダファイルか分からない等、ツールから利用し辛い側面がある

これを解決する仕組みとして後発で登場したのが Clang module という概念です。ヘッダファイルの include が単なるテキスト置換であったのに対し、モジュールはコンパイラがインタフェース定義をよりセマンティックに解釈できるように整備されました。複数箇所から参照されていてもそのパースとコンパイルは一度のみでよくなり、また独立してパースされるためにモジュール同士が影響を及ぼし合うこともありません。加えてどの言語で動作するか?等のメタデータも付与できるため、ツールからも扱いやすくなっています。

clang におけるモジュールファイルは .modulemap ファイルです。これはヘッダーファイルとモジュールという概念を繋ぐ Module Map を担うファイルであり、.swiftmodule ファイルとは異なりテキスト形式で、文法も定められています。どのヘッダーをどう公開するか?等の設定を記述することができるようになっています。3

インポートとは?

インポートは、モジュールファイルにて公開されているシンボルを、自身のコードベース上で利用できるようにするための構文, 言語機能です。4 Swift と Objective-C における import 構文について、せっかくなので軽く整理しておきます。

Swift におけるインポート

構文

Swift の import 構文は import module の形式で特定モジュールをインポートするのが一般的ですが、下記のように submodule (複数の Framework をまとめた Umbrella Framework を利用する場合に出てくる概念のようです) のみをインポートしたり、インポート対象をモジュール内の特定のシンボルのみに限定化することもできます。56

import module
import module.submodule
import importKind module.symbolName

未使用のシンボルは最適化の結果除去されるため、限定化してもビルド時間やバイナリサイズには影響しません。7 そのため、あえて利用するケースとしては、異なるモジュールをインポートする必要があるが、それらに同一名のシンボルが存在した場合に、片方のシンボルのみをインポートしたいケース等に限られてくると思われます。

Clang module のインポート

Swift 製ライブラリのモジュールファイルは Clang module ではなく swiftmodule であり、Swift コードから swiftmodule で公開されたモジュールは当然インポートできますが、Clang module をインポートすることもできます。これは、swiftc に Clang importer が内蔵されているためです。8
Swift から Objective-C を参照する方法としては Bridging Header を用意する方法が一般的ですが、Clang module として公開してそれをインポートする、というやり方をとることもできます。この方法は、kishikawa さんの発表でも触れられていましたね。

Objective-C におけるインポート

構文

Objective-C でモジュールをインポートする構文は @import です。3

@import std;

しかし、既存の #include#import ディレクティブをすべてこれに書き換えるのは大変です。そのため、モジュールが存在する場合には、clang はこれらを自動的に @import に書き換えてくれるようです。例えば、下記は @import std.io と結果的に同等になります。

#include <stdio.h>

Swift コードの参照

clang から swiftmodule を直接インポートすることはできません。Objective-C から Swift コードを参照する方法は既にたくさん記事がありますが、Xcode によって自動生成されるヘッダファイルをインポートするのが基本です。9 注意点としては、struct 等の値型は Objective-C からは参照できないことや、参照型の場合でも NSObject を継承している、あるいは @objc ディレクティブを付与することで Objective-C Runtime から参照可能にしておく必要があること、等があります。

ライブラリとは?

Static Library vs Dynamic Library

既に見てきたように、モジュールとインポートは密接な関係にあります。モジュールとして公開された Public な API を、自身のコードベース上でインポートし利用することが出来ます。しかし、参照できるのはあくまで公開情報のみです。実行時には非公開なライブラリの実装部分が必要です。
例えば、あるソースファイルがあるモジュールをインポートして利用するようなコードになっていた場合、その ソースファイル + モジュールファイル さえ存在すれば、コンパイルは正常に実行できます。この時出来上がるファイルはオブジェクトファイル (.o) と呼ばれます。しかし、オブジェクトファイル単体には参照先のモジュールの実装が含まれておらず、そのままでは実行できません。実装も含んだライブラリと連結 (リンク) して初めて実行できるようになります。その結果出来上がるファイルは実行可能ファイル (executable) と呼ばれ、この連結の過程はリンクといい、リンカによって実行されます。
リンク方式の違いによって、ライブラリは主に以下に分類されます。10

  • Static Library
    • 拡張子は .a
    • 実行可能ファイル内にライブラリ自体がコピーされ含まれる
    • アプリサイズが大きくなる
    • Static Library の更新にはアプリの更新が必要
  • Dynamic Library
    • 拡張子は .dylib
    • 実行可能ファイル内にライブラリ自体は含まれない
    • 外部ツール (ダイナミックリンカ, macOS の場合は dyld) によって、必要に応じてライブラリがロードされる
    • アプリサイズが小さくなる
    • アプリ側の更新をしなくとも Dynamic Library の更新が可能

iOS,macOS のシステムライブラリ (UIKit, Foundation 等) は Dynamic Library として提供されています。そのため、アプリの更新をしなくともシステムライブラリの更新が行えるようになっています。Dynamic Library である Mach-O ファイルを仮想メモリ上に展開する際には、アプリの実行のために dyld やカーネルによって様々な下準備が行われます。詳しくは WWDC 2016 のセッション にありますが、場合によっては Dynamic Library への依存を減らすことで、アプリの起動時間を早くするということもできるようです。11

Framework

Framework について説明する前に、Bundle について説明します。なぜなら、Framework は Bundle のサブセットだからです。Bundle とは、一定のルールに従ったディレクトリ構造、あるいはそれにアクセスするための API のことを指します。12 iOS, macOS ソフトウェアやそれらに関連するリースを持ち運ぶための表現であり、特定の拡張子を持っているとバンドルとして認識されることになります。その種類には App Bundle, Setting Bundle, Framework Bundle などがあります。実態はディレクトリ構造ですが、標準化された構造を持っているというのがポイントです。この標準化のおかげで、開発者はコード上から Foundation Framework の Bundle クラス経由で簡単に Bundle にアクセスできます。Bundle の構造は種類やプラットフォームによって異なりますが、API レベルで抽象化されているため、開発者がその違いを気にする必要はありません。基本的に手動で作成する必要はなく、Xcode によって自動生成されることがほとんどです。

そして Bundle の一種である Framework Bundle は、アプリケーション間で共有可能なリソースを含んだバンドルです。13 14 共有可能なリソースとは、例えば xib ファイルや画像ファイル、ソースコード等を指します。Dynamic/Static Library を含んでいることもあります。他の Bundle との違いは、Finder からは通常閲覧できないこと、Versioned bundle format というフォーマットをサポートしているため、複数バージョンのコードを保持でき、過去バージョンを参照できることなどがあります。

Dynamic/Static Library との大きな違いには、画像やドキュメント等のリソースを含むことができる点や、既述の通り複数バージョンを同一 Bundle に含められるため後方互換性を保てる点などがあります。

Mach-O ファイルとサポートツール

ライブラリに関連して、オブジェクトファイル, Static Library, Dynamic Library, Framework などの登場人物たちが登場してきました。Framework の実態はディレクトリですが、その他のファイルは、macOS/iOS においては同一のファイルフォーマットである Mach-O ファイルフォーマットに統一されています (Framework 内に Mach-O ファイルがライブラリとして含まれている場合もあります)。15 16
Mach-O ファイルフォーマットは Header, Load Commands, Segment の3つの領域から主に構成されていて、Header をのぞいてみると、サポートしているアーキテクチャや、どのファイルタイプか (オブジェクトファイル or DynamicLibrary or executable) などを知ることができます。このような情報を簡単に得たい場合に役立つツールがいくつかあり、後ほど利用するのでここで紹介しておきます。

otool

オブジェクトファイルの情報を調査するコマンドとしては、Linux には objdump がありますが、macOS における Mach-O オブジェクトファイルの調査には otool というツールが利用できます。これは例えば、-L で、対象の Mach-O ファイルが利用するライブラリの一覧を表示することが出来ます。

$ otool -L main
main:
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
    @rpath/libZoo.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.8.255)

他にも -l オプションを利用すると Load Commands の一覧が閲覧できます。Load Commands とは Mach-O ファイル内の領域の内の 1 つで、Header の次に位置します。ここには Mach-O ファイル自体の論理構造や、自信を仮想メモリ上に展開する際のレイアウトに関する情報などが含まれます。その一部には Dynamic Library をロードするために必要な情報もここに含まれており、これを後ほど参照することになります。

file

file コマンドは、ファイルの種別の判定に利用できます。Mach-O ファイルに対して利用すると、対象のファイルが実行可能ファイルか?Static Librayか?Dynamic Libraryか?オブジェクトファイルか?などを調べることができるため、手っ取り早くこれらの情報を得たい場合に便利です。

# 実行可能ファイルの場合
$ file MyApp.app/MyApp
MyApp.app/MyApp: Mach-O 64-bit executable x86_64

# Static Library の場合
$ file RxCocoa/RxCocoa.framework/RxCocoa
RxCocoa/RxCocoa.framework/RxCocoa: current ar archive

# Dynamic Library の場合
$ file RxCocoa/RxCocoa.framework/RxCocoa
RxCocoa/RxCocoa.framework/RxCocoa: Mach-O 64-bit dynamically linked shared library x86_64

Dynamic Library を手動で作成, ビルドする

ここまでで、モジュール, インポート, ライブラリ周りについて基本的な知識を復習してきました。この知識があれば、普段借りている Xcode の力を借りずとも、手作業でライブラリ (モジュール) の作成からインポート、ビルドまで行える気がしてきました。あえて swiftc を利用して手動で手順を踏んで行き、理解したことを確かめていきます。
今回試すのはシンプルに以下です。

  • Swift製のモジュール,ライブラリを作る
  • Swiftコードベースから作成したモジュールをインポートする
  • コンパイル,リンクを経て最終的に実行可能ファイルを作成し、実行する

ベースとなる Swift コードを用意する

モジュール内のコードと、それを参照するコードの2つが必要となります。

まずはモジュール側です。以下のような3つのファイルを作ってみました。これらを Zoo モジュールとして公開したいと思います。

Animal.swift
public protocol Animal {
    var name: String { get }
    var age: Int { get }
    func bark() -> String
}
Cat.swift
public struct Cat: Animal {
    public let name: String
    public let age: Int

    public func bark() -> String {
        return "\(name) < にゃーん🐱"
    }

    public init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}
Dog.swift
public struct Dog: Animal {
    public let name: String
    public let age: Int

    public func bark() -> String {
        return "\(name) < わんわん🐶"
    }

    public init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

さらに、この Zoo モジュールを参照する main.swift を用意します。

main.swift
import Zoo

let animals: [Animal] = [
    Dog(name: "ぽち", age: 1),
    Cat(name: "たま", age: 2)
]
animals.forEach { animal in
    print(animal.bark())
}

ディレクトリ構成は下記のような形にしてみます。

.
├── Zoo
│   └── src
│       ├── Animal.swift
│       ├── Cat.swift
│       └── Dog.swift
└── main.swift

モジュールがない状態でコンパイルする

試しに、この時点でビルドしようとしたらどんな風に怒られるのか試してみます。main.swift には import 文が記載されていますが、まだモジュールファイルも作成していないため、インポートはできないはずです。swiftc を利用したコンパイルを試してみましょう。
コンパイルでは、ソースファイル(.swift)からオブジェクトファイル(.o)ファイルを作成します。リンクはまだ行われておらず、実行可能ファイルではないため実行はできません。swiftc でオブジェクトファイルを出力するオプションは -emit-object ですが、-c とも記述できます。

$ swiftc -c main.swift
main.swift:1:8: error: no such module 'Zoo'
import Zoo
       ^

no such module と怒られてしまいました。モジュールの定義すらしていないので当然ですね。

モジュールを作成する

では、モジュールファイルを定義していきましょう。Objective-C の場合には .modulemap ファイルを記述していくことになりますが、Swift の場合、モジュールファイルを出力するのは簡単です。-emit-module オプションを利用して以下のように出力できます。

$ cd Zoo
$ swiftc -module-name Zoo -emit-module src/*
$ ls
Zoo.swiftdoc    Zoo.swiftmodule src

これでモジュールファイルができたので、インポートができるようになっているはずです。モジュールのインポートのためには、swiftc にモジュールの search path を伝える必要があり、-I オプションで指定できます。今回は Zoo ディレクトリ直下にモジュールファイルが出力されているので、./Zoo を import searh path として指定します。

$ cd ..

# 正常終了する
$ swiftc -c main.swift -I ./Zoo

# main.o (オブジェクトファイル) ができている
$ ls
Zoo        main.o     main.swift

正常にコンパイルできました!
せっかくなので file コマンドで確認してみると、Mach-O ファイルフォーマットのオブジェクトファイルであることがわかります。

$ file main.o
main.o: Mach-O 64-bit object x86_64

ライブラリがない状態でリンクする

オブジェクトファイルは main.swift をコンパイルした結果に過ぎません。実行可能形式にするためにはリンクを実行し、ライブラリのコードを実行できるようにする必要があります。この実行可能形式にするためのオプションとして、-emit-executable が用意されているので、試しに実行してみましょう。

$ swiftc -emit-executable main.o
Undefined symbols for architecture x86_64:
  "_$s3Zoo3CatV4name3ageACSS_SitcfC", referenced from:
      _main in main.o
  "_$s3Zoo3CatVAA6AnimalAAWP", referenced from:
      _main in main.o
  "_$s3Zoo3CatVN", referenced from:
      _main in main.o
  "_$s3Zoo3DogV4name3ageACSS_SitcfC", referenced from:
      _main in main.o
  "_$s3Zoo3DogVAA6AnimalAAWP", referenced from:
      _main in main.o
  "_$s3Zoo3DogVN", referenced from:
      _main in main.o
  "_$s3Zoo6AnimalMp", referenced from:
      _$s3Zoo6Animal_pMa in main.o
ld: symbol(s) not found for architecture x86_64

symbol(s) not found と怒られてしまいました。ld はリンカの名前です。オブジェクトファイルの生成には成功しましたが、リンクに失敗してしまっています。モジュールファイルだけ存在してライブラリが存在していないので、当然ですね。

ライブラリを作成する

では、ライブラリを作成していきます。swiftc にはライブラリ出力のためのオプションも用意されています。Dynamic Library を出力したい場合は -emit-library オプションを利用します。

$ cd Zoo
$ swiftc -module-name Zoo -emit-library src/*

# libZoo.dylib が作成されている
$ ls
Zoo.swiftdoc    Zoo.swiftmodule libZoo.dylib    src

モジュールファイル作成の時と同様、簡単に生成できました。file コマンドで確認してみると、確かに Mach-O ファイルフォーマットの Dynamic Library であることがわかります。

$ file libZoo.dylib
libZoo.dylib: Mach-O 64-bit dynamically linked shared library x86_64

それでは、改めてリンクを実施して実行可能ファイルを出力してみましょう。この時、モジュールの時と同様に、リンク対象のライブラリ search path 教えてやる必要があり、-L で指定できます。また、リンクすべきライブラリ名も指定する必要があるようで、こちらは -l で指定します。.Zoo 以下にある Zoo というライブラリとリンクしたいので、以下のようなコマンドを実行します

# 正常に実行される
$ swiftc -emit-executable main.o -L ./Zoo -lZoo

# main ができている
$ ls
Zoo        main       main.o     main.swift

無事にリンクを終えて、実行可能ファイルの main が生成できたようです!file コマンドで確認すると、確かに実行可能ファイルになっていることがわかります。

$ file main
main: Mach-O 64-bit executable x86_64

では、実行してみましょう!

$ ./main
dyld: Library not loaded: libZoo.dylib
  Referenced from: /Users/tasuwo/workspace/sandbox/adcl/swift-module/./main
  Reason: image not found
[1]    1165 abort      ./main

今度は Library not loaded と怒られてしまいました :thinking: モジュールもライブラリも作成して、各々の探索パスも設定して、ビルドもちゃんと通ったのに、ランタイムエラーになってしまいました。

Dynamic Library の場所を dyld に教える

もう一度エラーをよくみてみます。

$ ./main
dyld: Library not loaded: libZoo.dylib
  Referenced from: /Users/tasuwo/workspace/sandbox/adcl/swift-module/./main
  Reason: image not found
[1]    1165 abort      ./main

dyld というのは、ダイナミックリンカーの名前です。動的リンクでは、実行対象の実行形式ファイルに含まれていない、どこか別の場所に存在するライブラリを必要に応じてロードします。image not found とは、読んで字の通り、そのどこか別の場所に存在するはずのライブラリを、dyld が見つけられなかった、ということになります。

では、dyld はどこにライブラリを探しにいったのでしょう?

環境変数で指定する

otool -L を利用すると、dyld が探しにいく必要のあるライブラリのパス一覧を確認することができます。今回利用している libZoo.dylib について確認してみます。17

$ otool -L libZoo.dylib
libZoo.dylib:
        libZoo.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
        /usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.8.255)

libobjclibsystem, libswiftCore はシステムライブラリと思われます。これらは絶対パスで記述されている一方で、libZoo.dylib のみファイル名、相対パスで記述されていることがわかります。man ページを確認しにいくと、相対パスで記述されている場合、dyld は環境変数 DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH を順に探索しにいくようです。17

ならば、環境変数を設定してやれば解決するはずです。以下のようにパスを設定して実行してやります。

$ env DYLD_LIBRARY_PATH=${PWD}/Zoo ./main
ぽち < わんわん🐶
たま < にゃーん🐱

実行できました!

install name を指定する

環境変数を設定すれば Dynamic Library を見つけさせることはできましたが、そもそも他のシステムライブラリと同様に絶対パスを指定することはできないのでしょうか?
dyld による探索対象のパスを表す設定項目は install name と呼ばれ、Dynamic Library の生成時にリンカ (ld) によって埋め込むことができます。18 この時利用するリンカのオプションは -install_name <install name> です。swiftc は、-Xlinker オプションを利用することで、その後ろに連なるオプションをリンカに受け流すことができます。そのため、ld に -install_name ${PWD}/libZoo.dylib というオプションを受け渡したければ、swiftc に -Xlinker -install_name -Xlinker ${PWD}/libZoo.dylib というオプションを受け渡せば良い、ということになります。

それでは、これを付与して再度 Dynamic Libraryを生成しなおしてみましょう。

$ cd Zoo

# 正常に生成できる
$ swiftc -module-name Zoo -emit-library src/* -Xlinker -install_name -Xlinker ${PWD}/libZoo.dylib

# install name が絶対パスに変わっている
$ otool -L libZoo.dylib
libZoo.dylib:
    /Users/tasuwo/workspace/sandbox/adcl/swift-module/Zoo/libZoo.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
    /usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.8.255)

うまく設定できたようです。実行可能ファイルも生成し直して、実行しなおしてみます。

$ cd ..
$ swiftc -emit-executable main.o -L ./Zoo -lZoo
$ ./main
ぽち < わんわん🐶
たま < にゃーん🐱

この方法でもちゃんと実行できることが確認できました!

Runpath Search Path を指定する

相対パス, 絶対パス以外にも Dynamic Library へのパスを指定する方法があります。それが Runpath Search Path です。19 18 これは、探索対象のパスのリストを実行形式ファイルに、そのパスからの相対パスのみを Dynamic Library に埋め込んでおき、dyld にそれらを合わせてライブラリを探索しにいかせることが出来る方法です。

Dynamic Library の install name が @rpath から始まるパスであった場合、dyld はそれを Runpath Search Path を利用したパス指定であると判断します。試しに、Dynamic Library の install name を RunPath を利用したものに書き換えてみましょう。

$ swiftc -module-name Zoo -emit-library src/* -Xlinker -install_name -Xlinker @rpath/libZoo.dylib
$ otool -L libZoo.dylib
libZoo.dylib:
    @rpath/libZoo.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
    /usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.8.255)

さらに、実行可能ファイル側に探索対象のパス群を埋め込むことが出来ます。このパス群もコンパイラではなくリンカに対してオプションを渡す必要があるため、install name と同様に swiftc の -Xlink オプションを利用します。リンカに渡すオプションは -rpath <パス名> です。

$ swiftc -emit-executable main.o -L ./Zoo -lZoo -Xlinker -rpath -Xlinker ${PWD}/Zoo

あるいは、すでに存在するライブラリに対して、install_name_tool というツールを使うと rpath の書き換えが直接行えるそうなので、既存の Dynamic Library をどうにかしたい場合は、そちらを試すこともできそうです。19

こうして生成された実行可能ファイルには、指定した Rpath が埋め込まれているはずです。これは Dynamic Library ロード時の設定の一部として、Mach-O ファイルの Load Commands 領域に格納されています。Load Components 部を確認するためのコマンドは otool -l です。実行すると長々と出力されますが、その中に LC_RPATH という文言が含まれたセクションがあります。これが Rpath の設定です。18
実際に確認してみると、受け渡した Rpath がちゃんと設定されていることがわかります。

Load command 17
         cmd LC_RPATH
     cmdsize 72
        path /Users/tasuwo/workspace/sandbox/adcl/swift-module/Zoo (offset 12

これで Dynamic Library 側には install name に @rpath が含まれており、実行可能形式ファイルには RunPath が埋め込まれました。これで、dyld は Runpath Search Path を利用して Dynamic Library を発見してくれるはずです。実行してみましょう。

$ ./main
ぽち < わんわん🐶
たま < にゃーん🐱

実行できました :clap:

今回リンカのオプションとして設定した Runpath Search Path ですが、Xcode の Build Setting にも実は設定項目として存在します。project.pbxproj 上だと LD_RUNPATH_SEARCH_PATH という設定項目名です。Xcode に Embedded Library として追加されているライブラリは、Build Phases の Copy Files にて、この Runpath にファイルがコピーされているものと思われます。Runpath の設定がおかしかったりファイルのコピーができていなかったりした場合には、Xcode 上でも同様のエラーが発生することになるでしょう。

まとめ

以上、モジュール,インポート,ライブラリ,リンクについて自分の中で噛み砕きつつ、実際に手を動かしてエラーを出しながらビルドまで漕ぎ着ける話でした。Xcode で開発しているとほとんど裏でやられていることではありますが、個人的には、ふんわりとした知識を深掘りしてみることで、実際に Xcode のインポート周りでエラーが出たときにも怖がらずにエラーを読める、裏で何が行われているのかある程度想像できるので何が悪いかもある程度想像して調査に乗り出せる、等の利点がありました。

思いの外記事のまとめに時間がかかってしまい、前半で整理した基礎知識のうち、module map や Framework についてはあまり深掘りできなかったのが反省点です。iOS アプリのプロジェクトにて C 製の Static/Dynamic Library を clang で手元でビルドして利用するみたいなことも試してみた のですが、時間と気力の都合上記事に含めることが出来ませんでした。ここら辺は機会があればどこかでまとめていきたいです。

66
41
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
66
41