機械学習周りのプログラミング中心。 イベント情報
ポケモンバトルAI本電子書籍通販中

C++の標準入出力をTCPで送受信【やねうら王・ふかうら王iOS移植】

やねうら王はGUI(将棋所など)と標準入出力で通信する仕組みになっていますが、iOSでは標準入出力で他のプロセスと通信することができません。floodgate等で通信対局するためには、端末外と通信する必要があります。ここでは、iOSアプリの標準入出力を直接TCPに乗せて、Mac上で動作している将棋所と通信させます。ここで紹介しているやり方は、やねうら王に限らずstd::cout, std::cinを利用して通信するプログラム全般に応用できます。

以前shogi686microをTCP通信するように改造した際は、std::coutソースコード上で置き換えてstd::ostringstreamに送信する文字列を書き込み、送信用の関数に渡すようにしていました。

// もともとstd::coutに出力していた文字列をostringstreamに書き込み
ostringstream oss;
oss << "info score cp " << pos.evaluate() << " string static score" << endl;
socket_send_string(oss);

// 送信関数
void socket_send_string(ostringstream &oss) {
    string s = oss.str();
    const char* cstr = s.c_str();
    size_t len = strlen(cstr);
    if (send(fd_socket, cstr, len, 0) < len) {
        cerr << "partial send error" << endl;
    }
}

しかしながらやねうら王では文字列を送信する箇所が多いため、入出力箇所すべてのソースコードを書き替えるのは非効率です。代わりに、std::cout, std::cinの入出力相手を一括で差し替えることができます。

std::coutの差し替え

std::streambufを継承したクラスを作成し、overflow関数をオーバーライドします。std::cout.rdbuf()により、独自のクラスに出力先を切り替えることができます。overflow関数に出力される文字が渡されるので、そこでネットワークに送信する、ファイルに書き込むなど任意の処理を実装することが可能です。

class myoutstreambuf : public std::streambuf {
public:
    myoutstreambuf() {
    }

protected:
    int overflow(int nCh = EOF) {
        // 出力される文字が1文字ずつ渡される
        // ここでsocketに送信
        std::cerr << "myoutstreambuf overflow " << nCh << std::endl;
        return 0;
    }
};

void run_out() {
    myoutstreambuf myout;
    std::streambuf *prev_rdbuf = std::cout.rdbuf(&myout);
    std::cout << "myout" << std::endl; // 1文字ずつの文字コードがoverflow関数内でstderrに出力される

    std::cout.rdbuf(prev_rdbuf); // mystreambufが破棄される前に出力先を戻さないとsegmentation faultが発生する
}

std::cinの書き換え

std::streambufを継承したクラスを作成し、uflow関数をオーバーライドします。std::cin.rdbuf()により、独自のクラスに入力元を切り替えることができます。cinから文字が読み取られるときにuflow関数が呼ばれるので、そこでネットワークから受信する、ファイルから読み込むなど任意の処理を実装することが可能です。

class myinstreambuf : public std::streambuf {
public:
    myinstreambuf(): mydata("hello\nworld\n"), myidx(0) {
    }

protected:
    int uflow() {
        // 1文字ずつ返す(ここではメモリ上に用意した定数)
        int c = mydata[myidx];
        std::cerr << "uflow " << c << std::endl;
        if (c) {
            myidx++;
            return c;
        } else {
            return EOF;
        }
    }

private:
    const char* mydata;
    int myidx;
};

void run_in() {
    myinstreambuf myin;

    std::streambuf *prev_rdbuf = std::cin.rdbuf(&myin);

    std::string line;
    while (std::getline(std::cin, line)) {
        std::cerr << "read " << line << std::endl;
    }

    std::cerr << "read EOF" << std::endl;

    std::cin.rdbuf(prev_rdbuf); // mystreambufが破棄される前に出力先を戻さないとsegmentation faultが発生する
}

パフォーマンスを重視する場合は複数文字を一気に読み書きする関数も実装できるようですが、将棋の通信では1文字単位の処理で十分です。 これを用いることで、やねうら王の思考エンジンの入出力をTCPで他のマシンとつなぐことが可能になります。ただ、iOSアプリ内のGUIを経由しないので局面などを表示することができません。C++からソケットを直接触るのではなくSwift言語にコールバックするような実装に変えれば表示も可能になります。

将棋所の設定

select766.hatenablog.com

ソースコード

コード全体 標準入出力(cin/cout)を独自の入出力関数で置き換える例 · GitHub

ソケット通信と組み合わせたバージョンのコード やねうら王を改造してUSIプロトコルをTCPに載せるコード · GitHub