LoginSignup
8
6

More than 5 years have passed since last update.

Rustでwho(FFIとlibcを試してみる)

Last updated at Posted at 2017-01-22

目的

RustでFFIを試してみたかったので、少ないコード量で実装する

このページのスコープ

スコープ対象内

  • RustでFFIを使う
  • libcを使う

スコープ対象外

  • 効率が良かったり、実行時間が短いコードを書く
  • エラーハンドリングなどをしっかり実装した堅牢なコードを書く

題材

whoコマンド

環境

ubuntu16.04(環境が違うとコードも変わるので注意)

仕様

手元の環境ではwhoをそのまま使うとこんな感じの出力がされる

% who
bonchi     tty7         2017-01-22 12:32 (:0)
bonchi     pts/20       2017-01-22 12:36 (tmux(4371).%0)

実装

C実装

  • whoコマンドはutmpファイルを読み込んで、その中身を表示している
  • utmpファイルはutmpエントリの構造体の連続になっている
  • utmpエントリはutmp.hファイルでstruct utmpとして定義されている
  • utmp.h/usr/include/utmp.hにある

/usr/include/utmp.hを覗くと、以下のようになっている

/usr/include/utmp.h
/* Get system dependent values and data structures.  */
#include <bits/utmp.h> 

ので、今度は/usr/include/x86_64-linux-gnu/bits/utmp.hを見てみる

/usr/include/x86_64-linux-gnu/bits/utmp.h
/* The structure describing an entry in the user accounting database.  */
#define UT_LINESIZE     32 
#define UT_NAMESIZE     32 
#define UT_HOSTSIZE     256

struct utmp
{
  short int ut_type;            /* Type of login.  */
  pid_t ut_pid;                 /* Process ID of login process.  */
  char ut_line[UT_LINESIZE];    /* Devicename.  */
  char ut_id[4];                /* Inittab ID.  */
  char ut_user[UT_NAMESIZE];    /* Username.  */
  char ut_host[UT_HOSTSIZE];    /* Hostname for remote login.  */
  struct exit_status ut_exit;   /* Exit status of a process marked as DEAD_PROCESS.  */
/* The ut_session and ut_tv fields must be the same size when compiled 32- and 64-bit.  This allows data files and shared memory to be shared between 32- and 64-bit applications.  */
  #ifdef __WORDSIZE_TIME64_COMPAT32
    int32_t ut_session;           /* Session ID, used for windowing.  */
    struct
    {
      int32_t tv_sec;             /* Seconds.  */
      int32_t tv_usec;            /* Microseconds.  */
    } ut_tv;                      /* Time entry was made.  */
  #else
    long int ut_session;          /* Session ID, used for windowing.  */
    struct timeval ut_tv;         /* Time entry was made.  */
  #endif

  int32_t ut_addr_v6[4];        /* Internet address of remote host.  */
  char __glibc_reserved[20];            /* Reserved for future use.  */
};

/* 略 */

/* The structure describing the status of a terminated process.  This type is used in `struct utmp' below.  */
struct exit_status
{
  short int e_termination;    /* Process termination status.  */
  short int e_exit;           /* Process exit status.  */
};

私の環境では__WORDSIZE_TIME64_COMPAT32true
一番単純なC実装はutmpファイルからutmpエントリをひとつずつ読み込んでそこから必要な情報を出力してやればよい

simple_who.c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <utmp.h>

int main() {
  struct utmp current_record;
  int utmpfd;
  int reclen = sizeof(current_record);

  if ((utmpfd = open(UTMP_FILE, O_RDONLY)) == -1) {
    exit(1);
  }

  while (read(utmpfd, &current_record, reclen) == reclen)
    /* current_recordから必要な情報を出力 */
    close(utmpfd);
    return 0;
  }
}            

Rust実装

Rustでwhoを実装するにもC互換なstruct utmpとそれに関連する他のstruct(ut_tv, exit_status)と、定数を定義してutmpファイルからutmpエントリをひとつずつ読み込んでそこから必要な情報を出力してやれば良い

定数

rust_who.rs
pub const UT_LINESIZE: usize = 32;
pub const UT_NAMESIZE: usize = 32;
pub const UT_HOSTSIZE: usize = 256;
static UTMP_FILE_PATH: &'static str = "/var/run/utmp";
  • UTMP_FILE_PATHはutmpファイルの場所を表しており、/usr/include/paths.h_PATH_UTMPとして定義されていた

struct

rust_who.rs
extern crate libc;
use libc::{c_short, pid_t, c_char, int32_t, c_int, size_t, c_void};

#[repr(C)]
pub struct exit_status {
    pub e_termination: c_short,
    pub e_exit: c_short,
}

#[repr(C)]
pub struct ut_tv {
    pub tv_sec: int32_t,
    pub tv_usec: int32_t,
}

