何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『 Linuxプログラミングインタフェース』を気合いで読む会 第1回:ファイルIO前半戦

転回

Unix/Linuxプログラミング 理論と実践』をダシにしてRustのお勉強、システムプログラミングよりのPythonのお勉強をしようと思い立ったものの、『Unix/Linuxプログラミング 理論と実践』の who コマンド実装でmacOSとDocker上のUbuntu Linuxの違いで疲れ果ててしまった。 また、いい本だと聞いて積ん読になっていた『 Linuxプログラミングインタフェース』には who参照元であった utmpx 構造体について詳しく書いてあるなどうれしいことが多かったので、転回して『 Linuxプログラミングインタフェース』を読もうと思い立った。

概要

4章からシステムコールAPIの説明が始まり、最初はファイルIOから説明される。 UNIXシステムではデバイスもファイルの一種として扱うことができるので、ファイルの取り扱いは重要かつ基礎となる。 ファイルを開くとそのファイルにファイルディスクリプタを割り当てる。 ファイルIOで重要になるのが4つのシステムコールopenread, write, closeである。

最初は概観を掴むために、cpコマンドの簡易版を実装する。

Python

Pythonでは通常、ファイルを開く際は組み込み関数open()を使うが、osモジュールを駆使してシステムコールopenread, write, closeを使ったファイルオープンおよび処理もできる。ファイルモードに関してはstatモジュールも援用する。

import argparse
import os
import stat
import sys

BUF_SIZE = 1024


def copy_file(src, dst):
    try:
        src_fd = os.open(src, os.O_RDONLY)
    except FileNotFoundError:
        print(f"No such file: {src}.")
        return os.EX_NOINPUT
    except PermissionError:
        print(f"input {src} can not open.")
        return os.EX_NOPERM

    dst_flag = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
    dst_mode = (
        stat.S_IRUSR
        | stat.S_IWUSR
        | stat.S_IRGRP
        | stat.S_IWGRP
        | stat.S_IROTH
        | stat.S_IWOTH
    )
    try:
        dst_fd = os.open(dst, dst_flag, dst_mode)
    except OSError:
        print(f"output {dst} can not open.")
        return os.EX_CANTCREAT

    while True:
        read_bytes = os.read(src_fd, BUF_SIZE)
        if len(read_bytes) == 0:
            break
        written_bytes_len = os.write(dst_fd, read_bytes)
        if len(read_bytes) != written_bytes_len:
            print("could not write whole buffer.")
            return os.EX_IOERR
    try:
        os.close(src_fd)
    except OSError:
        print("cloud not close input file.")
        return os.EX_IOERR
    try:
        os.close(dst_fd)
    except OSError:
        print("cloud not close output file.")
        return os.EX_IOERR

    return os.EX_OK


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source_file")
    parser.add_argument("target_file")
    args = parser.parse_args()
    ret_code = copy_file(args.source_file, args.target_file)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

よりPythonらしくかこうと思うならば、以下の通り。 C言語でファイルを扱う際に、システムコールopenの代わりにfopenを使うのが自然であるのと同様に、Pythonでファイルを扱う際はos.openではなくopenを使うのが自然である。 また、with文を使えば自動的にファイルを閉じることができるので記述量が減り、かつ安心して書くことができる。

import argparse
import os
import sys
import stat


BUF_SIZE = 1024


def copy_file(src, dst):
    with open(src, "rb") as src_f, open(dst, "wb") as dst_f:
        while True:
            read_bytes = src_f.read(BUF_SIZE)
            if len(read_bytes) == 0:
                break
            try:
                written_bytes_len = dst_f.write(read_bytes)
            except OSError:
                print("could not write whole buffer.")
                return os.EX_IOERR
            if len(read_bytes) != written_bytes_len:
                print("could not write whole buffer.")
                return os.EX_IOERR
    try:
        dst_mode = (
            stat.S_IRUSR
            | stat.S_IWUSR
            | stat.S_IRGRP
            | stat.S_IWGRP
            | stat.S_IROTH
            | stat.S_IWOTH
        )
        os.chmod(dst, dst_mode)
    except OSError:
        print("could not change mode.")
        return os.EX_IOERR

    return os.EX_OK


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("source_file")
    parser.add_argument("target_file")
    args = parser.parse_args()
    ret_code = copy_file(args.source_file, args.target_file)
    sys.exit(ret_code)


if __name__ == "__main__":
    main()

Rust

RustのIO周りはRead, BufRead, Writeという3つのトレイトが基本となる。 closeに関しては多くのreader, writerが自動で閉じるように実装されているらしい。

以下の実装は『プログラミングRust』P.420の実装を大いに参考にしている。

use std::io::Write;

const BUF_SIZE: usize = 8 * 1024;

fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> std::io::Result<u64>
where
    R: std::io::Read,
    W: std::io::Write,
{
    let mut buf = [0; BUF_SIZE];
    let mut written = 0;
    loop {
        let len = match reader.read(&mut buf) {
            Ok(0) => return Ok(written),
            Ok(len) => len,
            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        };
        writer.write_all(&buf[..len])?;
        written += len as u64;
    }
}

fn main() -> std::io::Result<()> {
    let mut _args = Vec::new();

    for arg in std::env::args().skip(1) {
        _args.push(arg);
    }

    if _args.len() != 2 {
        writeln!(std::io::stderr(), "Usage: copy source_file target_file").unwrap();
    } else {
        // let source_file_path = Path::new(&_args[0]);
        // let target_file_path = Path::new(&_args[1]);
        // std::fs::copy(source_file_path, target_file_path)?;
        let mut source_file = std::fs::File::open(&_args[0])?;
        let mut target_file = std::fs::File::create(&_args[1])?;
        copy(&mut source_file, &mut target_file)?;
    }

    Ok(())
}

なお、コメントにある通り、ファイルをコピーするならばstd::fs::copyを使うのが普通のようである。