在 C++ 中使用 boost 与可执行文件交互

Interfacing with executable using boost in c++

我正在尝试将我正在编写的程序与现有的 C++ 国际象棋引擎 (stockfish) 连接起来。为此,我正在使用 Boost,但遇到了流程问题。 engine.exe 是我尝试与之交互的可执行文件,但它似乎在 uci 的第一个命令后终止。当程序到达第二个 while 循环时,sf.running() returns false 即使它从未终止。

#include <iostream>
#include <string>
#include <algorithm>
#include <boost/process.hpp>
#include <boost/filesystem.hpp>

using namespace std;

namespace bp = boost::process;

int main()
{
    boost::filesystem::path sfPath{ R"(engine.exe)" };
    
    bp::ipstream is;
    bp::opstream os;
    bp::child sf(sfPath, "uci", bp::std_out > is, bp::std_in < os);

    string line;
    getline(is, line);
    cout << line << endl;

    while(sf.running()) {
        getline(is, line);
        cout << line << endl;
    }

    os << "position startpos moves d2d4 g8f6 g1f3\n";

    os << "go\n";

    while (sf.running()) {
        getline(is, line);
        cout << line << endl;
    }
}

通过在我的机器上使用 stockfish 8 进行的快速测试,在 CLI 上提供命令(如您的 "uci")会导致它执行并退出。

Note: in complicated interfaces, it's possible to run into deadlocks with synchronous streams like this (see docs and perhaps ).

简单测试客户端

我试着先让它在同步模式下工作。为了好玩,我选择连接两个子进程,一个用于白色,一个用于黑色。我们可以让他们玩游戏:

int main() {
    MoveList game;
    Engine   white(game), black(game);

    for (int number = 1;; ++number) {
        game.push_back(white.make_move());
        std::cout << number << ". " << game.back();

        game.push_back(black.make_move());
        std::cout << ", " << game.back() << std::endl;

        if ("(none)" == game.back())
            break;
    }
}

现在我有两个工作实现:

同步实现

尽管可能会造成全双工 IO 死锁的可能性(如上文所述),但代码仍然相对简单:

using MoveList = std::deque<std::string>;

struct Engine {
    Engine(MoveList& game) : _game(game) { init(); }

    std::string make_move()
    {
        std::string best, ponder;

        auto bestmove = [&](std::string_view line) { //
            return qi::parse(line.begin(), line.end(),
                    "bestmove " >> +qi::graph >>
                    -(" ponder " >> +qi::graph) >> qi::eoi,
                    best, ponder);
        };

        bool ok = send(_game) //
            && command("go", bestmove);

        if (!ok)
            throw std::runtime_error("Engine communication failed");

        return best;
    }

  private:
    void init() {
        bool ok = true                                              //
            && expect([](std::string_view banner) { return true; }) //
            && command("uci", "uciok")                              //
            && send("ucinewgame")                                   //
            && command("isready", "readyok");

        if (!ok)
            throw std::runtime_error("Cannot initialize UCI");
    }

    bool command(std::string_view command, auto response, unsigned max_lines = 999) {
        return send(command) && expect(response, max_lines);
    }

    bool send(std::string_view command) {
        debug_out << "Send: " << std::quoted(command) << std::endl;
        _sink << command << std::endl;
        return _sink.good();
    }

    bool send(MoveList const& moves) {
        debug_out << "Send position (" << moves.size() << " moves)" << std::endl;

        _sink << "position startpos";

        if (!moves.empty()) {
            _sink << " moves";
            for (auto const& mv : moves) {
                _sink << " " << mv;
            }
        }
        _sink << std::endl;
        return _sink.good();
    }

    bool expect(std::function<bool(std::string_view)> predicate, unsigned max_lines = 999)
    {
        for (std::string line; getline(_source, line); max_lines--) {
            debug_out << "Echo: " << _source.tellg() << " " << std::quoted(line) << std::endl;
            if (predicate(line)) {
                debug_out << "Ack" << std::endl;
                return true;
            }
        }
        return false;
    }

    bool expect(std::string_view message, unsigned max_lines = 999)
    {
        return expect([=](std::string_view line) { return line == message; },
                      max_lines);
    }

    MoveList&    _game;
    bp::opstream _sink;
    bp::ipstream _source;
    bp::child    _engine{"stockfish", bp::std_in<_sink, bp::std_out>_source};
};

在我的系统上打印的演示 运行:

