How it works...

Well, that's a load of complicated code.

Let's start by introducing the structures that participate in this example:

  • SomeOsSpecificFunctionalityHandle [6] stands for an unspecified feature of your operating system that operates on some data and is presumably unsafe to use directly. We assume this feature locks some resource of the operating system that needs to be unlocked again.
  • SomeOsFunctionality  [9] represents a safe wrapper around the feature, plus some data T that might be useful for it.
  • SomeOsFunctionalityGuard [20] is an RAII guard created by using the lock function. When it is dropped, it will automatically unlock the underlying resource. Additionally, it can be directly used as if it was the data T itself.

These functions might look a bit abstract, as they don't do anything specific, but instead act on some unspecified OS feature. This is because most of the really useful candidates are already present in the standard library—see File, RwLock, Mutex, and so on. What's left are particularly domain-specific use cases when writing low-level libraries or dealing with some special, homemade resource that needs automatic unlocking. When you see yourself writing either, you will appreciate the elegance of RAII.

The implementation of the structs introduces some new concepts that might look a bit confusing if encountered for the first time. In the implementation of SomeOsSpecificFunctionalityHandle, we can spot some unsafe keywords [25 , 28 and 44] :

impl SomeOsSpecificFunctionalityHandle {
unsafe fn lock(&self) {
// Here goes the unsafe low level code
}
unsafe fn unlock(&self) {
// Here goes the unsafe low level code
}
}
...
fn lock(&self) -> SomeOsFunctionalityGuard<T> {
// Lock the underlying resource.
unsafe {
self.inner.lock();
}

    // Wrap a reference to our locked selves in a guard
SomeOsFunctionalityGuard { lock: self }
}

Let's start with the unsafe block [44 to 46]:

unsafe {
self.inner.lock();
}

The unsafe keyword tells the compiler to treat the previous block in a special way. It disables the borrow checker and lets you do all kinds of crazy stuff: dereference raw pointers like in C, modify a mutable static variable, and call unsafe functions. In return, the compiler doesn't give you any guarantees about it either. It might, for instance, access invalid memory, resulting in a SEGFAULT. If you want to read more about the unsafe keyword, check out its section in the second edition of the official Rust book at https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html.

Generally speaking, writing unsafe code should be avoided. It is, however, okay to do so when:

  • You're writing some code that directly interfaces with the OS and you want to create a safe wrapper around the unsafe parts, which is what we are doing here
  • You are absolutely 100% completely certain that what you're doing, in a very specific context, is actually not problematic, contrary to the compiler's opinion

If you're wondering why the unsafe block is empty, that's again because we are not using any actual OS resources in this recipe. If you wanted to use any, the code handling them would go in those two empty blocks.

The other use for the unsafe keyword is the following [25]:

unsafe fn lock(&self) { ... }

This marks the function itself as unsafe, meaning that it can only be called inside unsafe blocks. Remember, calling unsafe code in a function doesn't make the function automatically unsafe because the function could be a safe wrapper around it.

Let's move on now from our hypothetical low-level implementation of SomeOsSpecificFunctionalityHandle to our realistic implementation of its safe wrapper, SomeOsFunctionality[33]. Its constructor comes with no surprises (see Chapter 1, Learning the Basics and the Using the constructor pattern recipe if you need a refresher on that):

fn new(data: T) -> Self {
let handle = SomeOsSpecificFunctionalityHandle;
SomeOsFunctionality {
data,
inner: Box::new(handle),
}
}

We simply prepare the underlying OS functionality and store it with the user-provided data in our struct. We Box the handle because, as explained in a comment in the code earlier at lines [13 and 14], the low-level struct interfacing with the OS is often not safe to move. Because we don't want to restrict our user from moving our safe wrapper, however, we make the handle movable by putting in on the heap via a Box, which gives it a permanent address. What is then moved is simply the smart pointer pointing to the address. For more about that, read Chapter 5, Advanced Data Structures and the Boxing data recipe.

The actual wrapping takes place in the lock method:

fn lock(&self) -> SomeOsFunctionalityGuard<T> {
// Lock the underlying resource.
unsafe {
self.inner.lock();
}

// Wrap a reference to our locked selves in a guard
SomeOsFunctionalityGuard { lock: self }
}

When working with an actual OS feature or custom resource, you'll want to guarantee that self.inner.lock() is safe to call in this context before doing so, otherwise, the wrapper won't be safe. This is also where you can do interesting things with self.data, which you can potentially use in combination with the resource mentioned.

After locking our stuff up, we return a RAII guard with a reference to our structure [49] that will unlock our resource when it is dropped. Looking at the implementation of SomeOsFunctionalityGuard, you can see that we don't need to implement any kind of new function for it. We just need to implement two traits. We begin with Drop[54], which you have met in the previous recipe. Implementing it means that we can unlock the resource when the guard is dropped by accessing it through our reference to SomeOsFunctionality. Again, make sure to arrange the environment in a way that guarantees that self.lock.inner.unlock() is actually safe before calling it.

Since we are basically creating a kind of smart pointer to data, we can use the Deref trait [64]. Implementing Deref for B with a Target of A allows a reference to B to be dereferenced into A. Or in other, slightly less accurate words, it lets B act as if it was A. In our case, implementing Deref for SomeOsFunctionalityGuard with a Target of T means that we can use our guard as if it was the underlying data. Because this can cause great confusion to the user if implemented poorly, Rust advises you to only implement it on smart pointers and nothing else.

Implementing Deref is of course not mandatory for the RAII pattern, but can prove pretty useful, as we're going to see in a moment.

Let's look at how we can now use all of our fancy functions:

fn main() {
let foo = SomeOsFunctionality::new("Hello World");
{
let bar = foo.lock();
println!("The string behind foo is {} characters long",
bar.len());
}
}

The user should never have to use SomeOsSpecificFunctionalityHandle directly, as it's unsafe. Instead, he can construct an instance of SomeOsFunctionality, which he can pass around and store however he wants [73]. Whenever he needs to use the cool feature behind it, he can call lock in whatever scope he is in right now, and he will receive a guard that will clean up after him after the work is done [81]. Because he implemented Deref, he can use the guard directly as if it was the underlying data. In our example, data is a &str, so we can use the methods of str directly on our guard like we do in line [79] by calling .len() on it.

After this little scope ends, our guard calls unlock on the resource and, because foo independently still lives on, we can continue locking it again however much we want.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.123.189