WWDC以来、自分の過去の原稿を読み返している。IntelからARMへのアーキテクチャ移行がスタートされるということで、「Rosetta」と「ユニバーサルバイナリ」が再びスポットライトを浴びることになったからだ。特に後者のユニバーサルバイナリは、2度目どころか3度目、AppleによるNeXT Software買収の隠れたメリットを思い知らされている。昔話を含め、ユニバーサルバイナリとの付き合いかたをあれこれ綴ってみたいと思う。

NeXT以来の"伝統技能"

AppleによるNeXT Software買収といえば、まず思い浮かぶのはSteve Jobsというアイコン/カリスマの復活だが、会社の資産という意味ではNEXTSTEP/OPENSTEPで間違いない。

買収が決まった当時、NeXTは「Web Objects」という動的にWEBページを生成するサーバおよび開発フレームワークに注力しており、それもAppleの資産となったが、Mac OS Xに改名されAppleの屋台骨にまで成長したNEXTSTEP/OPENSTEPとは比ぶべくもない。Mac OS Xなかりせば、iOSやiPad OSといった派生OSも存在しないわけで、屋台骨どころかアイデンティティといっても大げさではないだろう。

個人的には、Mac OS Xおよび派生OSが"うまくやって"これたことの要因は「MAB」にあると思う。MABとはMulti Architecture Binaryの略で、複数区画に異なるバイナリを収録することで単一ファイルでも異種CPUによるネイティブな動作を可能にする。これは、NEXTSTEP以来採用され続けているバイナリフォーマット「Mach-O」ならではの伝統(?)技能だ。

もうお気づきと思うが、今回のWWDCで「Universal 2」として発表された機構は、このMABを応用したもの。ファイルサイズは多少膨らむが、x86_64バイナリとApple Siliconバイナリを単一のファイルにできるため、(バンドル形式の)アプリであろうがCUIコマンドであろうが、Intel MacでもApple Silicon Macでもネイティブ動作するバイナリファイルを提供できる。

PowerPCからIntelにアーキテクチャ変更したときも、i386/32 bitからx86_64/64 bitに移行したときも、基本的にはこの方法。スムースなプラットフォーム移行を実現する手段として、なかなかこれ以上のものはないのでは? MABはいまなお現役、枯れてはいるがマブい(これが言いたかった)技術なのだ。

  • このロゴを再び目にすることになろうとは

ユニバーサルバイナリの見分けかた

macOS Catalinaの現在、Finderにはアプリがユニバーサルバイナリ化されているかどうか見分ける手段がない。かつてのFinderには情報ウインドウに「Universal」や「32ビットモードで開く」という表示があるかどうかで判定できたが、そのような配慮はされなくなった。システム情報の「レガシーソフトウェア」欄にも、なにも表示されない。

しかし、アプリ/コマンドがユニバーサルバイナリかどうか、どのアーキテクチャのバイナリが収録されているかは、lipoコマンドで「-info」オプションを使い、引数にバイナリファイルを指定すれば確認できる。/usr/lib以下には、Catalinaの現在もi386とx86_64両方のバイナリを収録したライブラリを複数見つけられるはずだ。

$  lipo -info /usr/lib/libSystem.B.dylib 
  • 収録されているアーキテクチャを確認するだけなら、オプションは「-info」

動作環境にLion以前のシステムが含まれているアプリは、いまなおPowerPCバイナリが収録されていることが多い。たとえば、SDカードの初期化でお世話になる「SD Memory Card Formatter」(リンク)は、Mac OS X 10.7 Lion以降のシステムに対応しているが、オプションに「-detailed_info」を使うと、i386とx86_64、PowerPCという3種類のバイナリを収録していることがわかる。

$ lipo -detailed_info /Applications/SD\ Card\ Formatter.app/Contents/MacOS/SD\ Card\ Formatter
  • 3種のアーキテクチャに対応したバイナリを確認できる

ユニバーサルバイナリの作りかた

意外なほどあっさり作成できるのも、ユニバーサルバイナリの特長だ。実際にはXcodeなどのIDEを使うことになるだろうが、CUIレベルで見ればコンパイラでビルドするときに「-arch」オプションでアーキテクチャ名を指定するか、ビルド後にlipoコマンドで(アーキテクチャ別のバイナリを)つなぎ合わせるか、好みの方法を選択できる。

ただし、コンパイラからユニバーサルバイナリを生成する場合、対象アーキテクチャの共有ライブラリが存在しなければならない。一方、lipoコマンドは生成後のバイナリをつなぎ合わせるだけのため、ターゲットSDKの有無は問題にならない。

生成後のバイナリをつなぎ合わせる場合、たとえばカレントディレクトリにApple Silicon用の「as.out」とIntel用の「x86.out」という2つのバイナリがあり、これを1つにまとめた「uni.out」を作成するときには、以下の要領でlipoコマンドを実行すればいい。利用する機会は少ないかもしれないが、たったこれだけの作業でユニバーサルバイナリを作成できることは知っておいて損はない。

$ lipo -create as.out x86.out -output uni.out

ユニバーサルバイナリをスリム化する

lipoコマンドには、ユニバーサルバイナリから特定アーキテクチャのバイナリを取り除く機能もある。たとえば、Intel MacユーザはPowerPCバイナリがなくても困らないし、64bit Macのユーザは32bit(i386)バイナリを使うことはない。必要ないなら取り除いても問題ないわけで、削除したほうがストレージスペースの節約になる。

このとき利用するオプションが「-thin」と「-output」。前者では残したいアーキテクチャ名を、後者は対象のバイナリファイルを指定すればOK。以下の実行例では、カレントディレクトリにあるi386/x86_64/PowerPC対応の「UniBin」というユニバーサルバイナリを対象に、x86_64だけを残してスリム化している。ファイルサイズは約3分の1にまで減るため、わずかなムダも我慢ならない、という向きにはピッタリのユースケースだ。

$ lipo -thin x86_64 -output UniBin UniBin
  • 「lipo -thin ~」実行後に内容を確認してみると、収録されているバイナリの種類が減っていることがわかる