1. d2d4, d7d5
2. g1f3, g8f6
3. e2e3, e7e6
4. c2c4, b8c6
5. f1e2, f8e7
6. e1g1, e8g8
7. b1c3, c8d7
8. c1d2, e7d6
9. a2a3, d5c4
10. e2c4, e6e5
11. d4d5, c6e7
12. e3e4, h7h6
13. a1c1, c7c6
14. d5c6, d7c6
15. d1e2, d8d7
16. f1d1, e7g6
17. c3b5, f6e4
18. b5d6, e4d6
19. d2b4, g6f4
20. e2f1, c6f3
21. g2f3, d7h3
22. d1d6, h3h5
23. b4d2, a8d8
24. d6d8, h5g5
25. g1h1, f8d8
26. d2e3, b7b6
27. f1g1, g5f6
28. c1d1, d8d1
29. g1d1, f6f5
30. e3f4, f5f4
31. d1d8, g8h7
32. c4d5, g7g6
33. d8c7, h7g7
34. h1g2, b6b5
35. g2f1, a7a6
36. h2h3, f4f5
37. f1e2, f5f6
38. c7c5, h6h5
39. d5e4, f6e6
40. c5d5, g7f6
41. d5e6, f6e6
42. b2b4, e6e7
43. h3h4, e7e6
44. e4b7, e6d6
45. e2e3, d6c7
46. b7d5, f7f6
47. f3f4, c7d6
48. d5b7, e5f4
49. e3f4, d6d7
50. b7e4, g6g5
51. h4g5, d7e6
52. g5g6, f6f5
53. e4f5, e6f6
54. f5e4, h5h4
55. f4g4, h4h3
56. g4h3, f6e7
57. h3g4, e7f6
58. g4h4, f6e6
59. h4g5, e6e5
60. e4f5, e5d4
61. g6g7, d4c3
62. g7g8q, c3d2
63. g8d8, d2e1
64. d8b6, a6a5
65. b4a5, b5b4
66. b6e3, e1d1
67. f5d3, b4b3
68. e3e2, d1c1
69. a5a6, b3b2
70. e2e1, (none)

白方获胜

异步实现

为了完整起见,我想尝试异步实现。使用默认的 Asio 回调样式这可能会变得笨拙,所以我想使用 Boost Coroutine 作为堆栈协程。这使得实现可以与同步版本 99% 相似:

using MoveList = std::deque<std::string>;
using boost::asio::yield_context;

struct Engine {
    Engine(MoveList& game) : _game(game) { init(); }

    std::string make_move()
    {
        std::string best, ponder;

        boost::asio::spawn([this, &best, &ponder](yield_context yield) {
            auto bestmove = [&](std::string_view line) { //
                return qi::parse(line.begin(), line.end(),
                                 "bestmove " >> +qi::graph >>
                                     -(" ponder " >> +qi::graph) >> qi::eoi,
                                 best, ponder);
            };

            bool ok = send(_game, yield) //
                && command("go", bestmove, yield);

            if (!ok)
                throw std::runtime_error("Engine communication failed");
        });
        run_io();
        return best;
    }

  private:
    void init() {
        boost::asio::spawn([this](yield_context yield) {
            bool ok = true //
                &&
                expect([](std::string_view banner) { return true; }, yield) //
                && command("uci", "uciok", yield)                           //
                && send("ucinewgame", yield)
                && command("isready", "readyok", yield);

            if (!ok)
                throw std::runtime_error("Cannot initialize UCI");
        });
        run_io();
    }

    bool command(std::string_view command, auto response, yield_context yield) {
        return send(command, yield) && expect(response, yield);
    }

    bool send(std::string_view command, yield_context yield) {
        debug_out << "Send: " << std::quoted(command) << std::endl;
        using boost::asio::buffer;
        return async_write(_sink, std::vector{buffer(command), buffer("\n", 1)},
                           yield);
    }

    bool send(MoveList const& moves, yield_context yield) {
        debug_out << "Send position (" << moves.size() << " moves)" << std::endl;

        using boost::asio::buffer;
        std::vector bufs{buffer("position startpos"sv)};

        if (!moves.empty()) {
            bufs.push_back(buffer(" moves"sv));
            for (auto const& mv : moves) {
                bufs.push_back(buffer(" ", 1));
                bufs.push_back(buffer(mv));
            }
        }
        bufs.push_back(buffer("\n", 1));
        return async_write(_sink, bufs, yield);
    }

    bool expect(std::function<bool(std::string_view)> predicate, yield_context yield)
    {
        auto buf = boost::asio::dynamic_buffer(_input);
        while (auto n = async_read_until(_source, buf, "\n", yield)) {
            std::string_view line(_input.data(), n > 0 ? n - 1 : n);
            debug_out << "Echo: " << std::quoted(line) << std::endl;

            bool matched = predicate(line);
            buf.consume(n);

            if (matched) {
                debug_out << "Ack" << std::endl;
                return true;
            }
        }
        return false;
    }

