Rust

Rust로 online-judge 서버 만들어보기 [1]

초코민트냠냠 2022. 10. 26. 10:49
반응형

개요

러스트로 C언어 코드를 받아서 컴파일하고 테스트케이스를 돌려서 성공인지 실패인지 결과를 반환하는 프로그램을 만드는 것이 목표입니다.

 

프로그램은 도커 컨테이너에서 실행될 것이고, 백엔드랑은 http 통신을 합니다.

 

우선 이번에는 c파일을 컴파일해서 정답과 비교하는거 까지 만들어보았습니다.

간단하게 그려본 flow chart

이번에 진행한 내용

.예시로 만든 프로그램은 문자열을 입력받아 거꾸로 출력하는 프로그램입니다.

#include <stdio.h>

int main(int argc, char **argv)
{
    char str[100];
    int i = 0, j;
    scanf("%s", str);
    while(str[i] != '\0')
        i++;
    for(j = i - 1; j >= 0; j--)
    {
        printf("%c", str[j]);
    }    
    printf("\n");
    return 0;
}

 

파일 입출력

우선 입력파일과 정답파일을 준비합니다. 

// input.txt
hello
// answer.txt
olleh

 

그리고 main.rs에서 input.txt를 불러옵니다. Rust는 std::fs::File에 파일 입출력 라이브러리를 가지고 있습니다. 러스트의 권장 방식대로 File을 Buffer에 읽어들이고 String으로 변환합니다.

 

	let input_file = File::open("test_code/input.txt").unwrap();
        	let mut buf_reader = BufReader::new(input_file);
        	let mut input_text = String::new();
        	buf_reader.read_to_string(&mut input_text).unwrap();
        	println!("input_text:  {}", input_text);

 

 

C파일 컴파일

Rust 스탠다드 라이브러리에 Command가 있습니다. 커맨드로 shell을 실행하고 gcc로 컴파일을 합니다.

	Command::new("sh")
            .arg("-c")
            .arg("gcc -o test.out test_code/test.c")
            .output()
            .expect("failed to execute process");

 

프로그램 실행

마찬가지로 Command를 사용해서 컴파일된 test.out을 실행시키고 아까 입력파일에서 받은 테스트케이스를 전달합니다.

실행된 프로세스에 표준입력과 표준출력을 연결해야 합니다. std::process::Stdio라이브러리를 사용해서 간단히 구현할 수 있습니다.

 

표준입출력과 표준출력을 연결해주고 spawn()을 하면 표준입출력이 연결된 Child 프로세스를 받아올 수 있습니다.

	let mut child = Command::new("sh")
            .arg("-c")
            .arg("./test.out")
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .spawn()
            .expect("failed to execute process");

 

그리고 child 프로세스의 표준입력에 아까 input.txt에서 읽어온 값을 전달합니다. 그리고 결과값을 받아옵니다.

	{
            let stdin = child.stdin.as_mut().expect("failed to open stdin");
            stdin
                .write_all(input_text.as_bytes())
                .expect("failed to write to stdin");
	}
    	let output = child.wait_with_output().expect("failed to wait on child");

 

output.stdout을 하면 Vec<u8>로 값을 반환합니다. 그리고 러스트의 문자열에 맞게 UTF-8로 변환합니다. 

        let output_text = match str::from_utf8(&output.stdout) {
            Ok(v) => v.trim(),
            Err(_) => panic!("failed to convert output to string: {:?}", output.stdout),
        };
        println!("output_text: {}", output_text);

 

정답과 비교

아까 input.txt를 불러왔던것처럼 answer.txt를 불러와서 아까 가져왔던 output_text와 비교합니다.

        let answer_file = File::open("test_code/answer.txt").unwrap();
        let mut buf_reader = BufReader::new(answer_file);
        let mut answer_text = String::new();
        buf_reader.read_to_string(&mut answer_text).unwrap();

        assert_eq!(output_text, answer_text.trim());

        println!("Success!");

 

