Librsvg seems to have reached a point where it is easier to switch parts of C development to Rust than to continue using C. What’s more, more and more of its code already uses Rust.

Lately, I’ve been switching back and forth between C and Rust. It seems to me that C is becoming more of a relic.

C language elegy

I fell in love with C about 24 years ago. At The time, I was learning C through a Spanish edition of “The C Programming Language” (2nd edition, by Brian Kernighan and Dennis Ritchie, so it is sometimes referred to as K&R). Before that, I had used Turbo Pascal, which also had Pointers and required manual memory management, while C was new and powerful.

K&R is known for its unique writing style and concise code style. It even teaches you how to implement simple malloc() and free() functions yourself, which is really fun. Moreover, some features of the language itself can be implemented by itself.

For the next few years, I used C all the time. It is a lightweight programming language that implements the Unix kernel with almost 20,000 lines of code.

GIMP and GTK+ taught me how to use C for object-oriented programming, and GNOME taught me how to use C to maintain large software projects. A 20,000-line project can be fully understood by a person in a few weeks.

But the size of the code base is no longer comparable, and our software has much higher expectations of a standard library for programming languages.

Some good experiences with C

I learned how to implement object-oriented programming in C language by reading Pov-Ray source code for the first time.

Read the GTK+ source code to learn about the clarity, cleanliness, and maintainability of C code.

Read the source code of SIOD and Guile to learn how to implement the Scheme parser in C.

Write an initial version of GNOME Eye in C and tune MicroTile rendering.

Some bad experiences with C

When I was on the Evolution team, a lot of things kept falling apart. Valgrind didn’t exist at that time. In order to get Purify, you had to buy a Solaris machine.

Debug the gnOMe-VFS thread deadlock.

Debugging Mesa with no results.

Take the initial version of Nautilus-share and find that the code doesn’t use free().

You want to refactor code, but you don’t know how to manage memory.

Trying to package code, only to find global variables everywhere and no static functions.

But anyway, let’s talk about some of the things that Rust has that C doesn’t.

Automatic Resource Management

The first article I read about Rust was “Rust means never having to close a socket” (blog.skylight/rust-mean-…) . Rust borrowed ideas from C++, such as RAII (Resource Acquisition Is Initialization) and smart Pointers, added the principle of single ownership of values, and provided automated decision-making for Resource management.

  • Automation: There is no need to call free() manually. Memory is automatically released when used, files are automatically closed when used, and mutex is automatically released when out of scope. If you want to encapsulate external resources, you basically just implement the Drop trait. Encapsulated resources are like part of a programming language because you don’t need to manage its life cycle.
  • Decisiveness: Resources are created (memory allocation, initialization, open files, etc.) and then destroyed out of scope. There is no such thing as garbage collection: the code is finished. The life cycle of program data looks like a function call tree.

If I keep forgetting to call these methods (free/close/destroy) while writing code, or if I find that code I wrote in the past has forgotten to call them, or even called them incorrectly, THEN I don’t want to use these methods anymore.

The generic

Vec<T> is really a vector of elements T, not just an array of object Pointers. After compilation, it can only be used to store objects of type T.

It takes a lot of code in C to do something like that, so I don’t want to do that anymore.

Traits are more than just interfaces

Rust is not an object-oriented programming language like Java. It has traits that look like Java’s interfaces — you can use them for dynamic binding. If an object implements Drawable, you can be sure that the object has the draw() method.

However, traits are more powerful than that.

Association types

Traits can contain associative types. Take Rust’s Iterator for example:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__pub trait Iterator {
  type Item;
  fn next(&mut self) -> Option<Self::Item>;
}__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

That is, when we implement Iterator, we must also specify an Item type. When you call the next() method, if there are more elements, you get Some (the user-defined element type). If the element is iterated over, None is returned.

Association types can reference other traits.

