짤막한 Rust 생활기 { 정적 변수 drop에 대해서 }

maxtnuk·2021년 7월 18일
2

Rust 생활기

목록 보기
1/1
post-thumbnail

안녕하세요 현재 프로그랩 언어 Rust를 주언어로 사용하고 있는 개발자입니다.
Rust는 c언어의 단점을 보완하고자 나온 언어로 현재는 마이크로소프트나 구글에서도 여러 오픈소스 프로젝트를 내며 사랑받고 있는 언어입니다.
저 또한 Rust만의 특징과 사전 오류탐지와 같은 기능에 매료되어서 주언어로 계속 사용되고 있습니다.
어느 날 제가 개인 프로젝트를 진행하다가 이런 문제점이 발생했습니다.

struct Test{
    a: i32,
}
impl Drop for Test{
    fn drop(&mut self){
        println!("droped");
    }
}

static mut HELLO: Test = Test {
    a: 0,
};
fn main(){
	println!("main hello")
}

저는 여기서 자연스럽게 HELLO가 프로그램 종료하면서 drop 함수를 실행하고 싶었습니다.
하지만 Rust는 static변수에 대해서는 drop함수를 수행하지 않는 문제점이 있었습니다.
ManuallyDrop도 해보고 시도를 하였지만 결국 태생부터가 정적변수여서 그렇게 할수 없더라고요.
그렇게 고민하다가 나온 해결책이 ctor,dtor이었습니다.

Ctor,Dtor

ctor,dtor은 linker가 만들어주는 segment로 전자는 프로그램 시작시, 후자는 프로그램 종료시에 실행이 됩니다. 그래서 이 영역에 함수를 정의를 하면 적절한 시기에 함수를 수행을 합니다. 즉 이 영역들을 적절히 사용을 하면 drop함수를 수행할 수 있게 되는 겁니다.

각 영역의 이름은 저장되는 파일 포멧에 달라집니다. 파일 포멧에는 Mach O,ELF등이 있습니다.
ELF의 segment중에서는 .init_func.NNNN,.fini_func.NNNN가 있습니다. 각각 프로그램 실행전, 프로그램 실행후에 해당하는 영역입니다.
Mach o와 같은 경우 __DATA,__mod_init_func,__DATA,__mod_term_func에 해당합니다.
이를 이용해서 코드를 작성하면 다음과 같습니다.

struct Test{
    a: i32,
}
impl Drop for Test{
    fn drop(&mut self){
        println!("droped");
    }
}

static mut HELLO: Test = Test {
    a: 0,
};


extern "C" fn __init_hello(){
    println!("init program");
}

extern "C" fn __fini_hello(){
 	unsafe{
       	drop(&HELLO);
    }
    println!("finish program");
}

#[link_section = "__DATA,__mod_init_func"]
#[used]
pub static __init_section: extern "C" fn() = __init_hello;

#[link_section = "__DATA,__mod_term_func"]
#[used]
pub static __fini_section: extern "C" fn() = __fini_hello;


fn main() {
    unsafe{
        println!("HELLO a: {}",HELLO.a);
    }
    println!("main hello")
}

실행 결과 다음과 같이 나옵니다.

init program
HELLO a: 0
main hello
finish program

program 시작시와 종료시에 함수 호출은 정상적으로 나오지만 drop함수가 호출이 되지 않았네요. ㅠㅠ
일단 코드 설명은 다음과 같습니다.

extern "C" fn __init_hello(){
    println!("init program");
}

extern "C" fn __fini_hello(){
	unsafe{
       	drop(&HELLO);
    }
    println!("finish program");
}

먼저 linker 영역에 함수를 삽입하기 위해서는 외부에서도 사용 가능해야 하기 때문에 extern을 부여하였습니다.

#[link_section = "__DATA,__mod_init_func"]
#[used]
pub static __init_section: extern "C" fn() = __init_hello;

