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をいくつか実装したので、気が向いたらブログに書きます。