프로그래밍 - 체스 엔진 stockfish/FIFO/Bash 리디렉션과 통신

프로그래밍 - 체스 엔진 stockfish/FIFO/Bash 리디렉션과 통신

저는 작은 체스 프로그램을 작성하려고 합니다. 실제로는 체스 GUI에 가깝습니다. 체스 GUI는 플레이어가 컴퓨터와 대결하는 동안 내부적으로 Stockfish 체스 엔진을 사용해야 합니다.

나는 stockfish를 설치했고 이를 터미널에서 실행하고 STDIN 및 STDOUT을 통해 통신할 수 있습니다. 예를 들어 "isready"를 입력하면 stockfish는 "readyok"로 응답합니다.

이제 저는 Linux의 일부 IPC 방법을 통해 체스 GUI에서 stockfish와 지속적으로 통신할 수 있도록 노력하고 있습니다. 처음에는 파이프를 살펴봤지만 파이프 통신이 단방향이기 때문에 포기했습니다. 그런 다음 FIFO 및 Bash 리디렉션에 대해 읽었으며 이제 시도해 보겠습니다. stockfish의 출력 중 한 줄을 읽을 수 있기 때문에 일종의 효과가 있습니다. 그러나 이것은 첫 번째 행에서만 작동합니다. 그런 다음 FIFO를 통해 stockfish에 "isready"를 보내고 stockfish의 다음 출력을 읽으려고 하면 응답이 없습니다. Bash 리디렉션을 사용하여 stockfish의 STDIN 및 STDOUT을 FIFO로 리디렉션했습니다.

터미널에서 stockfish를 시작하기 위해 이 스크립트를 실행합니다.

#!/bin/bash

rm /tmp/to_stockchess -f
mkfifo /tmp/to_stockchess

rm /tmp/from_stockchess -f
mkfifo /tmp/from_stockchess

stockfish < /tmp/to_stockchess > /tmp/from_stockchess

./stockfish.sh를 사용하여 이 스크립트를 호출합니다.

예를 들어, 저는 다음과 같은 C 프로그램을 가지고 있습니다. (저는 C를 처음 접합니다.)

#include <stdio.h> 
#include <stdlib.h>


int main(void) {
    
    FILE *fpi;
    fpi = fopen("/tmp/to_stockchess", "w");

    FILE *fpo;
    fpo = fopen ("/tmp/from_stockchess", "r");

    char * line = NULL;
    size_t len = 0;
    ssize_t read;

    read = getline(&line, &len, fpo);
    printf("Retrieved line of length %zu:\n", read);
    printf("%s", line);

    fprintf(fpi, "isready\n");

    read = getline(&line, &len, fpo);
    printf("Retrieved line of length %zu:\n", read);
    printf("%s", line);

    fclose (fpi);
    fclose (fpo);

    return 0;
}

터미널에 프로그램 출력(그러나 프로그램은 중지되지 않고 대기함):

Retrieved line of length 74:
Stockfish 11 64 POPCNT by T. Romstad, M. Costalba, J. Kiiski, G. Linscott

아아, 이것은 지속적으로 작동하지 않습니다(지금은 루프가 없습니다. 루프 없이 두 번 이상 읽으려고 했습니다). 예를 들어 stockfish 스크립트는 한 터미널에서 한 줄을 읽은 후 한 터미널에서 종료됩니다(연속적으로 실행하는 대신). 실행) 코드 출력 FIFO. 또는 stockfish 출력 FIFO에서 한 줄의 출력을 읽을 수도 있습니다. Stockfish를 사용하여 STDIN 및 STDOUT을 통해 IPC를 수행하는 더 쉬운 방법이 있다면 나도 시도해 볼 수 있습니다. 감사해요.

답변1

이미 C를 사용하고 계시기 때문에 stockchess관리에도 C를 사용하시는 것을 추천드립니다. popen()다음을 제공하는 라이브러리 기능이 있습니다.단방향프로세스에 파이프 - 사용 사례에 적합하지 않습니다. 그러나 직접 설정할 수도 있습니다.

다음 예제 프로그램을 고려하십시오.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/**
 * Creates two pipes, forks, and runns the given command.  One pipe is
 * connected between the given *out and the standard input stream of the child;
 * the other pipe is connected between the given *in and the standard output
 * stream of the child.
 *
 * Returns the pid of the child on success, -1 otherwise.  On error, errno
 * will be set accordingly.
 */
