Interoperabilidade com Python: PyO3 e bindings

1. Introdução ao PyO3 e ecossistema de bindings Rust↔Python

PyO3 é a principal biblioteca para criar bindings entre Rust e Python, permitindo que código Rust seja chamado diretamente do Python com performance nativa. Diferentemente de abordagens como ctypes (que requer wrappers manuais) ou CFFI (que depende de interfaces C), PyO3 oferece integração profunda com o sistema de tipos de ambas as linguagens, gerenciamento automático de memória via contagem de referências do Python, e suporte a async/await.

O ecossistema inclui maturin (ferramenta de build e publicação), setuptools-rust (integração com setuptools tradicional) e suporte nativo a PyPI. Casos de uso típicos incluem aceleração de código Python computacionalmente intensivo, exposição de bibliotecas Rust seguras (com garantias de memória) e criação de extensões nativas para frameworks científicos como NumPy.

2. Configuração do ambiente e primeiro binding

Para começar, instale as ferramentas necessárias:

python -m venv .venv
source .venv/bin/activate
pip install maturin

Crie um novo projeto:

maturin init --bindings pyo3

Isso gera a estrutura:

meu_projeto/
├── Cargo.toml
├── pyproject.toml
├── src/
│   └── lib.rs

No src/lib.rs, implemente uma função simples:

use pyo3::prelude::*;

#[pyfunction]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[pymodule]
fn meu_projeto(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(add, m)?)?;
    Ok(())
}

Compile e teste:

maturin develop
python -c "import meu_projeto; print(meu_projeto.add(3, 4))"  # Saída: 7

3. Tipos, conversões e manipulação de dados

PyO3 realiza conversão automática entre tipos Rust e Python:

use pyo3::prelude::*;
use std::collections::HashMap;

#[pyfunction]
fn process_data(numbers: Vec<f64>, mapping: HashMap<String, bool>) -> PyResult<(f64, Vec<String>)> {
    let sum: f64 = numbers.iter().sum();
    let active_keys: Vec<String> = mapping
        .into_iter()
        .filter(|(_, v)| *v)
        .map(|(k, _)| k)
        .collect();
    Ok((sum, active_keys))
}

Para strings, use &str ou String:

#[pyfunction]
fn greet(name: &str) -> String {
    format!("Olá, {}!", name)
}

Coleções como Vec<T> mapeiam para listas Python, HashMap<K,V> para dicionários. Para manipulação mais fina, use PyList e PyDict:

#[pyfunction]
fn manipulate_list(py: Python, lst: &PyList) -> PyResult<()> {
    lst.append(42)?;
    Ok(())
}

4. Expondo structs e objetos complexos com #[pyclass]

Transforme structs Rust em classes Python completas:

#[pyclass]
struct Contador {
    #[pyo3(get, set)]
    valor: i64,
    nome: String,
}

#[pymethods]
impl Contador {
    #[new]
    fn new(nome: String) -> Self {
        Contador { valor: 0, nome }
    }

    fn incrementar(&mut self, passo: i64) {
        self.valor += passo;
    }

    #[getter]
    fn nome(&self) -> &str {
        &self.nome
    }

    #[staticmethod]
    fn from_valor_inicial(valor: i64) -> Self {
        Contador { valor, nome: String::from("padrão") }
    }
}

Uso em Python:

c = Contador("meu_contador")
c.incrementar(5)
print(c.valor)  # 5
print(c.nome)   # "meu_contador"

Para herança, use #[pyclass(subclass)]:

#[pyclass(subclass)]
struct Animal {
    nome: String,
}

#[pymethods]
impl Animal {
    #[new]
    fn new(nome: String) -> Self {
        Animal { nome }
    }
}

5. Tratamento de erros e exceções entre as linguagens

Converta Result Rust em exceções Python:

use pyo3::exceptions::PyValueError;

#[pyfunction]
fn dividir(a: f64, b: f64) -> PyResult<f64> {
    if b == 0.0 {
        return Err(PyValueError::new_err("Divisão por zero não permitida"));
    }
    Ok(a / b)
}

Crie exceções personalizadas:

#[pyclass]
#[derive(Clone)]
struct MeuErro {
    mensagem: String,
}

impl std::fmt::Display for MeuErro {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.mensagem)
    }
}

impl std::error::Error for MeuErro {}

impl From<MeuErro> for PyErr {
    fn from(err: MeuErro) -> PyErr {
        PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(err.mensagem)
    }
}

Para capturar panics Rust:

use std::panic::catch_unwind;

#[pyfunction]
fn operacao_arriscada() -> PyResult<i32> {
    let resultado = catch_unwind(|| {
        // código que pode panicar
        42
    });

    match resultado {
        Ok(valor) => Ok(valor),
        Err(_) => Err(pyo3::exceptions::PyRuntimeError::new_err("Panic capturado")),
    }
}

6. Gerenciamento de lifetime, GIL e concorrência

O token Python<'py> garante acesso ao GIL:

#[pyfunction]
fn operacao_lenta(py: Python) -> PyResult<()> {
    // Libera o GIL para operações bloqueantes
    py.allow_threads(|| {
        // código Rust pesado aqui
        std::thread::sleep(std::time::Duration::from_secs(2));
    });
    Ok(())
}

Para callbacks, cuidado com lifetimes:

#[pyclass]
struct CallbackHandler {
    callback: Py<PyAny>,
}

#[pymethods]
impl CallbackHandler {
    #[new]
    fn new(callback: Py<PyAny>) -> Self {
        CallbackHandler { callback }
    }

    fn executar(&self, py: Python) -> PyResult<()> {
        self.callback.call0(py)?;
        Ok(())
    }
}

7. Empacotamento, distribuição e publicação

Configure Cargo.toml:

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

[lib]
crate-type = ["cdylib"]

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

E pyproject.toml:

[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[project]
name = "meu_projeto"
requires-python = ">=3.8"

Gere wheels multiplataforma:

maturin build --release
maturin publish  # requer login no PyPI

Testes integrados com pytest:

# tests/test_modulo.py
import meu_projeto

def test_add():
    assert meu_projeto.add(2, 3) == 5

8. Boas práticas, profiling e limitações

Performance: Use PyO3 quando a computação for intensiva (loops, processamento de dados). Para overhead pequeno, Python puro pode ser mais produtivo.

Profiling:

import timeit
import meu_projeto

# Compare performance
python_time = timeit.timeit("sum(range(1000000))", number=100)
rust_time = timeit.timeit("meu_projeto.soma_rust(range(1000000))", number=100)

Limitações:
- Custo de conversão para tipos grandes (e.g., arrays NumPy complexos)
- Overhead do GIL em chamadas frequentes
- Dificuldade com tipos genéricos complexos

Alternativas: Para computação numérica, considere Cython ou Numba. rust-cpython é legado e não recomendado para novos projetos.

PyO3 representa o estado-da-arte para bindings Rust↔Python, combinando segurança de memória do Rust com a flexibilidade do ecossistema Python.

Referências