Cocoa Application における専用 Framework の使用法と Swift への対応:動的ライブラリ(.dylib)を含めて

macOS における一般的な Application は Cocoa API を用いていることから Cocoa Application と呼ばれている。 Cocoa Application の構造はバンドル構造を採用しており、Framework や動的ライブラリを内部に含めて配布できる。 この方法の利点は /Library/Frameworks や /usr/local/lib へのインストールを必要とせず Application を利用できる点であるが、 Application は任意のディレクトリで実行される可能性があり、外部 API を呼び出す以上は実行時に動的リンクするパスを解決する方法が必要となる。 macOS の採用する Mach-O loader は動的リンクで利用するパスに相対パスを与える手法として現在三つのものが用意されている。 @executable_path、@loader_path、@rpath である。ここでは Xcode 10.x においてこれら相対パス表現を用いてライブラリを活用する方法について纏める。

動的ライブラリの役割と典型的なトラブル

動的ライブラリは Application 中の主だった処理を分離独立させ、再利用性、保守性を高める目的で用いられるものである。 例えば慣習的に動的ライブラリ "X" を利用する際はファイル名は libX.dylib であると仮定されており、 コンパイラオプションに "-lX" と与える事で実行時に libX.dylib を参照できる様にリンクしてくれる。正しい名称でなければ
ld: library not found for -lX
clang: error: linker command failed with exit code 1 (use -v to see invocation)
といった様にライブラリが見つからないと出力され、ビルドは失敗する。 この様なエラーは、ヘッダを正しく読み取っている為に一見異常がない様に見えるが、シンボル(函数や定数の実体など)が見つからない場合も出力される。 一方、ビルドが成功したとしてもライブラリが適切に配置されていない場合、
dyld: Library not loaded: @loader_path/libX.dylib
	Referenced from: <Executable Path>
	Reason: image not found
と言ったように出力され、実行時の動的リンクに失敗する。以上の問題は基本的にビルドオプションの設定に誤りがある為に生ずるものである。 以下では Xcode workspace として統合しない Xcode Project として Frameworks、Application を別々に開発する場合の設定例について示す。 なお以上の問題例は -L オプションや DYLD_* といった環境変数によって対処することもできるが、以下の例は比較的やりやすい例を示しておく。

実行バイナリ、ライブラリの依存関係を確認するコマンド

macOS で実行バイナリやライブラリのシンボルやリンク先を調べる上で便利なコマンドとして otool、nm、tapi がある。必要に応じて macOS 13.x における Finder.app / Cocoa Framework を例に説明する。

otool は実行バイナリやライブラリの依存関係を確認できるコマンドである。L オプションを指定する。例えば Finder.app は下記の通りとなる。