int bi_popen(const char* const command, FILE** const in, FILE** const out)
{
    const int READ_END = 0;
    const int WRITE_END = 1;
    const int INVALID_FD = -1;

    int to_child[2] = { INVALID_FD, INVALID_FD };
    int to_parent[2] = { INVALID_FD, INVALID_FD };

    *in = NULL;
    *out = NULL;

    if (command == NULL || in == NULL || out == NULL) {
        errno = EINVAL;
        goto bail;
    }

    if (pipe(to_child) < 0) {
        goto bail;
    }

    if (pipe(to_parent) < 0) {
        goto bail;
    }

    const pid_t pid = fork();
    if (pid < 0) {
        goto bail;
    }

    if (pid == 0) { // Child
        if (dup2(to_child[READ_END], STDIN_FILENO) < 0) {
            perror("dup2");
            exit(1);
        }
        close(to_child[READ_END]);
        close(to_child[WRITE_END]);

        if (dup2(to_parent[WRITE_END], STDOUT_FILENO) < 0) {
            perror("dup2");
            exit(1);
        }
        close(to_parent[READ_END]);
        close(to_parent[WRITE_END]);

        execlp(command, command, NULL);
        perror("execlp");
        exit(1);
    }

    // Parent
    close(to_child[READ_END]);
    to_child[READ_END] = INVALID_FD;

    close(to_parent[WRITE_END]);
    to_parent[WRITE_END] = INVALID_FD;

    *in = fdopen(to_parent[READ_END], "r");
    if (*in == NULL) {
        goto bail;
    }
    to_parent[READ_END] = INVALID_FD;

    *out = fdopen(to_child[WRITE_END], "w");
    if (*out == NULL) {
        goto bail;
    }
    to_child[WRITE_END] = INVALID_FD;

    setvbuf(*out, NULL, _IONBF, BUFSIZ);

    return pid;

bail:
    ; // Goto label must be a statement, this is an empty statement
    const int old_errno = errno;

    if (*in != NULL) {
        fclose(*in);
    }

    if (*out != NULL) {
        fclose(*out);
    }

    for (int i = 0; i < 2; ++i) {
        if (to_child[i] != INVALID_FD) {
            close(to_child[i]);
        }
        if (to_parent[i] != INVALID_FD) {
            close(to_parent[i]);
        }
    }

    errno = old_errno;
    return -1;
}

int main(void)
{
    FILE* in = NULL;
    FILE* out = NULL;
    char* line = NULL;
    size_t size = 0;

    const int pid = bi_popen("/bin/bash", &in, &out);
    if (pid < 0) {
        perror("bi_popen");
        return 1;
    }

    fprintf(out, "ls -l a.out\n");
    getline(&line, &size, in);
    printf("-> %s", line);

    fprintf(out, "pwd\n");
    getline(&line, &size, in);
    printf("-> %s", line);

    fprintf(out, "date\n");
    getline(&line, &size, in);
    printf("-> %s", line);

    // Since in this case we can tell the child to terminate, we'll do so
    // and wait for it to terminate before we close down.
    fprintf(out, "exit\n");
    waitpid(pid, NULL, 0);

    fclose(in);
    fclose(out);

    return 0;
}

프로그램에서 함수를 정의합니다 bi_popen. 이 함수는 실행될 프로그램의 경로와 입력을 위한 두 가지 경로를 입력으로 사용합니다 FILE*.in~에서명령 및 out출력도착하다주문하다.

bi_popen두 개의 파이프를 설정합니다. 하나는 상위 프로세스에서 하위 프로세스로의 통신용이고 다른 하나는 하위 프로세스에서 상위 프로세스로의 통신용입니다.

다음으로 bi_popen fork새 프로세스를 만듭니다. 하위 프로세스는 표준 출력을 상위 파이프의 쓰기 끝에 연결하고 표준 입력을 상위 파이프의 읽기 끝에 연결합니다. 그런 다음 이전 파이프 파일 설명자를 정리하고 execlp실행 중인 프로세스를 지정된 명령으로 바꿉니다. 새 프로그램은 파이프의 표준 입력/출력 구성을 상속합니다. 한번 성공하면 execlp다시는 돌아오지 않습니다.

상위 프로세스의 경우 fork0이 아닌 값이 반환되면 상위 프로세스는 파이프의 불필요한 끝을 닫고 fdopen생성할 FILE*관련 파이프와 연결된 파일 설명자를 사용합니다. in이 값으로 매개변수를 업데이트 하고 출력합니다. 마지막으로 버퍼링되지 않도록 출력을 out사용합니다. 따라서 하위 프로세스로 전송되는 내용을 명시적으로 플러시할 필요가 없습니다.execlpFILE*

main기능은 사용 방법의 예입니다 bi_popen. bi_popenas 명령을 사용하여 호출 됩니다 /bin/bash. 스트림 에 기록된 out모든 내용은 실행을 위해 bash로 전송됩니다. bash가 표준 출력으로 인쇄하는 모든 내용을 읽을 수 있습니다 in.

다음은 이 프로그램의 작동 방식에 대한 예입니다.

$ ./a.out
-> -rwxr-xr-x 1 user group 20400 Aug 29 17:09 a.out
-> /home/user/src/bidirecitonal_popen
-> Sat Aug 29 05:10:52 PM EDT 2020

main일련의 명령이 하위 프로세스(여기)에 기록되고 해당 bash명령은 예상된 출력으로 응답합니다.

귀하의 경우 "/bin/bash"를 "stockfish"로 바꾼 다음 이를 사용하여 out명령을 작성 stockfish하고 in응답을 읽을 수 있습니다.

관련 정보