概要
新規 Xcode project を自動生成する技術 にて Xcode プライベートフレームワークを用いたプログラムを作成しました。
Xcode プライベートフレームワークはプライベートというだけあり一切のドキュメントは存在せず、ヘッダーファイルすら公開されていません。
そんな状態から API を読み解き、実用的なプログラムから利用するためにはどうすればいいのかを記します。
目的
Xcode プライベートフレームワークを利用したい目的はいろいろ考えられます。
最もよくあるシチュエーションは、Xcode プラグインを作成するときでしょう。
たとえば XVim というプラグインは Vim 風のキーバインドを Xcode で実現するためのものです。
これは Xcode Source Editor Extension などの公開 API では実現できない機能を含むため、かなりヘビーにプライベートフレームワークを利用しています。
他にも 新規 Xcode project を自動生成する技術 で紹介した xcnew のように、CLI から Xcode を利用したいときにも役に立つかもしれません。
利用するプログラミング言語
ここ数年で Swift が飛躍的に普及しましたが、プライベートフレームワークを利用するときは Objective-C を利用した方がメリットが大きいです。
というか、利用する機能によっては Swift では実装不可能になると思います。
- LLVM modulemap を作成しなくてもヘッダーだけでフレームワークが利用できる
-
extern
宣言が存在するので、シグネチャさえわかれば外部で定義された関数を呼べる - duck typing をサポートしているので型情報がわからなくてもメソッドを呼べる
利用するプライベートフレームワークやクラスを探す
さて、実際にプライベートフレームワークを利用したプログラムを書くときに最初につまづくのが、そもそもどのフレームワークを利用したら良いかわからないことです。
また、フレームワークを特定できたとして、利用するクラスやメソッドがわからないと何もできません。
利用するフレームワークやクラスを探すには、以下のような方法を取ります。
すでにプライベートフレームワークを利用しているプロジェクトのソースコードを読む
先に紹介した XVim のようにプライベートフレームワークをすでに利用しているプロジェクトのソースコードを読み、利用したい機能が存在するか確認します。
最も手軽で、しかも使い方も同時にわかるので可能ならこの選択肢を選ぶのがいいと思います。
一方で、これまで OSS として公開されていない機能を利用する場合はこの方法は使えません。
プライベートヘッダーを公開しているプロジェクトのソースコードを読む
mmmulani/class-dump-o-tron のように、GitHub 場でプライベートフレームワークのヘッダーを公開しているプロジェクト内を検索することで、利用したいフレームワークを探します。
何も準備がいらないのでこの方法もなかなか手軽ですが、公開されているヘッダーのバージョンが古かったりするので注意が必要です。
自力でヘッダーをダンプしてソースコードを読む
nygard/class-dump を利用して、プライベートフレームワークのみならず、任意のバイナリからシンボルを抽出してヘッダーファイルを生成することができます。
やり方は class-dump でバイナリからヘッダーファイルを抽出する。 の記事で詳しく解説されています。
注意点として、class-dump
は今も開発が続いているものの、長らくリリースされていないため、パッケージマネージャーから入るバージョンは古いことが多いです。
README を参考にソースからインストールすることをお勧めします。
ダンプしてできたヘッダーは C のマクロや一部の型情報が欠落したものになります。
具体的には、メソッドシグネチャの NSString *
などの型宣言はすベて id
に置き換わっています。
これは Objective-C がオブジェクト型の型情報をバイナリに保持しないためで、仕方がありません。
メソッド名やラベルなどから実際の型を推測することになります。
プロジェクトのビルド設定をする
さて、晴れて利用したい Xcode プライベートフレームワークやクラスが見つかったら、次にそれを利用するプログラムのプロジェクトを作成します。
基本的には通常通り macOS 向けのアプリケーションを作成しますが、ヘッダーとフレームワークの検索パス設定を変更する必要があります。
プライベートヘッダーをプロジェクト内にコピー
あらかじめ class-dump
を利用してヘッダーを生成し、プロジェクト内にコピーしておきます。
コピー先のパスはプロジェクト内であればなんでも構いませんが、ここではいったん $SRCROOT/PrivateHeaders
にコピーしたと仮定して進めます。
ヘッダーの検索パスの設定
プロジェクトのターゲットの Build Settings を開き、Header Search Paths
に以下のようにコピーされたヘッダーのパスを設定します。
フレームワークの検索パスの設定
Xcode プライベートフレームワークは /Applications/Xcode.app/Contents/Frameworks
や /Applications/Xcode.app/Contents/SharedFrameworks
に存在します。
これら2つのパスを設定するため、Framework Search Paths
に以下のように設定します。
$(DEVELOPER_DIR)
は通常 /Applications/Xcode.app/Contents/Developer
に展開されるため、このパスからの相対パスとして指定することで、ビルドに利用する Xcode と同じバージョンの Xcode プライベートフレームワークを利用することができます。
内部ログ出力の設定
必須ではありませんが、Xcode はもともとデバッグ用のログを出力する機構を備えています (ドキュメントは例によってありませんが)。
メインのスキームを開き、起動引数に -DVTDefaultLogLevel N
(N
は 1以上の整数) を設定すると、Xcode プライベートフレームワーク内の実行ログが出力されるようになります。
プライベートフレームワーク内のクラスやメソッドの使い方を調べる
プロジェクト設定ができたところで、いよいよ実装です。
実用的なプログラムを実装するためには Xcode プライベートフレームワークの名前やクラス名を知っているだけではダメで、使い方そのものを把握しなければなりません。
そのためには、以下のような方法があります。
とりあえずメソッドを呼んでみる
Xcode 内部ではアサーションを多用しているため、事前条件を満たさないメソッド呼び出しは多くの場合実行時エラーになります。
そのため、原始的ですが 何も考えずにメソッドを呼んでみる ことで、エラーメッセージを参考にある程度使い方を把握することができます。
たとえば、試しに [IDETemplateFactory instantiateTemplateForContext:options:whenDone:]
メソッドを呼んでみましょう。
使い方がわからないので、まずは全引数を nil
にしてみます。
[[[IDETemplateFactory alloc] init] instantiateTemplateForContext:nil options:nil whenDone:nil];
ここでコンソールを見ると、有用なログが出力されています。
2019-11-03 16:46:27.447570+0900 xcnew[71607:21689971] [MT] DVTAssertions: ASSERTION FAILURE in /Library/Caches/com.apple.xbs/Sources/IDEFrameworks/IDEFrameworks-15405/IDEFoundation/TemplateInfrastructure/IDETemplateFactory.m:29
Details: method -[IDETemplateFactory instantiateTemplateForContext:options:whenDone:] is a responsibility of subclasses of IDETemplateFactory
Object: <IDETemplateFactory: 0x10cdd3990>
Method: -instantiateTemplateForContext:options:whenDone:
Thread: <NSThread: 0x10cd0c190>{number = 1, name = main}
Hints:
method -[IDETemplateFactory instantiateTemplateForContext:options:whenDone:] is a responsibility of subclasses of IDETemplateFactory
というところから、 IDETemplateFactory
は抽象クラスであること、サブクラスのメソッドを呼べばうまく動作するであろうことがわかります。
次は IDETemplateFactory
のサブクラスをヘッダーから探し、それを呼んでみれば別の手がかりが得られそうですね。
このような 試しに呼んでみる を何十回も繰り返すことで、徐々に使い方が見えてきます。
コンパイル後のアセンブリを読む
このようにして使い方を探っていると、ヘッダーファイルや実行ログからは得られない情報が必要になることがあります。
通常のアプリケーション開発ではブレークポイントをセットして実行することで動作を追うことができますが、一応同じ方法が使えます。
先ほどの IDETemplateFactory
のログ出力と同じ情報を、ブレークポイントを利用して使い方を探れるかやってみましょう。
こんな感じで見たいメソッドにブレークポイントをセットします。
実行後、先ほどの箇所でブレークするので、 step into します。
すると SIGABRT
で停止し、Debug Navigator にスタックトレースが表示されています。
対応するデバッグシンボルが埋め込まれていないのでアセンブリで表示されますが、恐れることはありません。
まずは一つ階層を潜り、8 - [IDETemplateFactory instantiate...]
と表示されている部分を見てみます。
落ちる直前に _DVTAssertionFailureHandler
に対する callq
命令が呼ばれているので、アサーションで落ちていることがわかります。
さらにその上の 0x100d82365 <+115>
ではアサーションのエラーメッセージと思われる文字列が表示されています。
代入先の %r10
レジスタは _DVTAssertionFailureHandler
に対する callq
命令の直前で pushq
されているので、 _DVTAssertionFailureHandler
関数に対する最後の引数であることが推測できます。
_DVTAssertionFailureHandler
が何かはさらに階層を潜って調べればわかりますが、標準のアサーション実装である NSAssertionHandler に近いものであると考えると、最後の引数が description
であることは想像がつきますね。
こんな感じでアセンブリのソースコードリーディングを繰り返すことで、ログが表示してくれない情報を拾うことができます。
ここでは紹介しませんでしたが、シンボリックブレークポイントを利用して直接呼び出していない部分でブレークするテクニックもよく利用します。
逆アセンブラを使う
アセンブリを読むことで論理的にはあらゆる実行バイナリの動作を把握することができるとはいえ、一括で変換して任意のメソッドを読めたら便利ですし、普段高級な言語を読み書きしている身には擬似コードなど読みやすいフォーマットだとより助かります。
そんなときには、Hopper などの mach-O 対応逆アセンブラを使うことで、バイナリの内容をアセンブラや擬似コードに変換して読むことができます。
試しに Hopper をインストールして、DVTFoundation.framework を逆アセンブルしてみましょう。
Hopper を開いたら Try the Demo
を選択してデモ版を起動します。
メニューバーから File
→ Open...
を選択し、開きたいバイナリのパス (今回は /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/DVTFoundation
) を選択します。
パース時のオプションを聞かれますが、デフォルトのままで大丈夫です。
画面の中央上部にある segmented control から表示方式を選択できます。
ここでは擬似コードで表示してみます。
画面左側のペインから Proc.
を選ぶと関数シンボルのみが表示されます。
ここでは試しに検索バーに DVTFilePath filePathForFileURL
と入れてみます。
検索結果を選択して右側ペインを見ると、以下のように擬似コードに変換されたアセンブリが見られます。
このメソッド [DVTFilePath filePathForFileURL:]
は [NSURL isFileURL]
を満たす引数が渡されないとアサーションで落ちるということがはっきりわかりますね。
終わりに
以上、長々と Xcode プライベートフレームワークを利用したプログラムを作成するための情報をまとめました。
どうもこの世界はニッチすぎるのか、外から得られる情報がとても限られており、普段の開発のようにググって調べる手段があまり使えません。
そのため結果的に既存のコードやダンプしたコードを読むことが多くなりますが、一方でドキュメントには書かれていない Xcode 内部の仕組みなど、得がたい知見を得ることができます。
もしチャンスがあれば Xcode プライベートフレームワークで遊んでみることをオススメします。