SwiftコードとC関数との間でのデータ変換について
Xcode 6.0 Beta
前回は、Swiftコード内で自作C関数を呼び出すやり方を調べて、テストコードを掲載した。
うまく動くことは動いたが、あまり詳細な部分については解説できなかった。今回は、ハマリポイントというか、主に型変換について、そのとき苦労したことを補足説明したいと思う。
説明とはいっても、私自身、まだまだSwiftを学習し始めたばかりなので、誤ったことが記述されていたり、もっと効率の良いやり方がある可能性は十分にある。もしお気づきの方がいたら、是非ご指摘願いたい。
それでは、前回作成したSwiftのテストコードからコメントを排除し、ポイントとなる行に番号をつけてマークしたものを以下に掲載する。後で、コメントとしてつけた番号の部分について、それぞれ説明することとする。
【main.swift】
import Foundation let table = ["ABC", "DEF", "GHI", "JKL"] let list = LinkedListCreate() for s in table { // 1 var array: CChar[] = s.cStringUsingEncoding(NSUTF8StringEncoding)! // 2 let size: UInt = UInt(sizeof(CChar) * array.count) // 3 LinkedListAddElement(list, &array, size) } // ... for var itr = LinkedListGetIterator(list); LinkedListHasNext(&itr); { // 4 let c: COpaquePointer = LinkedListNextElement(&itr) // 5 let s: String = String.fromCString(UnsafePointer<CChar>(c)) println(s) } LinkedListRelease(list)
0. Swift内で定義されているC用のデータ型一覧
まず、大前提として、Swiftの基本データ型はプリミティブ型ではなく、オブジェクト型である。
let i = 1
と記述した場合、i は Int というクラスのインスタンスになる。このInt型は当然、Cのint型と同じではないし、暗黙の型変換はできない。*1
Cのintに相当するのは、Swiftでは CInt という型である。
Cの型に対応するSwift型は、以下を参照。一覧としてまとまっている。
1. StringをCChar配列に変換する
var array: CChar[] = s.cStringUsingEncoding(NSUTF8StringEncoding)!
ここでやりたいことというのは、「Swiftの文字列をvoid*ポインタとしてCの関数に渡す」ということである。
ここで1つ注意点。どういうわけか、cStringUsingEncodingメソッドが返すのは、CStringではなく、CChar? である((cStringUsingEncodingの戻り値がCChar配列であるというのは、メソッド名の命名ミスか、返す型が間違ってるんじゃないのか、とすら思う。cCharArrayUsingEncodingならば納得がいくのだが。ちなみに、CChar と CString の間での暗黙型変換はできないので、「cStringUsingEncodingが返しているのは実はCString型で、それをCChar配列に変換して代入している」という可能性はない。))。一方、NSString#UTF8Stringは、CString型であることに注意。
Stringや、NSStringをCStringに変換することは容易い。しかし、今回の例では、const char*ではなく、void*で渡すようにCの関数が設計されている。Cの関数がもしも、const char* を引数として受け取るのであれば、
LinkedListAttachElement(list, s.bridgeToObjectiveC().UTF8String)
とすれば良いだけであって、この部分の3行はとても簡潔になる。
ところがどっこい、Swift内では、const char* を void* として扱う(渡す)ことはできない。(実はできる。これを書いている同日にそのやり方が判明したので、後で記述する。ここでは「そのやり方がない」という体で話を進める。)
先ほどリンクした公式ドキュメントの中段から下の方に、Swiftで用意されている、C用のポインタ型も掲載されている。void* を引数で渡すには、Swiftでの型がCMutableVoidPointer型でなければならない。ここでは、2ステップ後で、CMutableVoidPointer型を得るための前準備として、文字列をいったん、CChar配列に変換している。
ここで一番注意しなければならないこと。それは行末の ! である。
Swiftの ! 命令は、オプショナル型の値が nil でないことを主張し、ラップを外すことにあたる。
一方、Cのvoid*、即ちSwiftのCMutableVoidPointerは、オプショナル型変数のアドレスをそのまま(そのままといっても、変数の前に & をつける必要があるが)渡すことができる。これは罠である。オプショナル型変数のアドレスをCの関数にポインタとして渡した場合、そのポインタの参照外し後の値は、NULLである*2。そして、CMutableVoidPointerは、オプショナル型のアドレスを受け取ることを許容する。つまり、この行の行末に ! を付けずに、型推論を使って、CChar[]? 型とした場合、このコードはコンパイルは通るものの、うまく動かないという結果になる。前の記事で、慣れるまで型推論させない方がいいかもよ、と書いたのは、こういうところではまる可能性があるからであった。
2. Cの引数がunsigned intである場合は、SwiftからはCUnsignedIntか、UIntで渡す
let size: UInt = UInt(sizeof(CChar) * array.count)
Intではダメ。
Int → CInt → int
UInt → CUnsignedInt → unsigned int
の変換は自動でやってくれるので、つまり、Cの引数がunsigned intであれば、SwiftからUIntの変数をそのまま渡すことができる。厳密には、今回のコンパイル環境では、size_tはunsigned longであるが、SwiftからはUIntで渡して問題ない。
この行の、
sizeof(CChar) * array.count
の部分は、Int型とInt型の乗算であり、結果もInt型であるが、
UInt(sizeof(CChar) * array.count)
の部分で、UIntオブジェクトを生成、その際に初期化パラメータとしてInt値を与えているということになるので、最終的に定数sizeに入る値はUIntオブジェクトである。
行番号2の行は、次の行とあわせて、以下のように書くこともできる。
let size = sizeof(CChar) * array.count // この時点では定数sizeはInt型 LinkedListAddElement(list, &array, UInt(size)) // 渡すときにUInt型を別途生成
2行をまとめて、
LinkedListAddElement(list, &array, UInt(sizeof(CChar) * array.count))
としてもOK
さらに、以下のように最初からCUnsignedLongとして計算させることもできる。
let size: CUnsignedLong = sizeof(CChar).bridgeToObjectiveC().unsignedLongValue * array.count.bridgeToObjectiveC().unsignedLongValue LinkedListAddElement(list, &array, size) // 受け取る側がsize_t型なので、CUnsignedLongをそのまま渡せる
しかしながら、Objective-Cブリッジをかまさないで、直接IntからCIntに変換するようなことはできないのだろうか・・・。私が知らないだけで、何かやり方があるのかもしれない。
3. SwiftからCの関数にポインタを渡すには、&variable
LinkedListAddElement(list, &array, size)
準備が整ったので、ここでようやくCの関数を呼び出している。
このやり方はCで変数のアドレスを渡すときと似ているのでわかりやすい。配列変数も &array で渡す点だけ注意。
リンクした公式ドキュメントの
You can call it in any of the following ways:
のあたりに、ポインタ渡しの具体例の一覧が掲載されている。
さて、ここでC関数に渡している引数arrayは、CChar[] 型であるが、&をつけて渡しているので、CMutableVoidPointerとして渡していることになる。Swift変数(varで定義したもの)に & をつければ、なんでもCなんちゃらPointerとして参照可能っぽい。だが、letとして定義した定数をポインタで参照することはできない。
今回はvoid*として渡したいので、CMutableVoidPointerというなんでもありの型を使ってしまっているが、たとえば、const int* を使いたいときは、CConstPointer
ちなみに、変数listは、すでにポインタ型(正確にはUnsafePointer
4. C関数から返却されたvoid*ポインタは、SwiftではCOpaquePointerとして受け取る
let c: COpaquePointer = LinkedListNextElement(&itr)
引数で void* を渡すときはCMutableVoidPointer型としたが、戻り値を void* で受け取るにはCOpaquePointer型とするようだ。*3。
var p: COpaquePointer = nil var m: CMutableVoidPointer = nil p = m // NG m = p // OK
ということから、CMutableVoidPointerは、COpaquePointerを許容できるようなので、Cからのvoid*戻り値をCMutableVoidPointerで取ることもできる。
今回は void* を参照しているのでこのようにしているが、ポインタの型が決まっているのであれば、UnsafePointer
5. CStringからStringを生成する
let s: String = String.fromCString(UnsafePointer<CChar>(c))
String.fromCStringメソッドの引数は、もちろんCString型である。と思われる*4。実は、Swiftの基礎クラス、たとえばStringクラスの公式のクラスリファレンスのようなものが見当たらない。いったいどこにあるのじゃー。ひょっとして、まだBetaだから今後変更するかもしれない、ということで、まだ公開されていないのかしら。
これは単なる私の予想だが、fromCStringは、引数としてUnsafePointer
そして、COpaquePointerをUnsafePointer
情報不足のため、確かなことが言えずに申し訳ないが、ともかく、私の環境で試したところ、この行のコードで正しくvoid*ポインタからStringオブジェクトへの変換ができた。
荒技?
「1. StringをCChar配列に変換する」のところで、「CStringをCMutableVoidPointerに変換することはできない」というようなことを書いた。これの前の記事にもそう書いた。
以下は、これが良いやり方なのかどうかはわからないが、「一応こうやったら動いた」というレベルの情報である。
【怪しいコード!注意!】
import Foundation let table = ["ABC", "DEF", "GHI", "JKL"] let list = LinkedListCreate() for s in table { // ポインタの内容をC関数内でコピーするのではなく、そのまま持たせるように変更 LinkedListAttachElement(list, reinterpretCast(s.bridgeToObjectiveC().UTF8String) as UnsafePointer<CChar>) } // ... for var itr = LinkedListGetIterator(list); LinkedListHasNext(&itr); { let c: COpaquePointer = LinkedListNextElement(&itr) let s: String = String.fromCString(UnsafePointer<CChar>(c)) println(s) } // C関数内で参照している全ポインタの所有権を剥奪 LinkedListDetachAllElements(list) LinkedListRelease(list)
これにて、String → CString → UnsafePointer
reinterpretCastという関数自体、Appleの公式から見つけた情報ではないので、この関数の存在や用途については、現時点ではあまり信用しないでいただきたい。
名前から想像するに、「再解釈キャスト」なので、おそらくサイズが合う限り、あらゆるポインタ型に変換できるのではないだろうか。
上記の「怪しい」コードは、冒頭で提示したコードの内容と少し異なる。cStringUsingEncodingによってCChar配列を生成した場合、それは文字列の一時的なコピーとしての配列となるが、UTF8Stringで取得したCStringは、文字列ポインタそのものを取得できているように見える。
それゆえ、C関数内で、ポインタの参照先のデータをコピーする、という処理から、ポインタをそのまま保持(即ち共有)する、という処理に変えている。SwiftとCで同じポインタを共有するわけだから、それだけ考えても少々危険な香りが漂ってくる。しかしこれが仕様として正しい挙動だと確信が持てるようになれば、文字列をコピーするよりも効率良く処理できる可能性がおおいにでてくるので、今後もう少し詳しく調べてみたいところだ。
ところで、C++にもreinterpret_castという命令文があるが、以前私が、「用意されてんだから使っておけばいいだろ!」と言ったら、C++の有識者に、「互換性がどうでこうで、あまり良い作法じゃないからやめとけ」と怒られたことがある。私には未だに、reinterpret_castをどういうときにどう使えば良いのか、いまいち理解できていない。
まあ、いずれにせよ、深い理解を伴わずに無理矢理ポインタをキャストするようなことは危険なので、やめておいた方が良いだろう。
さて、今回も長くなった。
SwiftとCとの間でのデータ渡しで、ここ数日私がはまった内容はだいたいこんなところだ。
もうこれ以上は公式ドキュメントによる詳しい解説なしでは戦えそうにない。
あとは勇者たちに任せた・・・
私はもっとぬるい場所に戻って、Swiftの基礎文法の学習でもすることにする。
では、さらばだ。
*1:ただしこれは、Swift内で暗黙変換ができないという話であって、後述するように、Cの関数の引数としてSwift変数を渡すときには、SwiftのIntからCのintへの自動変換はやってくれる。
*2:これは実際にやってみたらそうなった、ということであって、仕様として本当に、「オプショナル型のアドレスをCにポインタとして渡したらNULLを指すポインタになる」ということなのかどうかは、現時点では私にはわからない。
*3:CMutableVoidPointerとCOpaquePointerの間にどういう違いがあるのかは今のところよくわかっていないが、reinterpretCastで、CString → COpaquePointerは動いたが、CString → CMutableVoidPointerは実行時に落ちた。サイズが違うとのこと。
*4:もちろん、CString値を渡した場合もきちんと動作した。