252297167 © Yuri Arcurs | Dreamstime.com
Rust Dreamstime 252297167 Promo

An Objective Look at Rust

Oct. 12, 2022
Rather than implementing an object-oriented programming architecture, the Rust language offers modular programming support, using traits rather than classes.

This article is part of TechXchange: Rusty Programming

What you’ll learn

  • How Rust supports objects.
  • What are Rust’s traits?
  • Cross-language interface challenges.

C++ and Java follow a traditional object-oriented programming (OOP) approach that uses a hierarchical class structure with inheritance for objects. They both support abstract classes to provide interface definitions. We won’t delve into these details as they get quite complex; instead, we’ll examine how Rust provides modular programming support since it doesn’t follow the OOP architecture.

Rust doesn’t have classes, rather it has traits. Check out Reading Rust for C Programmers if you haven’t scanned any Rust code.

To show the basics, we implement a simple stack with a limited number of functions, including push, pop, and top (of stack) using fixed-size elements. This isn’t a practical implementation as the Rust standard Vector (Vec) type does all this and more, such as handling any element type.

Likewise, this presentation is designed to highlight the basic use of traits versus class-based OOP; thus, don’t necessarily use any of this example in an application. Also, traits exist in other programming languages, including C++ and Java.

Implementing a Basic Stack

To start with, a trait essentially defines an interface similar to an abstract class in other languages. The main difference is that the trait only defines the methods used and not the structure normally referenced by the keyword self. In this case, we define a very basic Stack interface trait for usize elements.

trait Stack {
    fn push(&mut self, value:usize);
    fn pop(&mut self);
    fn top(self) -> usize;
}

Next, we define a particular structure for our stack that will hold 10 elements. We also include a Default implementation to initialize our Stack10 structure. Again, this isn’t how someone would actually want to implement even a simple stack, because it’s using a fixed number of elements and the element type is usize. Rust is adept at supporting definitions that are template- and generic-oriented, but that’s for another article.

The definition we use assumes Rust’s zero-based single-dimension arrays. No explicit bounds checking is done, although implicit runtime checks would catch things like underflow errors.

#[derive(Copy,Clone)]
struct Stack10 {
  values: [usize; 10],
  index: usize,
}

impl Default for Stack10 {
    fn default() -> Self {
        Stack10 {
            values: [0_usize; 10],
            index: 0,
        }  
    }
}

The #[derive(Copy,Clone)] line defines the Copy and Clone traits for Stack10 that are used implicitly later in the program. The Default trait is used to initialize our structure. There are other ways to do this, but this seemed like the simplest approach. The values array is initialized with zeros and the index value is set to zero as well. It also is the number of elements in the stack.

As an aside, the initialization of values is done with a value of 0_usize. It’s Rust’s way of writing a typed literal value. In this case, we could have written just 0 as the type is implied. However, Rust leans toward explicit specification to let the compiler know the programmer’s intention.

Now that we have our traits and structure defined, we can define the implementation of the functions/methods for Stack10. Again, this is explicit rather than a more typically generic approach to defining the implementation. Push just saves the parameter value and updates the index while pop simply decrements the index. The top method returns the current top of stack.

impl Stack for Stack10 {
    fn push(&mut self, value:usize) {
        self.values[self.index] = value;
        self.index = self.index + 1;
    }
    fn pop(&mut self) {
        self.index = self.index - 1;
    }
    fn top(self) -> usize {
        return self.values[self.index-1];
    }
}

Now for a sample application that uses the structure and definitions. The main function starts by initializing the Stack10 variable named x. It then does some pushes and a pop followed by printing the current state. Note the x.pop at the beginning that has been commented out. It would generate a runtime error because the pop method would try to decrement index from zero to -1. As index is unsigned, this underflow causes a runtime error.

fn main() {
    let mut x:Stack10 = Default::default();
    // x.pop(); // will generate a runtime error
    x.push(10);
    x.push(11);
    x.push(12);
    x.pop();
    println!("top is {}",x.top());
    println!("index is {}",x.index);
    println!("stack is {:?}",x.values);
}

This is the printed result for our simple program:

top is 11
index is 2
stack is [10, 11, 12, 0, 0, 0, 0, 0, 0, 0]

We can handle one type of change to our system with our current trait definition, but not another. The first would be to change the number elements since the Stack trait doesn’t explicitly reference that. Though we could easily define a Stack20 or Stack100 for different size stacks, the type of element remains usize. That’s the part we can’t change via the implementation alone. Rust generic templates can address such a situation. The definition would look like this:

