A practice of rewriting foundation software from C to Rust(3)

Practice of C to Rust

Huailong Zhang
Rustaceans
9 min readJan 30, 2024

--

Preface

Inspired by “Google use Rust to rewrite the Android system and all Rust codes have zero memory security vulnerabilities” [1] in 2022, and with a strong interest in following the trend of Rust, I am trying to convert a foundation software from C into Rust. The primary purpose of this article is to communicate with everyone on the usage of Rust by recording these common and interesting problems I encountered during the conversion and how to solve these problems.

Problem Description

Let me continues the new problem encountered during this rewriting practice in this blog: panic error handling. As we all know that Rust’s error handling mechanism [2] can essentially be divided into Unrecoverable Errors and Recoverable Errors. For the former, when a very bad thing occurs, the user can choose to create an unrecoverable error through the panic! macro (of course it may also be due to implicit errors which occurs in runtime, such as divided by zero and out of index range, etc.). For the latter, Rust’s Result (which is essentially a special enumeration type containing only two enumeration members OK and Err) is generally used to handle possible errors, such as file opening errors, file reading or writing errors etc. There is one thing that needs to be clearup about the panic error of divided by zero. Thanks to Rust’s powerful compiler, unlike other programming languages such as C and Golang, the following Rust code actually report an error during the compilation phase:

fn main() {
let numerator = 42;
let denominator = 0;
// This line will result in a compilation error
let result = numerator / denominator;
println!("Result: {}", result);
}

For above code, the compiler reports following error, the Rust version in my environment is: rustc 1.75.0 (82e1608df 2023–12–21)):

error: this operation will panic at runtime
--> src/main.rs:6:18
|
6 | let result = numerator / denominator;
| ^^^^^^^^^^^^^^^^^^^^^^^ attempt to divide `42_i32` by zero
|
= note: `#[deny(unconditional_panic)]` on by default

An extra note: I point this out not to mean that divided by zero errors would not occur in Rust, but to illustrate that the Rust compiler helps engineers to find code errors as early as possible during the compilation stage to make the code more reliable and robust.

Come back to the theme, the problem I encountered is that I need to deal with runtime Unrecoverable Errors that appeared in Rust code, and could not let the program to be terminated due to this unrecoverable errors. Some readers may ask: Since Rust defines Unrecoverable Errors, which are unrecoverable errors, why do I still stubbornly need to deal with this kind of error? To answer this question, we still need to discuss it based on my scenario. First of all, since my scenario is to convert a foundation software written in C language into Rust (100% conversion cannot be achieved yet), there may be some situations that are different from a pure Rust project. And I think that a project with both C and Rust code should be normal for a long time in the future (for example, Linux already has a patch implemented by Rust, and I believe there would be other Rust patches for Linux in the future). Regarding the “different situation” mentioned above, I can give you an example. As we all know, there are three ways to pass an array as a parameter to a function in C language [3]:

  1. Pass an array as pointer variable to a function
void foo(int* array)

2. Pass an array as a reference to a function

void foo(int array[])

3. Pass an array to a function as an array with the specified size

void foo(int array[SIZE])

There are many ways to pass an array to a function in C language. Not only that, we all know that when an array is accessed in out of index range in C code, its behavior is unpredictable, that is, an error may or may not occur. So in this case, when we need to convert a vast amount of C code into Rust code, the original C code may not report an error, but a panic error of array out of index range may appear in the Rust code. Of course, this is just a concreted example. In Rust, everyone habitually uses unwrap() to deal with possible panic errors. In a pure Rust project, we may have enough confidence to decide how to deal with both Unrecoverable Errors and Recoverable Errors. But in a mixed situation, such as a project where C and Rust are mixed together, these behaviors may not be what we expected in some cases due to similar problems that can cause the entire program to terminate. Therefore, The problem I encountered in my practice is that I have to handle the panic errors correctly without terminating the entire program after the implicit panic error occurs in such mixed project.

Solution

