Xcode の Playground で実行ファイルを生成して実行してみる

Swift プログラミング

Playground から C ソースコードをビルドして実行しようと試みたのですけど mac の Gatekeeper に阻まれたみたいで失敗。

それを回避して実行する方法を探してみました。


Xcode の Playground 上で、次のような感じで C 言語のソースコードを気軽に試せるように したくなりました。 Swift の文字列として C 言語のコードを書いて、その実行結果をすぐさまデバッグコンソールで確認したい、みたいな感じです。

let code: String = """

printf("%lu", sizeof(10));

"""

try! execute(code)

これを実現するにあたって、次のような流れを考えたのですけど、簡単には実現できませんでした。

  1. 文字列から C ソースファイルを作成する(/tmp フォルダーに保存)
  2. 生成した C ソースファイルを clang でコンパイルする(実行ファイルは /tmp フォルダーに保存)
  3. 生成した実行ファイルを Foundation.Process クラスを使って実行する
  4. 標準出力の内容を print 関数でターミナルに出力する

このようにすると Foundation.Process.launch のタイミングで次のエラーが発生して、Playground の実行がそこで終わってしまったのでした。

do {

    let process = Process()

    process.launchPath = "/usr/bin/clang"
    process.arguments = ["-o", "/tmp/playground.out", "file.c"]
    process.launch()
}

do {

    let process = Process()

    process.launchPath = "/tmp/playground.out"
    process.launch()
}

EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

実行できない原因を探る

最初は App Sandbox か何かでそもそも実行ファイルを動かすことが許されていないのかと思ったのですけど、Playground で生成した実行ファイルはダメでも、ターミナルで生成した実行ファイルであれば実行できたことから 実行ファイルの実行が許可されていないわけではない 様子が窺えました。

それなら、Playground で生成したファイルとターミナルで生成したファイルとで、パーミッションに違いがないか調べてみると、次のような違いを確認することができました。Playground で作った方にだけ @ というパーミッションが添えられています。

-rwxr-xr-x@ 1 tomohiro  wheel  8432 11 23 11:19 playground.out
-rwxr-xr-x  1 tomohiro  wheel  8432 11 23 11:20 terminal.out

これは拡張属性が添えられていることの証のようです。そこで xattr -l playground.out コマンドを実行してみると、Playground から作成したファイルには com.apple.quarantine という属性が添えられている ことが確認できました。

com.apple.quarantine: 0082;5a1634f5;;

拡張属性を手作業で削除すると実行できる様子

これが原因なのかを確認するために、試しに次のようにして拡張属性を削除してみることにしました。 そうしたところ Playground 内から Foundation.Process を使って 実行できるようになった ので、原因はこれで間違いなさそうです。

xattr -d com.apple.quarantine playground.out

検疫隔離 (Quarantine)

そもそもこの拡張属性 com.apple.quarantine とは何だろうと思って調べてみると、どうやら MacOS Lion 頃から搭載された OS の Gatekeeper という機能が使う拡張属性のようでした。この属性の値として、いつどこから入手したファイルなのか、実行を許可したかといった情報が記録されているようです。

拡張属性をプログラムを使って削除したい

Playground から生成したファイルであっても拡張属性 com.apple.quarantine を削除してあげれば実行できるとなれば、この操作も Playground からプログラムで実行したいところです。

xattr コマンドでの削除は無視される様子

そこで、いちばん単純な方法として xattr -d com.apple.quarantineFoundation.Process を使って実行する方法を思いついたのですけど、Playground から次のようにしてみても、無視されてしまって この方法では拡張属性を削除することはできない様子 でした。ターミナルから実行すればちゃんと拡張属性を削除できるのですけれど。

let process = Process()

process.launchPath = "/usr/bin/xattr"
process.arguments = ["-d", "com.apple.quarantine" , path] as [String]
process.launch()

LSQuarantine では制御できなそう

それならそもそも API が提供されていないか探してみると、おそらく CoreServiceLaunchServices に分類される LSQuarantine が、この辺りの制御に関係する API の様子でした。

ここに規定されているキーを使って CFURL で指定したファイルの検疫情報を CFURLSetResourcePropertyForKey 関数などを使って書き換えられるようになっているようなのですけど、ここで指定できそうなキーは次のような感じで、今のところの知識では 出所の情報を記録するためのもので、検疫の許可状態を制御するようなものはなさそう に見えました。

もっとも、プログラムコードで 簡単に com.apple.quarantine 属性の実行許可にまつわる値を変えられたとしたら検疫の意味がなくなりそう なので、ひとまずは別の手立てを考えてみるのが筋的に良さそうな気がします。

リソースフォークを削除する

拡張属性を削除したいとなれば、拡張属性が記憶されている場所を 無理やり消せたら いいのかなと思って、その手段がないか考えてみてたのですけど、そういえば mac では、こういった特殊な情報って リソースフォーク に保存されているのが一般的なのを思い出しました。

macOS の HFS フォーマット系のディスクに保存されたファイルには "データフォーク" と "リソースフォーク" という2つの保存場所があって、前者は一般的なデータを、後者はアイコンなどのリソースを、ひとつひとつのファイルに対して保存できるようになっています。