#[link_section = "__DATA,__mod_term_func"]
#[used]
pub static __fini_section: extern "C" fn() = __fini_hello;

그리고 그 함수들을 대입할 수 있도록 static 변수로 정의후 extern함수 포인터를 대입을 합니다. 이때 link_section attribute를 통해 저장하기 원하는 영역을 지정할 수 있습니다. 저 같은 경우에는 맥을 사용해서 파일 포맷 Mach o에 맞춰서 진행했습니다.그리고 used를 통해 컴파일러 상에서 무리한 최적화를 피하고 유지할 수 있게 합니다.
이렇게 함으로써 우리가 원하는 함수를 넣는게 가능해집니다!!!

ptr Drop

이제 drop만 해결하면 되는데 위에서 말했듯이 태생부터가 static이어서 그럴 수 가 없었습니다. 그래서 이를 강제로 할 수 있게 하는 방법이 있습니다.
바로 ptr::drop_in_place입니다. 이 함수는 raw pointer를 기반해서 해당 객체를 lifetime과 상관없이 drop할 수 있게 해주는 함수입니다. 이를 이용해서 코드를 재작성하면 다음과 같습니다.

struct Test{
    a: i32,
}
impl Drop for Test{
    fn drop(&mut self){
        println!("droped");
    }
}

static mut HELLO: Test = Test {
    a: 0,
};


extern "C" fn __init_hello(){
    println!("init program");
}

extern "C" fn __fini_hello(){
    unsafe{
        let t_ptr = &mut HELLO as *mut Test;
        std::ptr::drop_in_place(t_ptr);
    }
    println!("finish program");
}

#[link_section = "__DATA,__mod_init_func"]
#[used]
pub static __init_section: extern "C" fn() = __init_hello;

#[link_section = "__DATA,__mod_term_func"]
pub static __fini_section: extern "C" fn() = __fini_hello;


fn main() {
    unsafe{
        println!("HELLO a: {}",HELLO.a);
    }
    println!("main hello")
}
init program
HELLO a: 0
main hello
droped
finish program

오!! drop이 되었습니다. 이제 static 변수도 프로그램 종료시 drop이 가능해집니다. 근데 코드 조금 지저분하긴 하네요. 이를 한번 macro로 작성해 봅시다.

macro_rules! static_drop {
    ($([$x:ident,$type:ty]),*) => {
        extern "C" fn __fini_hello(){
           $(
            unsafe{
                let t_ptr = &mut $x as *mut $type;
                std::ptr::drop_in_place(t_ptr);
            }
           )*
            println!("finish program");
        }

        #[cfg(target_os="macos")]
        #[link_section = "__DATA,__mod_term_func"]
        pub static __fini_section: extern "C" fn() = __fini_hello;

        #[cfg(not(target_os="macos"))]
        #[link_section = ".fini_array.65534"]
        pub static __fini_section: extern "C" fn() = __fini_hello;
    };
}

struct Test {
    a: i32,
}
impl Drop for Test {
    fn drop(&mut self) {
        println!("droped_{}", self.a);
    }
}

static mut HELLO: Test = Test { a: 0 };

static mut HELLO2: Test = Test { a: 2 };

static mut HELLO3: Test = Test { a: 3 };

static_drop!([HELLO, Test], [HELLO2, Test], [HELLO3, Test]);

fn main() {
    unsafe {
        println!("HELLO a: {}", HELLO.a);
    }
    println!("main hello")
}
HELLO a: 0
main hello
droped_0
droped_2
droped_3
finish program

macro를 써서 여러 개의 static변수를 손쉽게 정의를 하고 자연스럽게 drop이 되는 것을 볼 수 있습니다. 물론 여기서 proc_macro까지 쓰면 더 멋있는 형태가 나오지만 여기까지 하도록 하겠습니다.

긴 글 읽어주셔서 감사합니다!!

profile
Rust로 무난하게 개발하는 사람

1개의 댓글

comment-user-thumbnail
2023년 1월 16일

감사합니다.

답글 달기