Rust는 C++,C를 대체하는 로우레벨,고성능,고복잡 언어
작성한 코드에 메모리 오류 가능성이 있으면 컴파일조차 불가
Rust를 Python에서 동작시켜 성능 Advantage 확보 가능
2020년 1월 3일, 대세는 이미 Rust?

궤도에 오른 Rust
Rust에 대한 자세한 내용은 구글링을 부탁드리며 여기서는 간단히 소개하겠습니다. Rust는 2015년부터 유명세를 타게 된 시스템 프로그래밍 언어(Low-Level Language)이며 따라서 C/C++ 언어와 같은 레이어에 속해 있습니다.
Rust의 큰 특징 중 하나는 C언어에서 코딩 실력의 영역에 있던 메모리 관리 문제를 프로그래밍 언어가 해결해준다는 것입니다. 바로 메모리 문제가 생길 것 같으면 소스코드 컴파일 조차! 안 되는 Rust의 특징에 있습니다(C언어도 스마트 포인터로 이러한 메모리 관리 편의를 제공합니다).

Rust가 주목받는 이유 중 하나는 바로 웹어샘블리(WASM)입니다. WASM은 한 마디로 웹 브라우저에서 JavaScript 대신 C/C++/Rust를 사용하게 하는 표준입니다(C/C++/Rust 등 저수준 언어로 작성된 코드를 웹으로 컴파일해 실행). 웹어샘블리를 사용하면 웹에서 기존에 돌리기 힘들었던 엄청나게 무거운 애플리케이션도 실행할 수 있게 됩니다.
제가 알기로 Rust VS C는 그냥 메모리 관리를 프로그래머에 맡긴다vs아니다 수준으로 단순하게 논의할 수 있는 내용이 아닙니다. 시스템 레벨의 복잡한 이슈가 얽혀 있으며 두 언어는 구조는 서로 다른데, 배우기는 똑같이 어렵습니다(Rust는 입문자용 언어가 절대 아닙니다).
하지만 Rust를 지원/사용하는 곳은 점점 늘어나고 있습니다. 현재 Rust를 일부라도 지원/사용하는 곳으로 알려진 곳은 Dropbox, Facebook, Amazon AWS Lambda, Mozilla Firefox, Reddit입니다.
Rust를 Python에서 실행하기: Overview
그러면 이제 이번 포스트에서 본래 다루려고 했던 주제인 Rust in Python에 관해 소개합니다. Python 사용자들이 대부분 공감하는 것은 (최적화가 되어있지 않은 or 더 최적화하기 어려운) Python 코드는 매우 느리다는 것입니다.
사람들은 그래서 NumPy, SciPy같은 C언어 기반 수치 연산 라이브러리를 사용하거나,아예 파이썬 인터프리터를 들어내고 C언어 컴파일러(Cython)를 쓰거나 JIT 컴파일 기능을 제공하는 라이브러리(Numba)를 사용해 성능을 높입니다.
그런데 이제 Python 코드 성능을 높이기 위해 Rust를 사용할 수 있습니다. Python에서 Rust를 실행하려면 아래 단계를 따릅니다.
- Rust로 Worker(=고성능 연산이 필요한 코드)를 작성
- Worker를 Rust로 컴파일
- Python에서 Rust를 Import
- Python에서 Rust Worker코드를 실행
Rust를 Python에서 실행하기: Rust 코드 작성
먼저 Rust를 설치한 후 아래와 같이 새로운 Rust 프로젝트인 rust_in_python을 생성합니다. rustup을 쓰면 윈도우, 맥, 리눅스 등 대부분의 환경에서 손쉽게 Rust를 설치할 수 있습니다. 이후 설명은 리눅스 기준입니다.
cargo new rust_in_python
Cargo(카고)는 Rust의 빌드 시스템 및 패키지 매니저로 프로젝트 관리, 코드 빌드, 의존성 라이브러리 다운로드, 라이브러리 빌드 등 Python 에코시스템의 pip이나 Anaconda와 비슷한 역할을 합니다.
그리고 rust_in_python 프로젝트의 소스코드 파일인 src/main.rs를 src/lib.rs로 바꿔줍니다. 이는 우리가 작성할 Rust 프로그램이 스탠드얼론으로 작동하는 프로그램이 아니라, 다른 프로그램에서 Import해서 사용할 라이브러리이기 때문입니다.
mv src/main.rs src/lib.rs
이제 이 src/lib.rs에 헬로월드 함수를 작성해보겠습니다.
#[no_mangle]
extern fn hello() {
println!("Hello from rust");
}
컴파일러는 컴파일을 할 때 함수명을 일정한 규칙에 따라 바꿔주는 작업을 하며 이를 Name Mangling이라고 합니다. 그런데 우리는 이 함수를 Rust가 아닌 Python에서 실행해야 하므로 Rust 스타일의 함수명이 아닌 C 스타일의 함수명이 필요합니다. 왜냐하면 우리가 나중에 Python에서 Rust 라이브러리를 로드하기 위해 사용할 도구(FFI)는 C로 작성된 라이브러리를 로드할 때 사용하는 도구이기 때문입니다. 따라서 #[no_mangle]이라는 flag를 붙이면 일단 우리가 작성한 함수를 Rust 스타일의 함수명으로 바꾸는 작업을 생략합니다.
extern 키워드는 Rust에서 이 함수가 외부 언어에서 호출될 수 있음을 알려주는 키워드입니다.
Rust를 Python에서 실행하기: Rust에서 컴파일
그리고 이제 우리가 작성한 헬로월드 함수가 담긴 lib.rs를 컴파일하면 됩니다. 그런데 이 코드를 그냥 컴파일하면 확장자가 *.rlib이라는 파일이 나타납니다. *.rlib은 Rust Static 라이브러리 파일을 뜻하는 확장자이며 이 라이브러리는 Rust에서만! 사용할 수 있음을 말합니다. 따라서 이 라이브러리를 다른 언어에서 사용할 수 있도록 Dynamic 라이브러리 파일로 컴파일해야 합니다. 그래야 이 라이브러리를 Python의 외부 함수 인터페이스(FFI)가 인식하도록 하여 Python에서 Import할 수 있습니다.
lib.rs를 Dynamic 라이브러리로 컴파일하려면 Cargo.toml 파일에 다음을 추가하면 됩니다.
[lib]
crate-type = ["dylib"]
이제, cargo를 통해 이 코드를 컴파일합니다.
cargo build --release
Rust를 Python에서 실행하기: Python에서 Rust를 Import 및 실행
이제 Python 메인(main.py)을 만들고 Rust로 작성한 라이브러리 파일을 Import해 실행해봅니다. main.py에 다음을 작성합니다.
from ctypes import CDLL
lib = CDLL("target/release/librust_in_python.dylib")
lib.hello()
파이썬의 ctypes 라이브러리가 바로 파이썬의 외부 함수 인터페이스(FFI) 중 하나입니다. 이 라이브러리는 CDLL이라는 클래스를 통해 사용자가 C로 작성된 라이브러리를 파이썬에 로드할 수 있게 합니다. 아까 Rust에서 컴파일해 만든 *.dylib 경로를 사용해 라이브러리를 로드하고 헬로월드 함수를 실행합니다.
python main.py
Rust를 Python에서 실행하기: Parameter 입력, Return값 받기
이제 Rust에서 만든 함수에 입력(=파라미터)을 주고 그 결과(=리턴)을 받아보겠습니다. 그런데 Rust는 Typed, Python은 Untyped 언어이므로 Rust 함수를 Python에서 실행하려면 함수에 넘겨줄 파라미터가 어떤 타입이고 함수 실행 결과는 어떤 타입으로 반환할 것인지를 명시해야만 합니다.
#[no_mangle]
extern fn add(a: f64, b: f64) -> f64 {
return a + b;
}
Rust로 작성한 이 함수는 Float64타입 2개를 입력으로 받아서 이들을 서로 더하여 결과값도 Float64타입으로 반환하는 덧셈함수입니다.
cargo build --release
이제 파이썬 파트를 보겠습니다.
from ctypes import CDLL, c_double
lib = CDLL("target/release/librust_in_python.dylib")
lib.add.argtypes = (c_double, c_double)
lib.add.restype = c_double
result = lib.add(1.5, 2.5)
print(result) # 4.0
lib.add.argtypes은 직관적으로 알 수 있듯이 Rust 덧셈함수에 넘겨줄 2개의 입력 argument 타입을 명시한 튜플입니다: ctypes 라이브러리에서 사용하는 타입 목록
마찬가지로 lib.add.restype으로 return값의 타입을 정의합니다.
| Python | C | Rust |
|---|---|---|
| c_bool | – | |
| c_byte | char | i8 |
| c_ubyte | unsigned char | u8 |
| c_short | short | i16 |
| c_ushort | unsigned short | u16 |
| c_int | int | i32 |
| c_uint | unsigned int | u32 |
| c_long | long | i64 |
| c_ulong | unsigned long | u64 |
| c_float | float | f32 |
| c_double | double | f64 |
※ Array & List
Rust
#[no_mangle]
extern fn sum(arr: [i32; 5]) -> i32 {
let mut total: i32 = 0;
for number in arr.iter() {
total += number;
}
return total;
}
Python
from ctypes import CDLL, c_int
lib = CDLL("target/release/librust_in_python.dylib")
lst = [1, 2, 3, 4, 5]
# Create the memory of the list size
seq = c_int * len(lst)
arr = seq(*lst)
result = lib.sum(arr)
print(result)
※ Class & Complex Data Type
Rust
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[no_mangle]
fn greet_point(p: Point) {
println!("x: {}, y: {}", p.x, p.y);
}
Python
from ctypes import CDLL, Structure, c_double
lib = CDLL("target/release/librust_in_python.dylib")
class Point(Structure):
_fields_ = [
('x', c_double),
('y', c_double)
]
p = Point(x=1.2, y=3.4)
lib.greet_point(p)
이번 포스팅을 작성하면서 Rust에 대해 알 수 있어서 좋았습니다. 그런데 과연 Python에서 굳이 Rust로 작성한 라이브러리를 써야할 일이 얼마나 있을까라는 생각이 들었고(파이썬이 여러 방법으로 C/C++ 기반 라이브러리를 지원하는데 굳이…) 성능 벤치마크가 있다면 보고 싶어졌습니다.
제가 중간중간에 수정도 하고 더 찾아서 보충도 하긴 했지만, 지금까지는 너무 남이 써 놓은 포스팅이나 뉴스만 옮겨오는 것 같습니다. 조만간 기회가 된다면 제가 했던 강화학습 프로젝트에 대해 잠시 다뤄보겠습니다. 읽어주셔서 감사합니다~!