Why Geph was rewritten in Rust?

I wonder what was the line of reasoning in deciding to rewrite the project from Go to Rust? I'm interested in this matter as both are modern languages, designed with different goals but are usually compared against each other.

Arguments on Rust:

  • No GC, manual and strict memory management
  • Better performance
  • Unstable libraries and fewer server tools
  • Difficult to learn and slow to code (?)

Arguments on Go:

  • Easier to learn and faster to code-up and reach to a working solution
  • Good concurrency model
  • Good server libraries and tools
  • GC therefore random hiccups
  • Being a Google project therefore having potential of getting abandoned

Now in the context of a privacy-oriented VPN app, I'm really eager to know your experience on short-comings of Go and general comments on your decision :D

No GC, manual and strict memory management

That's actually false. Rust has automatic memory management just like Go, and unlike C: you don't need to manually allocate and deallocate memory. It's more accurate to describe Rust has having compile-time memory management, rather than runtime memory management, just like the distinction between static and dynamic typing (while manual memory management would be analogous to writing assembly without types). I never found memory management in Rust any more diffcult than in Go.

Difficult to learn and slow to code

I would disagree with this. Rust is definitely a little harder to learn, but once I got the hang of it I was much more productive in Rust than Go. This is because Rust is often much more expressive than Go. For example, to sum up the squares of a slice of items in Rust I just need to do

some_slice.map(|x| x * x).sum()

while in Go I need something like

accum := 0
for _, value := range someSlice {
    accum += value * value
}

Go also has no language-level prevention of race conditions, resource leaks, etc, and forgetting to defer in the right place, lock the right mutex, etc often causes horrible consequences.


As for Go, it is deceptively easy to learn, but extremely amenable to hard-to-maintain spaghetti code if you don't rigorously document everything, due to things like the lack of generics, a statement-oriented rather than expression-oriented design leading to the use of a lot of dummy variables like accum above, etc.

Go's concurrency model, as a language, is also horribly overrated. First of all, anything Go can do, Rust can also do with libraries like async-channel, except with a more expressive type system and language.

I also think that Rust's async model, despite the immature libraries at the moment, is one of the best ways of organizing highly concurrent, high-reliability network code (perhaps second to Erlang). For example, let's say you want to do "get webpage 1 and webpage 2 simultaneously, returning the first one that responded, and cancel if the whole thing takes too long". In Rust it'll just be

use smol::prelude::*;
use smol_timeout::TimeoutExt
...

get_webpage_1().race(get_webpage_2()).timeout(Duration::from_secs(60)).await?

Doing the same in Go would require spinning at least 2 more goroutines, setting up channels to cancel, etc, and it would be nontrivial to clean up your goroutines in all possible scenarios. In Rust all that is handled automatically.

The only advantages of Go in my experiences are more libraries and (much) faster compilation time. But Geph is largely designed from the ground up (it doesn't even use TLS in its core logic), and Rust's powerful language features mean you get a correct program in much less compile-debug cycles than Go, so on balance Rust is much more suitable for Geph than Go.

2 Likes

Thank you very much for sharing your experience! Much appreciated!

1- Personally, I know a little C to get by, and no C++. Do you recommend to first master C or C++ then move on to Rust? Or do both in parallel?

2- How much did it take for you to do it in Rust? Was it your first project in Rust? How did you learn it? The official rust-book?

About the async part, there is this thread:
https://www.reddit.com/r/rust/comments/lg0a7b/benchmarking_tokio_tasks_and_goroutines/

I think fzf being faster than skim is related to goroutines performing better.

  1. I would say that you don't need to know C or C++. I don't know much C++ and haven't written much C outside of school.

  2. This isn't my first project in Rust, but it's the first production project I wrote in Rust. I learned Rust in a few weeks by reading the Rust book and implementing little things.

1 Like

As I mentioned, Rust's strength in async is not actually performance, but rather expressivity. One of the strengths of Go is an extremely optimized asynchronous runtime (courtesy of massive amounts of work done by Google), but that's really the compiler and not the language itself. For example, channels in Go are extremely optimized and tightly integrated with the scheduler, and they are still a bit faster than the best Rust asynchronous channel implementations. I'd expect Rust async runtimes to catch up to Go as the async ecosystem matures.

2 Likes