When solving this problem, my first consideration was to find a Golang-like panic recovery mechanism [4] in Rust. Although Rust provides a panic hook [5] mechanism which allows users to customize some behaviors when panic errors occur, panic hooks cannot solve the problem of program termination. So currently, it seems that there is no similar panic recovery mechanism in Rust, and it is not very firm that unrecoverable errors should not be recovered. I say “not very firm” because Rust provide some spaces for me to solve this problem in std::panic::catch_unwind [6]. std::panic::catch_unwind mainly captures panic errors that may occur by calling a closure. Based on this method, after doing corresponding experiments, I applied it to the practice and also placed the sample code on my github [7], and welcome everyone to discuss it together.

In the sample code, there are two folders corresponding to 2 situations:

  1. rust-panic-without-handling is the binary program code folder that does not handle panic errors.
  2. rust-panic-with-handling is a folder of binary program code that handles many panic errors via std::panic::catch_unwind. These panic errors include: divided by zero, InvalidDigit and out of index range panics.

The basic logic of above two Rust programs is that user makes 3 times loop inputs through standard IO input, each time inputting the numerator and denominator required for calculation, and then performs the numerator/denominator operation through the Rust code to calculate, then store the calculation results into an i32 array with a fixed length of 3, and finally iterate the array and print the values in the array. The experiments scenarios are as follows:

  1. In any input loop, inputting denominator as 0 to trigger a divided by zero panic error
  2. In any input loop, inputting non-numeric values to trigger an InvalidDigit panic error, such as, inputting 56x.
  3. Panic errors are bound to occur. Throws an out of index range panic error by accessing an array with fixed length 3 and iterate this array from index 0 to index 3.

The sample code that does not handle panic errors is as follows:

use std::io;
use std::io::Write;