    bool expect(std::string_view message, yield_context yield)
    {
        return expect([=](std::string_view line) { return line == message; },
                      yield);
    }

    void run_io() {
        _io.run();
        _io.reset();
    }

    boost::asio::io_context _io{1};
    bp::async_pipe          _sink{_io}, _source{_io};
    bp::child _engine{"stockfish", bp::std_in<_sink, bp::std_out> _source, _io};

    MoveList& _game;
    std::string _input; // read-ahead buffer
};

最明显的区别是从 iostream 样式 IO 切换到基于缓冲区的 IO。使用此版本的另一个测试 运行:

1. d2d4, d7d5
2. g1f3, g8f6
3. e2e3, e7e6
4. c2c4, b8c6
5. f1e2, d5c4
6. e2c4, f8d6
7. e1g1, e8g8
8. b1c3, e6e5
9. d4d5, c6a5
10. c4d3, c7c6
11. e3e4, c6d5
12. e4d5, c8g4
13. h2h3, g4f3
14. d1f3, h7h6
15. c1d2, a8c8
16. f3e2, d6b4
17. f1d1, e5e4
18. c3e4, f6d5
19. d2b4, d5b4
20. e4c3, b4d3
21. d1d3, d8h4
22. d3d7, f8d8
23. a1d1, d8d7
24. d1d7, h4g5
25. e2d1, a5c6
26. d7b7, c8d8
27. d1g4, g5g4
28. h3g4, d8d2
29. c3e4, d2d1
30. g1h2, a7a5
31. h2g3, f7f6
32. f2f3, g8f8
33. b7c7, c6b4
34. a2a3, b4d3
35. b2b3, d1b1
36. c7a7, b1b3
37. a7a5, b3b2
38. a3a4, d3c1
39. a5a8, f8f7
40. e4d6, f7e6
41. d6f5, c1e2
42. g3h2, g7g5
43. a8a6, e6d7
44. a6f6, e2f4
45. f6h6, b2g2
46. h2h1, g2a2
47. h6a6, a2a1
48. h1h2, a1a2
49. h2g1, f4h3
50. g1f1, a2f2
51. f1e1, f2f3
52. a6a7, d7c8
53. f5d6, c8b8
54. a7b7, b8a8
55. b7b4, f3g3
56. d6f5, g3a3
57. e1d2, h3f4
58. b4e4, a8b7
59. d2c2, f4d3
60. e4e3, a3a4
61. c2d3, a4g4
62. e3e7, b7c6
63. e7e5, g4g1
64. d3e2, g1c1
65. f5d4, c6d6
66. e5g5, c1c5
67. g5g8, d6e5
68. e2d3, c5c1
69. g8g5, e5f6
70. g5f5, f6g6
71. f5f2, c1c7
72. d3e4, c7e7
73. e4d5, e7d7
74. d5e5, d7e7
75. e5d6, e7e8
76. f2f1, e8a8
77. d4c6, g6g5
78. d6c5, a8e8
79. f1d1, g5f4
80. d1g1, e8e3
81. g1c1, e3e8
82. c1a1, e8e2
83. a1a4, e2e4
84. a4a7, e4e2
85. c5d5, e2h2
86. a7a1, h2d2
87. c6d4, f4e3
88. a1a4, e3f4
89. a4a3, d2d1
90. a3f3, f4g4
91. f3f8, d1a1
92. d4c2, a1a5
93. d5e4, a5a4
94. c2d4, g4g3
95. e4d3, a4a1
96. d3c3, a1e1
97. c3c4, e1c1
98. c4b3, c1a1
99. d4e2, g3g4
100. e2c3, g4g5
101. f8b8, a1h1
102. b8c8, g5f6
103. c8e8, h1h4
104. c3d5, f6f5
105. b3c3, h4e4
106. e8d8, f5e6
107. d5b4, e4h4
108. b4d3, e6e7
109. d8g8, e7d7
110. g8g6, d7e7
111. g6a6, e7d7
112. a6a8, h4h3
113. c3d4, h3h4
114. d4c3, h4h3
115. c3d4, d7c7
116. d3c1, h3h1
117. c1a2, h1a1
118. a2c1, a1a8
119. c1b3, a8h8
120. d4e5, h8h5
121. e5e6, c7b6
122. b3d4, b6c5
123. d4f5, h5h1
124. e6e5, h1e1
125. e5f4, c5c4
126. f5e3, c4d3
127. e3g4, e1a1
128. f4e5, a1a2
129. e5f5, d3d4
130. f5f4, a2a8
131. f4f5, a8a5
132. f5e6, a5g5
133. g4f6, g5g1
134. e6f5, g1a1
135. f6d7, a1e1
136. d7f6, e1e5
137. f5f4, e5a5
138. f6g4, d4d5
139. g4e3, d5e6
140. f4e4, a5a4
141. e4f3, e6e5
142. e3g4, e5f5
143. g4e3, f5e6
144. e3g4, a4a3
145. f3e4, a3b3
146. g4e3, b3b4
147. e4f3, e6d6
148. e3g4, b4a4
149. g4e3, d6c5
150. e3g4, c5d5
151. g4e3, d5d4
152. f3f4, d4d3
153. f4f3, a4e4
154. e3f5, e4e6
155. f3f4, e6e1
156. f5g3, e1c1
157. f4e5, c1c4
158. g3f5, c4c5
159. e5f4, c5b5
160. f5d6, b5a5
161. d6f5, a5a4
162. f4e5, a4c4
163. e5d5, c4b4
164. f5d6, b4h4
165. d5e5, h4h5
166. e5f4, h5h1
167. f4e5, h1e1
168. e5f5, e1a1
169. d6c4, d3c4
170. f5e4, a1e1
171. e4f5, c4d4
172. f5f6, d4d5
173. f6f7, e1e6
174. f7f8, d5e5
175. f8g7, e6f6
176. g7h7, e5f5
177. h7g7, f5g5
178. g7h7, f6f7
179. h7h8, g5g6
180. h8g8, f7f6
181. g8h8, f6f8
182. (none), (none)

