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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}

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 on Result and Option to implicitly panic 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("Request: {:#?}", http_request);
}

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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::io::BufReader;
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        // i noticed the shadowing now
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let content = buf_reader.bytes().collect();
    stream.write_all(content).unwrap();
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::io::{prelude::*, BufReader}
use std::net::{TcpListener, TcpStream}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();
        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let mut buf: [u8; 512]; // not sure if this will work

    while (let count = buf_reader.read(&mut buf).unwrap()) > 0 {
        stream.write_all(&buf[0..count]).unwrap();
    }
}

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:

1
2
3
4
5
6
while let Ok(count) = reader.read(&mut buf) {
    if count == 0 {
        break;
    }
    ...
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::io::{prelude::*, BufReader};
use std::net::{TcpListener, TcpStream};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();
        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let mut buf_reader = BufReader::new(&mut stream);
    let mut buf: [u8; 512] = [0; 512]; // not sure if this will work

    let count = buf_reader.read(&mut buf).unwrap();
    stream.write_all(&buf[0..count]).unwrap();
}

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 :)