LoginSignup
1
1

More than 3 years have passed since last update.

SwiftによるPOSIX Socketラッパーの設計と汎用化

Last updated at Posted at 2020-11-25

この記事は、iOSアプリ開発から公開までの流れ の第11章です。

本稿では、Swift 向けの POSIX Socket のラッパーを設計します。
また、今後のアプリ開発で使用可能とするために汎用化も検討したいと思います。

1. POSIX Socket の呼び出し方について

前回、POSIX Socket を使用して簡単な Ping を実装してみました。
そのソースコードにおける以下の 2 つの呼び出し方に着目してみます。

Step 1. ICMP ソケットを作成

POSIXSocket_OnePing.playground - socket(2)
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 を送信

POSIXSocket_OnePing.playground - sendto(2)
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 に対応しているため、特に何も考えずに呼び出すことができたのです。
1.jpeg
その他の代表的な単純型も以下のように対応しています。
なお、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 の方です。
2.jpeg
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 相当のシステムコールトレース機能を装備することで使い勝手を高めることができそうです。
3.jpeg

わざわざラッパーを作るくらいなら CFSocket でいいのでは?とも感じますが、
前回の考察にあるように制御メッセージでカーネルタイムスタンプを取得したいため、POSIX Socket を使います。

5. ラッパーの汎用化に向けて

今回の Ping では、アドレス情報は IPv4 のみ、ソケットラッパー関数やソケットオプションも Ping で使用するものに限定して実装します。

最終的には UNIX ドメインと IPv6 にも対応し、ソケットタイプは TCP,UDP,ICMP にフル対応し、iOS で使用可能なすべてのソケット関数・すべてのソケットオプション・すべての制御メッセージを扱えるようにしたいと思っています。

このソケット基盤の仕様は、別途 Swifty POSIX Socket Foundation で取りまとめ、 GitHub でのプログラム公開を目指します。(予定は未定)

終わり。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1