Rust Traits, First Crack
Back in October, as some of you might be aware, I moved out of daily ops and CTI analysis. Since then,Iâve been working on two projects that are being developed in Rust andGo. Both are great, but as historically a systems-level programmer specializing in Cand C++, prior to entering cybersecurity, theannouncement of Rust 1.0, and what it promised todeliver, has intrigued me since I started reading about it. A couple years ago, when we were all pickingup new hobbiesto maintain sanity, I took some time of my own to finally sit down and learn Rust. In addition toThe Rust Programming Language (TRPL), I also found the following two resourcesextremely helpful:
* Rustlings - A series of hands-on learning challenges built into a validation framework
* A Gentle Intro Into Rust - A great companion to TRPL, by Steve Donovan
* Rust by Example - Official companion documentation to TRPL
For anyone wanting to get started with Rust, these were fairly solid introductions for me. I wonât spend any moretime dwelling on these, as I started this entry to discuss a specific feature of the language: _Generics_ using theRust `trait` feature.
# Example of an Existing Trait
The most common vector through which a novice Rust programmer might first encounter traits, is through the implementationof the `fmt::Display` trait. Hereâs the definition of this traitfrom the Rust `std` library:
pub trait Display { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;}
Traits are used to describe language-level APIs that provide uniform access patterns and/or operations acrossmultiple data types.
## Traits, Interfaces, Protocols!
In this role, they offer functionality commonly seen in C++âs abstract classes,or Javaâs `interface` feature. Used as above, theywill functionally fill a role much more similar to the latter, as the `trait` feature is limited to defining a specificationwithout an _implementation_ , which is similar to the `interface` keywordâs role in Java. The `interface` keyword in Java is commonlyused to achieve the outcome for which _multiple inheritance_ is often relied upon for, in C++, as Java maintains a stricter set ofinheritance rules that donât allow for multiple parent classes.
Similar language features can be foundin ObjectiveC/C++âs `@protocol`.Curiously enough, ObjectiveC/C++ use the keyword `@interface` to define the type-specific definition, and rely upon `@protocol` todefine more generic API definitions that can be implemented by multiple data types.(Click here for another example of ObjectiveC`@protocol` and `@interface`). As it owes its lineage to the Smalltalk-inspired ObjectiveC, Swift similarlyadopts this lexicon, too.
# Definition vs. Implementation: The Division of Responsibilities
Among the key language-level constraints encountered with Rust is that type definitions (encapsulated data, such as member variables) andimplementation are written as separate organizational blocks. For example, the below is a `struct` that contains two member attributes, `_a`and `_b`, that are private-access:
struct TwoNums { _a: u32, _b: u32,}
And, the following is the _implementation_ of this data types methods, implementing a basic getter and setter for each, as well asa straightforward constructor:
impl TwoNums { fn new() -> Self { Self { _a: 0, _b: 0, } } fn set_a(&mut self, new_a: u32) { self._a = new_a; } fn set_b(&mut self, new_b: u32) { self._b = new_b; } fn get_a(&self) -> u32 { self._a } fn get_b(&self) -> u32 { self._b }}
Then, in our `main` function, we might try to write the following code:
fn main() { let mut o: TwoNums = TwoNums::new(); // Instantiate a new instance of TwoNums o.set_a(1); o.set_b(2); println!("{}", o);}
Trying `cargo run` on this will result in an error similar to the following:
error[E0277]: `TwoNums` doesn't implement `std::fmt::Display` --> src/main.rs:33:20 |33 | println!("{}", o); | ^ `TwoNums` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `TwoNums` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
The compiler here is pointing out that our `TwoNums` type doesnât implement the `Display` trait. In Rust, the`println!()` macro expects the parameters corresponding to the positional arguments of the provided format `str` toimplement this trait. Again, this approach is very similar to how you might integrate a custom-defined type up tothe library-standard APIs in Java. In effect, to implement a `trait` in Rust is to declare anâis-Aâ relationship in Rust. Unlike a lot of other languages,however, the structure is more rigid and you always declare in terms of â<TYPE> is a <TRAIT>â. In this case,we need to make `TwoNums` a type that is _Display_ -able.
# Trait Implementation
Looking at the `trait` definition from above again, the definition defines a single member function (or, _method_)that must be implemented:
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
In this definition, Rust is declaring the following needs from your code:
* The method must be named `fmt`
* The method must return a `Result` type with `()` on success, and an error on failure
* The method will receive `&self` (it must be called on an instance of the type)
* The method will receive a mutable reference to a `Formatter` instance, in var `f` - this is where we write our data
Generally, youâll learn the interface API from the code, but will have to refer to the Rust Docs tounderstand better what Rust expects you to do with these paramters. For the `Display` trait. For our example:
* `std::fmt::Display` trait
Reading the documentation, it explains that you are expected to call the `write!()` macro on simple data types (or othercomplex data types that already implement `Display`) in order to display your data into the buffer (which is provided by `f`). Inthe case of our example, lets say that we want the data to be displayed as a string similar to the following:
[a=1, b=2]
The _implementation_ of the `Display` trait for our custom type `TwoNums` would look like below, and would be implementedas a third independent block of code:
impl fmt::Display for TwoNums { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[a={}, b={}]", self._a, self._b); Ok(()) }}
At the top of the source file, youâll also want to make sure to import the `std::fmt` library, with the following declaration:
use std::fmt;
# Conclusion
Once complete, the compiler error that was caused earlier should no longer occur, and the applicationshould build & run, displaying the following output:
[a=1, b=2]
Having written in a number of different languages over my career, I felt it would be helpful to tryrelating the Rust `trait` feature to some other common mechanisms used in other langauges used toachieve similar modularity and code reuse capabilities.
# Full Source of src/main.rs
The full source code of the final working program, built using the fragments above, is provided below:
use std::fmt;struct TwoNums { _a: u32, _b: u32,}impl TwoNums { fn new() -> Self { Self { _a: 0, _b: 0, } } fn set_a(&mut self, new_a: u32) { self._a = new_a; } fn set_b(&mut self, new_b: u32) { self._b = new_b; } fn get_a(&self) -> u32 { self._a } fn get_b(&self) -> u32 { self._b }}impl fmt::Display for TwoNums { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[a={}, b={}]", self._a, self._b); Ok(()) }}fn main() { let mut o: TwoNums = TwoNums::new(); o.set_a(1); o.set_b(2); println!("{}", o);}