[Rust] 트레잇 객체의 구조 (번역)

이전 포스팅: https://m.blog.naver.com/sssang97/221700352899

트레잇 객체 안에는 컴파일러가 관리하는 vtable이라는 특별한 녀석이 있다.
가상함수테이블(virtual function table)이라 부르는, 함수 포인터의 집합인데.
트레잇 객체에서 메서드를 호출할 때는 항상 이걸 통해서 메서드를 적합한 꺼내온다. image 트레잇 객체는 아주 간단하면서도 복잡하다.
핵심 표현법과 레이아웃은 꽤 직관적이다.
하지만 꼬여있는 오류 메세지와 놀라운 행동들이 존재한다... image 간단한것부터 보자. 트레잇 객체의 런타임 표현이다.

std::raw 모듈을 보면 복잡한 내장타입과 동일한 레이아웃의 구조체들이 있는데. 여기에 트레잇 객체들(trait objects)도 포함된다.

**pub struct TraitObject **
**{ **
**    pub data: *mut (), **
**    pub vtable: *mut (), **
} image 보면. &Foo 같은 트레잇 객체는 dara 포인터와 vtable 포인터로 구성된다고 할 수 있다. image data 포인터는 트레잇 객체에 저장된 구상 타입 T의 데이터를 참조한다.
그리고 vtable 포인터는 T에서 구현한 Foo에 해당하는 vtable을 참조한다. image vtable은 필수적으로 함수포인터의 구조체인데.
구현해놨던 각 메서드의 머신코드들을 가리킨다.

그래서 trait_object.method() 같은 메서드 호출은 vtable의 올바른 포인터를 가져와서 동적인 호출을 수행한다.

예제다.
**struct FooVtable **
**{ **
**    destructor: fn(*mut ()), **
**    size: usize, **
**    align: usize, **
**    method: fn(*const ()) -> String, **
**} **

// u8:
**fn call_method_on_u8(x: const ()) -> String **
**{ **
**    **// 컴파일러는 이 함수가 호출될 때
    // x가 u8의 포인터임을 보장함
**    let byte: &u8 = unsafe { **
**        &
(x as *const u8) **
**    }; **
**    byte.method() **
**} **

**static Foo_for_u8_vtable: FooVtable = FooVtable **
**{ **
**    destructor: */ compiler magic */,
**    size: 1, **
**    align: 1, **
**    **// 함수포인터로 캐스팅
**    method: call_method_on_u8 as fn(*const ()) -> String, **
**}; **

// String:
**fn call_method_on_String(x: const ()) -> String **
**{ **
**   ** // 컴파일러는 이 함수가 호출될 때
    // x가 String의 포인터임을 보장함
**    let string: &String = unsafe { **
**        &
(x as *const String) **
**    }; **
**    string.method() **
**} **

**static Foo_for_String_vtable: FooVtable = FooVtable **
*{ **
**    destructor: / compiler magic */
, **
** ****  ** // 64비트 컴퓨터에 대한 값들임
**    size: 24, **
**    align: 8, **
**    method: call_method_on_String as fn(*const ()) -> String, **
};


image 각 vtable마다 붙어있는 destructor 필드는 vtable 타입의 리소스를 청소할 함수의 포인터다.

u8는 trivial이라 아니지만, String은 메모리를 free할 것이다.

이건 Box 같은 트레잇 객체를 관리할때 필요하다. 해당 객체가 스코프를 벗어날 때, Box 할당뿐만 아니라 내부의 타입도 청소해야 하기 때문이다.

size 필드는 지워진 타입의 크기고, align 필드는 alignment 요구사항이다.


image Foo를 구현한 일부 값들이 있다고 치자.
생성의 명시적 형식과 Foo 트레잇 객체의 사용은 약간 비슷해보일 수도 있다.
(타입 불일치는 무시한다. 어쨌든 다 포인터다.)

**let a: String = "foo".to_string(); **
**let x: u8 = 1; **
// let b: &Foo = &a;
**let b = TraitObject **
**{ **
****  ****// 데이터 저장
**    data: &a, **
**   **// 메서드들 저장
**    vtable: &Foo_for_String_vtable **
**}; **

// let y: &Foo = x;
**let y = TraitObject **
**{ **
**    **// 데이터 저장
**    data: &x, **
**    **// 메서드들 저장
**    vtable: &Foo_for_u8_vtable **
**}; **

// b.method();
(b.vtable.method)(b.data; **
// y.method();
(y.vtable.method)(y.data;**


참조
https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html#representation#representation