For example, in Rust, a for loop can be used to iterate over any object that implements IntoIterator.

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__pub trait IntoIterator { /// The type of the iterated element type Item; type IntoIter: Iterator<Item=Self::Item>; fn into_iter(self) -> Self::IntoIter; }__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

When you implement this trait, you must provide both the Item and IntoIter types, which must implement Iterator to maintain Iterator state.

In this way, a type network can be set up, with types referring to each other.

String cutting

I previously published an article about the lack of string cutting features in C (people.gnome.org/~federico/b…). “Explains this pain point in C.

Dependency management

Previously, implementing dependency management required:

  • Call PKG-config manually or through automated tool macros.
  • Specifies the header and library file paths.
  • Basically, you need to manually ensure that the correct version of the library files are installed.

In Rust, you just write a Cargo. Toml file and specify the version of the dependent library in that file. These dependency libraries are automatically downloaded or retrieved from a specified location.

test

Unit testing in C is difficult for several reasons:

  • Internal functions are usually static. That is, they cannot be invoked by external files. The test program needs to use the #include directive to include source files, or use #ifdefs to remove these static functions during testing.
  • You need to write a Makefile to link the test program to some of the dependent libraries or some of the code in it.
  • You need to use test frameworks, register test cases with them, and learn how to use them.

In Rust, code like this can be written anywhere:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__#[test] fn test_that_foo_works() {  assert! (foo() == expected_result); }__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

Then run cargo Test to run the unit tests. This code is simply linked into the test file, and you don’t need to manually compile anything, write makefiles or extract internal functions for testing.

To me, this is a killer feature.

The documentation that contains the tests

In Rust, you can document comments written using Markdown syntax. The test code in the comments is executed as a test case. That is, you can unit test a function while explaining how to use it:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__/// Multiples the specified number  by two /// /// ``` /// assert_eq! (multiply_by_two(5), 10); /// ``` fn multiply_by_two(x: i32) -> i32 { x * 2 }__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

The sample code in the comments is executed as a test case to ensure that the documentation is in sync with the actual code.

Hygienic Macro

Rust’s hygiene macros avoid problems with C macros, such as things in macros that mask identifiers in code. Rust does not require all symbols in macros to use parentheses, such as Max (5 + 3, 4).

There is no automatic transformation

While many bugs in C are caused by unintentionally converting ints to short or char, this is not the case in Rust, which requires the transition to be displayed.

There will be no integer overflow

This one needs no further explanation.

In safe mode, there is almost no undefined behavior in Rust

Code written in Rust’s “safe” mode (outside of the unsafe{} code block) should be treated as a bug if undefined behavior occurs. For example, it’s perfectly fine to move a negative integer to the right.

Pattern matching

When switching on an enumerated type, the GCC compiler warns if all values are not processed.

Rust provides pattern matching by handling enumerated types in match expressions and returning multiple values from a single function.

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__impl f64 { pub fn sin_cos(self) ->  (f64, f64); } let Angle: f64 = 42.0; let (sin_angle, cos_angle) = angle.sin_cos(); __Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

Match expressions can also be used on strings. Yes, strings.

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__let color = "green"; match color { "red" => println! ("it's red"), "green" => println! ("it's green"), _ => println! ("it's something else"), }__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

Is it hard for you to guess what the following function does?

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__my_func(true, false, false)__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

But if pattern matching is used on function arguments, then things are different:

__Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__pub struct Fubarize(pub bool); pub struct Frobnify(pub bool); pub struct Bazificate(pub bool); fn my_func(Fubarize(fub): Fubarize, Frobnify(frob): Frobnify, Bazificate(baz): Bazificate) { if fub { ... ; } if frob && baz { ... ; }}... my_func(Fubarize(true), Frobnify(false), Bazificate(true)); __Fri Mar 09 2018 10:31:40 GMT+0800 (CST)____Fri Mar 09 2018 10:31:40 GMT+0800 (CST)__Copy the code

Standard error handling

In Rust, it is no longer a matter of simply returning a Boolean to indicate an error, or simply ignoring an error, or handling exceptions through non-local jumps.

#[derive(Debug)]

When creating a new type (such as a struct with a large number of fields), use #[derive(Debug)]. Rust automatically prints the type for debugging, eliminating the need to manually write functions to get the type information.

closure

Function Pointers are no longer needed.

conclusion

In multithreaded environments, Rust’s concurrency control mechanism prevents data race conditions. I think this is good news for those of you who often write multithreaded concurrent code.

C is an ancient language that might be a good choice for a uniprocessor Unix kernel, but it’s not a good language for today’s software.

Rust has a learning curve, but I think it’s totally worth it. It’s hard to learn because it requires developers to have a good understanding of the code they’re writing. I think Rust is a language that can make you a better developer, and it’s a tool that you can use to solve problems.

people.gnome.org/~federico/b…

Thanks to Guo Lei for planning and reviewing this article.