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.
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.
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.
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.
This is the printed result for our simple program:
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:
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.
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