SJ cartoon avatar

Development Hello, Rust, My Old Friend

I've had Rust (the language, not the movie nor the video game) in the periphery of my programming languages since about 2016. Every now and again, I'd install the latest compiler and play around a bit. Nothing special, just getting a feel for the language and tooling, but it just never stuck with me for whatever reason.

Rust has never made sense for me. I've spent over 10 years professionally developing C++ applications, utilities, and libraries - so there is a fair body of knowledge that I don't want to just throw away to learn the next cool thing. Whenever I see "Rust vs C++" comparisons online, I feel like the C++ they compare against was from 20-30 years ago. Since about 2010, C++ has jumped by leaps and bounds and the language definitely feels more "modern" - to the point that it's a shock to my system when I'm moving from Kotlin or Swift to C, but that same shock isn't there when moving to C++20.

To point out what a changed language it is, I write exclusively using recent versions of C++ (11, 14, 17, 20 - as available), and I haven't suffered a segfault or memory leak in production in a decade. That's not a brag, not at all, I'm just pointing out that a developer using modern C++ even slightly correctly can quash entire classes of bugs that plagued the C++ developers of yesteryear.

I should also point out that I'm absolutely not a C++ fanboy. I would much rather use Python, but I have use cases where I just need a programming language that is highly performant, compiles to machine code, and can be made small and/or cross-platform. It's not all roses either... C++'s build tooling and package management story? Well, that's a rant for another day (it's also one of the reasons something like Rust and Cargo are interesting to me).

All that aside, I've been dreaming up some large-scale, multi-year projects that I will need to have a team of developers working on. This is where C++ falls by the wayside a bit. There are so many ways to solve any given problem, and the C++ API and standard library ecosystem is so large, that it will be difficult to keep code quality up to par without intensive code reviews, tooling, and time-consuming effort.

Enter Rust

One of the potential benefits I see in Rust (similar to what some people have said about Go), is that the language forces certain constraints on you and your team. While those constraints will definitely be infuriating at the time, in the long run, future maintainability, onboarding, and the difficulty of introducing certain classes of bugs might just be worth it.

Another benefit is the interop with Python, which C++ has via C-bindings, or C++ and pybind11 (fantastic project, BTW). The Pants project has made it clear that Rust and Python can work together for anything that I'll need to do. This was also a complete non-starter, so if it wasn't for projects like Pants and PyOxidizer, I wouldn't even be looking at Rust.

Since I'm pretty well-versed in both C and C++, I'll be tackling learning Rust from that angle and background. Trying to see if what I would ordinarily use C++ for, I could replace with Rust. I'll start out with some small, contrived projects in C++ and then see what happens when I do them in Rust.

Most importantly, I'll be comparing against modern C++, not that pre-2010ish dumpster fire.

Hello World, obviously

Well, where else would we even start?

C++

// main.cpp
#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
}
clang++ -std=c++20  -o hellocpp main.cpp
./hellocpp

I usually prefer the C-style printf over the C++ stringstream solutions. Streams are nice, efficient, and they can do some cool things - but a feature of modern languages is a nice simple print command that just works, and also allows clean string interpolation.

The C++20 specification has that (actually, just pulls in the well known fmt library), but neither GCC nor clang have fully implemented it as of today (MSVC has, though). It replaces all that streaming business with a nice std::format(&quot;Hello {}&quot;, &quot;World&quot;)

Rust

// main.rs

fn main() {
    println!("Hello, world!");
}
rustc -C opt-level=3 -o hellorust main.rs
./hellorust

Alright, so basically identical to C++. That's a good start. Using fn for function declarations is fine, but as I mention at the end of this post, I really wish language creators would just settle on using similar keywords at some point. def, fn, fun, func, function... Give me a break.

I was also able to make this work when I accidentally excluded the semi-colon. I hope this isn't a Javascript situation again, where you can (but don't have to) use semi-colons. Just pick a side and stick with it. Good news is that the major sources of docs and examples for beginners appear to use semi-colons.

Upon further research, this may just be because println is a macro and implicitly using {}, and Rust doesn't need semi-colons after curly braces. If true, that's already a bit annoying - but from what I understand, when I start using cargo fmt, stuff like that should be standardized and normalized away (much like clang-format).

However, slightly more research has shown this might be specifically because it is the last statement in the function, which has an implicit return if the last statement does not have a semi-colon.

That exclamation point came out of nowhere though. It appears that the exclamation point is there to specify that I'm calling a macro, not a function. I don't know whether a Rust macro bears similarity to a C/C++ macro just yet, but I'm mildly irritated that the most basic code example possible starts by introducing an "advanced feature". At the same time, I get it, since in other modern-ish languages, we just take it for granted that there is some magical floating print function always available.

Anyways, I'm sure somehow I'll survive that exclam.

What about exit codes?

C++ allows int main() to leave the return empty, and it's assumed to be 0. I've never really liked that, because it's so trivial to be explicit (not that it really matters that much one way or the other).

// main.cpp

#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

And in Rust...

// main.rs

fn main() {
    println!("Hello, world!");
    std::process::exit(0);
}

Now, when you run the function, followed by checking the return code:

