A taste of Rust

orignal URL : http://lwn.net/Articles/547145/

 

Rust, Mozilla project 에서 여러가지 새로운 feature들을 추가한 새로운 언어이다. 새로운 feature 중에 "safety"에 중점을 맞추었다. compiler level에서 감지하고 예방할 수 있는 error의 범위를 증가시키는 노력을 하여 최종 product code에서 error 가 감소하도록 했다.


"safety" 를 증가시켜주기 위한 언어들의 디자인의 방법에는 일반적으로 두가지가 있다. 하나는 좋은 code를 쉽게 만들수 있도록 지원하는 것이고, 다른 하나는 나쁜 code 만드는 것을 어렵게 하는 것이다. 전자는 여지껏 구조적 programming, 다양한 type, 향상된 encapsulation(high-level 개념으로 개발자의 목표를 쉽게 표현할 수 있도록 하는) 통해 많은 성공을 거두었다. 후자의 접근은 전자와 같이 널리 발전하지도, 성공하지도 못한 상태인데 나쁜 code를 만들게 되는 기능을 제거하는 것이 필요하기 때문이다. 물론 잠재적으로 위험한(?) 기능들은 대게 매우 쓸모있다. 전형적인 예제로는 goto 문이 있는데, 알려진 error를 내포할 수도 있는 것이지만 아직가지 절차적 program 언어에서 사용되고 있다. 


어째든 Rust 언어는 두 번째 접근 방법을 이룰 수 있도록 한 언어이다. C 언어나 java 같은 언어에서 제공하는 몇개의 feature를 사용하지 않거나 매우 제약적으로 Rust 에서는 사용된다. Rust는 이와 같은 feature를 사용하지 않는 것을 보완하기 위해 충분히 지원하도록 노력했다. 이 중 한 방법으로 Rust 에서는 다른 언어에서 Runtime 에 발생할 법한 문제를 compile time에서 발견할 수 있도록 노력했다. 이 것은 Runtime debugging의 비용을 줄일 뿐만 아니라 고객으로 부터 지원 요청의 횟수를 줄일 수도 있다. 


다른 언어에서 사용하는(structures, arrays, pointers 등) type을 Rust 에서도 지원한다 하지만 대게 제한적으로 운영이 된다. 개발자가 진정으로 말하기 원하는 것과 유사하게 모양을 갖추고, type의 확장 및 control 방법등 다양한 방법이 있다. 


Rigidly defined areas of doubt and uncertainty


low-level 기능의 접근을 제공하는 Modular-2 언어에서 system module을 반영하는 움직임으로 Rust 는 "unsafe" 로 선언하여 사용되는 함수와 code를 허용한다.  (있긴 하지만 safety한 code를 위해 반영에 조심하는 듯)


이것은 진정으로 safe 한 언어를 제공하는데 실패라고 볼 수 있지만 그것은 너무 단순하게 생각한 것이다. 

우선, Gödel's incompleteness theorem에서는 어떤 programming 언어서든 많이 충분히 기능을 제공해야 한다. 그것이 비록 "safe"한 것을 고려하지 못하더라도. 그러나 경험적인 level에서 볼때, code가 compiler에 의해 생성된 program context 가 "unsafe" 한 것을 갖고 있다는 것을 볼 수 있다. bug는 compliler 가 발견할 수 있다는 어떤 희망도 없을 때 잘못된 행동을 할 수 있는 가능성이 높을 것이다. 사실 완벽하게 safety 한 것은 불가능하다.


"unsafe" 한 code가 있는 것은 피할 수 없다고 볼때, compiler 가 target program에 unsafe 했던 부분을 나타내게 하던가 target language 의 library 에 구현을 하는 것으로 하는 것이 가능하다. 이러한 선택은 "unchecked" 괸 code 가 허용되는 부분에 명백히 표시를 해두게 되면 다른 부분은 굉장히 "safe"한 영역이 될 수 있다는 것이다. 또한 code reviewer에 의해 주의깊게 볼 수 있도록 표시를 해두는 것이며 이로 인해 개발의 편의성을 더 둘 수 있다는 것이다.


만약 Rust compiler(rust 로 만들어졌다)를 보게 된다면, 우리는 111 개의 소스 code가 있는 것을 알 것이고 그 중에 33개의 "unsafe"한 것을 포함한다는 것도 알 수 있다. 30%는 잠재적인 안전하지 않는 것을 포함하지만 70%는 특정 error 를 확실히 포함하지 않는 것을 보장 하고 이것은 좋은 code 이다라고 생각할 수 있다.


Safer defaults


Rust 가 어떻게 safety를 증가시켰는지 살펴 보도록 하자.


다른 언어에서 "const"(C, Go) 나 "final"(Java)와 같은 keyword 를 가지고 한번의 생성으로 같은 값을 항상 참조할 수 있도록 만들었는데 Rust 는 반대로 접근했다. 바꿀 수 없는(like const)를 default로 가지고 수정할 수 있는 변수에는 "mut" keyword 를 쓰게 했다. 

예제)

     let pi = 3.1415926535;

pi 는 바꿀 수 없는 변수이다. 하지만, 

     let mut radius = 1.496e11;

은 나중에 변경 가능한 변수가 된다. mut 의 선택은 mutable의 모음만을 기록한 것이다. 개발자가 변경가능한 변수에 대해 현명하게 사용할 수 있도록 한번 더 생각하게 하는 그런 취지(?) 인듯 하다.


Pointers are never NULL


C 에서 대게 pointer의 error 는 pointer 연산이  제한적이고 하지 말아야 하는 행동에 의해 생긴다. Rust 는 NULL pointer 를 허가하지 않고 dereference pointer 로 의 접근은 유효한 object 를 찾아내서 결과를 return 할 것이다. 만약 정말로 없는, 유효하지 않는 pointer를 사용하려고 하면 compile-time에 찾아낼 수 있다.


유효하거나 NULL 인지를 판단함으로써 linked list나 이진 트리에서 data 의 입력이나 검색으로 사용하는 경우가 많다. 이를 위해 Rust library 에서는 "Option' 이라는 parameter type을 제공한다.


만약 T 가 어떤 type이라면, Option<T> 는 type T 에 연관된  Some tag 와 None tag을 포함하는 variant record 이다. 

Rust 는 "match" 문을 제공한다.(다른 언어의 "case" 나 "typecase"의 일반화한 것이다.) 


Option 의 정의

    pub enum Option<T> {

        None,

        Some(T),

   }

예제,

   struct element {

       value: int,

       next: Option<~element>,

   }

위의 element 구조체는 정수 linked list 를 만드는데 사용할 수 있다. element 의 ~ 는 구조체의 pointer를 표현한 것이며 C 에서 *를 사용하는 것과 같다. 나중에 pointer의 다른 type을 살펴 볼 것이다. ~(tilde)는 "owned" pointer 이고 @(sign)은 "managed" pointer 이다.


null-able pointer는  Rust에서 가능하지만 기본적으로 safe 하게 구현하기 위해서는 pointer는 NULL이 될 수 없다. 만약 null-able 한 포인터를 사용하기 위해서는 명시적으로 검증이 되어야 한다.

예를 들면,

   match next {

     Some(e) => io::println(fmt!("%d", next.value)),

     None      => io::println("No value here"),

  }


Rust 에서 배열("vector" 라고 부른다)은 배열의 index로 부터 접근하기 전에 range 내부에 있는지 compiler가 알수 있도록 할 필요가 없다. 그것은 추정컨데 많은 경우에 index가 범위안에 있다는 것을 type을 통해 알수 있다. (이것도 pointer와 비슷하단다) 


이와 같은 것으로 runtime 에 NULL pointer 참조는 발생하지 않을 것이다. 하지만 array 의 bound error는 runtime error로 발생할 수 있다. 그것은 고의적인지 아닌지 알수 없고, 나중에 변경 되어 수정되는 것인지도 알 수 없다.


Parameterized Generics


