- The purpose of Rust as a language, really,
is to be a safer version of C++, I think,
that's being honest about it.
Rust as a language, and as a library,
puts in place many layers of protection
to avoid many of the painful and expensive errors
that we could make in C++.
Such as having a null pointer
that we try to dereference, the program crashes,
or memory allocation bug, you know the kind of thing
where you try to delete an object twice
or you forget to delete an object
and you have a memory leak?
These are classic errors in C++.
Very difficult to do these in Rust
because the Rust language and the library
stops you from doing it.
Thread race conditions.
Trying to access static data in C++ would be fine
but it's potentially dangerous,
because multiple threads
could then step on each other's toes.
You need to have some kind
of safety mechanism in place like a mutex for example.
So Rust as a language stops us
from doing things that could damage our code.
But in some occasions this layer of protection
in Rust actually gets in the way.
Maybe we really do need to access direct memory addresses.
Or you know, low level resources in some kind of direct way.
So for example, dereferencing the raw pointer,
updating static shared data,
some kind of register related code.
So in these edge conditions
Rust allows us to opt out of the security mechanism
and to define what's called unsafe code.
When you define unsafe code
it consciously bypasses Rust protection layer
and allows us to do low level operations
without any protection from Rust.
That's a good thing and a bad thing
you're on your own now, you better make sure
that the code is safe, that it behaved properly.
So to define this kind of edge condition operation
you have an unsafe keyword, you define an unsafe block
and in that block you can do anything you like.
Rust will let you do anything.
Basically you can dereference raw pointers
access shared data, anything you want to
without Rust getting in the way.
But be careful because Rust isn't protecting you anymore.
You're on your own.
It's a simple enough concept.
So let's have a look at an example
in the usual project.
And we'll have a look at main
then we'll see demo unsafe code and then we'll run it.
So here we are in the source, main.
That's the demo and here's the code.
So I've got an unsafe block here.
Okay, let me step you through the code.
In fact, I just run it first
because there are some other little things I want to try.
After running through the code
as it currently stands.
Right, so from the top I declare a couple
of normal mutable integers,
x will be 101 after those two statements
and y will be 201.
And then I've declared some raw pointers.
This is the first time we've seen this syntax.
Rust does have raw pointers.
Until now we've been declaring references
which are kind of protected by Rust
and the borrow checker, et cetera.
But you can have raw pointers
which are literally just addresses in memory.
So p1 is a pointer to an integer.
The calls keyword you can imagine it means
that the pointer used in the pointer using p1
it treats the integer as constant.
It's like a pointer to constant data.
X itself could be modified
but using this pointer will treat the data as constant.
So I would say p1 is a pointer to constant data
and you give it the address of x.
So here the ampersand really does mean address.
It doesn't mean reference or borrow.
It means address as it would do in C and C++.
So this code here would be quite familiar
to a C and C++ developer.
Take the address of x, store the address in p1,
and treat the data, x effectively, as constant.
You've got to use
either the const keyword or the mat keyword
when you declare a pointer.
You've gotta consciously say the pointer
will allow the data to change like p2.
P2 is a multiple pointer, or rather, it allows you
to change the underlying data via the pointer.
So you take the address of y,
you have to say mut here as well.
You have to make it clear to the compiler
that you know what you're doing.
Yes, I do know what I'm doing.
I am taking a mutable address.
I do envisage the value of y changing via this pointer.
Take the address and allow you to change the value of
and you can store that in a mutable pointer.
Okay? So in the first case, p1,
when you say star const, it designates a roll pointer
which treats the underlying data as constant.
And then when you say star mut, you declare
you designate roll pointer that treats the underlying data
as potentially mutable.
So interestingly, declaring pointers isn't unsafe.
You can declare pointers and that's fine.
It's only when you dereference the pointer
that it's potentially dangerous.
So I can declare my pointers in normal code
but to dereference the pointers, I have to do that
in an unsafe block or an unsafe function.
So I dereference p1,
and it'll give me the contents of p1.
In other words, the contents of x really
and the contents of x is 101.
I can't do this.
I can't change the underlying value
because p1 promises to treat the data as constant.
I can't then try to change the underlying data.
I mean syntactically, that would be the right syntax
but I can't actually change the value
because p1 promised to treat the data as constant.
So that would gimme a compiler error.
I'm gonna prove the point.
Let's uncomment that code and run it again.
And let's see, right, on line 20,
p1 is a star cons pointer.
So the data it refers to cannot be written.
Okay, clear enough.
So don't do that.
I can change the value that p2 points two obviously
because p2 is a mutable pointer
so that allows me to change the value.
So y afterwards will be 222.
So I guess what I could do is I could print
the value of y afterwards as well.
That would be quite a good proof, wouldn't it?
To say that the value of y is actually what's changed.
So we say println!,
let's print the value of y afterwards and why not?
So this will change the value the p2 points to,
which is y. Y was 201,
y will become 222,
and we can verify the y really is 222 afterwards.
Okay, so p2 really does point to y
and y really has changed afterwards.
I guess one last thing I'm going to do is to
try to do all this pointer related manipulation outside
of an unsafe block.
And you can't.
Okay, so, if I move that code to the outer scope
and I try to dereference pointers in normal code
at that point we were in full Rust compiler mode.
It won't let us do it.
Loads of errors. Good.
Because I'm trying to do something that's pretty dangerous.
I'm trying to dereference a raw pointer.
The raw pointer could be null.
Or a dangling pointer or unaligned.
This could basically cause us
to have a very bad day in the office.
So you can't dereference raw pointers.
it has to go in an unsafe block like that.
Okay, so I'll reinstate the unsafe block
and then just for due diligence
I'm gonna run the application again just to prove
that it works and it does.
So obviously using unsafe code is something that you do
because you have to, you try to avoid it where possible
because you are taking life in your own hands here.
So use it when you need to
but only when you really need to.
Most times avoid using unsafe code.
The Rust compiler is there for your benefit
but it gives you this kind
of escape route if you really need it.