$ otool -L /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder:
	/System/Library/PrivateFrameworks/TimeMachine.framework/Versions/A/TimeMachine (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 1561.60.100)
	/System/Library/Frameworks/ContactsUI.framework/Versions/A/ContactsUI (compatibility version 1.0.0, current version 1808.8.0)
	/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1454.96.0)
	/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox (compatibility version 1.0.0, current version 911.10.0)
	/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 822.37.0)
	/System/Library/Frameworks/Quartz.framework/Versions/A/Frameworks/QuickLookUI.framework/Versions/A/QuickLookUI (compatibility version 1.0.0, current version 0.0.0)
	/System/Library/PrivateFrameworks/SystemMigration.framework/Versions/A/SystemMigration (compatibility version 1.0.0, current version 866.0.0)
	/System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/DesktopServicesPriv (compatibility version 1.0.0, current version 895.6.1)
	/System/Library/Frameworks/CloudKit.framework/Versions/A/CloudKit (compatibility version 1.0.0, current version 736.16.0)
	/System/Library/PrivateFrameworks/UserActivity.framework/Versions/A/UserActivity (compatibility version 1.0.0, current version 98.10.0)
	/System/Library/PrivateFrameworks/IconServices.framework/Versions/A/IconServices (compatibility version 1.0.0, current version 97.6.0)
	/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration (compatibility version 1.0.0, current version 963.50.9)
	/System/Library/PrivateFrameworks/Bom.framework/Versions/A/Bom (compatibility version 2.0.0, current version 194.0.0)
	/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
	/System/Library/PrivateFrameworks/SystemAdministration.framework/Versions/A/SystemAdministration (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/PrivateFrameworks/CoreUI.framework/Versions/A/CoreUI (compatibility version 1.0.0, current version 494.1.0)
	/System/Library/Frameworks/ImageCaptureCore.framework/Versions/A/ImageCaptureCore (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore (compatibility version 1.2.0, current version 1.11.0)
	/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/PrivateFrameworks/Backup.framework/Versions/A/Backup (compatibility version 1.0.0, current version 1054.6.1)
	/System/Library/PrivateFrameworks/AOSAccounts.framework/Versions/A/AOSAccounts (compatibility version 1.0.0, current version 1.9.95)
	/System/Library/Frameworks/QuickLook.framework/Versions/A/QuickLook (compatibility version 1.0.0, current version 0.0.0)
	/usr/lib/libicucore.A.dylib (compatibility version 1.0.0, current version 59.1.0)
	/System/Library/Frameworks/NetFS.framework/Versions/A/NetFS (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices (compatibility version 1.0.0, current version 50.0.0)
	/System/Library/PrivateFrameworks/Tourist.framework/Versions/A/Tourist (compatibility version 1.0.0, current version 1.0.52)
	/System/Library/PrivateFrameworks/Suggestions.framework/Versions/A/Suggestions (compatibility version 1.0.0, current version 213.0.0)
	/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox (compatibility version 1.0.0, current version 492.0.0)
	/System/Library/PrivateFrameworks/DiskImages.framework/Versions/A/DiskImages (compatibility version 1.0.8, current version 480.60.3)
	/usr/lib/libDiagnosticMessagesClient.dylib (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libCoreStorage.dylib (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/PrivateFrameworks/DiskManagement.framework/Versions/A/DiskManagement (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libcsfde.dylib (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/PrivateFrameworks/PerformanceAnalysis.framework/Versions/A/PerformanceAnalysis (compatibility version 1.0.0, current version 194.0.0)
	/System/Library/Frameworks/Quartz.framework/Versions/A/Frameworks/ImageKit.framework/Versions/A/ImageKit (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/SecurityInterface.framework/Versions/A/SecurityInterface (compatibility version 1.0.0, current version 55109.50.6)
	/System/Library/Frameworks/Contacts.framework/Versions/A/Contacts (compatibility version 0.0.0, current version 0.0.0)
	/System/Library/PrivateFrameworks/AppleSystemInfo.framework/Versions/A/AppleSystemInfo (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/Collaboration.framework/Versions/A/Collaboration (compatibility version 1.0.0, current version 80.0.0)
	/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 58286.70.14)
	/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 22.0.0)
	/System/Library/PrivateFrameworks/Sharing.framework/Versions/A/Sharing (compatibility version 1.0.0, current version 1050.22.3)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
	/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork (compatibility version 1.0.0, current version 902.3.1)
	/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1454.96.0)
	/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics (compatibility version 64.0.0, current version 1161.21.2)
	/System/Library/Frameworks/CoreText.framework/Versions/A/CoreText (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/ImageIO.framework/Versions/A/ImageIO (compatibility version 1.0.0, current version 1.0.0)
	/System/Library/Frameworks/SecurityFoundation.framework/Versions/A/SecurityFoundation (compatibility version 1.0.0, current version 55185.50.5)

ここに出力されるライブラリが存在しない場合やパスが解決できない場合は実行時に dyld: Library not loaded が引き起こされる。

nm は実行バイナリやライブラリで定義されていたり利用しているシンボルを一覧に出力するものである。例えば Cocoa Framework は下記の通りとなる。

$ nm /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa
0000000000000ff8 S _CocoaVersionNumber
0000000000000fd0 S _CocoaVersionString
                 U dyld_stub_binder

シンボルのアドレス、シンボルの種類(S, U など)、シンボル名が出力される。 Cocoa Framework は Foundation、AppKit、CoreData を読み込むためのライブラリなのでシンボルはバージョン管理用の定数のみと極めて少ない。

最後に最近登場した tapi である。Xcode の SDK(例えば Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs)に含まれるライブラリはバイナリは現在全てテキスト化され実体がない。 拡張子 .tbd とあるファイルがバイナリに変わるテキストである。tapi はこのテキストを出力するコマンドであり、標準の PATH では使えないが、Xcode Toolchains には含まれているのでパスを通せば使用できる。

$ export PATH=${PATH}:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
$ tapi stubify <Library Path>

<Library Path> に .dylib や Framework の実行ファイルを指定すると .tbd ファイルを得ることができる。 バイナリに変わって SDK で用いれられているだけあって、otool や nm に比べると情報量が多い。 例えば Xcode 10.x の Cocoa Framework における Cocoa.tbd は次の様な内容になっている。

--- !tapi-tbd-v3
archs:           [ x86_64 ]
uuids:           [ 'x86_64: 4D5D4968-E233-3598-B434-E1009D48A628' ]
platform:        macosx
install-name:    /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa
current-version: 23
objc-constraint: none
exports:
  - archs:           [ x86_64 ]
    re-exports:      [ /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit,
                       /System/Library/Frameworks/CoreData.framework/Versions/A/CoreData ]
    symbols:         [ _CocoaVersionNumber, _CocoaVersionString ]
...

実行バイナリ、ライブラリの依存関係を操作するコマンド

macOS では依存関係を変更する場合は install_name_tool を使う。例えば次の様にすれば <Library Path> で <Old Refrenced Path> にリンクしているパスを <New Refrenced Path> に変更する。

$ install_name_tool -change <Old Refrenced Path> <New Refrenced Path> <Library Path>

ライブラリのインストール先を <Refrenced Path> へ変えたい場合は次の様にする。

$ install_name_tool -id <Refrenced Path> <Library Path>

相対パスで利用する専用 Framework

Cocoa Application では Application の実行ファイルは <CFBundleName>.app/Contens/MacOS/<CFBundleExecutable> に配置されるのが一般的である。 CFBundle* は Application バンドルの Info.plist に定義されている CFBundle が参照する定数である。 もしこの Application 専用の Framework をバンドルしたい場合は <CFBundleName>.app/Contens/Frameworks に配置するのが一般的である。

Application 側で設定する内容は 1. Build Settings と 2. Build Phases についてである。 1. Build Settings では Search Paths > Framework Search Paths (FRAMEWORK_SEARCH_PATHS) にビルド時に参照する専用 Framework の配置されているディレクトリを指定する。 *.xcodeproj ファイルのあるディレクトリに参照したい専用 Framework を配置している場合はここに $(PROJECT_DIR) を追加する。 2. Build Phases では Copy Files を追加し、Destination に Frameworks を指定した上で 1. で参照した専用 Framework を追加しておくだけでよい。

専用 Framework で設定する内容は 1. Build Settings である。1. Build Settings では Deployment > Installation Directory (INSTALL_PATH) に @rpath を指定、 Linking > Dynamic Library Install Name (LD_DYLIB_INSTALL_NAME) に $(DYLIB_INSTALL_NAME_BASE:standardizepath)/${EXECUTABLE_PATH} を指定、 Linking > Dynamic Library Install Name Base (DYLIB_INSTALL_NAME_BASE) に @rpath を指定しておく。 また Linking > Run Search Paths (LD_RUNPATH_SEARCH_PATHS) に @executable_path/../Frameworks と @loader_path/Frameworks を追加する。 @executable_path は Application にバンドルさせる場合に有効で @loader_path は別の Framework にこの Framework をバンドルする場合に使える設定である。

専用 Framework に公開用のヘッダを追加しておきたい場合は対象のヘッダファイルを Build Phases の Headers で Public に指定すれば自動的にコピーされる。

相対パスで利用する専用 Framework で動的ライブラリ dylib を参照する場合

vecLib Framework の様に専用 Framework で dylib を参照したい場合がある。この場合 Framework をリンクすれば参照している dylib を自動的にリンクするかどうかという問題が生じる。 vecLib は C ライブラリを使いやすい様にするために用意された Framework であるから dylib を自動的にリンクしている。ここではその設定例について考える。 例えば tapi コマンドに関して Cocoa Framework を例に示しているが、自動的にリンクする Framework や dylib は、この tbd ファイルでは re-exports に示されることになる。 専用 Framework でこの re-exports を追加するには、実は Xcode の Build Settings で Re-Exported とある Linking Option を適切に設定すればよい。

libX.dylib と libY.dylib とが専用 Framework の実行ファイルと同じディレクトリ(EXECUTABLE_FOLDER_PATH=*.framework/Versions/A)に設置される場合の設定について考える。 この場合も 1. Build Settings と 2. Build Phases についての設定が必要で、かつ先述した Run Search Paths などの設定が必要である。 1. Build Settings では Linking > Re-Exported Library Names (REEXPORTED_LIBRARY_NAMES) に X と Y を追加する。これはコンパイラの -l オプションで用いるものと同じである。 Framework を自動的にリンクさせるには Linking > Re-Exported Library Names (REEXPORTED_FRAMEWORK_NAMES) に同様に情報を追加する。 加えて必要があれば Linking > Other Linker Flags (OTHER_LDFLAGS) にこれらのライブラリの位置を指定する。 *.xcodeproj ファイルのあるディレクトリに参照したいライブラリを配置している場合はここに -L$(SRCROOT) を追加する。 2. Build Phases では Link Binary With Libraries に libX.dylib, libY.dylib とリンクさせるライブラリを列挙し、 ヘッダをコピーする場合と同様に Copy Files を追加して Destination に Executables を指定、libX.dylib, libY.dylib とライブラリを指定する。 ライブラリ libX.dylib, libY.dylib のヘッダが必要であれば、これまで同様にヘッダをコピーする。 一方で CUI Tool としてコマンドを内包させたりする場合にライブラリ libX.dylib, libY.dylib などを利用している場合がある。 この様な場合、そのコマンドやライブラリ libX.dylib, libY.dylib そのものの依存関係に修正が必要な場合もあるが、Run Script を追加して修正することが可能である。 それぞれ otool -L として状況を確認すべきであるが、今の場合例えば libX.dylib は @loader_path/libX.dylib とすべきである。

TARGETS=(libX.dylib libY.dylib)
COMMANDS=(myTool)

COMMAND_INSTALL_DIR=${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}
LIBRALY_INSTALL_DIR=${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}

for TARGET in ${TARGETS[@]} ; do
	libraryPath=${LIBRALY_INSTALL_DIR}/${TARGET}
	
	for COMMAND in ${COMMANDS[@]} ; do
		commandPath=${COMMAND_INSTALL_DIR}/${COMMAND}
		install_name_tool -change /usr/local/lib/${TARGET} @loader_path/../${TARGET} ${commandPath}
	done
	install_name_tool -id @loader_path/${TARGET} ${libraryPath}
done

このスクリプトでは二つの事を行なっている。一つは install_name_tool -change である。これは Resources にコマンド myTool がコピーされて libX.dylib, libY.dylib を利用していた場合に、 正しくライブラリを参照できる様に変更している。変更前の情報は otool -L で表示されている内容と同じものを指定する必要があるので、/usr/local/lib は適宜書換えねばならない。 もう一つは install_name_tool -id で、これは libX.dylib, libY.dylib のインストール位置を正しく指定できる様に決定している。

Swift で動的ライブラリ dylib を参照したい場合

Swift において Objective-C の Framework を参照する場合は Bridging Header を追加すればよいが、動的ライブラリ dylib を参照する場合は .modulemap を利用する。 これは Clang で導入されている Modules を利用する物である。Modules はヘッダを事前コンパイルすることでコンパイル時間を短くする物で、Swift はこれを参考にビルドを行う。 例えば現在の Clang では #import <Foundation/Foundation.h> は @import Foundation; に等しい構文となっており、.modulemap をもとに読み込むべきヘッダが決定されている。 動的ライブラリ dylib を含む Accelerate.framework では以下の様な .modulemap が定義されている。

framework module Accelerate {
  umbrella header "Accelerate.h"
  export *
  module * { export * }
  
  framework module vImage [extern_c] {
    umbrella header "vImage.h"
    export *
    module * { export * }
  }

  framework module vecLib {
    umbrella header "vecLib.h"
    export *
    module * { export * }

    module vDSP_translate {
      requires x86, !x86_64
      header "vDSP_translate.h"
      export *
    }
    module Sparse_Solve {
      header "Sparse/Solve.h"
      // Sparse/SolveImplementation.h should not be included without
      // context/directly. Make it textual, same for its dependency
      // SolveImplementationTyped.h
      textual header "Sparse/SolveImplementation.h"
      textual header "Sparse/SolveImplementationTyped.h"
      export *
    }
    module LinearAlgebra {
      header "LinearAlgebra/LinearAlgebra.h"
      // These headers aren't supposed to be included directly or
      // have their own submodules since they depend on context.
      textual header "LinearAlgebra/object.h"
      textual header "LinearAlgebra/matrix.h"
      textual header "LinearAlgebra/vector.h"
      textual header "LinearAlgebra/splat.h"
      textual header "LinearAlgebra/arithmetic.h"
      textual header "LinearAlgebra/linear_systems.h"
      textual header "LinearAlgebra/norms.h"
      export *
    }
    module Quadrature {
      header "Quadrature/Quadrature.h"
      // These headers aren't supposed to be included directly or
      // have their own submodules since they depend on context.
      textual header "Quadrature/Integration.h"
      export *
    }
  }
}

例えば umbrella header は読込先のヘッダで #include されているヘッダを全て解析するものである。 link を指定することで dylib などを任意にリンクさせることができるが、re-exports する専用 Framework を用意していたり、 ビルド時に -l オプションを適宜指定すればこの設定は不要である。

相対パス @executable_path、@loader_path、@rpath について

動的リンクで利用できる相対パスは @executable_path、@loader_path、@rpath の三種類がある。 @executable_path は実行ファイルからみたパスを意味するもので、一般には Application やコマンドなどの実行ファイルである。 @loader_path はローダ(dyld)が現在読み込み中のライブラリが設置されているディレクトリを意味する。 @rpath は @executable_path とも @loader_path とも読み替えられる意味を持っている。

  1. 自作ライブラリのリンク時に Undefined symbols for architecture エラーが発生する。
  2. Library not loaded エラー?ここを見直そう
  3. dynamic library の OS Xの補足
  4. Modulemap 経由で C のライブラリを利用する
  5. Clang Modulesについて
  6. Clang 9 documentation > Modules
  7. @executable path, @load path and @rpath (wincent.com)
技術考󠄁 > Cocoa Application における専用 Framework の使用法と Swift への対応:動的ライブラリ(.dylib)を含めて
Copyright© R01[2019]. All Rights Reserved.