何かを書き留める何か

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

『Unix/Linuxプログラミング 理論と実践』をダシにしてRustのお勉強をする会

些細なる第一歩

プログラミング言語Rustの勉強をしたいと常日頃考えていたが、中々手を出せずにいた。 ずっと仕事ではPythonを使っているが、それしかできないのは流石に幅が狭かろう、と。 少しだけ『The Rust Programming Language 2nd edition』を読んでみたが、最初の章のサンプル止まりで仕事が忙しくなって放ったらかしにしていた。 気づくとその邦訳も発売されてしまった。 『プログラミングRust』の冒頭に、何かプログラミングしながら学びましょうという旨の記述があり、それならばずっと積ん読になっていた『Unix/Linuxプログラミング 理論と実践』をダシにしてRustのお勉強、システムプログラミングよりのPythonのお勉強をしようと思い立った。このエントリはその1回目である。

第1章 Unixシステムプログラミングの全体像

Unix/Linuxプログラミング 理論と実践』の中身を詳しく書くと著作権的にアレになるので、この本の大まかな流れのみにする。

  1. 実際のプログラムを観察する
  2. 使われているシステムコールを使う
  3. 自分で書いてみる

第1章ではmoreコマンドの実装が取り上げられている。 まず、標準入力のみを使ったバージョンから。

Python版は、C言語版をそのまま翻訳したものである。

import sys
import typing

PAGELEN = 24


def main():
    if len(sys.argv[1:]) == 0:
        do_more(sys.stdin)
    else:
        for f in sys.argv[1:]:
            with open(f) as in_f:
                do_more(in_f)


def do_more(f: typing.TextIO):
    """
    PAGELEN行読み込み、ユーザーの入力を待つ。
    """

    num_of_lines: int = 0

    for line in f:
        if num_of_lines == PAGELEN:
            reply: int = see_more()
            if reply == 0:
                break
            num_of_lines -= reply
        print(line, end="")
        num_of_lines += 1


def see_more() -> int:
    """
    メッセージを出力して応答を待ち、入力値に応じて進める行数を返す。

    qならば終了、スペースならば次のページ、エンターならば次の行分進める。
    """
    print("\033[7m more? \033[m")
    c: str = input()
    if c == "q":
        return 0
    if c == " ":
        return PAGELEN
    if c == "":
        return 1
    return 0


if __name__ == "__main__":
    main()

次に、Rust版。

use std::io::BufRead;

const PAGELEN: i32 = 24;

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

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

    if _args.len() == 0 {
        let stdin = std::io::stdin();
        do_more(stdin.lock())?;
    } else {
        for arg in &_args {
            let file = std::fs::File::open(arg)?;
            let buffered_reader = std::io::BufReader::new(file);
            do_more(buffered_reader)?;
        }
    }
    Ok(())
}

fn do_more<R>(reader: R) -> std::io::Result<()>
where
    R: BufRead,
{
    let mut num_of_lines = 0;
    for line_result in reader.lines() {
        let line = line_result?;
        if num_of_lines == PAGELEN {
            let reply = see_more();
            if reply == 0 {
                break;
            }
            num_of_lines -= reply
        }
        println!("{}", line);
        num_of_lines += 1;
    }
    Ok(())
}

fn see_more() -> i32 {
    println!("more?");
    let mut c = String::new();
    std::io::stdin().read_line(&mut c).ok().expect("Failed");
    if c == "q\n" {
        return 0;
    }
    if c == " \n" {
        return PAGELEN;
    }
    if c == "\n" {
        return 1;
    }
    return 0;
}

これらの実装は、標準入力しか使っていないので、キーボード入力と標準入力の区別がついていない。 そこで/dev/tty経由でキーボードの入力を取得する。

Python版では標準モジュールtermiosttyを使う。 以下の記事を大いに参考にした。 qiita.com

import sys
import termios
import tty
import typing

PAGELEN = 24


