PyO3를 이용한 Rust와 Python의 가변 데이터 공유 기법
(blog.lilyf.org)결론 (Conclusion)
PyO3가 Rust의 라이프타임(lifetime)을 사용하는 구조체를 직접 Python에 노출하지 못하는 것은 처음에는 한계처럼 보일 수 있습니다. 하지만 Rust 표준 라이브러리와 PyO3는 이러한 한계를 극복할 강력한 도구들을 제공합니다. std::mem::take
와 std::mem::replace
는 변경 가능한 참조(mutable reference)와 소유된 값(owned value)을 능숙하게 다룰 수 있게 해주며, Arc
와 Mutex
는 공유되는 변경 가능한 데이터를 Python에 노출하는 데 매우 유용합니다. 특히 PyO3의 MutexExt
는 Python과 함께 뮤텍스를 사용할 때 데드락을 방지하는 필수적인 도구입니다.
주요 내용 요약
이 문서는 Django의 템플릿 언어를 Rust로 재구현하는 프로젝트에서 Rust와 Python 간에 변경 가능한(mutable) 데이터를 공유하면서 마주친 기술적 문제와 그 해결 과정을 단계별로 설명합니다.
-
배경: Django 템플릿 언어는
context
라는 객체를 사용하여 템플릿에 동적 데이터를 제공합니다. 프로젝트의 Rust 구현체에서는 이context
를 Rust 구조체로 정의했으며, 템플릿 태그를 렌더링할 때 변경 가능한 참조(&mut Context
)로 전달해야 합니다. -
초기 문제: Rust 코드의 변경 가능한 참조(
&mut Context
)를 커스텀 태그 실행을 위해 Python 함수로 전달해야 합니다. 하지만 Python은 Rust의 라이프타임을 이해하지 못하며, Rust-Python 연동 라이브러리인 PyO3는 소유권이 있는 값(owned value)을 요구하기 때문에 참조를 직접 전달하면 컴파일 에러가 발생합니다. -
해결 과정:
-
소유권 문제 해결:
std::mem::take
를 사용하여&mut Context
에서 소유권을 잠시 가져와 Python에 전달 가능한 소유된Context
객체를 생성합니다. Python 코드 실행 후에는std::mem::replace
를 사용하여 처리된Context
를 다시 원래의 참조 위치로 돌려놓으려 시도합니다. -
'Moved Value' 에러 해결: 하지만 이 과정에서
Context
객체가 Python 함수로 이동(move)된 후 다시 사용하려 할 때 "use of moved value" 컴파일 에러가 발생합니다. 이 문제를 해결하기 위해Arc
(Atomic Reference Count)를 도입하여Context
를 감쌉니다. 이를 통해 소유권을 옮기지 않고도 Python에 복제된 참조(clone
)를 전달할 수 있습니다. -
Python의 참조 유지 시 처리: Python이
Context
에 대한 참조를 계속 유지할 경우Arc::try_unwrap
을 통한 소유권 회수가 실패할 수 있습니다. 이 경우,Context
내부 데이터를 깊은 복사(deep clone)하는clone_ref
와 같은 fallback 메소드를 구현하여 데이터를 복제합니다. -
Python에서의 데이터 변경 허용: 최종적으로 Python 코드가
Context
를 읽기만 하는 것이 아니라 변경도 할 수 있도록Mutex
를 도입합니다.Arc<Mutex<Context>>
구조를 사용하여 여러 스레드에서 안전하게 데이터에 접근하고 수정할 수 있도록 보장합니다. 이때 Python 인터프리터와의 데드락을 방지하기 위해 PyO3가 제공하는MutexExt
의lock_py_attached
메소드를 사용합니다.
-
소유권 문제 해결: