러스트 프로젝트 생성하기

파이썬 바인딩이란?

파이썬 바인딩은 다른 프로그래밍 언어로 작성된 코드를 파이썬에서 사용하는 것을 의미합니다.

파이썬용 러스트 바인딩을 사용하면 파이썬에서 러스트로 함수를 호출하고 데이터를 전달할 수 있으므로 두 언어의 강점을 모두 활용할 수 있습니다. 이 기능은 테스트를 거쳐 안정적으로 작성된 대규모 라이브러리를 파이썬에서 활용하거나 파이썬 코드의 특정 섹션을 러스트로 변환하여 속도를 높이고자 하는 경우에 유용합니다.

파이썬에서 러스트 바인딩을 생성하는 데 가장 널리 알려진 프로젝트는 PyO3입니다. 이 프로젝트는 러스트로 파이썬 모듈을 작성하거나 파이썬 런타임을 러스트 바이너리에 임베드하는 데 사용할 수 있습니다. PyO3는 파이썬 패키징 및 바인딩이 포함된 러스트 상자를 작성하는 도구인 Maturin이라는 또 다른 프로젝트를 활용합니다.

PyO3

PyO3는 파이썬에서 러스트 코드를 실행할 수 있고, 반대로 러스트에서 파이썬 코드를 실행할 수 있도록 도와주는 크레이트입니다. 우리는 파이썬에서 러스트 코드를 실행하는 방법을 배워 보겠습니다. 파이썬에서 러스트 코드를 호출해 높은 성능 향상을 달성한 다양한 예시가 있습니다. 아래에서 그 중 유명한 몇 가지 패키지를 소개합니다.

  • orjson Fast Python JSON library 10배
  • fastuuid Python bindings to Rust's UUID library
  • cryptography Python cryptography library with some functionality in Rust

maturin

maturin은 최소한의 구성으로 러스트로 작성한 파이썬 패키지를 빌드할 수 있는 도구입니다.

$ pipenv install maturin --dev
$ pipenv shell

maturin으로 pyo3 프로젝트를 시작합니다. -b 옵션을 주면 pyo3를 빌드 시스템으로 해서 프로젝트가 생성됩니다.

$ maturin init -b pyo3
  ✨ Done! New project created string_sum

프로젝트를 생성하면 다음과 같은 폴더 구조가 만들어집니다.

.
├── Cargo.lock
├── Cargo.toml
├── Pipfile
├── pyproject.toml
├── src
    └── lib.rs

Cargo.toml파일에서 패키지와 라이브러리의 이름을 "fibonacci"로 변경합니다.

[package]
name = "fibonacci"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "fibonacci"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.16.5", features = ["extension-module"] }

pyproject.toml 파일에서도 프로젝트 이름을 "fibonacci"로 수정합니다.

[build-system]
requires = ["maturin>=0.13,<0.14"]
build-backend = "maturin"

[project]
name = "fibonacci"
requires-python = ">=3.7"
classifiers = [
    "Programming Language :: Rust",
    "Programming Language :: Python :: Implementation :: CPython",
    "Programming Language :: Python :: Implementation :: PyPy",
]

라이브러리 크레이트 만들기

#[pyfunction]은 PyO3 라이브러리에서 제공하는 어트리뷰트로, 러스트 함수를 파이썬 함수로 정의하는 데 사용할 수 있습니다. 러스트 함수에 #[pyfunction] 어트리뷰트를 추가하면 PyO3는 해당 함수가 일반 파이썬 함수인 것처럼 파이썬에서 호출할 수 있는 코드를 생성합니다.

#[pymodule]은 PyO3 라이브러리에서 제공하는 어트리뷰트로, 러스트 함수를 파이썬 모듈로 정의하는 데 사용할 수 있습니다. 러스트 함수에 #[pymodule] 어트리뷰트를 추가하면 PyO3는 해당 함수를 파이썬 모듈의 초기화 함수로 사용할 수 있는 코드를 생성합니다.

모듈에 함수를 추가하려면 add_function 메서드를 사용합니다. 이렇게 하면 모듈 내에서 함수를 호출 가능한 객체로 사용할 수 있습니다.

use pyo3::prelude::*;

fn _run(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => _run(n - 1) + _run(n - 2),
    }
}

#[pyfunction]
fn run(n: u64) -> PyResult<u64> {
    Ok(_run(n))
}

/// A Python module implemented in Rust.
#[pymodule]
fn fibonacci(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(run, m)?)?;
    Ok(())
}

파이썬에서 러스트 코드 실행해 보기

개발 모드로 빌드해보기

maturin develop 명령어를 사용하면, 러스트 패키지를 빌드한 다음 파이썬 가상환경에 패키지를 자동으로 설치해줍니다. 이때 러스트 컴파일 타겟이 [unoptimized + debuginfo]가 되는데, 빠른 개발을 위해 코드 성능보다는 컴파일 속도를 중요하게 생각한 옵션입니다.

$ maturin develop      
🔗 Found pyo3 bindings
🐍 Found CPython 3.8 at /Users/.local/share/virtualenvs/ch14-4UzrGkRt/bin/python
   Compiling pyo3-build-config v0.16.5
   Compiling pyo3-ffi v0.16.5
   Compiling pyo3 v0.16.5
   Compiling fibonacci v0.1.0 (/Users/code/Tutorials/sap_rust_tutorial/ch14)
    Finished dev [unoptimized + debuginfo] target(s) in 12.64s
📦 Built wheel for CPython 3.8 to /var/folders/74/l6jhlmk114g8kx1pzz2s9fm80000gn/T/.tmpBh1Xiw/fibonacci-0.1.0-cp38-cp38-macosx_10_7_x86_64.whl
🛠  Installed fibonacci-0.1.0

만일 가상환경에서 실행하지 않을 경우 에러가 발생하므로 주의하세요!

$ maturin develop
💥 maturin failed
  Caused by: You need to be inside a virtualenv or conda environment to use develop (neither VIRTUAL_ENV nor CONDA_PREFIX are set). See https://virtualenv.pypa.io/en/latest/index.html on how to use virtualenv or use `maturin build` and `pip install <path/to/wheel>` instead.

main.py 파일을 만들고 다음 코드를 추가합니다. 파이썬으로 피보나치 수열을 구하는 함수 pyrun 을 추가해 러스트 구현체와 성능을 비교해봅니다.

import time

from fibonacci import run


def pyrun(n: int):
    if n < 2:
        return n

    return pyrun(n - 1) + pyrun(n - 2)


N = 35

start = time.time()
result = pyrun(N)
print(f"python: {time.time()-start:.2f}, result: {result}")
start = time.time()
result = run(N)
print(f"rust: {time.time()-start:.2f}, result: {result}")

실행 결과

$ python main.py
python: 3.13, result: 9227465
rust: 0.10, result: 9227465

릴리즈 모드로 빌드해보기

빌드 옵션을 --release 로 주면, 러스트 코드를 최대한 최적화해서 컴파일한 바이너리가 패키지로 만들어지게 됩니다. 컴파일 타겟이 [optimized]인 걸 알 수 있습니다.

$ maturin build --release
🔗 Found pyo3 bindings
🐍 Found CPython 3.8 at /Users/.local/share/virtualenvs/temp-nO4s4P8m/bin/python3
   Compiling target-lexicon v0.12.4
   Compiling once_cell v1.13.1
   Compiling proc-macro2 v1.0.43
   Compiling libc v0.2.132
   Compiling quote v1.0.21
   Compiling unicode-ident v1.0.3
   Compiling syn v1.0.99
   Compiling autocfg v1.1.0
   Compiling parking_lot_core v0.9.3
   Compiling cfg-if v1.0.0
   Compiling smallvec v1.9.0
   Compiling scopeguard v1.1.0
   Compiling unindent v0.1.10
   Compiling indoc v1.0.7
   Compiling lock_api v0.4.7
   Compiling pyo3-build-config v0.16.5
   Compiling parking_lot v0.12.1
   Compiling pyo3-ffi v0.16.5
   Compiling pyo3 v0.16.5
   Compiling pyo3-macros-backend v0.16.5
   Compiling pyo3-macros v0.16.5
   Compiling fibonacci v0.1.0 (/Users/code/temp)
    Finished release [optimized] target(s) in 20.61s
📦 Built wheel for CPython 3.8 to /Users/code/temp/target/wheels/fibonacci-0.1.0-cp38-cp38-macosx_10_7_x86_64.whl

이제 파이썬 코드를 그대로 실행하면 최적화된 패키지로 실행이 가능합니다.

실행 결과

$ python main.py
python: 3.03, result: 9227465
rust: 0.03, result: 9227465