リソースフォークはおそらくプログラムで編集できるようになっていると思うのですけど、その方法を知らないため、それを調べるか迷ったのですけど、そういえば mac では FAT32 フォーマットのようなリソースフォークを持てない場合でも、隠しファイルを作ってリソースを保存しておく仕組み が用意されていたのを思い出しました。

つまり、リソースフォークが隠しファイルで存在するなら 普通にファイルを消す操作でリソースフォークを削除できる はずです。

拡張属性の削除に成功

そこで試しに Playground から FAT32 フォーマットのディスクにファイルを作り、そのリソースフォーク用の隠しファイルを削除してみました。例えば a.out という名前のファイルであれば、そのリソースフォークは ._a.out という名前で保存されているので、それを削除します。

rm ._a.out

このようにしてみたところ、拡張属性を示す @ がパーミッションから消え、Playground からも直接実行できるようになりました。拡張属性の情報はリソースフォークに記録されていると思って間違いなさそうです。

FAT32 フォーマットされたボリュームを用意する

これで Playground から生成したファイルであっても実行できる可能性が見えてきましたけど、ひとつ難しい問題として残っているのが どうやって FAT32 フォーマットのボリュームを用意するか です。

自分の環境だけで実行できれば良いのなら、何らかのパーティションをひとつ FAT32 フォーマットしておけば良いのですけど、Playground を他の人にも使って欲しい場合には、そのために FAT32 パーティションを用意してもらうのは負担です。

RAMDISK を使う

Playground によって自動で FAT32 ボリュームを用意できないかと思って考えてみたところ、ソフトウェア側でボリュームを作る手段として RAM ディスク があるのを思い出しました。

幸いなことに macOS では 管理者権限がなくても hdiutil コマンドと diskutil コマンドを使って RAM ディスクを作成できます。例えば次のようにすることで 600,000 セクターの RAM ディスクを TESTDISK という名前で作成できます。 このようにして作成したボリュームは /Volumes/TESTDISK からアクセスできます。

diskutil eraseDisk FAT32 TESTDISK `hdiutil attach -nomount ram://600000`

最初にバッククォートで括られたコマンドが実行されて、RAM ディスクが作成されます。このとき /dev/disk45 みたいなデバイス名が得られるので、それを diskutil コマンドに渡して FAT32 形式で初期化します。このとき、ディスクのラベル名に TESTDISK を設定しています。

Playground からも作成可能

このコマンドを Foundation.Process を使って Playground 上で実行してみても、ちゃんと RAM ディスクを作成することができました。

このボリュームに対してファイルを書き出せば、拡張属性が隠しファイルに書き出されるので、その 隠しファイルを普通に消せば、ファイルから拡張属性を削除できる ことになります。そして 拡張属性を削除できれば、そのファイルを Playground からファイルを実行することができることになる はずです。

C ソースコードをビルドして実行する

これで、無理やり感はありますけれど『Playground からファイルを作成して、それを Playground から実行する』ための手段は全て揃った感じなので、ここで早速 『clang を使ってバイナリーファイルを作成して、それを Playground から実行する』 ということをしてみました。

なお、以下のコードは、順番に Playground に貼っていけば動くようにしてあります。

環境構築

1. 環境的なところを整理する

とりあえず、使用するファイル等の情報は次のような感じにしてみます。

import Foundation

let label = "TESTDISK"
let sectors = 600000
let basePath = "/Volumes/\(label)"
let sourcePath = "\(basePath)/source.c"
let executionPath = "\(basePath)/a.out"
let resourcePath = "\(basePath)/._a.out"

label は RAM ディスクにつけるボリューム名で、そのディスクのサイズは sectors で表現しておきます。サイズは一般に 512 byte/sector らしいので、それを踏まえたサイズを指定します。小さいとフォーマットできなかったりするみたいなので、うまく行かない場合はサイズを大きくしてみると良いかもしれません。

RAM ディスクが作られれば、それは /Volumes ディレクトリー内のラベル名からアクセスできるので basePath をそれにして、その中にソースファイル source.c と実行ファイル a.out を格納することにします。FAT32 フォーマットのボリュームの場合、リソースフォークが ._ で始まるファイル名に格納されるので、つまり a.out のリソースフォークは ._a.out です。

2. RAM ディスクを構築する

それでは、まずは RAM ディスクの構築からです。そのために hdiutil コマンドを使って RAM ディスクをアタッチ します。 これを実行すると、標準出力に /dev/disk53 みたいな デバイス名 が出力できるので、それを Pipe で拾っておきます。このとき、後ろに余計な空白文字が含まれてしまう様子だったので、それを削除した上で deviceName 変数に入れています。

let process = Process()
let output = Pipe()

process.launchPath = "/usr/bin/hdiutil"
process.arguments = ["attach", "-nomount", "ram://\(sectors)"]
process.standardOutput = output
process.launch()

let outputData = output.fileHandleForReading.readDataToEndOfFile()
let outputText = String(data: outputData, encoding: .utf8)!

