3.5. io:: - Reader / Writer / 표준 입출력
rok::std::io 모듈은 byte stream 추상화와 더불어 표준 입출력 (stdin/stdout/stderr) 을 함께 제공합니다. 별도의 AsyncRead/AsyncWrite 트레잇은 두지 않습니다. 이 모듈의 API 는 호출한 OS 스레드를 막습니다. stackless 코루틴은 보통 함수 안에서 중단할 수 없으므로 sync 호출이 task 안에서 저절로 async 가 되지는 않으며, task 만 중단하는 I/O 는 타입별 *_async 메서드로 둡니다 (소켓 3.2.0, 파일 3.1.4a).
3.5.0. IoError
struct IoError {
kind: IoErrorKind;
code: i32; // OS errno
message: str; // OS 메시지 (strerror)
}
enum IoErrorKind {
Other, // 그 밖의 errno
NotFound, // ENOENT
PermissionDenied, // EACCES, EPERM
AlreadyExists, // EEXIST
NotADirectory, // ENOTDIR
IsADirectory, // EISDIR
DirectoryNotEmpty, // ENOTEMPTY
InvalidInput, // EINVAL
BrokenPipe, // EPIPE
Interrupted, // EINTR
WouldBlock, // EAGAIN
TimedOut, // ETIMEDOUT, 시간 제한
StorageFull, // ENOSPC
CrossesDevices, // EXDEV
Unsupported, // ENOTSUP, ENOSYS
UnexpectedEof, // 기대한 바이트 전에 끝남 (read_exact 등)
InvalidData, // 데이터가 형식에 맞지 않음 (UTF-8 등)
}
kind 는 errno 에서 정해지며 (UnexpectedEof / InvalidData 는 라이브러리가 정함), 원래 값은 code 에, OS 가 주는 설명은 message 에 남습니다.
3.5.1. AsBytes
trait AsBytes { fn byte_slice(self) -> Slice<u8>; } // UTF-8 일 필요 없음
impl AsBytes for str { .. }
impl AsBytes for List<u8> { .. }
impl AsBytes for Slice<u8> { .. }
impl<const N: usize> AsBytes for [u8; N] { .. } // 사본의 view (b"...", include_bytes!)
쓰기 함수 (fs, net, Writer) 의 데이터 인자는 AsBytes 를 받습니다. AsBytes 는 rok::core 에 (Slice 옆, rok::core::collections::slice) 정의되며 rok::std::io::AsBytes 로 다시 내보냅니다.
3.5.1a. Reader / Writer 트레잇
trait Reader {
fn read(ref self, buf: List<u8>) throws (IoError) -> usize; // buf[0..n] 을 채움, 0 = 끝
// default 메서드
fn read_exact(ref self, buf: List<u8>) throws (IoError); // buf 전체, 모자라면 UnexpectedEof
fn read_to_end(ref self) throws (IoError) -> List<u8>;
fn read_to_str(ref self) throws (IoError) -> str; // UTF-8 검증
fn read_line(ref self) throws (IoError) -> str; // `\n` 포함, 끝이면 ""
fn lines(ref self) -> Lines<Self>; // Iterator<Item = Result<str, IoError>> 구현
fn bytes(ref self) -> Bytes<Self>; // Iterator<Item = Result<u8, IoError>> 구현
fn raw_fd(self) -> RawFd; // io::copy 가 커널에 넘길 수 있는 OS descriptor; 없거나, stream 이 buffer / timeout / non-blocking 이면 -1 (기본값)
}
trait Writer {
fn write(ref self, buf: List<u8>) throws (IoError) -> usize;
fn flush(ref self) throws (IoError);
// default
fn write_all(ref self, buf: List<u8>) throws (IoError);
fn write_str(ref self, s: str) throws (IoError);
fn write_fmt(ref self, s: str) throws (IoError); // f-string 결과를 씀
fn raw_fd(self) -> RawFd; // io::copy 가 커널에 넘길 수 있는 OS descriptor; 없거나, stream 이 buffer / timeout / non-blocking 이면 -1 (기본값)
}
trait Seek {
fn seek(ref self, pos: SeekFrom) throws (IoError) -> u64;
// default
fn rewind(ref self) throws (IoError); // seek(Start(0))
fn stream_position(ref self) throws (IoError) -> u64; // seek(Current(0))
}
바이트 buffer 는 Slice<u8> 가 생기기 전까지 List<u8> 로 주고받습니다. read 는 미리 길이를 잡아 둔 buf 를 채웁니다 (List 는 handle 이므로 호출자가 결과를 봅니다). 트레잇 메서드는 ref self 를 받고, 핸들 타입의 고유 메서드는 self 를 받습니다.
이 세 트레잇은 File, TcpStream, UnixStream, 그리고 stdin/out/err 모두에서 일관되게 구현되어 있어, 동일한 코드로 다양한 stream 을 다룰 수 있게 합니다.
3.5.2. 표준 입출력
다음으로, 프로세스의 가장 흔한 I/O 통로인 표준 입출력을 들여다봅니다.
fn stdin() -> Stdin;
fn stdout() -> Stdout;
fn stderr() -> Stderr;
struct Stdin { /* read line / bytes / etc. */ }
struct Stdout { /* tty 면 line-buffered, pipe 면 block-buffered */ }
struct Stderr { /* unbuffered */ }
impl Reader for Stdin { ... }
impl Writer for Stdout { ... }
impl Writer for Stderr { ... }
impl Stdin {
fn lock(self) -> StdinLock; // exclusive lock - 멀티스레드 안전
fn read_line(self) throws (IoError) -> str;
fn lines(self) -> Lines<Stdin>; // Iterator<Item = Result<str, IoError>> 구현
}
stdout() 은 print / println 과 같은 런타임 buffer 를 공유하므로 둘을 섞어 써도 출력 순서가 유지됩니다. 이 buffer 는 프로세스가 끝날 때, flush() 를 부를 때, buffer 가 찰 때 비웁니다. stderr 는 buffer 없이 바로 씁니다. stdin() 도 하나의 공유 buffer 를 씁니다.
stdout/stderr 에서 자주 쓰이는 print / println / eprint / eprintln 은 rok::core::print::* 에 자리 잡고 있으므로, 묵시 use rok::core::**; 를 통해 별도 import 없이 자동으로 노출됩니다.
3.5.3. BufReader / BufWriter / LineWriter
원시 stream 을 그대로 쓰면 system call 비용이 누적되기 쉽기에, 표준 라이브러리는 다음과 같은 buffering wrapper 를 함께 제공합니다.
struct BufReader<R: Reader> { /* internal buffer */ }
struct BufWriter<W: Writer> { /* internal buffer */ }
struct LineWriter<W: Writer> { /* line-buffered */ }
impl<R: Reader> BufReader<R> {
fn new(inner: R) -> BufReader<R>;
fn with_capacity(cap: usize, inner: R) -> BufReader<R>;
fn into_inner(self) -> R;
}
impl<R: Reader> Reader for BufReader<R> { ... }
impl<W: Writer> BufWriter<W> {
fn new(inner: W) -> BufWriter<W>;
fn with_capacity(cap: usize, inner: W) -> BufWriter<W>;
fn into_inner(self) throws (IoError) -> W; // 남은 buffer 를 flush 한 뒤 돌려줌
}
impl<W: Writer> Writer for BufWriter<W> { ... }
impl<W: Writer> LineWriter<W> { // \n 이 든 write 마다 flush
fn new(inner: W) -> LineWriter<W>;
}
impl<W: Writer> Writer for LineWriter<W> { ... }
BufWriter 는 Drop 을 구현하므로 using 블록을 빠져나갈 때 남은 buffer 를 flush 합니다. 이때의 에러는 무시되므로, 에러를 확인하려면 빠져나가기 전에 flush() 를 직접 부릅니다. LineWriter 는 \n 을 포함한 write 마다 flush 합니다.
요컨대 작은 read 가 잦은 상황에서 system call 횟수를 줄이는 것이 핵심 효용입니다.
3.5.4. copy - io::copy(reader, writer)
stream 간 단순 복사는 별도 함수로 따로 묶어 두었습니다.
fn copy<R: Reader, W: Writer>(ref reader: R, ref writer: W) throws (IoError) -> u64;
내부적으로는 가능한 자리에서 splice / sendfile / copy_file_range 를 활용 하지만, 사용자 코드 입장에서는 단순한 호출 한 번이면 충분합니다. descriptor 두 개 (raw_fd) 가 주어지면 커널 경로를 descriptor 종류에 따라 고르며, 그렇지 않으면 read / write 를 씁니다.
3.5.5. 사용 예
지금까지의 조각들을 조합한 전형적인 사용 모습은 다음과 같습니다.
use rok::std::io::{stdin, stdout, BufReader, BufWriter};
// 줄 단위 처리
let stdin = stdin().lock();
for line in stdin.lines() {
let line = try line.get(); // 반복자 원소는 Result - get() 으로 에러 흐름 복귀
process(line);
}
// 버퍼 쓰기
let f = try fs::create("output.txt");
let buf = BufWriter::new(f);
for x in xs {
try buf.write_str(f"{x}\n");
}
try buf.flush(); // 명시 flush - 해제 시점의 silent fail 회피
// 복사
let src = try fs::open("a.txt");
let dst = try fs::create("b.txt");
try io::copy(ref src, ref dst);