Dangling pointers problem in C vs Rust
I encounter this original blog about Stack Memory and Dangling Pointer problem in Zig language, then I asked myself, how about other system programming languages, like C language and Rust, do they have the same problem? This blog is a very short article comparing how those languages deal with this problem.
TLDR
C language obviously has Dangling Pointer problem too, you have to solve this problem manually. Meanwhile, Rust doesn't allow you to even compile the code, because it has a guard rail - its ownership system.
The C language implementation
Here is my C implementation of the dangling pointer problem demonstrated in the original blog:
#include <stdio.h>
#include <stdlib.h>
// declare User struct
struct User
{
int id;
};
// function that create an User struct in stack memory,
// then return it pointer, which cause the dangling pointer problem.
struct User *init(int id)
{
struct User user;
user.id = id;
return &user;
}
// the main function
int main()
{
struct User *user1 = init(100);
struct User *user2 = init(1000);
printf("user1.id = %d\n", user1->id);
printf("user2.id = %d\n", user2->id);
}
The output on my machine one time (the output is different everytime you run it):
user1.id = 1000
user2.id = 10862404
which show that user1
is inheriting the value of user2
and user2
value are pointing to some invalid memory region, cause its id
attribute to be nonsensical value.
C language solution
This is my C solution using malloc
function:
#include <stdio.h>
#include <stdlib.h>
// declare User struct
struct User
{
int id;
};
// function that create an User struct, but it ...
struct User *init(int id)
{
// ... use heap memory instead, using the `malloc` function
struct User * user;
user = (struct User *) malloc(sizeof(struct User));
user->id = id;
return user;
}
int main()
{
struct User *user1 = init(100);
struct User *user2 = init(1000);
printf("user1.id = %d\n", user1->id);
printf("user2.id = %d\n", user2->id);
// and remember to free the heap memory after used, each pointer,
// one and only one time.
free(user1);
free(user2);
}
Now the output is consistent:
user1.id = 100
user2.id = 1000
The problems with this solution are:
you have to manually free the memory after use,
you will get memory leak application if you forget to free some pointers
you also get security issue if you free the same pointer more than once, because you may cause the modification of unexpected memory locations.
The Rust implementation
Rust prevent programmer from shooting themself in their foot, the equivalent code doesn't event compile:
fn main() {
let user1 = User::init(100);
let user2 = User::init(1000);
println!("user1.id = {}", user1.id);
println!("user2.id = {}", user2.id);
}
struct User {
id: i32,
}
impl User {
fn init(id: i32) -> &User {
let u = User { id: id };
&u
}
}
The errors when you compile are:
|
13 | fn init(id: i32) -> &User {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
13 | fn init(id: i32) -> &'static User {
| +++++++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `hello-world` (bin "dangling_pointers") due to previous error
If you naively think that you can get away with this error just by following the compiler's suggestion of change (adding static
lifetime), you are wrong, because now there is new error with this modified code:
fn main() {
let user1 = User::init(100);
let user2 = User::init(1000);
println!("user1.id = {}", user1.id);
println!("user2.id = {}", user2.id);
}
struct User {
id: i32,
}
impl User {
fn init(id: i32) -> &'static User {
let u = User { id: id };
&u
}
}
The error now is:
|
15 | &u
| ^^ returns a reference to data owned by the current function
For more information about this error, try `rustc --explain E0515`.
error: could not compile `hello-world` (bin "dangling_pointers") due to previous errorp
Rust simply doesn't allow a function to return a reference to a data owned by that function itself. You have to move the ownership of this data back to the caller. For more details, please checkout this article about dangling references in "The Rust Programming Language" book.
Rust solution
So here is the solution in Rust:
fn main() {
let user1 = User::init(100);
let user2 = User::init(1000);
println!("user1.id = {}", user1.id);
println!("user2.id = {}", user2.id);
}
struct User {
id: i32,
}
impl User {
fn init(id: i32) -> User {
let u = User { id: id };
u
}
}
Ending
Dangling pointers is just one in many problems that make C a unsafe language to work with.
For modern system programming, I prefer Rust, and If I need more low-level control, I might choose Zig. Rust is like the new C++, and Zig is like the new C. I think they both have their own strength, and I want to see more open-source software written in both of these languages in future.
This article is limited only to my personal experience, there are probably more ways to prevent dangling pointers in C that I don't know yet. Please let me know if I'm wrong.
Happy coding,
Subscribe to my newsletter
Read articles from Giang Ngo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by