初心者がRustのstd::netのソースコードを読んでみた
前書き
PHPerKaigi 2024に来ていてPostmanさんのブースに立ち寄ったところ, 使ってみたくなったのでサーバーを立てることに. 折角なので勉強中のRustを使うことにした. とはいえ, どのフレームワークが良いのかわからないので, 素のTCPを採用.
Web Serverが立ったのでソースコードを読んでみた.
この記事はRustをそこまでわかっていない人が書いているので, 嘘が書いてあるかもしれないです. ほどほどに参考にするように.
Reference
本記事で参考にしたHTTP Serverの実装
GitHub Gist: http_server.rs
Main
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
println!("Listening for connections on port {}", 8080);
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(|| {
handle_client(stream)
});
}
Err(e) => {
println!("Unable to connect: {}", e);
}
}
}
}
- 僕「TcpのListenerのオブジェクトを作ってlocalhostの8080に繋げる」
- 僕「listenerのincoming() methodが実行されて何かが戻ってくる感じか」
- 僕「何戻ってくるんだろ」
#[stable(feature = "rust1", since = "1.0.0")]
pub fn incoming(&self) -> Incoming<'_> {
Incoming { listener: self }
}
-
僕「なるほど、selfのstructと同じ情報を持ったIncoming Structが帰ってくる感じか」
-
僕「あれ?これ実行一回で実行終わっちゃわないか...?
-
ChatGPT「RustのFor文(PHPでいうforeach)の動きはIteratorをinの後に取って,
fn next(&mut self) -> Option<Self::Item>
を呼び続けるよ」 -
僕「ほう」
-
参照: The Rust Programming Language: Processing a Series of Items with Iterators
-
僕「これがRustのIterator traitか. 確かにnext methodがあってOptionalでSelf::Item返してるな」
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
- 僕「コードジャンプで飛んでみるか」
#[stable(feature = "rust1", since = "1.0.0")]
impl<'a> Iterator for Incoming<'a> {
type Item = io::Result<TcpStream>;
fn next(&mut self) -> Option<io::Result<TcpStream>> {
Some(self.listener.accept().map(|p| p.0))
}
}
- 僕「確かにtraitをimplementしてるわ」
- 僕「それでacceptは何をしてるんだ?」
#[stable(feature = "rust1", since = "1.0.0")]
pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
// On WASM, `TcpStream` is uninhabited (as it's unsupported) and so
// the `a` variable here is technically unused.
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
self.0.accept().map(|(a, b)| (TcpStream(a), b))
}
- 僕「0ってなんだよ」
- ChatGPT「Tuple Structだな」
- 僕「はえー」
Tuple structs are similar to regular structs, but its fields have no names. They are used like tuples, with deconstruction possible via let TupleStruct(x, y) = foo; syntax. For accessing individual variables, the same syntax is used as with regular tuples, namely foo.0, foo.1, etc, starting at zero.
タプル構造体は通常の構造体に似ているが、フィールドに名前がない。これらはタプルのように使用され、let TupleStruct(x, y) = foo;構文によって分解することができます。個々の変数へのアクセスには、通常のタプルと同じ構文が使用されます。つまり、0から順にfoo.0、foo.1などです。
参照: Rust Document: Struct
- 僕「じゃあ, 最初の要素にアクセスしているわけか」
- 僕「最初の要素って何?」
#[stable(feature = "rust1", since = "1.0.0")]
pub struct TcpStream(net_imp::TcpStream);
- 僕「TcpStreamってやつが入ってるんだな」
- 僕「こいつは
net_imp
のTcpStreamかー」
pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
let mut storage: c::sockaddr_storage = unsafe { mem::zeroed() }
let mut len = mem::size_of_val(&storage) as c::socklen_t;
let sock = self.inner.accept(&mut storage as *mut _ as *mut _, &mut len)?;
let addr = sockaddr_to_addr(&storage, len as usize)?;
Ok((TcpStream { inner: sock }, addr))
}
- 僕「おk, TcpStreamが帰ってくるわけね. コードの感じからして低レイヤはlibc使ってそうだな」
- 僕「ということは例のfor文のstreamはTcpStreamに詰め替えた
TcpStream { inner: sock }
とaddr
のTupleがIteratorで帰り続けるわけね、把握」 - 僕「続き読むか」
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(|| {
handle_client(stream)
});
}
Err(e) => {
println!("Unable to connect: {}", e);
}
}
}
- 僕「このtheread::spawnってなんだ? 」
- ChatGPT「これ. Threadを立てて, JoinHandleってやつを返してくれる. JoinHandleはThread内の戻り値かErrでpanic!を返してくれる」
Rust Document: Function std::thread::spawn
use std::thread;
let computation = thread::spawn(|| {
// Some expensive computation.
42
});
let result = computation.join().unwrap();
println!("{result}");
- 僕「joinを使うとmain threadの実行をとどめてくれる感じなのかな?」
- ChatGTP「せやで. Web Serverのような実行結果にかかわらず, main thread動かし続けたい場合はさっきみたいな書き方になるね」
- 僕「確かに」
Handler
- main関数でやっていることがわかったので, TcpStreamを処理している
handle_client
関数をみていく
fn handle_read(mut stream: &TcpStream) {
let mut buf = [0u8 ;4096];
match stream.read(&mut buf) {
Ok(_) => {
let req_str = String::from_utf8_lossy(&buf);
println!("{}", req_str);
},
Err(e) => println!("Unable to read stream: {}", e),
}
}
fn handle_write(mut stream: TcpStream) {
let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body>Hello world</body></html>\r\n";
match stream.write(response) {
Ok(_) => println!("Response sent"),
Err(e) => println!("Failed sending response: {}", e),
}
}
fn handle_client(stream: TcpStream) {
handle_read(&stream);
handle_write(stream);
}
- 僕「
handle_read
から読んでいくか. 最初は4096Byte Stackから保存してるな. 次はTcpStreamのread()
methodか. こいつはjumpしないとわからなさそう.」
impl Read for &TcpStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> io::Result<()> {
self.0.read_buf(buf)
}
fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
self.0.read_vectored(bufs)
}
#[inline]
fn is_read_vectored(&self) -> bool {
self.0.is_read_vectored()
}
}
- 僕「あー、確か,
tcp.rs
のTcpStream
の0番要素はsys_common/net.rs
のAPIとなるstructで, そっちに実装があるやつだっけ?」 - 僕「jumpして検索すれば出るかな」
pub struct TcpStream {
inner: Socket,
}
impl TcpStream {
// (略)
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
// (略)
}
- 僕「おっ、でたでた. Socket Structにもreadがあってbufferを読むのか」
- 僕「Socketみにいくかー.
unix/net.rs
にあるっぽい」
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
let mut buf = BorrowedBuf::from(buf);
self.recv_with_flags(buf.unfilled(), 0)?;
Ok(buf.len())
}
- 僕「知らんやつでてきたな」
pub struct BorrowedBuf<'data> {
/// The buffer's underlying data.
buf: &'data mut [MaybeUninit<u8>],
/// The length of `self.buf` which is known to be filled.
filled: usize,
/// The length of `self.buf` which is known to be initialized.
init: usize,
}
impl<'data> From<&'data mut [MaybeUninit<u8>]> for BorrowedBuf<'data> {
#[inline]
fn from(buf: &'data mut [MaybeUninit<u8>]) -> BorrowedBuf<'data> {
BorrowedBuf { buf, filled: 0, init: 0 }
}
}
- 僕「あー、参照渡ししてるからlifetime考えないといけないやつか. ここれと受け取った参照と同じだけ生きてくれればいいからこんな感じなのかな」
- ChatGPT「asciiアートで書くとこんな感じ, 'data lifetimeが共有されていることがミソ」
Time
│
│ +-- 'data lifetime begins
│ │
│ │ +-- BorrowedBuf<'data> is created
│ │ │
│ │ │ [MaybeUninit<u8> slice] <---- 'data ----> [BorrowedBuf<'data>]
│ │ │
│ │ │ (Both the slice and BorrowedBuf live happily, sharing the same lifetime)
│ │ │
│ │ │
│ │ │
│ │ │ (You can use BorrowedBuf, knowing it's safely borrowing from the slice)
│ │ │
│ │ │
│ │ +-- BorrowedBuf<'data> might be dropped here, but can live until 'data ends
│ │
│ +-- 'data lifetime ends; slice goes out of scope
│ (BorrowedBuf<'data> must also be out of scope by now if not already)
│
V
- 僕「おk、結局sliceと0, 0で初期化してるだけね」
- 僕「じゃ、次の
recv_with_flags()
をみてくか
fn recv_with_flags(&self, mut buf: BorrowedCursor<'_>, flags: c_int) -> io::Result<()> {
let ret = cvt(unsafe {
libc::recv(
self.as_raw_fd(),
buf.as_mut().as_mut_ptr() as *mut c_void,
buf.capacity(),
flags,
)
})?;
unsafe {
buf.advance(ret as usize);
}
Ok(())
}
- 僕「やっぱ低レイヤーはCに任せている感じなんだな」
- 僕「OKで返してるってことは用意した4096Byteの配列に書き込んでる感じなんだろうな」
- 僕「次の回で壊したいなあ...とりあえず何してるかわかったし次行くか」
fn handle_read(mut stream: &TcpStream) {
let mut buf = [0u8 ;4096];
match stream.read(&mut buf) {
Ok(_) => {
let req_str = String::from_utf8_lossy(&buf);
println!("{}", req_str);
},
Err(e) => println!("Unable to read stream: {}", e),
}
}
- 僕「
String::from_utf8_lossy()
を見るか」
Converts a slice of bytes to a string, including invalid characters.
Strings are made of bytes (u8), and a slice of bytes (&[u8]) is made of bytes, so this function converts between the two. Not all byte slices are valid strings, however: strings are required to be valid UTF-8. During this conversion, from_utf8_lossy() will replace any invalid UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER, which looks like this: �
- 僕「あー、壊れてる可能性があるutf8のByte列を壊れていないutf8に変換してくれる関数か. 便利だな」
- 僕「今回は4096Byteの固定長のBufferを取ったからそれ以上のutf8のByte列がHTTP Requestで飛んでくると壊れるだろうし, それはそっか」
#[must_use]
#[cfg(not(no_global_oom_handling))]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str> {
let mut iter = Utf8Chunks::new(v);
let first_valid = if let Some(chunk) = iter.next() {
let valid = chunk.valid();
if chunk.invalid().is_empty() {
debug_assert_eq!(valid.len(), v.len());
return Cow::Borrowed(valid);
}
valid
} else {
return Cow::Borrowed("");
};
const REPLACEMENT: &str = "\u{FFFD}";
let mut res = String::with_capacity(v.len());
res.push_str(first_valid);
res.push_str(REPLACEMENT);
for chunk in iter {
res.push_str(chunk.valid());
if !chunk.invalid().is_empty() {
res.push_str(REPLACEMENT);
}
}
Cow::Owned(res)
}
- 僕「実装みた感じもそうだね」
- 僕「最後に
handle_write()
読むか」
fn handle_write(mut stream: TcpStream) {
let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body>Hello world</body></html>\r\n";
match stream.write(response) {
Ok(_) => println!("Response sent"),
Err(e) => println!("Failed sending response: {}", e),
}
}
- 僕「あー, TcpStreamね」
impl Write for TcpStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
self.0.write_vectored(bufs)
}
#[inline]
fn is_write_vectored(&self) -> bool {
self.0.is_write_vectored()
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
- 僕「あー例の
std::sys_common net.rs
に飛ばされるんですね. わかります」
Rust Document: std::sys_common
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
let len = cmp::min(buf.len(), <wrlen_t>::MAX as usize) as wrlen_t;
let ret = cvt(unsafe {
c::send(self.inner.as_raw(), buf.as_ptr() as *const c_void, len, MSG_NOSIGNAL)
})?;
Ok(ret as usize)
}
- 僕「おけ, libcっすね」
読んでみてわかったこと
- Rustのライブラリは読みやすい
- ジャンプが基本効く
- Library Dockが丁寧
- 公式 Documentが読みやすい
- Rustの低レイヤーはlibcを利用していて, Rust側のAPIはそれらをいい感じにwrapしてくれている
- libcのAPIをそのまま叩いているライブラリをwrapしているライブラリを呼び出して使っている感覚に近い
- libc読むのは正直しんどいのでGo言語のように低レイヤーもRust化すると嬉しいなあ
- 逆に高級な部分についてはRustで完結するので嬉しい
- まだまだ知らない文法あると気付いた
- ざっと文法を確認はしてたつもりだが, 厳密な理解が足りていないなと反省
Discussion