Unsafe traits and implementations

Apart from functions, traits can also be marked as unsafe. It isn't obvious why we would need unsafe traits. One of the primary motivations for unsafe traits existing in the first place is to mark types that cannot be sent to or shared between threads. This is achieved via the unsafe Send and Sync marker traits. These types are also auto traits, which means that they are implemented for most types in the standard library whenever appropriate. However, they are also explicitly opted out for certain types, for instance, the Rc<T>. An Rc<T> does not have an atomic reference counting mechanism and if it were to implement Sync and later be used in multiple threads, then we might end up with the wrong reference counts on the type, which could lead to early frees and dangling pointers. Making Send and Sync unsafe puts the onus on the developer to only implement it, that is, if they have proper synchronization in place for their custom types. Send and Sync are marked as unsafe because it's incorrect to implement them for types that have no clear semantics on how types behave when mutated from multiple threads.

Another motivation for marking traits as unsafe is to encapsulate operations that are likely to have an undefined behavior by a family of types. As we've already mentioned, traits, by their nature, are used to specify a contract that implementing types must hold. Now, let's say your types contain entities from FFI boundaries, that is, a field that contains a reference to a C string, and you have many of these types. In this case, we can abstract away the behavior of such types by using an unsafe trait and then we can have a generic interface that takes types that implement this unsafe trait. One such example from Rust's standard library is the Searcher trait, which is an associated type of the Pattern trait,which is defined at https://doc.rust-lang.org/std/str/pattern/trait.Pattern.html. The Searcher trait is an unsafe trait that abstracts the notion of searching an item from a given byte sequence. One of the implementers of Searcher is the CharSearcher struct. Marking it as unsafe removes the burden on the Pattern trait to check for valid slices on valid UTF-8 byte boundaries and can give you some performance gains in string matching.

With the motivation for unsafe traits covered, let's look at how we can define and use unsafe traits. Marking a trait as unsafe doesn't make your methods unsafe. We can have unsafe traits that have safe methods. The opposite is also true; we can have a safe trait that can have unsafe methods within it, but that doesn't signify that the trait is unsafe. Unsafe traits are denoted in the same way as functions by simply prepending them with the unsafe keyword:

// unsafe_trait_and_impl.rs

struct MyType;

unsafe trait UnsafeTrait {
unsafe fn unsafe_func(&self);
fn safe_func(&self) {
println!("Things are fine here!");
}
}

trait SafeTrait {
unsafe fn look_before_you_call(&self);
}

unsafe impl UnsafeTrait for MyType {
unsafe fn unsafe_func(&self) {
println!("Highly unsafe");
}
}

impl SafeTrait for MyType {
unsafe fn look_before_you_call(&self) {
println!("Something unsafe!");
}
}

fn main() {
let my_type = MyType;
my_type.safe_func();
unsafe {
my_type.look_before_you_call();
}
}

In the preceding code, we have all kinds of variations with unsafe traits and methods. First, we have two trait declarations: UnsafeTrait, which is an unsafe trait and SafeTrait, which is safe. We also have a unit struct called MyType, which implements them. As you can see, unsafe traits require the unsafe prefix to implement MyType, letting the implementer know that they have to uphold the contracts that are expected by the trait. In the second implementation of the SafeTrait on MyType, we have an unsafe method that we need to call within the unsafe block, as we can see in the main function.

In the following sections, we'll be exploring a handful of languages and how Rust interoperates with them. All of the related APIs and abstractions that Rust provides to communicate safely back and forth between languages is colloquially termed the Foreign Function Interface (FFI). As part of the standard library, Rust provides us with built-in FFI abstractions. Wrapper libraries on top of these provide seamless cross-language interaction.

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

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