def main():
    if len(sys.argv[1:]) == 0:
        do_more(sys.stdin)
    else:
        for f in sys.argv[1:]:
            with open(f) as in_f:
                do_more(in_f)


def do_more(f: typing.TextIO):
    """
    PAGELEN行読み込み、ユーザーの入力を待つ。
    """

    num_of_lines: int = 0

    for line in f:
        if num_of_lines == PAGELEN:
            reply: int = see_more()
            if reply == 0:
                break
            num_of_lines -= reply
        print(line, end="")
        num_of_lines += 1


def see_more() -> int:
    """
    メッセージを出力して応答を待ち、入力値に応じて進める行数を返す。

    qならば終了、スペースならば次のページ、エンターならば次の行分進める。
    """
    print("\033[7m more? \033[m")
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    try:
        tty.setcbreak(sys.stdin.fileno())
        c = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSANOW, old)
    if c == "q":
        return 0
    if c == " ":
        return PAGELEN
    if c == "\n":
        return 1
    return 0


if __name__ == "__main__":
    main()

次に、Rust版。以下のcrateを使った。

dcuddeback.github.io

tty周りの設定はPythonのttyモジュールの設定を大いに参考にした。

use std::io::BufRead;
use std::os::unix::prelude::*;
use termios::*;

const PAGELEN: i32 = 24;

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

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

    if _args.len() == 0 {
        let stdin = std::io::stdin();
        do_more(stdin.lock())?;
    } else {
        for arg in &_args {
            let file = std::fs::File::open(arg)?;
            let buffered_reader = std::io::BufReader::new(file);
            do_more(buffered_reader)?;
        }
    }
    Ok(())
}

fn do_more<R>(reader: R) -> std::io::Result<()>
where
    R: BufRead,
{
    let mut num_of_lines = 0;
    for line_result in reader.lines() {
        let line = line_result?;
        if num_of_lines == PAGELEN {
            let reply = see_more();
            if reply == 0 {
                break;
            }
            num_of_lines -= reply
        }
        println!("{}", line);
        num_of_lines += 1;
    }
    Ok(())
}

fn see_more() -> i32 {
    println!("more?");
    let stdin = std::io::stdin();
    let stdin_fd = stdin.as_raw_fd();
    let mut old_termios = Termios::from_fd(stdin_fd).unwrap();
    match tcgetattr(stdin_fd, &mut old_termios) {
        Ok(termios) => termios,
        Err(_error) => panic!("I Cannot open terminal attributes!!!"),
    };

    let mut new_termios = Termios::from_fd(stdin_fd).unwrap();
    new_termios.c_lflag = new_termios.c_lflag & !(ECHO | ICANON);
    new_termios.c_cc[VMIN] = 1;
    new_termios.c_cc[VTIME] = 0;
    match tcsetattr(stdin_fd, TCSAFLUSH, &mut new_termios) {
        Ok(termios) => termios,
        Err(_error) => panic!("I Cannot set terminal attributes!!!"),
    };

    let mut c = String::new();
    std::io::stdin()
        .read_line(&mut c)
        .ok()
        .expect("I Cannot read from stdin!!!");
    match tcsetattr(stdin_fd, TCSAFLUSH, &mut old_termios) {
        Ok(termios) => termios,
        Err(_error) => panic!("I Cannot set terminal attributes!!!"),
    };

    if c == "q\n" {
        return 0;
    }
    if c == " \n" {
        return PAGELEN;
    }
    if c == "\n" {
        return 1;
    }
    return 0;
}

/dev/tty経由でキーボードの入力を取得する。」と書いたが、実は嘘である。 C言語版では/dev/ttyfopenで開いているが、上記のPythonやRustではtermiosの値を変更しているので意味合いが違う。

感想

  • システムプログラミング感はないが、第2章でfopenではないopenシステムコールが登場する予定。
  • Python版は比較的スラスラかけたが、tty周りは厳しかった。
  • Rustは非常に難儀した。まず、Result型の処理の仕方が甘い。

これからも頑張っていきたい(小並感)。