Rustで並行処理に便利なバッファーを実装してみた(後編)。
先日書いた以下の記事の続きです。
Rustで並行処理に便利なバッファーを実装してみた(前編)。 - Ri for (Real Estate|Residence) Influencers
Drop traitとSend traitの組み合わせで、並行処理におけるバッファーのメモリの管理がとても簡単にできるというところまで紹介しましたので、次はバッファーにおけるメソッドをいくつか実装してみようと思います。まずはloadメソッドを紹介します。このメソッドはバッファーの中身のリファレンスを返します。実装は以下のとおりです。
99 macro_rules! impl_buffer { 100 101 ($name:ident, io::$iot:ident) => { 102 103 #[allow(dead_code)] 104 impl<T: io::$iot> $name<T> { 105 .... 113 114 pub unsafe fn load(&mut self) -> &mut [u8] { 115 let ptr = self.inner.load(Ordering::Relaxed); 116 slice::from_raw_parts_mut(ptr, self.size()) 117 } 118 }
実はこの実装を見て、loadメソッドで返すreferenceが何故mutableになっているかが自分でもよく思い出せないのですが(直すの面倒なので今のところ放置..)、ポインターではなくリファレンスを返していることには理由があります。それに言及するにはRust言語の特徴のひとつである、'Lifetime'について述べる必要がありそうです。
Lifetimeを理解するにはまず、(Raw)ポインターとリファレンスがどうちがうかを理解する必要があります。メモリデータの所有権にかかわる話なのですが、説明するのが少し億劫なので以下リンクを参照してください。
https://doc.rust-lang.org/beta/book/references-and-borrowing.html
まあLifetimeをひとことで言うと、あるメモリのリファレンスがどのブロックの中で使えるかを管理するための機能です。例えば、上述のload関数は暗黙的にlifetimeが付加されて、呼び出されたブロックの中でしか返り値となるリファレンスを使うことができません。このリファレンスが(Raw)ポインターだったと仮定してみましょう。(Raw)ポインターとはすなわちc言語でのポインターを指すので、OwnershipとかBorrowingとか関係ないので、呼び出したブロックから外れてもデータを参照することができてしまうことがあります。うっかり変なグローバル変数にポインターを格納してしまった場合、そのメモリが開放されてしまった後もそのデータにアクセスしてしまうことになり、もしその変数が外部から操作できるものであったとしたらプログラムにおける重大な脆弱性となる危険が生まれてしまいます。私はあまり自分のコーディング力を信用していないので、そのチェックをコンパイラーに投げてしまえるのは実はとてもありがたいと思っています。
次にInput用とOutput用のバッファを実装したので、read関数とwrite関数をそれぞれ実装しています。まず以下にてread関数について紹介します。
141 impl<T: io::Read> ReadBuffer<T> { 142 ... 151 152 pub fn read(&mut self, input: &mut T) -> io::Result<usize> { 153 let buf = unsafe { self.load() }; 154 input.read(buf) 155 } 156 }
Read traitについては以下を参照してください。
std::io::Read - Rust
色々な用途に使えるように、Read traitのメソッドを実装したものであればソースから読み込むことができるようにジェネリッククラスにしています。想定されるストリームとしてはファイルやソケット、あとはAlsaなんかだとCapture用途にセットされたPCMデバイスとかのインプットでしょうか。
次にOutput用途のwrite関数です。
160 impl<T: io::Write> WriteBuffer<T> { 161 .... 180 181 fn write(&mut self, output: &mut T) -> io::Result<usize> { 182 let buf = unsafe { self.load() }; 183 output.write(buf) 184 } 185 }
Write traitについてもRead traitと同様です。詳細は以下のリンクを参照してください。
std::io::Write - Rust
ちなみに両方の関数で返り値の型として定義されているResult型は、HaskellでいうEitherモナドみたいな機能を持っています。処理の失敗時のハンドリングができる便利な機能です。まあ、一般的な関数型プログラミングのような記述ができる言語は大概このような機能をもっているので、説明は割愛します。
最後に簡単なテストを書いたので載せておきます。
232 #[cfg(test)] 233 mod test { 234 235 use super::*; 236 use std::fs::File; 237 use std::io::Read; 238 use std::process::Command; 239 use std::str::from_utf8; 240 use std::thread; 241 use std::time::Duration; 242 243 const RIFF : &'static str = "RIFF"; 244 const FILEPATH : &'static str = "/usr/share/sounds/k3b_error1.wav"; 245 const BUFSIZE : usize = 4; 246 const BUFALIGN : usize = 1; 247 const OUTPUT_FILE : &'static str = "out"; 248 249 #[test] 250 fn buffer_test() { 251 252 let mut f = File::open(FILEPATH).unwrap(); 253 let mut rbuf = ReadBuffer::<File>::new(BUFSIZE); 254 255 rbuf.read(&mut f).unwrap(); 256 257 let slice = unsafe { 258 rbuf.load() 259 }; 260 261 let riff = from_utf8(slice) 262 .unwrap(); 263 264 assert_eq!(RIFF, riff); 265 266 let mut wbuf = WriteBuffer::<File>::new(slice, 267 BUFALIGN); 268 assert_eq!(4usize, wbuf.size()); 269 270 let mut out = File::create(OUTPUT_FILE).unwrap(); 271 wbuf.write(&mut out).unwrap(); 272 273 let mut ifstream = File::open(OUTPUT_FILE).unwrap(); 274 let mut st = String::new(); 275 ifstream.read_to_string(&mut st).unwrap(); 276 assert_eq!(RIFF, st.as_str()); 277 278 Command::new("rm").arg(OUTPUT_FILE) 279 .output() 280 .unwrap(); 281 } ... 312 }
とりあえずWAVファイルのRIFFヘッダだけ読み込んで読み書きがちゃんとできているかをチェックしています。Rustのユニットテストの書き方は以下を参照してください。
Testing
あと、ここで実装したバッファを使ったNonBlockingIOをいくつか実装したので、気が向いたらブログに書きます。