一场非常枯燥、拖延的车残局,但黑方最终在胜利中跌跌撞撞

Note the engine might get stuck in an infinite loop - assuming that the 50-move rule/3-fold repetition do not automatically lead to draw.

参考资料

  • 文件synchronous.cpp

    #include <boost/process.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <iomanip>
    namespace bp = boost::process;
    namespace qi = boost::spirit::qi;
    
    using MoveList = std::deque<std::string>;
    static inline std::ostream debug_out(nullptr /*std::cerr.rdbuf()*/);
    
    struct Engine {
        Engine(MoveList& game) : _game(game) { init(); }
    
        std::string make_move()
        {
            std::string best, ponder;
    
            auto bestmove = [&](std::string_view line) { //
                return qi::parse(line.begin(), line.end(),
                        "bestmove " >> +qi::graph >>
                        -(" ponder " >> +qi::graph) >> qi::eoi,
                        best, ponder);
            };
    
            bool ok = send(_game) //
                && command("go", bestmove);
    
            if (!ok)
                throw std::runtime_error("Engine communication failed");
    
            return best;
        }
    
      private:
        void init() {
            bool ok = true                                              //
                && expect([](std::string_view banner) { return true; }) //
                && command("uci", "uciok")                              //
                && send("ucinewgame")                                   //
                && command("isready", "readyok");
    
            if (!ok)
                throw std::runtime_error("Cannot initialize UCI");
        }
    
        bool command(std::string_view command, auto response, unsigned max_lines = 999) {
            return send(command) && expect(response, max_lines);
        }
    
        bool send(std::string_view command) {
            debug_out << "Send: " << std::quoted(command) << std::endl;
            _sink << command << std::endl;
            return _sink.good();
        }
    
        bool send(MoveList const& moves) {
            debug_out << "Send position (" << moves.size() << " moves)" << std::endl;
    
            _sink << "position startpos";
    
            if (!moves.empty()) {
                _sink << " moves";
                for (auto const& mv : moves) {
                    _sink << " " << mv;
                }
            }
            _sink << std::endl;
            return _sink.good();
        }
    
        bool expect(std::function<bool(std::string_view)> predicate, unsigned max_lines = 999)
        {
            for (std::string line; getline(_source, line); max_lines--) {
                debug_out << "Echo: " << _source.tellg() << " " << std::quoted(line) << std::endl;
                if (predicate(line)) {
                    debug_out << "Ack" << std::endl;
                    return true;
                }
            }
            return false;
        }
    
        bool expect(std::string_view message, unsigned max_lines = 999)
        {
            return expect([=](std::string_view line) { return line == message; },
                          max_lines);
        }
    
        MoveList&    _game;
        bp::opstream _sink;
        bp::ipstream _source;
        bp::child    _engine{"stockfish", bp::std_in<_sink, bp::std_out>_source};
    };
    
    int main() {
        MoveList game;
        Engine   white(game), black(game);
    
        for (int number = 1;; ++number) {
            game.push_back(white.make_move());
            std::cout << number << ". " << game.back();
    
            game.push_back(black.make_move());
            std::cout << ", " << game.back() << std::endl;
    
            if ("(none)" == game.back())
                break;
        }
    }
    
  • 文件asynchronous.cpp

     #include <boost/asio.hpp>
     #include <boost/asio/spawn.hpp>
     #include <boost/process.hpp>
     #include <boost/process/async.hpp>
     #include <boost/spirit/include/qi.hpp>
     #include <iomanip>
     namespace bp = boost::process;
     namespace qi = boost::spirit::qi;
     using namespace std::literals;
    
     using MoveList = std::deque<std::string>;
     using boost::asio::yield_context;
    
     static inline std::ostream debug_out(nullptr /*std::cerr.rdbuf()*/);
    
     struct Engine {
         Engine(MoveList& game) : _game(game) { init(); }
    
         std::string make_move()
         {
             std::string best, ponder;
    
             boost::asio::spawn([this, &best, &ponder](yield_context yield) {
                 auto bestmove = [&](std::string_view line) { //
                     return qi::parse(line.begin(), line.end(),
                                      "bestmove " >> +qi::graph >>
                                          -(" ponder " >> +qi::graph) >> qi::eoi,
                                      best, ponder);
                 };
    
                 bool ok = send(_game, yield) //
                     && command("go", bestmove, yield);
    
                 if (!ok)
                     throw std::runtime_error("Engine communication failed");
             });
             run_io();
             return best;
         }
    
       private:
         void init() {
             boost::asio::spawn([this](yield_context yield) {
                 bool ok = true //
                     &&
                     expect([](std::string_view banner) { return true; }, yield) //
                     && command("uci", "uciok", yield)                           //
                     && send("ucinewgame", yield)
                     && command("isready", "readyok", yield);
    
                 if (!ok)
                     throw std::runtime_error("Cannot initialize UCI");
             });
             run_io();
         }
    
         bool command(std::string_view command, auto response, yield_context yield) {
             return send(command, yield) && expect(response, yield);
         }
    
         bool send(std::string_view command, yield_context yield) {
             debug_out << "Send: " << std::quoted(command) << std::endl;
             using boost::asio::buffer;
             return async_write(_sink, std::vector{buffer(command), buffer("\n", 1)},
                                yield);
         }
    
         bool send(MoveList const& moves, yield_context yield) {
             debug_out << "Send position (" << moves.size() << " moves)" << std::endl;
    
             using boost::asio::buffer;
             std::vector bufs{buffer("position startpos"sv)};
    
             if (!moves.empty()) {
                 bufs.push_back(buffer(" moves"sv));
                 for (auto const& mv : moves) {
                     bufs.push_back(buffer(" ", 1));
                     bufs.push_back(buffer(mv));
                 }
             }
             bufs.push_back(buffer("\n", 1));
             return async_write(_sink, bufs, yield);
         }
    
         bool expect(std::function<bool(std::string_view)> predicate, yield_context yield)
         {
             auto buf = boost::asio::dynamic_buffer(_input);
             while (auto n = async_read_until(_source, buf, "\n", yield)) {
                 std::string_view line(_input.data(), n > 0 ? n - 1 : n);
                 debug_out << "Echo: " << std::quoted(line) << std::endl;
    
                 bool matched = predicate(line);
                 buf.consume(n);
    
                 if (matched) {
                     debug_out << "Ack" << std::endl;
                     return true;
                 }
             }
             return false;
         }
    
         bool expect(std::string_view message, yield_context yield)
         {
             return expect([=](std::string_view line) { return line == message; },
                           yield);
         }
    
         void run_io() {
             _io.run();
             _io.reset();
         }
    
         boost::asio::io_context _io{1};
         bp::async_pipe          _sink{_io}, _source{_io};
         bp::child _engine{"stockfish", bp::std_in<_sink, bp::std_out> _source, _io};
    
         MoveList& _game;
         std::string _input; // read-ahead buffer
     };
    
     int main() {
         MoveList game;
         Engine   white(game), black(game);
    
         for (int number = 1;; ++number) {
             game.push_back(white.make_move());
             std::cout << number << ". " << game.back();
    
             game.push_back(black.make_move());
             std::cout << ", " << game.back() << std::endl;
    
             if ("(none)" == game.back())
                 break;
         }
     }