윈도우에서 돌리려면 Command::new("cmd")로 실행해야 합니다. (윈도우와 리눅스의 코드비교 예시)

    if cfg!(target_os = "windows") {
        Command::new("cmd")
            .args(["/C", "echo hello"])
            .output()
            .expect("failed to execute process")
    } else {
        Command::new("sh")
            .arg("-c")
            .arg("echo hello")
            .output()
            .expect("failed to execute process")
    };

 

아무튼 실행해보면 잘 동작합니다.

 

도커로 실행하기

우분투 환경에서 실행해봤습니다. 도커파일은 다음과 같이 작성했습니다.

FROM rust:alpine AS builder

WORKDIR /online_judge

RUN cargo init .
COPY ./Cargo* ./
RUN cargo build --release && \
  rm target/release/deps/online_judge*

COPY . .
RUN cargo build --release

FROM ubuntu:latest

RUN ["apt-get", "update"]
RUN ["apt-get",  "install", "-y",  "gcc"]

WORKDIR /usr/local/bin

COPY --from=builder /online_judge/target/release/online_judge .
COPY ./test_code ./test_code

CMD ["./online_judge"]

 

다음에 진행할 내용

process격리와 system-call을 막는 작업을 할 예정입니다. linux의 seccomp를 쓰면 시스템 콜을 막을 수 있다는거 같은데 자세히 알아봐야겠습니다.

 

참조

https://doc.rust-lang.org/std/process/struct.Command.html#method.args

 

Command in std::process - Rust

Executes the command as a child process, waiting for it to finish and collecting all of its output. By default, stdout and stderr are captured (and used to provide the resulting output). Stdin is not inherited from the parent and any attempt by the child p

doc.rust-lang.org

https://docs.rs/rustc-std-workspace-std/1.0.1/std/process/struct.Stdio.html

 

std::process::Stdio - Rust

Describes what to do with a standard I/O stream for a child process when passed to the stdin, stdout, and stderr methods of Command. A new pipe should be arranged to connect the parent and child processes. With stdout: use std::process::{Command, Stdio}; l

docs.rs

https://stackoverflow.com/questions/19076719/how-do-i-convert-a-vector-of-bytes-u8-to-a-string

 

How do I convert a Vector of bytes (u8) to a string?

I am trying to write simple TCP/IP client in Rust and I need to print out the buffer I got from the server. How do I convert a Vec<u8> (or a &[u8]) to a String?

stackoverflow.com

https://rust-lang-nursery.github.io/rust-cookbook/os/external.html

 

External Command - Rust Cookbook

Runs git log --oneline as an external Command and inspects its Output using Regex to get the hash and message of the last 5 commits. use error_chain::error_chain; use std::process::Command; use regex::Regex; error_chain!{ foreign_links { Io(std::io::Error)

rust-lang-nursery.github.io

https://doc.rust-lang.org/std/process/struct.Stdio.html

 

Stdio in std::process - Rust

Calls U::from(self). That is, this conversion is whatever the implementation of From for U chooses to do.

doc.rust-lang.org

https://m.blog.naver.com/kore2758_/221259202488

 

[Linux] GCC를 이용한 컴파일

GCC(GNU Compiler Collection) GCC는 이름 그대로 GNU 프로젝트의 오픈 소스 컴파일러 컬렉...

blog.naver.com

https://www.daleseo.com/dockerfile/

 

Dockerfile에서 자주 쓰이는 명령어

Engineering Blog by Dale Seo

www.daleseo.com

https://www.daleseo.com/dockerfile/

 

Dockerfile에서 자주 쓰이는 명령어

Engineering Blog by Dale Seo

www.daleseo.com

https://int-i.github.io/rust/2021-10-03/docker-rust/

 

Docker로 Rust 앱 배포 (Feat. cargo-chef) - 인하대학교 인트아이

Rust 프로젝트를 Docker로 배포할 때 발생할 수 있는 문제점과 해결방안을 모아놓은 문서입니다. 기본적인 프로젝트 빌드 Dockerfile 생성부터 빌드 캐시를 최대한...

int-i.github.io

https://medium.com/@saschagrunert/demystifying-containers-part-i-kernel-space-2c53d6979504

 

반응형