무료한 회사 생활, 즐거운 배움!

막바지를 향해가는 회사생활(?)에 새로운 자극이 필요한 차에 Rust를 배워보고 싶어졌다! 사실 이와 별개로 나의 접을 수 없는 FE 욕심에 Flutter를 공부하고 있긴 한데, 이것만 하긴 좀 심심하기도 해서 세계인이 사랑하는 언어 Rust를 함께 공부해보고 싶어졌다!

그리고 어째선지 내가 즐겨 보는 블로그들에서 Rust를 소개하는 글들이 많아졌는데, 마침 새로운 배울거리를 원하던 나에게 딱 걸려버린것..

Day 1

들어가기 전에

Rust 공부를 시작하면 Cargo를 만나게 된다. Rust Ecosystem에서 Rust 어플리케이션을 빌드하고 실행하는 표준 도구다. 여기서 Rust Ecosystem은 몇 가지 툴로 이루어져있는데, rustc 라는 Rust 컴파일러와 dependency manager인 cargo, Rust toolchain installer & updater인 rustup 이다.

Rust?

Rust는 2015년에 1.0 버전을 출시한 새로운 프로그래밍 언어다. Rust는..

  • C++와 비슷한 역할을 하는 정적 컴파일 언어.
  • LLVM을 백엔드로 사용한다.
  • 다양한 플랫폼과 아키텍처를 지원한다.
  • 펌웨어와 부트로더, 스마트 디스플레이, 휴대전화, 데스크톱, 서버 등 많은 장치에 이용된다.

왜 Rust?

  • 컴파일 타임 memory safety
  • 정의되지 않은 런타임 동작이 적음.
  • 현대적인 언어 특성들.

컴파일 타임에 보장되는 것들

  • 초기화되지 않은 변수 없음
  • 메모리 누수 없음 (거의)
  • double free 없음
  • use-after-free 없음
  • null pointer 없음
  • 잊혀진 locked mutexes 없음
  • 스레드간 data races 없음
  • iterator invalidation 없음

가능한가..?

런타임에 보장되는 것들

  • 배열 접근이 바운드 확인.
  • Integer overflow 정의됨.

가능한가..?

현대적인 특성들

  • 언어 특성
    • 열거형(Enums)과 패턴 매칭
    • 제네릭
    • No overhead FFI (모르겠음)
    • Zero-cost abstraction (모르겠음)
    • 훌륭한 컴파일러 에러
    • 빌트인 의존성 매니저
    • 빌트인 테스트 지원
    • 훌륭한 서버 프로토콜 지원

메모리 관리

전통적으로 메모리 관리하는 두 가지 방식이 있다.

  • 직접 관리하는 방식: C, C++, Pascal..
  • 런타임에 자동으로 관리하는 방식: Java, Python, Go, Haskell, ..

Rust는? 컴파일 타임에 올바른 메모리 관리를 강요함으로서 완전한 제어와 안정성을 확보한다.

Stack vs Heap

  • Stack
    • 컴파일 타임에 고정된 크기를 가짐.
    • 엄청 빠름 (스택 포인터를 옮기기만 하면 되니까)
    • 관리하기 쉬움 (function call 따라가기)
    • 뛰어난 메모리 지역성 (locality)
  • Heap
    • 런타임에 유동적인 크기
    • 스택보다 조금 느림.
    • 메모리 지역성 보장 없음

Ownership of Rust

fn say_hello(name: String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name); // 최초로 say_hello를 호출하는 순간 main은 name에 대한 ownership을 포기한다.
    // say_hello(name); // 주석을 해제하면 compile error!
    // 이를 피하려면 say_hello 함수의 시그니쳐를 call by reference 형태로 변경하고 name의 reference(&name)을 넘기거거나, 명시적으로 .clone()을 이용해야 함.
}

Borrowing

함수를 호출할 때 ownership을 전달하는 방법 대신, 값을 빌려줄(borrwoing) 수 있다.

#[derive(Debug)]
struct Point(i32, i32);

fn add(p1: &Point, p2: &Point) -> Point {
    Point(p1.0 + p2.0, p1.1 + p2.1)
}

fn main() {
    let p1 = Point(3, 4);
    let p2 = Point(10, 20);
    let p3 = add(&p1, &p2); // add function이 두 Point를 빌려받아 새로운 Point를 반환한다.
    println!("{p1:?} + {p2:?} = {p3:?}");
}

Rust에서 borrow 제약 조건

어떤 값(T)에 대해

  • 동시에 하나 이상의 &T 값들 혹은
  • 동시에 하나의 &mut T 값 만 가질 수 있다. (위 두 조건은 mutual exclusive)
fn main() {
    let mut a: i32 = 10;
    let b: &i32 = &a;
    
    // println!("b: {b}"); // 아래 print문을 위로 옮기면 b는 c가 존재하는 시점에 useless 하다는 것을 컴파일러가 알기때문에 에러 없음. 
    {
        let c: &mut i32 = &mut a;
        *c = 20;
    }

    println!("a: {a}");
    println!("b: {b}"); // compile error! c가 mut a를 빌려갔기 때문에 규칙에 위배!
}

빌려온 값은 수명이 있다.

  • 수명은 생략될 수 있음.
  • 명시할 수도 있음(&'a Point)
  • &'a Point는 최소 a의 수명만큼 유효한 빌려온 Point라고 해석.
  • 수명은 항상 컴파일러에 의해 추론되고, 직접 할당할 수 없음(할당과 명시는 다름).
  • 수명은 제약 조건을 생성하며, 컴파일러가 유효한 솔루션을 확인한다.

Resources