Stephen Smith's Blog

Musings on Machine Learning…

Rust Never Sleeps

leave a comment »


Introduction

Rust is a relatively new programming language that first appeared in 2010. The purpose of Rust is to provide a systems programming language with the performance of C/C++ but without all the memory/pointer/multithreading/security problems. Version 6 of the Linux kernel will be adding support for writing modules in Rust and many companies have been writing their backend web servers in Rust.

I thought I’d have a look at programming in Rust and had a go on both a regular Raspberry Pi as well as with embedded Rust on the Raspberry Pi Pico. In this article we’ll look at a simple program to generate a Koch snowflake, a relatively simple program that still shows some of the features of Rust. The program listing is at the end of the article.

How to Create Graphics?

Rust doesn’t include a giant do everything runtime like C# has in .Net or Java has in the JRE. Instead people outside of the core language group have developed wrapper libraries of various commonly used systems. For GUI and graphics, I chose to use the wrapper for GTK3 as this seems to have good support and the included Cairo 2D drawing library is fairly good.

This works, but you can’t write GUI type design patterns like MVC in Rust because their memory management schemes are inherently unsafe or require garbage collection. Rust enforces the lifetime of every object and ensures there are no hanging references, this makes the sea of objects model most GUIs use where each object has references to a bunch of other objects illegal. Rust recommends alternate design patterns, but if you are wrapping an existing legacy library then you will have to work around a few issues.

Installing Rust and Required Libraries

Rust is easy to install, but it only installs Rust components. Any wrapped components need to be installed separately, in our case GTK3’s development components:

    sudo apt-get install libgtk-3-dev

Now we can install Rust, using rustup from their website: https://rustup.rs/. The current command to do this is:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

But best to copy the command from their website, in case it’s been updated. Then at the prompt select “1 – proceed with installation” to download and install Rust.

The installation adds a number of environment variables to the shell initialization scripts, to proceed you need to either restart a new terminal window or run:

    source "$HOME/.cargo/env"

Now we can create our Rust project, first creating a folder for our Rust projects and then using Rust’s cargo utility to create a project.

    mkdir rust
    cd rust
    cargo new fractal1 --bin --vcs none

The “–vcs none” part is to start cargo creating a github repository as well. We now have a working Rust program, which if we run with:

    cargo run

Will compile the simple program and print “Hello World”. The first time you run, the compilation takes a while since there are a number of system components that need to be compiled the first time.

To produce the Koch snowflake fractal, copy the Cargo.toml file at the bottom of this article over the one created by cargo and then copy the main.rs file over the created file in the src folder.

With these in place, type:

    cargo run

Then after everything compiles, the program should run and produce a window with the Koch snowflake at level 3.

Notes on the rust.rs

The GTK3 wrapper is from https://gtk-rs.org/. I based the program on one of the examples in their Github repository. I found these examples to be the best source of information on the usage of this library as the documentation tends to be of a reference nature and doesn’t show how to put the parts together into a working program.

If you’ve seen any of my other blog articles where I created a Koch snowflake as an example, then you might wonder why I don’t store the Cairo drawing context in the TurtleGraphics struct rather than having it as a parameter to most method calls. The reason is Rust’s rules around avoiding dangling pointers, where you need to ensure the lifetime rules of every object. This means you can’t just store references in objects without guaranteeing their lifetime is less than the reference. In most practical cases this is difficult and the hoops to jump through are far worse than just passing it as a parameter. Not as object oriented perhaps, but on the other hand, no dangling pointers and no garbage collection.

Summary

Rust wants to replace C as a system programming language for projects such as the Linux kernel, web servers and even video games. Rust isn’t a simple language, it is large and some of its parts are quite complex. The safety rules to ensure that pointers are correct place a lot of limitations on what you can do as a programmer. The Rust parts of your program are safer than corresponding C programs, but with the state of things today, your Rust program will wrap a lot of components written in C/C++ that could be doing all sorts of bad things with pointers. The other limitation is that you can’t easily call Rust objects from other languages, limiting some interoperability scenarios.

I’m going to continue learning and playing with Rust. It has a number of appealing features, but I’m not sure if its complexity and strict enforcement of lifetime and ownership rules are too constraining.

Source Code Listings

The Cargo.toml file:

[package]
name = “fractal1”
version = “0.1.0”
edition = “2021”

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
gtk = “*”
gio = “*”
gdk = “*”
cairo = “*”

The main.rs follows:

use std::f64::consts::PI;
use gtk::prelude::*;
use gtk::DrawingArea;
use gtk::cairo::{Context};

const STARTX:f64 = 50.0;
const STARTY:f64 = 130.0;
const STARTANGLE:f64 = 0.0;
const LEVEL:i64 = 3;

fn build_ui(application: &gtk::Application) {
    drawable(application, 500, 500, |_, cr| {
        cr.set_source_rgb(0.0, 0.0, 0.0);
        cr.set_line_width(1.0);
        cr.move_to(STARTX, STARTY);
        let tg = TurtleGraphics{x:STARTX, y:STARTY, angle:STARTANGLE};
        let mut kf = KochFlake{tg: tg};
        kf.koch_snowflake(cr, LEVEL);
        cr.stroke().expect(“Invalid cairo surface state”);
        Inhibit(false)
    });
}

fn main() {
    let application = gtk::Application::new(
        Some(“com.github.gtk-rs.examples.cairotest”),
        Default::default(),
    );
    application.connect_activate(build_ui);
    application.run();
}

pub fn drawable<F>(application: &gtk::Application, width: i32, height: i32, draw_fn: F)
where
    F: Fn(&DrawingArea, &Context) -> Inhibit + ‘static,
{
    let window = gtk::ApplicationWindow::new(application);
    let drawing_area = Box::new(DrawingArea::new)();
    drawing_area.connect_draw(draw_fn);
    window.set_title(“Koch Snowflake”);
    window.set_default_size(width, height);
    window.add(&drawing_area);
    window.show_all();
}

struct KochFlake
{
    tg: TurtleGraphics
}

impl KochFlake
{
    pub fn koch_snowflake(&mut self, cr:&Context, level:i64)
    {
        self.tg.turn( 60 );
        self.koch_snowflake_side(cr, level , 400);
        self.tg.turn( -120 );
        self.koch_snowflake_side(cr, level, 400);
        self.tg.turn( -120 );
        self.koch_snowflake_side(cr, level, 400);
    }

    pub fn koch_snowflake_side(&mut self, cr:&Context, level:i64, size:i64)
    {
        if level == 0
        {
            self.tg.move_dist( cr, size );
        }
        else
        {
            self.koch_snowflake_side( cr, level – 1, size / 3 );
            self.tg.turn( 60 );
            self.koch_snowflake_side( cr, level-1, size / 3);
            self.tg.turn( -120 );
            self.koch_snowflake_side( cr, level-1, size / 3);
            self.tg.turn(60);
            self.koch_snowflake_side( cr, level-1, size / 3);
        }
    }    

}

struct TurtleGraphics
{
    x:f64,
    y:f64,
    angle:f64
}

impl TurtleGraphics
{
    pub fn move_dist(&mut self, cr:&Context, dist:i64)
    {
        self.x = self.x + dist as f64 * f64::cos( self.angle * PI / 180.0);
        self.y = self.y + dist as f64 * f64::sin( self.angle * PI / 180.0);
        cr.line_to(self.x, self.y); 
    }

    pub fn turn(&mut self, angle_increment:i64)
    {
        self.angle = self.angle + angle_increment as f64;
    }
}

Written by smist08

August 8, 2022 at 12:37 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: