I’ve been learning Rust and I thought the Protohackers challenge would be a good way to learn it.
Here I document my thoughts and learnings as I go through it. [It’s a WIP]
Problem 0: Smoke Test
A TCP Echo server.
Before thoughts:
A TCP server must be fairly straightforward. If I remember correctly, the way to do it with sockets is to listen on one socket, pass received connections onto other separate sockets for each such that each connection is handled asynchronously, and then keep listening for more connections on the original listening socket.
There must be some TCP or socket library in Rust that will expose these capabilities.
There is documentation in the original Rust lang book about it: https://doc.rust-lang.org/book/ch20-01-single-threaded.html. This is great!
This is the example used:
|
|
It makes sense, we are binding a listener to 127.0.0.1:7878
. I learnt now that unwrap()
is used to stop the program in case of errors. bind
returns a Result<T, E>
type and this
is a lazy way to handle it. This reminds me of panic()
in Go. I suspect that unwrap
can
be used for other monad-y types as well. This is kind of true in that it is defined for
Result
and Option
types but only because it has been explicitly defined for them.
TIL,
unwrap()
is used onResult
andOption
to implicitlypanic
when something is wrong (e.g. result is not successful or option is none) and return the relevant contained value otherwise.
The above function is quite high level, as in that we don’t need to convert our host address to a required type before hand, or create a separate socket for each new connection by hand. It doesn’t do anything with the connection though and only logs it. It’s also not asynchronous.
The documentation goes on to describe how a connection can be handled. The stream is read through
an io::BufReader
. This is interesting.
|
|
TIL, Rust uses a Ruby like syntax for its lambdas, as seen in the above
.map(|result| result.unwrap())
. I like it.
I wonder what the .collect()
method is doing there. So apparently it seems to be that
take_while()
only returns an iterator, and collect()
converts that to some other container
type (such as Vec
here).
This is getting into traits and I need to get some more context about this. Also interesting
is that a Vec
type was used here and not a list or array type. Okay so Vectors are Rust’s
lists.
The stream that is passed to the connection is two-way, and can be written to with its
write_all()
method which takes a bytes
type as its only argument.
Implementation:
I have enough context now to write an echo server in Rust, although it won’t be asynchronous. I’ll write a synchronous version first and then get to an async version of it.
This is my first draft (writing here directly, not in a code editor):
|
|
I hope this compiles but I’m not sure. Let’s try.
Okay, an import is missing of TcpStream
. It is in std::net
.
I got some “method not defined” kind of errors by the linter but they disappeared when
I imported "std::io::prelude::*"
. This is weird because I wanted to use methods on
TcpStream
and BufReader
.
This code doesn’t compile because collect()
won’t be able to collect into a fixed-size array which
is expected by the write_all
method. I guess I’ll have to do it the old-fashioned way with a buffer
and then iterate untill the reader is consumed.
|
|
One very interesting thing I learnt is that the connection is closed automatically when the stream variable goes out of scope. This is very handy. Very elegant? There could be drawbacks of this implicit closing, but for now it seems like a very elegant solution to the resource closing problem.
Let’s see if this compiles.
Okay so the inline definition in the conditional of while
is not valid syntax. Is there
an elegant way to do this? I’ll ask ChatGPT to understand what’s the status quo way of doing
this in Rust.
ChatGPT recommends this instead:
|
|
This certainly does the job and looks elegant enough. I’m not all happy about it but it is clear enough.
I’ll go with this.
Now I have run into a borrowing problem. stream
was borrowed by BufReader::new
first and then
again in write_all
. This is kinda a pain.
I went with having one big buffer, reading data to it, and then writing that back to the stream. This is not optimal though, and I would like to find a way to do it cleanly.
This is my solution until then:
|
|
This works fine. The problem is that now I can’t compile it on my droplet because it’s out of disk space and I can’t install cargo on it. Grr.
I saw this interesting blog post about building an OS in Rust that I want to follow. I think I’ll do that instead and come back to Protohackers when I have some more money in the bank :)