let deviceName = outputText.split(separator: " ")
    .first
    .map(String.init)!

RAM ディスクをアタッチしたら、フォーマットして使えるようにします。今回は FAT32 形式でフォーマットするのが大事なポイントです。diskutil コマンドに eraseDisk 動詞を添えて、フォーマット形式、ラベル名、初期化するデバイス名を指定して実行します。初期化には少し時間がかかるので waitUntilExit メソッドで コマンドの処理が終了するのを待つ ようにします。

Process.launchedProcess(launchPath: "/usr/sbin/diskutil",
                        arguments: ["eraseDisk", "FAT32", label, deviceName])
    .waitUntilExit()

ビルドと実行

これで RAM ディスク周りの環境が整ったので、引き続き C ソースコードのビルドと実行を行なっていきます。

1. C ソースコードを用意する

まずは実行したい C 言語のソースコードを用意します。今回は Swift の文字列で code 変数に用意しておくことにしました。

let code = """
#include <stdio.h>

int main() {

printf("⭐️ Hello World.\\n");
return 0;
}
"""

ただ、それを ファイルに保存しておかないとコンパイラーに渡せない 気がするので、文字列に用意されている write メソッドを使ってファイルに書き出しておきます。書き出す先は FAT32 フォーマット済みの RAM ディスク です。そのパスは冒頭で sourcePath 変数に用意しておきました。

try! code.write(toFile: sourcePath, atomically: true, encoding: .utf8)

2. C ソースコードをコンパイルする

C 言語のコードを保存した sourcePath のファイルを、コンパイラー clang でコンパイルします。これまでと同じように Foundation.Process を使って clang を実行して、コンパイル処理が終わるのを waitUntilExit で待ちます。処理が終われば executionPath で指定したパスに 実行ファイルが生成 されます。

Process.launchedProcess(launchPath: "/usr/bin/clang",
                        arguments: ["-o", executionPath, sourcePath])
    .waitUntilExit()

3. 実行ファイルの拡張属性を削除する

これで実行ファイルはできましたけど、今回は macOS の Gatekeeper の都合でそのままでは実行できないので、拡張属性に保存されているそれに関する情報を削除します。FAT32 であれば 単純にリソースフォークファイルを削除する ことで対応できる様子でした。

Process.launchedProcess(launchPath: "/bin/rm",
                        arguments: [resourcePath])
    .waitUntilExit()

この処理をしないと、実行ファイルを実行したときに EXC_BAD_INSTRUCTION でハングアップします。

4. 実行ファイルを実行する

ここまでできたら、あとは普通に実行ファイルを実行することができます。

実行結果が欲しい場合は、RAM ディスクを構築する手順でやったみたいに Pipe を使って標準出力や標準エラーの内容を取得するようにします。そうしなければ、標準出力に結果が出力される様子です。Playground の場合はデバッグコンソールが出力先です。

Process.launchedProcess(launchPath: executionPath,
                        arguments: [])
    .waitUntilExit()

後始末

これで Playground を使って『C 言語のソースコードをコンパイルして実行する』という目的は達成できました。 そのために、今回は RAM ディスクを作成したので、最後に後始末をしておきます。

7. RAM ディスクをイジェクトする

実行ファイルの実行が終われば、今回はもはや RAM ディスクは不要なので hdiutildetach 動詞を使って RAM ディスクを macOS からイジェクトします。

Process.launchedProcess(launchPath: "/usr/bin/hdiutil",
                        arguments: ["detach", deviceName])
    .waitUntilExit()

Playground から C ソースコードを実行できた

そんな感じで、些か無理やりな感じもしますけれど、ひとまず Playground 上で C 言語のソースコードをコンパイルして実行することが実現できました。今回のコードであれば、デバッグコンソールには次の結果が表示されていると思います。

Started erase on disk105
Unmounting disk
Creating the partition map
Waiting for partitions to activate
Formatting disk105s1 as MS-DOS (FAT32) with name TESTDISK
512 bytes per physical sector
/dev/rdisk105s1: 594768 sectors in 74346 FAT32 clusters (4096 bytes/cluster)
bps=512 spc=8 res=32 nft=2 mid=0xf8 spt=32 hds=54 hid=2048 drv=0x80 bsec=595968 bspf=581 rdcl=2 infs=1 bkbs=6
Mounting disk
Finished erase on disk105
⭐️ Hello World.
"disk105" unmounted.
"disk105" ejected.

ほとんどを Pipe を通さず標準出力に流してあったのでログが膨れていますけど、とりあえず ⭐️ Hello World. が、今回の C ソースコードをビルドして実行した時の出力結果です。それ以外の出力を抑えたいときには、各コマンドを実行するときに使う Foundation.Process クラスの standardOutput プロパティーや standardError プロパティーに Pipe を代入してから launch メソッドを呼ぶようにして、結果をデバッグコンソール以外に流したり、捨てたりするようにします。

変数 code に格納した C ソースコードの内容を変えれば、すぐさま Playground がそれをコンパイルして実行結果を出力してくれるので、ちょっとした C 言語の挙動を知りたい時には便利だったりするかもしれません。