% ./hellorust
Hello, world!
% echo $?
0

What about something 5% less trivial?

Alright, helloworld was fun, but let's at least declare and assign some stuff before wrapping up.

C++

// main.cpp

#include <iostream>

auto sum(int a, int b) -> int {
    return a + b;
}

int main() {
    auto a = 43;
    auto b = -1;
    auto result = sum(a,b);
    std::cout << "Sum of " << a << " + " << b << " = " << result << std::endl;
    return result;
}

Rust

fn sum(a: i32, b: i32) -> i32 {
    return a + b;
}

fn main() {
    let a = 43;
    let b = -1;
    let result = sum(a,b);
    println!("Sum of {} and {} is {}", a, b, result);
    std::process::exit(result);
}

These aren't the same

Rust, like a lot of modern languages or recent updates to slightly older languages, has made the decision that instantiation is immutable by default. I'm a huge fan of this paradigm, and unfortunately, it's a little clunky in C++. C++ does not have a single keyword, but you can prefix const in front of variable declarations (and I'm not getting into the const-correctness flamewars).

I don't really see this often in the wild related to primitives, but to compare apples-to-apples, this is more accurate:

// main.cpp

int main() {
    const auto a = 43;
    const auto b = -1;
    const auto sum(a, b);
    ...
}

We could, alternatively, make Rust declarations mutable:

fn main() {
    let mut a = 43;
    let mut b = -1;
    let mut result = sum(a, b);
    ...
}

I do wish C++ came out with a new default-const keyword like let or val or something, but oh well.

Echoing what I wrote above for the helloworld example, at this trivial level, the code is nearly identical. But, these are all just 1-2 method, single file examples. If the code was drastically different, I'd be very concerned.

Things I've noticed

With the most basic examples I could come up with, doing almost nothing, the two languages snippets are reasonably identical.

  • Both are statically typed
  • Both use curly braces to scope functions
  • Both use // for comments
  • Both use ; to end statements
  • String formatting is similar
  • The keywords are similar
  • Rust variable declaration is cannot be re-assigned by default, while C++ requires the added const keyword for similar functionality

I don't think a C++ or Rust developer would be confused reading the other. We haven't gotten to the really crazy syntax stuff yet, nor have we created objects, nor have we had to manage memory, but just starting to get familiar with the baseline functions seems reasonable for today.

No one can agree on functions

Whenever I work in a new language, I always find method declarations fascinating for some reason. No one can agree on a way to declare methods.

# Typescript
function myFunction(param1: number): string {}
const myFunction = (param1: number) => string {}

# Python
def my_function(param1: int) -> str:

# Kotlin
fun myFunction(param1: Int): String {}

# Swift
func myFunction(param1: int) -> bool {}

# C++
bool myFunction(int param1) {}

# C++ 11 and onwards
auto myFunction(int param1) -> bool {}

# Rust
fn myFunction(param1: i32) -> bool {}

No one can agree on keywords in general

I think there is this feeling that each language needs to be a special little snowflake, and any copying keywords is a sign of weakness.

# Typescript
var param1 = ...;
let param2 = ...;
const param3 = ...;

# Python
param1 = ...
param2: Final = ...

# Kotlin
var param1 = ...;
val param2 = ...;
const val param3 = ...;

# Swift
var param1 = ...
let param2 = ...

# C++
int param1 = ...;
const int param2 = ...;

# C++ 11 and onwards
auto param1 = ...;
const auto param2 = ...;

# Rust
let mut param1 = ...;
let param2 = ...;

Rust is a bit chunky

After compiling the original helloworld examples, I performed the release compilation and noticed that the Rust binary was about 20x the size of the C++ binary.

16K   hellocpp
375K  hellorust

I kept an eye on this, because the first time I tried Rust, the same sample compilation ended up over 500K (maybe even past an MB). While googling around, I found a bunch of reasons for this involving static linking of Rust libs (remove with -C prefer-dynamic), debug symbols (remove with -C debuginfo=0), etc etc... And honestly, I don't care. If this were microcontroller development, where every KB counts, then yeah - this would be unacceptable. But, on my computer with terabytes of space... shrug

What's up next?

As I'm a "dive right in, read the docs later" kinda person, I've come up with some samples I'd like to write in C++ and in Rust to see how they look and behave when compared. Over the next (some amount of time), here are some samples I'll post about:

  • Fibonacci calculation - A CPU-heavy, single-threaded, single-core sample - likely recursive and iterative
  • File I/O - Walk through the filesystem, parse files, and perform some action on them
  • Database access - Pretty self explanatory, I'm curious how memory and concurrency constraints work with databases
  • Image manipulation (single core) - Take a large image, split it into multiple blocks, and perform some action on each block with multiple threads
  • Image manipulation (multi-core) - Take a large image, split it into multiple blocks, and perform some action on each block with multiple cores
  • Protobuf generation - I use protobufs heavily in my day-to-day, so this will give me some idea of the Rust protobuf generator
  • gRPC client and server - Tacking onto protobuf generation is gRPC generation in order to write microservices
  • WASM - Something something WebAssembly something something - I really haven't thought this one through, but both C++ and Rust can compile to WASM