Rust 에서는 "void" pinter 나 down casts(예를 들면 void 에서 특정한 type으로 casting) 를 허가하지 않는다. 이런 사용을 정당화 하기 위해 parameterized type 과 함수를 사용하여 지원한다. 이것은 C++ 에서 사용되는 template와 java에서 Generic Class 와 형태가 유사하다. Rust 에서 Generic(이에 대한 정의는 여기를 참조)은 단순한 syntax를 가지고 어떤 type이든 함수든 간에 type parameter들을 가질 수 있다.


예제)

    struct Pair<t1, t2> {

      first: t1,

      second: t2,

   }


위의 구조체를 변수에 선언한다.

   let foo = Pair { first:1, second: '2'};


foo 에 대한 명시적인 type을 주진 않았지만, 초기화 변수를 넣어주었다. 이러한 선언은 Rust에서 이렇게 읽을 수 있다

   let foo:Pair<int, char> = Pair { first:1, second:'2'};

또한 특정 변수의 값을 읽어오기 위해 아래와 같이 호출할 수 있다.

   first(foo);

이렇게 하면 compiler는 int 값을 return 한다는 것을 알수 있을 것이다. 사실 int의 복사본이 반환된다(reference가 아님)


Rust는 모든 type 선언(struc, array, enum), 함수 그리고 "interfaces"나 "virtaul class" 와 같은 기본 특성들 모두 parameter 화 할 수 있다. 이 의미는 complier 는 항상 down cast에 대한 요구를 피할 수 있도록 모든 type에 관해 충분히 인지 할 수 있다는 것이다.


Keep one's memories in order


마지막으로 rust의 "safety" 에 관련 사항으로 memory allocation 과 concurrency 를 어떻게 지원하는지 확인할 것이다. 


요즘 다른 많은 언어처럼, Rust 는 할당된 memory에 대해서 명시적으로 release("free" 또는 "delete") 하도록 개발자에게 요구하지 않는다. 그 보다는 memory 해제에 관련해서는 runtime 에 제공할 수 있도록 한다.  local 변수로 stack 에 할당하는 것을 보강하는 두 가지 machanism이 제공된다. 이 두가지의 machanism은 task 에 기반한 multiprocessing mode에 연관되어 있다.


첫번째 machanism은 garbage collection의 대상이 되는 "managed" 할당이라는 것을 제공한다. 이것들은 마지막에 사용한 객체가 사용을 마무리 했다면 할당된 메모리를 해제할 것이며 소멸자가 정의되어 있다면 제거될 것이다. "Managed allocation"은 heap 영역에 할당되며 당연한 얘기겟지만 allocation을 요청한 task 에서만 접근가능할 것이다. task 가 종료되면 task에서 할당된 도느 heap memory는 해제될 것을 보장받는다. 현재 구현은 이렇게 할당된 memory의 reference count를 이용해서 관리를 하지만 나중에는 mark/sweep scheme으로 변경될 것으로 보인다.


두번째 machanism은 하나의 reference(strong reference)만 갖도록 보장하는 것이다. 만약 하나의 reference 가 떨어져 나갔다면 그 객첵은 해제된다. 이것은 system이 알아서 하나의 참조만 있다고 추측하는 것이 아니라 Rust 에서 제공하는 "once" 와 "owned" keyword를 사용해서 명시적으로 알려줘야 한다. 


이 single-reference(하나의 참조만 허가하는) 할당은 common heap에 할당되며 task간 전달이 가능하다. 하나의 reference만 가질 수 있기 때문에 , 다른 task가 접근 사용하여 race condition에 빠지는 것은 불가능하다. 


May I borrow your reference?


"Owned" 값은 "borrowed" 참조를 통해 추가적인 접근이 가능하다. "Borrowed 참조"는 특정 strong 참조의 문맥내에서 사용되어 질수 있다. 이 의미는 borrowed 된 참조의 원본(?)이 제거되나 범위 밖에서 사용되어 질 때는 borrowed 참조는 유효하지 않는 영역으로 바뀌며 그것을 사용하려고 드는 곳이 있다면 compile time의 error를 낼것이다.


