目的
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
を覗くと、以下のようになっている
/* Get system dependent values and data structures. */
#include <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_COMPAT32
はtrue
一番単純なC実装はutmpファイルからutmpエントリをひとつずつ読み込んでそこから必要な情報を出力してやればよい
#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, ¤t_record, reclen) == reclen)
/* current_recordから必要な情報を出力 */
close(utmpfd);
return 0;
}
}
Rust実装
Rustでwhoを実装するにもC互換なstruct utmp
とそれに関連する他のstruct(ut_tv
, exit_status
)と、定数を定義してutmpファイルからutmpエントリをひとつずつ読み込んでそこから必要な情報を出力してやれば良い
定数
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
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],
}
}
}
- Cのstructを
#[repr(C)]
で宣言している - structのメンバーはC互換の型にしたいのでlibc crateを使っている
-
Defalut
trateの実装は必須ではないのですが、rustでは変数の宣言時に初期化をする必要があり、いちいち値を入れるのが面倒なので簡単に0で初期化できるようにしておく
関数
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_tipe
がUSER_PROCESS
でないものを飛ばせば良い。UNIX timeは適当に変換すれば良い
ソースはこちら
https://github.com/bon-chi/rust_who
備考
このソースは特定の環境でしか動作しない(Ubuntu16.04)
というのもutmp.h
は端末によって中身が違うからだ。例えばOS Xの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