Rustの`std::io::Write::write`が`Ok(0)`を返すことについて
はじめに
先日、hyper
のストリーム[1]をtokio-rustls
でラップした際に、tokio::io::AsyncWrite::poll_write
が返すOk(0)
の解釈の違いによって問題が出たので、そのときに調べたことをまとめます
こちらは厳密にはtokio
のAsyncWrite
の話ですが、実質同じなのでstd::io::Write
の話として記事を続けます
そのときのissue
起きたこと
こちらのコードはtokio-rustls
にあるTLSストリームの実装の一部です
ここではshutdownの際にwhileループでなにやら今まで書ききれてなかったデータを書き込もうとしています
しかし、tokio-rustls
がhyper
のストリームをラップしていた場合、この段階でこのwrite
に対して常にOk(0)
を返すのでself.session.wants_write()
が一生true
のままになり無限ループに陥ってしまいます
結局、https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.write には
A return value of Ok(0) typically means that the underlying object is no longer able to accept bytes and will likely not be able to in the future as well, or that the buffer provided is empty.
と書かれているので、hyper
側がOk(0)
を返すのは正しい挙動で、rust-tls
側が受け取ったOk(0)
を特別扱いする必要があるということでまとまりました
std::io::Write::write
がOk(0)
を返すことについての第一印象
しかし https://github.com/rustls/tokio-rustls/issues/92#issuecomment-2507878251 のコメントの通り、ストリームがもうこれ以上書き込めないよ
ということを表明するためにOk(0)
を返すのはあんまりなデザインだと思わざるを得ません
POSIXのwrite(2)
にもそのような話はないっぽい
普通に考えて、仮にstd::io::Write::write_all
を自分で実装するとしたら、自分も上記のtokio-rustls
のコードみたいに書く自信があります
できればもうこれ以上書き込めないよ
というときには https://doc.rust-lang.org/std/io/enum.ErrorKind.html からWriteZero
とかそれっぽいやつを選んで返してほしいものです
ちなみにstd::io::Write::write_all
はOk(0)
を特別に扱っているのでそれが原因で無限ループすることはありません
rustc内の議論
上記のデザイン上の疑問について調べていたところ、目を引くissueが見つかりました
主題はstd::io::Write::write_all
がOk(0)
をどう扱うかについてですが
IMHO a sane writer should return an error when it is already at the end. This would be in line with eg. a block device (a typical size-limited object to write to...). Ok(0) doesn't really make any sense to me. If you can't write anything yet, return EWOULDBLOCK (or, well, block), if you can't write anything because of some error, return the error. But don't just do nothing (or purely some unrelated internal stuff) and return 0, that makes no sense.
So in that sense, treating Ok(0) as an error condition is acceptable, because it's something that shouldn't happen anyway. (And yes, IMO the slice impl could just as well be changed)
というコメントがあり、個人的に納得しました。かなりまとめるとwrite
にOk(0)
を返すのはそもそも異常なので、受け取った側がそれをエラーとして扱うのは妥当だということです
まとめ
個人的には以下のような結論に至りました。
-
std::io::Write::write
に対してOk(0)
が帰ってきた場合、受け取った側はそれをエラーとして扱うべき - だからといって
std::io::Write::write
実装側がもうこれ以上書き込めないよ
というときにOk(0)
を返すのは良くない。普通にstd::io::Error
を返すべき - そもそも
Result<std::num::NonZeroUsize>
返せよ。Breaking Changeなのを差し引いてもさすがにこれはオタクすぎるか -
std::io::Write::write_all
はOk(0)
をエラーとして扱う。無限ループするわけにはいかないので[2]これは正しい -
std::io::Write::write_all
がそういう挙動をしている以上 https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.write に A return value of Ok(0) typically means that the underlying object is no longer able to accept bytes and will likely not be able to in the future as well, or that the buffer provided is empty. と書くのは妥当
-
具体的には
hyper
のHTTP/2サーバーでUpgradeされたストリーム ↩︎ -
関連するLibs-API Meeting https://hackmd.io/@rust-libs/SJUBKd-lK ↩︎
Discussion