trait Stack {
    fn push(&mut self, value:T);
    fn pop(&mut self);
    fn top(self) -> T;
}

The rest of the code needs to change to address this change and it’s possible to use constant parameters to specify the size of the array.

Rusty Polymorphism

So far, we’ve been dealing with static definitions. The compiler can figure out which underlying function needs to be called. Rust does support dynamic dispatch akin to virtual class methods in other OOP languages, but how it works as well as the syntax is a bit different.

For this part, we come up with a new trait for an Animal and generate a collection of animals with different attributes. Here, we just have different implementations for the name function that prints the name of the animal. Obviously, the trait’s set of functions and the structures used can be more complex than our example, but the idea is the same.

trait Animal { fn name (&self); }
struct Rabbit; 
impl Animal for Rabbit { fn name(&self) { println!("Rabbit"); } } 

struct Cat; 
impl Animal for Cat { fn name(&self) { println!("Cat"); } } 

struct Dog; 
impl Animal for Dog { fn name(&self) { println!("Dog"); } } 

fn main() 
{
  let mut animals:
  Vec box= Vec::new();
  animals.push(Box::new(Dog));
  animals.push(Box::new(Rabbit));
  animals.push(Box::new(Cat));
  for animal in animals.iter() 
  {
    animal.name();
  } 
}
This example also used the built-in Vec type, which implements a dynamically expandable array of elements. The type includes functions like push and pop, but the type is much more complex than our prior example. We also take advantage of the Box trait to package up structures being pushed into the vector.

The line that defines animals include Box and the dyn keyword. The actual elements in the array are a pair of pointers (see figure). One points to the actual elements structure, while the other points to a jump table that matches the trait. Rust knows to use the jump table to find the function in the line that prints the animal name, animal.name().

Of course, in our example, there’s no structure and the jump table has only one function. However, in more advanced applications, the structures can vary in size and more than one function will likely be referenced by the table.

Languages like C++ use jump tables for objects associated with classes that have virtual methods, but the object structures put the jump table pointer at the start of the object. This has the advantage of only needing one point. The disadvantage, though, is the data structure is now prefixed with a pointer. This isn’t much of a problem if the object is a standalone item, but if it’s embedded in another structure, it may not be as desirable. 

Mixing Rust with C++

While it’s possible to interface Rust with C and C++, it’s typically done using the lowest common denominators: standalone functions, basic structures, and basic data values like integers. It’s possible to replicate Rust’s polymorphic structure in C and vice versa, but that can be somewhat arduous. The same is true for traits and C++’s OOP support.

Things become more challenging if Rust is mixed with code written in other programming languages when dealing with Rust’s memory management and other checking, which are very important for Rust programmers. C has almost no checking, while languages like Rust, C++, and Ada have a considerable amount. Such checking is designed to make it easier for programmers as well as help reduce errors by having the compiler check the application.

If you want to play with Rust without installing on your machine you just need a browser to access https://play.rust-lang.org/. You can paste the examples here or explore Rust. The tool is limited, but if you’re experimenting with Rust. you probably don’t have a couple megabytes of source code.

Another resource that may be useful to C++ programmers is MaulingMonkey’s C++ vs Rust page. It includes a number of links to Rust-related resources. It comes with a useful warning:

“WARNING: Terrible explainations (sic) of Rust, by someone who doesn't really know it, can be found below.”

I could probably add that warning here, since I’ve not done a lot of applications in Rust to date.

Read more articles in TechXchange: Rusty Programming

Sponsored Recommendations

Near- and Far-Field Measurements

April 16, 2024
In this comprehensive application note, we delve into the methods of measuring the transmission (or reception) pattern, a key determinant of antenna gain, using a vector network...

DigiKey Factory Tomorrow Season 3: Sustainable Manufacturing

April 16, 2024
Industry 4.0 is helping manufacturers develop and integrate technologies such as AI, edge computing and connectivity for the factories of tomorrow. Learn more at DigiKey today...

Connectivity – The Backbone of Sustainable Automation

April 16, 2024
Advanced interfaces for signals, data, and electrical power are essential. They help save resources and costs when networking production equipment.

Empowered by Cutting-Edge Automation Technology: The Sustainable Journey

April 16, 2024
Advanced automation is key to efficient production and is a powerful tool for optimizing infrastructure and processes in terms of sustainability.

Comments

To join the conversation, and become an exclusive member of Electronic Design, create an account today!