SNS を見ていたら不意に興味をそそられた「巨大なタプルを Array に変換する」をテーマにポインターを活用してデータを捌いてらしてる投稿を見つけて。それに感動を覚えたので、今回は急遽話題を変えて、それをヒントにさらにスリムに表現する手立てがないかを考えてみた — そんな話をしてみることにしますね。よろしくお願いします。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #283
00:00 開始 00:29 C 言語由来の固定長配列の扱いについて 01:21 タプルと配列の相互変換は永遠のテーマ 04:33 リフレクションってほとんど使われない印象 04:56 リフレクションで C 言語由来の文字列を扱う 05:48 固定長配列なら愚直に変換する方法もある 06:36 リフレクションを使う具体例 08:06 uname 関数 08:49 uname 関数について調べてみる 10:35 utsname 構造体のサイズ 13:27 固定長配列とタプルとの変換で必要な情報 14:07 配列をタプルに変換するのは困難 14:55 ここまでの状況整理 15:23 リフレクションでタプルの値を取り出す 23:40 ひとつひとつ愚直に取り出す例 26:01 データ長が変化した際の備えが難しそう 27:23 ポインターを使って取り出す方法 33:57 C 文字列は終端文字が必要 35:21 C 文字列を扱う前提での別解 36:36 構造体にポインターでアクセス 42:35 C 文字列に着目した取出方法のおさらい 43:53 どの方法が良いかはそれぞれの特徴を踏まえて判断を 44:42 クロージング ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #283
では、始めていきますね。ついさっき見つけたばかりの話題なんですが、星水さんのツイッターと某フォーラムの投稿、この二つがちょっと個人的に目を引きました。
リフレクションを使わずにデバイスモデル名を取得する方法についてです。CのAPIで提供されている固定長配列はSwiftで扱うのが非常に難しいため、リフレクションを使うことが一般的ですが、リフレクションは微妙だという発想があるようです。リフレクションを避けて別の方法で実現する提案があったのですが、これはなかなか興味深いです。特に、Objective-Cの方がスマートだという意見もある中でのことです。
実際にポインターから巨大なデータを扱う方法など、いろいろと試行錯誤が必要ですが、タプルから配列に変換するというテーマはよく取り上げられます。少なくとも、自分の中ではタプルを配列に変換したり、その逆を行いたいという思いがあります。ただ、Swiftでタプルを変換するのは難しい部分がありますね。昔、タプルを配列に変換しようと試みたことがある人も多いのではないでしょうか。
例えば、星水さんのツイッターでは、Cの文字列に着目し、変換をかけています。最終的にはタプルを配列に変換するというより、Cの固定長配列に入っている文字列をSwiftの文字列に変換するという結論になっています。コードを見る限りでは、そういった方法で実現しているようです。
これは見ているだけでも面白いのですが、私も個人的に別の方法を考えてみたところ、ついさっき完成したので紹介します。そのままでは面白くないので、星水さんのティップスとその会話も交えながら解説していきます。固定長配列を扱う様々な組み合わせについても話しつつ、自分が思い浮かんだ別の方法も紹介したいと思います。
リフレクションを使うのも面白い手法だとは思いますが、Swiftでのリフレクションは特別危険な技術ではありません。実際、タプルや配列変換に使えるかもしれないとも思っています。そういった観点からも、興味深いテーマになると思います。
では、まずはこの具体的な例を見てみましょう。実は詳しくは見ていない状況ですが、どういった内容かを確認していきます。例えば、iPhone6,1
のようなデバイスモデル名を取得したい場合、Googleでもリフレクションを使った記事が多いです。Objective-Cの時代には比較的簡単に取れた記憶があります。
今回のテーマに関しては、リフレクションを使わずに実現する方法を紹介いただいた投稿もあり、興味深いです。コンパイル時にタプルを配列に変換するテクニックを使うと良いということで、実際に試してみる価値がありそうです。固定長配列であれば、このような技も使えるでしょう。 まず、これを読みますか。ブログに書いてあるのかな。リフレクションありと話しましたが、まずリフレクションのほうを見ていきますかね。もともとのボタンがこっちなので、リフレクションを使う方法について説明します。
まず、UTSName
という構造体をイニシャライズし、その後でuname
関数にその構造体へのポインタを渡すと、ここに書き込んでくれるというCスタイルのAPIについて話します。これを読んで得られたSystemInfo
構造体から、リフレクティングシステムインフォのマシン、この部分でモデル名を出力するのをMirror
を使ってやります。これをプレイグラウンドでやってみますね。
具体的には、こちらでMacOSで十分かな。C関係なので、Foundation
とDarwin
で十分でしょう。とりあえずこれでやってみますね。この後に、まずは以下のように書いてみましょう。
var sInfo = utsname()
これをイニシャライズして、空っぽなものを作るか、または全部をフィルするかしなければなりません。今回はまず空っぽでやってみます。
次に、uname
関数に&sInfo
を渡します。
uname(&sInfo)
これで、Int32
がステータスとして返されるはずです。戻り値が何なのかはネットで調べれば出てくるでしょう。プロダクトで使う際にはちゃんと調べた方がいいですね。ですが、今回は結果が取れれば十分です。
勉強会なので、uname
関数を検索して調べてみましょう。APIのドキュメントや、マニュアルページを見ると、大体のことが書いてあるはずです。この構造体は、成功した場合は終了ステータスを返し、失敗した場合はマイナス1が返されます。どのような場合に失敗するのかなども調べておくと良いでしょう。
次に、この構造体のサイズを調べる方法についても説明します。メモリーレイアウトを使ってUTSName
のサイズを取得することができます。
print(MemoryLayout<utsname>.size)
これを実行すると、構造体のサイズがわかります。このサイズをもとに、配列として扱う際の情報を取得することが可能です。また、プロパティがどのように定義されているかを確認することも重要です。
特に意味はないかもしれませんが、ディスネームやプロパティが出てくる場所を確認するのも一つの方法です。
全体として、256個の要素の配列
を使えば良いという結論に至ります。これで、リフレクションを使ってモデル名を出力する方法を理解していただけたでしょうか。 さて、そのようにしてUPSの名前を取得するマシンから、様々な作業を行うことができます。タプルから配列への変換は比較的簡単ですが、逆に配列をタプルにする場合はサイズが一致する前提で行う必要があります。静的キャストもサイズが異なるとランタイムエラーとなる仕様なので、その点に注意が必要です。例えば、配列をタプルに安全に変換するには、与えられたタプルと同じサイズの配列を切り出して変換する、といった安全設計を施す必要があります。
さて、今回の例ですが、UPSの名前を取得するマシン(utsname
構造体)は、256個のCキャラクターの配列を扱います。これをタプルに変換するのは大変だという話題です。リフレクションを使用する例では、次のことを行います。
まず、必要な情報を取得するためにutsname
構造体のアドレスを渡す必要があります。そのためには、変数に対してアドレスを取得(&
)を行います。以下のコードはその一例です。
var utsnameInstance = utsname()
withUnsafeMutablePointer(to: &utsnameInstance) {
// 安全なポインタ操作
}
次に、取得した情報をリフレクションを使用して操作します。Mirror
を使用してプロパティを取得し、それをreduce
で処理していきます。
以下にその例を示します。
let mirror = Mirror(reflecting: utsnameInstance)
let machineName = mirror.children.reduce("") { (result, element) -> String in
guard let value = element.value as? Int8, value != 0 else {
return result
}
return result + String(UnicodeScalar(UInt8(value)))
}
ここで、mirror.children
は各プロパティを訪問し、Int8の値を文字 (キャラクター) に変換して、安全に名前を構築します。このコードでは、全てのCキャラクターが取得され、UInt8
にキャストされてUnicodeスカラーに変換されます。それにより、最終的な文字列(マシン名)を得ることができます。
まとめると、UPSの名前を取得する方法として、まずutsname
構造体のポインタ操作を行い、次にリフレクションを用いて各フィールドをトラバースし、必要な情報を文字列として取得します。このアプローチにより、安全かつ効率的に情報を取得できます。 ここはシーケンスではないんですね。ただ、マシンインフォはマシンインフォシーケンスのアプリなので、シーケンスで良いですね。バリューが何かというと、コピーしてきたエレメントのことですね。なるほど、エレメントの子要素、つまりチルドレンのことです。これは重要です。自分もエレメントで間違えたことがあります。チルドレンというのはネームとエレメントのことですが、チルドレンはミラーチャイルドのことだったようです。ラベルとバリューをアプリケーションとして使っているので、それだけが欲しいということですね。だからエレメントではなく、ああ、そっか、チャイルドですね。どちらでも良いのかもしれませんが、チャイルドのバリューがシーケンスなので、バリューで良いです。シーケンスにしても差し支えないですね。
除去も指定していますね。ターミネーターでなければ、Cモジュレートで0で終わるので、この終わりが現れるまで繰り返すという形です。ここはホワイル(while
)のほうが良いかもしれませんね。なんとなくの予感ですが、ホワイルのほうがいいのではないかと思います。まあまあ、置いといて、結果をユニコードでプラスします。ユニコードスからuint8
で、もちろん、1ですね。ユニコードスからそれを取りたいわけです。確かにこれでモジュレートに変えています。ああ、なるほどね、リデュースを使っている設計なんですね。ガードで終わりを指定して、ループにするとバッファーを作らないといけないのかな。
また、エニーシーケンスとか使えば良いですが、今日はここが主体ではないので、これでいいでしょう。コードには何も間違いはありません。ただ、除去が2つあるのが気になりますが、そこは自分のこだわりです。プリントしてみましょう。プリント、マシンネーム。こうすると、モジュレートでちゃんと出力されます。マシンネームってARM64モデル名ではないですね。これなんだっけ?でもマシン使ってなかったかな?まあ良いか。モジュレートでバレたから大丈夫です。デバイスモデル名、ARM64、とにかくモデル名になればいいですね。ARM64デバイスモデル名。
これがリフレクションです。まあまあ、少しここが曖昧ですが、リフレクションを使ってね。デルタレーションを使って、吉野さんもやることにしたんですね。なかなか始まっていないですね。こちらのほうが良いですね。リフレクションなしでやることにしたいみたいです。
パワーでフィーチャーをこうやって256にしましたよね。確かにそうです。これも下にブログに書いてあるトリックチェックですが、まあできるので悪くはないように見えます。ミラーをやめて貼って、これでフィーチャー配列にシステムインフォを定義します。変換できない場合もありますが、仕方ありません。配列を作った後で、これを文字列に変換します。新しい文字列でやっています。マシンネームはストリングのcString
。シーケンスの配列という形で渡してあげれば良いのです。
動きました。ARM64というふうに手に入りました。コードとしてはスマートですが、256個という保証がありません。サイズが変わったときに困るわけです。例えば256ではなくて512とか、そういった長さに変わっていたらデータが落ちてしまいます。例えば256文字以上の名前が付けられたとき、データが落ちるのです。なので、こちらよりはリフレクションのほうが良いかもしれませんが、こういった方法も一応あります。それで5文字までなら実行できます。
もっと賢く取ろうと提案したのが橋水さんの方法ですね。ポインターをうまく作る方法です。なかなか面白い方法です。名前を取るところまでOKですね。 それでは、シーターを作るところを見てみましょう。「withUnsafePointer」を使ってポインターを操作し、Cモジュールを作成し、それをアレイに変換するという工夫がされています。これを実際にプレイグラウンドで試してみると、ちょっと特殊な動作をしますが、これも面白いですよね。こうした発展ができる柔軟さは、結構大事なんじゃないかと思います。
今回は、このシーターを作っていこうという話ですね。「withUnsafePointer」を使いましたが、「unsafeポインター」でよかったのかな? 正しくは「withUnsafePointer」で良いでしょう。システムインフォメーションを処理するために、タプル型のポインターを取り、それを操作していきます。
例えば、システムインフォメーションでポインターを生成します。このポインターの型がタプル型の256個になるので、一旦「CChar」のポインターに変換します。そのときに「opaquePointer」を「withUnsafePointer」で型変換しています。純粋にタプルを変換したい場合には、この手法が適しているようです。
次に、Cモジュレーションについてです。効率良く「unsafeポインター」を使うわけですが、何か「ポインター」を楽に操作する方法はなかったでしょうか。「unsafeポインター」から「CChar」に変換し、そして「withUnsafePointer」で「CChar」の文字列を取っていきます。そして、その文字列をアレイに変換します。
let cString = ...
let array = Array(UnsafeBufferPointer(start: cString, count: strlen(cString) + 1))
このように「strlen」でプラス1してあげることで、C文字列の最後に0ターミネートを付けています。
結構頭を使いますが、うまく処理できたと思います。少し修正が必要だったので、変数を修正しました。
まとめると、「opaquePointer」を「CChar」に変換し、「UnsafeBufferPointer」を使用して文字列を操作する方法です。それでは今回の内容は以上です。お疲れ様でした。 こんな感じでうまくさばけましたよ、というお話です。こうしたときに、もうちょっと面白い方法があります。全然間違いないのですが、このポインターというやつを上手に扱って、しかもC文字列として扱うのです。扱う固定長配列がC文字列だからという大前提があるんですけど、この大前提を持っていないまま頑張って配列に変換しようとするのと、C文字列限定だけど変換するというのでは違います。
C文字列を別の文字列に変換するというのが、最終的には文字列にしたいわけです。逆の言い方をすると、C文字列をSwiftの文字列、要はキャラクターのコレクションにしてしまえば、別に配列じゃなくてもいいわけです。そういった発想をすると、このコードのおかげで発想が広がります。
具体的には、uts name
で情報を取って、このマシンを使っていきたい場合、このポインターを取ります。このときにまずポインターに変換したいなら、最初からポインターを作っちゃってもいいわけです。
let redisinfo = UnsafeMutablePointer<utsname>.allocate(capacity: 1)
こんな感じでポインターを取っちゃえば、情報を取るときに結構安泰となり、あとここでredisinfo
というわけです。mutable pointer
なのでね、ポインターは差し替わりませんが、この中身までは知りませんよということになりますね。
情報が取れて、ここでredisinfo
はポインターになっていますから、redisinfo
全体からマシンのところまでポインターを動かせば、まずマシンのポインターが取れるわけです。この動かし方が面白くて、
let pmachine = redisinfo.advanced(by: MemoryLayout.offset(of: \\utsname.machine))
これで、マシンのポインターが取れるわけです。ここでpmachine
をCChar
のポインターに変換します。
let pointer = pmachine.withMemoryRebound(to: CChar.self, capacity: 1) { $0 }
このようにすれば、CChar
のポインターを取得できます。あとはフォーマットを使用して文字列に変換します。
let machineName = String(format: "%s", pointer)
以上で、C文字列をSwiftの文字列に変換することができます。
この手法はどちらがいいかはそれぞれの状況によりますが、私のやり方よりも応用が効く場合もあります。この方法はC文字列だけでなく他の文字列にも適用できるので、リフレクションを使えばもっと汎用性が高くなります。
今日の内容は面白いテーマで、タプルからタプルじゃない状態に変えるという話題でしたが、個人的に非常に興味深かったので紹介しました。
では、今日はこれで終わりにしましょう。お疲れ様でした。ありがとうございました。