fn main() {
let mut try_times: i32 = 0;
let mut int_array: [i32; 3] = [0; 3];
println!("\n ###### Divide by zero ###### \n");
while try_times < 3 {
let current_time = try_times as usize;

// Get numerator from user input
let mut numerator = String::new();
print!("Please input the numerator: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut numerator).expect("Failed to read line");
let numerator: i32 = numerator.trim().parse().expect("Invalid input");

// Get denominator from user input
let mut denominator = String::new();
print!("Please input the denominator: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut denominator).expect("Failed to read line");
let denominator: i32 = denominator.trim().parse().expect("Invalid input");

// Perform division without validation
int_array[current_time] = numerator / denominator;
println!("Result is: {:?}", int_array[current_time]);
try_times += 1;
println!("##########################################");
}

println!("\n @@@@@@ Iteration @@@@@@ \n");
for i in 0..=3 {
println!("Iterate Element: {}", int_array[i]);
println!("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
}

println!("Complete the panic handle examples!");
}

From above Rust code, it’s obvious that no matter any panic error is triggered, the entire program is terminated immediately, and the output of the last line in the code println!(“Complete the panic handle examples!”); would never be seen.

The sample code for handling panic errors through std::panic::catch_unwind is as follows:

use std::io;
use std::io::Write;
use std::panic;

fn main() {
let mut try_times: i32 = 0;
let mut int_array: [i32; 3] = [0; 3];
println!("\n ###### Divide by zero ###### \n");
while try_times < 3 {
let current_time = try_times as usize;

// Handle divide by zero panic
let result_value = panic::catch_unwind(|| {
println!("This is the {}th to handle panic.", current_time);
// Get numerator from user input
let mut numerator = String::new();
print!("Please input the numerator: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut numerator).expect("Failed to read line");
let numerator: i32 = numerator.trim().parse().expect("Invalid input");

// Get denominator from user input
let mut denominator = String::new();
print!("Please input the denominator: ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut denominator).expect("Failed to read line");
let denominator: i32 = denominator.trim().parse().expect("Invalid input");

// Perform division without validation
numerator / denominator
});

match result_value {
Ok(result) => {
println!("No panic occur and the result is: {:?}", result);
int_array[current_time] = result;
},
Err(e) => {
if let Some(err) = e.downcast_ref::<&str>() {
println!("Caught panic: {}", err);
} else {
println!("Caught panic of unknown type");
}
},
};

try_times += 1;
println!("##########################################");
}

println!("\n @@@@@@ Iteration @@@@@@ \n");

for i in 0..=3 {
// Handle out of index range panic
let num_result = panic::catch_unwind(|| {
println!("Iterate Element: {}", int_array[i]);
});

match num_result {
Ok(()) => {
println!("No panic occur for this iteration");
},
Err(e) => {
if let Some(err) = e.downcast_ref::<&str>() {
println!("Caught panic: {}", err);
} else {
println!("Caught panic of unknown type");
}
},
};
println!("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
}

println!("Complete the panic handle examples!");
}

From above Rust code, it’s obvious that no matter any one or more panic errors are triggered, the entire program would always be running until the end of the code with the output via println!(“Complete the panic handle examples!”); .

It’s necessary to provide more explanation for the code that handles panic errors. First of all, std::panic::catch_unwind is a closure call, so I need to be careful when handling variables. As shown in code, in the closure call, current_time needs to be used to handle the update of the corresponding index element of the array. It should be immutable (cannot be defined as mut), so the code “let current_time = try_times as usize ;” exists. The reason why the variable must be immutable is related to the UnwindSafe trait that the data type passed in the closure may implement. Readers can learn about the data type that needs to implement this trait. In my case, it is &i32. And readers can also delete the relevant code above to the error message from Rust compiler. Secondly, if the closure calling needs to return information for external using, then the return information can be placed into the return value of the call. As shown in above code, the result_value returned by the first closure calling can be used by the subsequent match code block. Finally, a suggestion is that when using this closure, please include as little logic code as possible to capture panic errors. This can control the incoming data type (limited by the data type of the closure calling), and also enable panic errors are captured more accurately.

Of course, std::panic::catch_unwind has some limitations. As the documentation says: “This function only catches unwinding panics, not those that abort the process.”, “if a custom panic hook has been set, it will be invoked before the panic is caught, before unwinding.” Therefore, it may be useless to capture panic error via the function in this scenario. And more is “Unwinding into Rust code with a foreign exception (e.g. an exception thrown from C++ code) is undefined behavior.”.

Conclusion

This blog mainly describes a solution for program recovery after encountering certain panic errors in mixed project, so that the running program can not be passively terminated. After investigation, I found that Rust does not provide an overall panic error recovery mechanism. However, after comprehensively considering the project requirements and the function of std::panic::catch_unwind provided by Rust, I did some experiments and found out the solution which address the practice to restore the program even panic errors occur in it. However, I must point out that std::panic::catch_unwind has some limitations and cannot completely catch all panic errors. Therefore, I hope that readers need to be cautious about this solution when using it in your real projects.

About Author

Huailong Zhang (Steve Zhang) has worked for Alcatel-Lucent, Baidu and IBM to engage in cloud computing R&D, including PaaS and DevOps platform development. He is working in Intel SATG now, focusing on cloud native ecosystem, such as kubernetes and service mesh. He is also an Istio maintainer and has been a speaker at KubeCon, ServiceMeshCon, IstioCon, InfoQ/QCon and GOTC etc.

Related blogs

A practice of rewriting basic software from C to Rust(1)
A practice of rewriting basic software from C to Rust(2)

References

[1] https://security.googleblog.com/2022/12/memory-safe-languages-in-android-13.html
[2] https://doc.rust-lang.org/book/ch09-00-error-handling.html
[3] https://www.scaler.com/topics/c/pass-array-to-function-in-c/
[4] https://go.dev/blog/defer-panic-and-recover
[5] https://doc.rust-lang.org/std/panic/fn.set_hook.html
[6] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
[7] https://github.com/zhlsunshine/rust-panic-handling

Rustaceans 🚀

Thank you for being a part of the Rustaceans community! Before you go:

  • Show your appreciation with a clap and follow the publication
  • Discover how you can contribute your own insights to Rustaceans
  • Connect with us: X | Rust Bytes Newsletter

--

--

Huailong Zhang
Rustaceans

I am working on Intel SATG as a cloud software engineer and is Istio maintainer and been a speaker at KubeCon, ServiceMeshCon, IstioCon, QCon and GOTC