この記事は、iOSアプリ開発から公開までの流れ の第11章です。
本稿では、Swift 向けの POSIX Socket のラッパーを設計します。
また、今後のアプリ開発で使用可能とするために汎用化も検討したいと思います。
1. POSIX Socket の呼び出し方について
前回、POSIX Socket を使用して簡単な Ping を実装してみました。
そのソースコードにおける以下の 2 つの呼び出し方に着目してみます。
Step 1. ICMP ソケットを作成
let sockfd = Darwin.socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)
guard sockfd != -1 else {
print("socket: " + String(cString: strerror(errno)))
return
}
Step 2. 宛先アドレスを作成
Step 4. Echo request を送信
var to = sockaddr_in(sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
sin_family: UInt8(AF_INET),
sin_port: in_port_t(0).bigEndian,
sin_addr: in_addr(s_addr: inet_addr(IP_ADDRESS)),
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
/*(中略)*/
let sendData = Data(bytes: &echo, count: MemoryLayout<echoRequest>.size)
let tolen = socklen_t(to.sin_len)
let sent = withUnsafePointer(to: &to) { sockaddr_in in
sockaddr_in.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddr in
sendData.withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> size_t in
let unsafeBufferPointer = pointer.bindMemory(to: UInt8.self)
return Darwin.sendto(sockfd, unsafeBufferPointer.baseAddress, sendData.count, 0, sockaddr, tolen)
}
}
}
guard sent != -1 else {
print("sendto: " + String(cString: strerror(errno)))
return
}
2. 単純型の扱い
socket(2) の呼び出し方を確認してみます。
このシステムコールの引数 domain, type, protocol および復帰値は int 型です。
% man -s 2 socket
SOCKET(2) BSD System Calls Manual SOCKET(2)
:
SYNOPSIS
int
socket(int domain, int type, int protocol);
:
一方、Swift のプロトタイプは以下のとおり Int32 型です。
つまり、int が Int32 に対応しているため、特に何も考えずに呼び出すことができたのです。
その他の代表的な単純型も以下のように対応しています。
なお、C 言語では真偽値を int で表現するのが一般的なため、Swift の真偽値 Bool に対応する C の型はありません(C++ だとあるみたいです)。
C の型 | Swift の型 |
---|---|
char (signed char) | Int8 または CChar (CSingedChar) |
unsigned char | UInt8 または CUnsingedChar |
short | Int16 または CShort |
unsigned short | UInt16 または CUnsignedShort |
int | Int32 または CInt |
unsigned int | UInt32 または CUnsignedInt |
long | Int または CLong |
unsigned long | UInt または CUnsignedLong |
long long | Int64 または CLongLong |
unsigned long long | UInt64 または CUnsignedLongLong |
float | Float または CFloat |
double | Double または CDouble |
3. ポインタ型の扱い
sendto(2) の呼び出し方を確認してみます。
このシステムコールの引数 size_t および復帰値 ssize_t は、Swift のプロトタイプ(以下の画像)ではそれぞれ Int に対応しています。つまり、上述の対応表に従えば、それぞれ long に typedef されているはずです。
ただし、socklen_t については、IntXX や UIntXX に対応している訳ではないため、Swift ではそのまま socklen_t として扱う必要があるようです。
% man -s 2 sendto
SEND(2) BSD System Calls Manual SEND(2)
:
SYNOPSIS
ssize_t
sendto(int socket, const void *buffer, size_t length, int flags,
const struct sockaddr *dest_addr, socklen_t dest_len);
:
さて、重要なのは UnsafeRawPointer と UnsafePointer の方です。
Swift には、C 言語のポインタに相当する基本中の基本の機能がありません。
その代わりに、メモリの位置を指すポインタをデータ型(ポインタ型)として扱い、そのポインタ型が指すメモリ位置のデータにアクセスします。
C と Swift の対応関係は以下のとおりです。(T: 構造体を含む任意の型)
C の型 | Swift の型 |
---|---|
const T * | UnsafePointer<T> |
T * | UnsafeMutablePointer<T> |
const void * | UnsafeRawPointer |
void * | UnsafeMutableRawPointer |
NULL | オプショナル型の nil |
さて、ポインタと聞くと避けては通れない malloc と free ですが、上述の Ping での呼び出し方のようにポインタ型を用いてデータを C に受け渡す方法では、メモリの解放の必要はありません。Swift のコンパイラがうまくやってくれるそうです。
ただし、呼び出し先で malloc によりメモリ領域を確保するような場合は、解放しないとメモリリークしてしまいます。つまり、今回の Ping の場合、「Echo の送信関数」「Echo reply の受信関数」を C で作成して Swift から呼び出す実装方式はリスクが高いと言えそうです。
なお、この記事では、ポインタ型への変換方法 (withUnsafePointer, withUnsafeBytes など) について解説はしません(できません)。
素晴らしいSwiftのポインタ型の解説 や SwiftでUnsafePointer<T>などのポインタを扱う あたりの記事(Qiita)が参考になると思います。
4. ラッパー設計図
ここまで説明してきてラッパー関数の必要性を強く感じますね。
むしろ、ラッパー関数なしでは POSIX Socket を扱うのは無理だと思われます。
例えば、Data 型のデータを sendtoラッパー に渡せば、中でポインタ型に変換してくれて Darwin.sendto を呼び出すようにすると、ポインタ(メモリ)を意識せずにソケット操作を手軽に実行できそうです。
また、sockaddr_in や errno などもラッピングし、Swift に取り込まれていない不足定義 (IP, ICMP) も内包し、さらには Linux の strace 相当のシステムコールトレース機能を装備することで使い勝手を高めることができそうです。
わざわざラッパーを作るくらいなら CFSocket でいいのでは?とも感じますが、
前回の考察にあるように制御メッセージでカーネルタイムスタンプを取得したいため、POSIX Socket を使います。
5. ラッパーの汎用化に向けて
今回の Ping では、アドレス情報は IPv4 のみ、ソケットラッパー関数やソケットオプションも Ping で使用するものに限定して実装します。
最終的には UNIX ドメインと IPv6 にも対応し、ソケットタイプは TCP,UDP,ICMP にフル対応し、iOS で使用可能なすべてのソケット関数・すべてのソケットオプション・すべての制御メッセージを扱えるようにしたいと思っています。
このソケット基盤の仕様は、別途 Swifty POSIX Socket Foundation で取りまとめ、 GitHub でのプログラム公開を目指します。(予定は未定)
終わり。