Skip to content

Rust for ActionScript developers

Matheus Dias de Souza edited this page Apr 19, 2024 · 19 revisions

This ActionScript 3 parser is written in the Rust language, so all its modules are structured in a way specific to the Rust language.

This article will highlight main differences you might see from using a parser written in ActionScript 3 and a parser written in Rust.

Cargo

Cargo is the package manager integrated in the Rust installation.

When you publish your Cargo package to the public (the crates.io website), documentation is automatically generated by the docs.rs service.

Ownership

Rust features a type model that includes ownership rules.

The types &str and String are used to represent UTF-8 encoded text. The difference between them is:

  • &str is a slice of an existing string somewhere in memory. It is a "borrow", or a reference.
  • String is a string you own.

Rust implicitly converts &String to &str, where &String is a borrow of a String.

You may convert a &str to a String with .into() or .to_owned().

Move Semantics

In Rust, a variable whose type does not implement Copy is moved out of scope once it is passed to another function or variable.

Types that do not implement Copy include String, shared pointers such as Rc<T> and Box<T> and in general "owned" types.

To avoid moving unintentionally, you either borrow (&x) or clone (x.clone()) the variable.

let x = String::new();
let y = x;
let z = x; // ERROR

Borrowing and References

Borrow types, or reference "ref" types, are interchangeable terms in Rust. The borrow type &'my_lifetime T, or simply &T, represents a reference to data T at the lifetime 'my_lifetime.

Lifetimes are inferred or explicitly created in most places of Rust programs, thus omitted.

Note that the 'static lifetime is reserved and has a special meaning: the entire execution of the program.

fn m1<'my_lifetime>(argument: &'my_lifetime str) {}

// Equivalent
fn m2(argument: &str) {} 

There are "immutable" borrows and "mutable" borrows (&'my_lifetime mut T, or simply &mut T)

Mutability

Variables are immutable by default. mut is used to indicate they are immutable.

let x = 0;
let mut x = 0;

Based on explanations from the Rust community:

  • When a variable is immutable, it is transitively immutable.
  • When a variable is mutable, it is transitively mutable.

A mutable variable may be freely used as immutable variable. For example, &mut T implicitly converts to &T.

Cells

There are cases where the Rust's mutability concept is limited. You may sometimes use Cell<T> or RefCell<T> instead to contain your T data.

  • Use Cell for stack resources.
  • Use RefCell for heap resources.
use std::cell::Cell;

struct ExampleA {
    x: Cell<f64>,
}

impl ExampleA {
    pub fn new() -> Self {
        Self {
            x: Cell::new(0.0),
        }
    }

    pub fn x(&self) -> f64 {
        self.x.get()
    }
}

Destructuring and Pattern Matching

There are several ways to do pattern matching.

let group: (f64, f64) = (0.0, 0.0);
let (x, y) = group;

#[derive(Copy, Clone)]
enum E1 {
    M1(f64),
    M2,
}

let m1 = E1::M1(0.0);
let E1::M1(x) = m1 else {
    panic!();
};
if let E1::M1(x) = m1 {
    println!("{x}");
}
match m1 {
    E1::M1(x) => {
    },
    E1::M2 => {
    },
}
let q: bool = matches!(m1, E1::M1(_));

When you need to match a pattern in a Rc<T> type (that is, a reference counted type), you need to call .as_ref() in the Rc value.

// m1: Rc<E1>
match m1.as_ref() {
    _ => {},
}

Inheritance

Rust favours composition over inheritance. Further, there is no equivalent of the Java classes in Rust.

The programming patterns to use instead of inheritance where it is required vary:

  • Use a structure nested in another structure
struct ExampleA {
    example_b: Option<Box<ExampleA>>,
}
struct ExampleB(f64);
  • Use an enumeration whose variants may contain additional data
enum Example {
    A(f64),
    B {
        x: f64,
        y: f64,
    },
}
  • Use a trait. Traits are non opaque data types that may be implemented by opaque types (struct, enum, and primitive types). They are similiar to Java interfaces in certain ways.
trait ExampleTrait {
    fn common(&self) -> f64;

    fn always_provided(&self) {
        println!("Foo");
    }
}
struct Example1;
impl ExampleTrait for Example1 {
    fn common(&self) -> f64 {
        0.0
    }
}

Shared Pointers

Rc<T> (use std::rc::Rc;) represents a reference to T managed by reference counting.

Box<T> represents a heap reference to T, similiar to C++'s unique_ptr.

Deriving Traits

Traits may be

Implementations

struct ExampleStruct(f64);

impl ExampleStruct {
    pub fn method(&self) {
        println!("example_struct.0 = {}", self.0);
    }
}

let o = ExampleStruct(10.0);
o.method(); // example_struct.0 = 10.0

Error Handling

Unchecked exceptions that you arbitrarily throw are not a thing in Rust.

In Rust, there are "panics" (fatal exceptions or crashes) and functions that return Result<T, E>, where E is the error type and T is the result data type.

fn function_that_panics() {
    panic!("Panic message");
}

enum MyError {
    Common,
}
fn function_that_throws() -> Result<f64, MyError> {
    if true {
        Ok(10.0)
    } else {
        Err(MyError::Common)
    }
}

Propagate the error from another function by using the ? operator:

fn another_function() -> Result<f64, MyError> {
    function_that_throws()? * 2
}

Nullability

let mut x: Option<f64> = None;
x = Some(10.0);
if let Some(x) = x {
    println!("{x}");
}

Modules

lib.rs

mod ns1;

ns1.rs

mod foo;

ns1/foo.rs

pub static BAR: &'static str = "Bar";

Visibility

Everything is internal to the enclosing module by default, unless the pub qualifier is used.

There is pub, and pub(module_path).

Module paths

crate // The main module of your Rust crate
self // The enclosing module
super // The parent of the enclosing module

Importing Modules

use foo::bar::*;
pub use foo::qux::*;
use super::Something;

Constants

const versus static variables: there are certain differences between them. Also, const may appear in impl blocks, which allows for extra readability.

const EXAMPLE_1: f64 = 1_024.0 * 4;

struct ExampleStruct;
impl ExampleStruct {
    pub const EXAMPLE_CONST: &'static str = "Example string";
}

Constants are expanded ("inlined") wherever used, while statics are single memory locations.

Clone this wiki locally