Monday, April 25, 2022

Go vs Rust - Unsafe

Go

If you have a background of C language, you must know what pointer means and its usage. With pointer, you are free to operate any data at memory level which means you have great power, but this means that you have great responsibility as well. That's why it might be considered unsafe in lots of cases.

package main
import "fmt"
func double(x *int) {
    *x += *x
    x = nil
}
func main() {
    var a = 3
    double(&a)
    fmt.Println(a) // 6
    p := &a
    double(p)
    fmt.Println(a, p == nil) // 12 false
}

The major restrictions of pointer in GoLang are:

  • No mathematic operations can be performed on pointer
  • Cannot convert between different types of pointer
  • Different types of pointer cannot be compared with == or !=
  • Cannot assign one type of pointer to another type of pointer

GoLang Unsafe

There is also type unsafe pointer, it is unsafe.Pointer residing in unsafe package.

In unsafe package, there is a Pointer defined.

type ArbitraryType int
type Pointer *ArbitraryType

This is similar to void* in C. Also three additional functions are defined.

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

Sizeof returned the number of bytes x takes, it doesn't count the actual number of bytes its content takes. 

Offsetof returns the position where one member of a struct within the struct away from the beginning position of the struct

Alignof returns m which means the number of bytes which can be divided when align the memory in the struct.

unsafe package provide two important features:

  • Any pointer can be converted to unsafe.Pointer and vice versa
  • uintptr can be converted to unsafe.Pointer and vice versa

Get or update value of unexported property in struct

package main
import (
    "fmt"
    "unsafe"
)
type Programmer struct {
    name string
    language string
}
func main() {
    p := Programmer{"stefno", "go"}
    fmt.Println(p)
    name := (*string)(unsafe.Pointer(&p))
    *name = "qcrao"
    lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
    *lang = "Golang"
    fmt.Println(p)
}


Rust

Unsafe Superpowers

To switch to unsafe Rust, use the unsafe keyword and then start a new block that holds the unsafe code. You can take five actions in unsafe Rust, called unsafe superpowers, that you can’t in safe Rust. Those superpowers include the ability to:

  • Dereference a raw pointer
  • Call an unsafe function or method
  • Access or modify a mutable static variable
  • Implement an unsafe trait
  • Access fields of unions

Dereferencing a Raw Pointer

Different from references and smart pointers, raw pointers:

Are allowed to ignore the borrowing rules by having both immutable and mutable pointers or multiple mutable pointers to the same location
Aren’t guaranteed to point to valid memory
Are allowed to be null
Don’t implement any automatic cleanup

we can create raw pointers in safe code, but dereference raw pointers and read the data being pointed to that requires an unsafe block.

    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }

Calling an Unsafe Function or Method

    unsafe fn dangerous() {}
    unsafe {
        dangerous();
    }

Creating a Safe Abstraction over Unsafe Code

use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();
    assert!(mid <= len);
    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

Using extern Functions to Call External Code

Sometimes, your Rust code might need to interact with code written in another language. For this, Rust has a keyword, extern, that facilitates the creation and use of a Foreign Function Interface (FFI).

extern "C" {
    fn abs(input: i32) -> i32;
}
fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

We can also use extern to create an interface that allows other languages to call Rust functions. Instead of an extern block, we add the extern keyword and specify the application binary interface (ABI) to use just before the fn keyword. We also need to add a #[no_mangle] annotation to tell the Rust compiler not to mangle the name of this function. Mangling is when a compiler changes the name we’ve given a function to a different name that contains more information for other parts of the compilation process to consume but is less human readable. 

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

Accessing or Modifying a Mutable Static Variable

Static variables are similar to constants.  Static variables can only store references with the 'static lifetime, which means the Rust compiler can figure out the lifetime and we aren’t required to annotate it explicitly. Accessing an immutable static variable is safe.

Constants and immutable static variables might seem similar, but a subtle difference is that values in a static variable have a fixed address in memory. Using the value will always access the same data. Constants, on the other hand, are allowed to duplicate their data whenever they’re used.

Another difference between constants and static variables is that static variables can be mutable. Accessing and modifying mutable static variables is unsafe. 

static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}
fn main() {
    add_to_count(3);
    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

Implementing an Unsafe Trait

Another use case for unsafe is implementing an unsafe trait. A trait is unsafe when at least one of its methods has some invariant that the compiler can’t verify. We can declare that a trait is unsafe by adding the unsafe keyword before trait and marking the implementation of the trait as unsafe too.

unsafe trait Foo {
    // methods go here
}
unsafe impl Foo for i32 {
    // method implementations go here
}

Accessing Fields of a Union

The final action that works only with unsafe is accessing fields of a union. A union is similar to a struct, but only one declared field is used in a particular instance at one time. Unions are primarily used to interface with unions in C code. Accessing union fields is unsafe because Rust can’t guarantee the type of the data currently being stored in the union instance

No comments:

Post a Comment