위의 사항은 borrowed 참조를 다른 함수로 전달하거나 함수의 반환값으로 사용되질 때 용의하게 사용되어 질 수 있다. allocation type(owned, managed, on-stack 참조) 에 모두 적용될 수 있으며 borrowed 참조가 되어 전달되어 진후 각 할당 type에 맞게  사용할 수 있다.


borrowed 참조 전달은 함수 인자의 type으로 표시하여 쉽게 관리 될 수 있다. compiler가 안전한지 확인할 필요가 없을 것이다. 


borrowed 참조를 반환하는 것은 compiler가 이 참조가 borrowed 된것인지 혹은 이 참조의 유지해야하는 시간을 알 필요가 있는 부분을 약간의 trick을 이용한다. 그렇지 않으면 원본이 언제 제거되어 borrowed 참조가 사용되어 질 수 없는지 compiler가 알수 없기 때문이다. 


이 참조의 lifetime은 함수를 위한 type 인자의 machanism을 사용한다. 하나의 함수는 single quote(')를 사용하여 하나 혹은 그이상의 lifetime 인자를 선언할 수 있고 다양한 argument와 반환 값을 tagging 할 수 있다. Rust는 이런 parameter들의 실제 변수를 결정하기 위한 것과 반환값의 lifetime을 type 추정을 통해 알수 있다. 예를 들면,


   fn choose_one<'r>(first: &'r Foo, second: &'r Foo) -> &'r Foo

이 함수는 하나의 parameter(r)로 두 개의 argument와 반환값으로 적용된다.(first, second, return value). &는 borrowed 참조라는 의미.


실제 사용 방법은,

   baz = choose_one(bar, bat);

compiler는 r의 lifetime은 bar와 bat의 lifetime을 고려하여 효율적인 상호 유지를 할 수 있도록 해야한다. 반환값인 baz 또한 lifetime이 연관되어 있다. 이 lifetime을 벗어나서 사용되는 어떤 시도든 간에 compiler-time에서 error로 확인 가능하다. 이와 관련해서 다양한 예제는 "Borrowed Pointers Tutorial"을 참조


Time for that loophole


Stack, per-task heap, common heap 에 할당되는 object를 허용하는 것은 multi task 환경에서 race condition을 막을 수 있지만 높은 비용이 든다.


"unsafe" 한 영역을 사용하는 것은 위에서 얘기했다. Unsafe code는 "raw" memory 영역(즉 특별한 관리가 가능하지 않는) 할당이나 다른 type의 pointer로 casting 이 가능하다. 


예를 들어 두 개의 owned reference 가 접근하는 queue를 만들었는데, 하나는 queue에 add하는 것이고 다른 하나는 제거하는 것이 있다고 하자. 이 queue의 구현은 적당한 양의 "unsafe"한 code가 필요할 것이다. 하지만 queue의 모든 client는 전적으로 safe하다. 그래서 task간의 owned 참조를 옮기는 것이 가능하고 특정 data struct를 저장 할 수도 있어야 겠다. queue에서 마지막 참조가 없어졌을 때 알수 있어야 하고 적절히 응답을 해줘야 한다. 


Rust 의 standard library에 "pipe" 라 불리는 data 구조체가 구현되어 있다. 그리고 다른 더 복잡한 data 구조체도 구현이 되어 있으며 이는 owned 와 managed 참조만으로 구성되어 있다는 것을 보장하여 예기치 않은 concurrent 접근 및 잘못된 pointer의 사용으로 부터 안전하게 했다.


Reference

Rust Tutorial

Rust Refernece Manual

Blog post about Rust memory management


마지막 단락은 생략했다. Rust 로 큰 덩치의 project를 만들어봐야 safety에 관한 것을 느낄 수 있지 않을까하는 것과 아직 개발 진행중이라는 얘긴 듯하다. 


전반적으로 파악하기 어려운 문장들이 많아 번역이 자연스레 되지 못했던 것 같다. 


+ Recent posts