#[repr(C)]
pub struct utmp {
    pub ut_type: c_short,
    pub ut_pid: pid_t,
    pub ut_line: [c_char; UT_LINESIZE],
    pub ut_id: [c_char; 4],
    pub ut_user: [c_char; UT_NAMESIZE],
    pub ut_host: [c_char; UT_HOSTSIZE],
    pub ut_exit: exit_status,
    pub ut_session: int32_t,
    pub ut_tv: ut_tv,
    pub ut_addr_v6: [int32_t; 4],
    pub __glibc_reserved: [c_char; 20],
}

impl Default for exit_status {
    fn default() -> exit_status {
        exit_status {
            e_termination: 0,
            e_exit: 0,
        }
    }
}

impl Default for ut_tv {
    fn default() -> ut_tv {
        ut_tv {
            tv_sec: 0,
            tv_usec: 0,
        }
    }
}

impl Default for utmp {
    fn default() -> utmp {
        utmp {
            ut_type: 0,
            ut_pid: 0,
            ut_line: [0; UT_LINESIZE],
            ut_id: [0; 4],
            ut_user: [0; UT_NAMESIZE],
            ut_host: [0; UT_HOSTSIZE],
            ut_exit: Default::default(),
            ut_session: 0,
            ut_tv: Default::default(),
            ut_addr_v6: [0; 4],
            __glibc_reserved: [0; 20],
        }
    }
}

関数

rust_who.rs
extern "C" {
    pub fn read(fd: c_int, buf: *mut c_void, count: size_t) -> usize;
}

fn main() {
    let utmp_file = File::open(UTMP_FILE_PATH).unwrap();
    let mut utmp_struct: utmp = Default::default();
    let buffer: *mut c_void = &mut utmp_struct as *mut _ as *mut c_void;
    unsafe {
        while read(utmp_file.as_raw_fd(), buffer, mem::size_of::<utmp>()) != 0 {
            show_info(&utmp_struct);
        }
    }
}

fn show_info(utmp_struct: &utmp) {
    print!("{} ",
           String::from_utf8((utmp_struct.ut_user.iter().map(|&x| x as u8).collect())).unwrap());
    print!("{} ",
           String::from_utf8((utmp_struct.ut_line.iter().map(|&x| x as u8).collect())).unwrap());
    print!("{} ", utmp_struct.ut_tv.tv_sec);
    print!("({}) ",
           String::from_utf8((utmp_struct.ut_host.iter().map(|&x| x as u8).collect())).unwrap());
    println!("");
}
  • extern "C"ブロックで囲むことでCのread関数を呼べるようにしている
    • https://doc.rust-lang.org/book/ffi.html#foreign-calling-conventions
    • 呼び出し時はC関数の呼び出しなのでunsafeブロックで囲っている
    • Cのread関数では第一引数にファイルディスクリプタを渡す
    • Cのread関数では第二引数にvoidを渡す
      • voidを渡すところはlibcで定義されているc_voidを渡す
      • 読み取った情報をstruct utmpに入れて欲しいので、struct utmpのポインタを渡したいstruct utmp -> c_voidへの変換はここを参考にした
    • rustは変数の宣言時に初期化もしなくてはならないので、先ほど実装したDefault trateを使っている
  • show_info関数はstruct utmpに埋め込まれた情報からwhoに必要なものだけを出力している
    • struct utmpに埋め込まれた各メンバーの値を単純に見ても単なる数字の配列にしか見えない

結果

% cargo run
reboot ~ 1485055829 (4.4.0-59-generic) 
runlevel ~ 1485055839 (4.4.0-59-generic) 
LOGIN tty1 1485055839 () 
bonchi tty7 1485055935 (:0) 
bonchi pts/20 1485056209 (tmux(4371).%0) 

それっぽく出力された。
しかし、余計な行がいくつか増えているのと、ログイン時刻がUNIX timeになっている
余計な行はstruct utmpのメンバーのut_typeを見てut_tipeUSER_PROCESSでないものを飛ばせば良い。UNIX timeは適当に変換すれば良い
ソースはこちら
https://github.com/bon-chi/rust_who

備考

このソースは特定の環境でしか動作しない(Ubuntu16.04)
というのもutmp.hは端末によって中身が違うからだ。例えばOS Xのutmp.hを覗いてみると

usr/include/utmp.h
/*
 * This header file is DEPRECATED and only provided for compatibility
 * with previous releases of Mac OS X.  Use of these structures, especially
 * in 64-bit computing, may corrupt the utmp, wtmp and lastlog files.
 *
 * Use the utmpx APIs instead.
 */

/*略*/

struct utmp {
        char    ut_line[UT_LINESIZE];
        char    ut_name[UT_NAMESIZE];
        char    ut_host[UT_HOSTSIZE];
        long    ut_time;
}

となっているし、実際に先ほどのコードを実行すると、

00 0 (
)
  0 ()
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: FromUtf8Error { bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 213, 64, 103, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], error: Utf8Error { valid_up_to: 84 } }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.

と出力される

参考

http://siciarz.net/ffi-rust-writing-bindings-libcpuid/
http://stackoverflow.com/questions/24191249/working-with-c-void-in-an-ffi#comment37351252_24191977
https://doc.rust-lang.org/nomicon/other-reprs.html#reprc
https://doc.rust-lang.org/book/ffi.html

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6