2014年8月7日木曜日

[Swift] コマンドライン(Command Line)プログラムとして使う [2] 外部モジュールの作成と使用

前回の続き。
思ってたより全然面倒な話だったので、試行過程も記録しておく。


さて。なぜ使い回すソースコードをモジュール化するかというと、Objective-C と違い Swift の import はヘッダファイルではなくモジュールだから、ということになるだろう。

モジュールを作る


とりあえず作ってみる。
xcrun swift -help を眺めると、モジュール化できそうなオプションがいくつか見つかる。

まずはモジュール用のサブディレクトリを作り、モジュール用のソースコードを以下のように書いた。
/* modules/Example.swift: モジュールとして使われる方 */
public func MyFunction(name: String) -> String {
    return "Hello, \(name)"
}

public class Example {
    public let  name: String

    public init(name n: String) {
        name = n
    }
}
外から参照できるようにすべて public にした。そして以下のモジュール化コマンドを実行。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift \
    -frontend \
    -emit-module \
    -module-name Example \
    -emit-module-path modules/Example.swiftmodule \
    -emit-module-doc-path modules/Example.swiftdoc \
    modules/Example.swift
modules/ に Example.swiftmodule と Example.swiftdoc が作成される。

次はモジュールを使う側のファイルを(前回からの続きの command-6.swift として)作成。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift -I ./modules
import Example

println(MyFunction("Module"))

var  module = Example(name: "Command Line")
println("Hello, \(module.name)")
-I オプションでモジュールの場所を伝える。

しかし、これを実行するとエラーとなる。
% ./command-6.swift
LLVM ERROR: Program used external function '_TF7Example10MyFunctionFSSSS' which could not be resolved!
import は成功してるけど、シンボルを解決できていない。

swiftmodule の中には実行データが入ってないのか。思えば Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx には .swiftmodule、.swiftdoc の他 .dylib も一緒に入ってる。ということで dylib が必要なのだろう。


ライブラリを作る


ライブラリ化するため、今度はコンパイラ swiftc を使う。まずはオブジェクトファイルを作成。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swiftc \
    -emit-library \
    -emit-object modules/Example.swift \
    -o modules/Example.o
出来たオブジェクトファイル(modules/Example.o)から dylib を作成する。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun libtool \
    -macosx_version_min 10.9 \
    -L/Applications/Xcode6-Beta5.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
    -dynamic \
    -install_name @rpath/libExample.dylib \
    -lswiftCore \
    -lSystem \
    -o modules/libExample.dylib \
    modules/Example.o
これでライブラリが modules/libExample.dylib として作成された。

install_name オプションで @rpath を指定したのは executable 側が検索パスを決めるため。今回のケースでは dylib を別の場所で使うワケではないので影響しないが、.app に入れることも考えればあった方が良いオプション。

作成した dylib を使用するように先の command-6.swift を修正する。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift -I ./modules -L ./modules -lExample
import Example

println(MyFunction("Module"))

var  module = Example(name: "Command Line")
println("Hello, \(module.name)")
-L オプションでライブラリの場所を、-l でリンクするライブラリを指定した。

これで実行できるかと思いきや、またもやエラーが。
% ./command-6.swift
<unknown>:0: error: could not load shared library 'libExample'
ライブラリを見つけられていない。

これに小一時間ハマってしまったがなんのことはない、実行時カレントディレクトリに libExample.dylib があれば見つけてくれた。
-L オプションはリンク時のライブラリの場所であって、実行時のライブラリの場所ではないってことだ。

せっかくスクリプトっぽく使えると思ったのに(いちいちライブラリ化するのは我慢するとしても)ライブラリと同じディレクトリにいないと実行できないとかよろしくない。

実行時ライブラリ検索は LD_RUNPATH_SEARCH_PATHS だろうから、これを指定する方法を模索したのだが、残念ながらこれも結局 swiftc でビルドするしか無い、という結論に落ち着いた。
% sudo xcode-select -switch /Applications/Xcode6-Beta5.app
% xcrun swiftc -I ./modules -L ./modules -lExample -Xlinker -rpath -Xlinker ./modules -o ./command-6 ./command-6.swift

% ./command-6
Hello, Module
Hello, Command Line
実行成功。いやビルドしちゃってるから。当たり前すぎて感動がない、っていう。

リンカオプションとして rpath を指定するのだけど、この時ばかりは xcode-select -switch しないとエラーになってしまった。

中で使われているであろう xcodebuild がなんか解決できなくてエラーになっていたようなので、ビルドするのなら素直に -switch しときましょう、と。


まとめ


スクリプトのように書けると思われた Swift コマンドラインプログラムだがひとたび自作の外部モジュールを使うとなると、モジュールのライブラリ化が必要で、使う方は executable としてビルドするか、もしくはライブラリをカレントにコピーしないといけない。

そういうものかと思う一方で、ここら辺に関しては本来 xcrun swift に良きに計らってもらいたいという気持ちもある。

Xcode を使うことでこれらの手間を軽減できるかと思いきや、現状(Xcode6-Beta5)は Command Line Tool もモジュール関係もテンプレートがない。

今のところコマンドラインは、そう言う手段がある、という程度が無難かも。
正式版に期待。


参考



0 件のコメント:

コメントを投稿