| name | rust-pyo3-bridge |
| description | Bridge Rust and Python using PyO3 with proper GIL handling and async compatibility. Use when integrating Python ML models or libraries. |
PyO3 Python Bridge
Integrate Python code with Rust using PyO3 for ML inference and library access.
Setup
[dependencies]
pyo3 = { version = "0.20", features = ["auto-initialize"] }
Basic Python Bridge Structure
use pyo3::prelude::*;
use pyo3::types::{PyList, PyModule};
use std::path::PathBuf;
pub struct PythonBridge {
module_path: PathBuf,
initialized: bool,
}
impl PythonBridge {
pub fn new(module_path: PathBuf) -> Result<Self> {
Ok(Self {
module_path,
initialized: false,
})
}
pub fn initialize(&mut self) -> Result<()> {
if self.initialized {
return Ok(());
}
Python::with_gil(|py| {
let sys = PyModule::import(py, "sys")?;
let path: &PyList = sys.getattr("path")?.downcast()?;
let path_str = self.module_path.to_str()
.ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>(
"Invalid path"
))?;
path.insert(0, path_str)?;
Ok::<(), PyErr>(())
})?;
self.initialized = true;
Ok(())
}
}
Path Resolution (Windows-Aware)
fn find_python_module() -> Result<PathBuf> {
let candidates = vec![
PathBuf::from("python"),
PathBuf::from("../python"),
PathBuf::from("E:/project/python"),
];
for candidate in candidates {
let init_file = candidate.join("module/__init__.py");
if init_file.exists() {
let absolute = candidate.canonicalize()?;
let path_str = absolute.to_string_lossy();
let clean_path = if path_str.starts_with(r"\\?\") {
PathBuf::from(&path_str[4..])
} else {
absolute
};
return Ok(clean_path);
}
}
Err(Error::ModuleNotFound)
}
Calling Python Functions
impl PythonBridge {
pub fn call_function(&self, func_name: &str, arg: &str) -> Result<String> {
Python::with_gil(|py| {
let module = PyModule::import(py, "my_module")?;
let func = module.getattr(func_name)?;
let result = func.call1((arg,))?;
let output: String = result.extract()?;
Ok(output)
})
}
pub fn transcribe(&self, audio_path: &str) -> Result<String> {
Python::with_gil(|py| {
let embedders = PyModule::import(py, "embedders")?;
let transcribe_fn = embedders.getattr("transcribe")?;
let result = transcribe_fn.call1((audio_path,))?;
let : = result.()?;
(text)
})
}
}
Extracting Complex Types
pub fn embed_video(&self, video_path: &str) -> Result<Vec<f32>> {
Python::with_gil(|py| {
let embedders = PyModule::import(py, "embedders")?;
let embed_fn = embedders.getattr("embed_video")?;
let result = embed_fn.call1((video_path,))?;
let embedding: Vec<f32> = result.extract()?;
Ok(embedding)
})
}
pub fn analyze_video(&self, video_path: &str) -> Result<VideoAnalysis> {
Python::with_gil(|py| {
let module = PyModule::import(py, "analyzer")?;
let analyze_fn = module.getattr("analyze")?;
let result = analyze_fn.call1((video_path,))?;
: = result
.()?
.()
.();
: <> = result
.()
.()
.(|v| v.().())
.();
: = result
.()
.()
.(|v| v.().())
.();
(VideoAnalysis {
description,
keywords,
is_meme,
})
})
}
Passing Lists to Python
pub fn embed_images(&self, image_paths: &[String]) -> Result<Vec<f32>> {
Python::with_gil(|py| {
let embedders = PyModule::import(py, "embedders")?;
let embed_fn = embedders.getattr("embed_images")?;
let py_list = PyList::new(py, image_paths);
let result = embed_fn.call1((py_list,))?;
let embedding: Vec<f32> = result.extract()?;
Ok(embedding)
})
}
Async Integration with spawn_blocking
use tokio::task::spawn_blocking;
impl PythonBridge {
pub async fn transcribe_async(&self, audio_path: String) -> Result<String> {
let module_path = self.module_path.clone();
spawn_blocking(move || {
Python::with_gil(|py| {
let sys = PyModule::import(py, "sys")?;
let path: &PyList = sys.getattr("path")?.downcast()?;
path.insert(0, module_path.to_str().unwrap())?;
let embedders = PyModule::import(py, "embedders")?;
let result = embedders
.getattr("transcribe")?
.call1((&audio_path,))?;
result.extract::<>()
})
})
.?
.(|e| Error::(e.()))
}
}
Releasing GIL for Parallelism
Python::with_gil(|py| {
py.allow_threads(|| {
expensive_rust_computation()
})
});
GPU Memory Monitoring via pynvml
pub fn get_gpu_memory() -> Result<(u64, u64)> {
Python::with_gil(|py| {
let pynvml = PyModule::import(py, "pynvml")?;
pynvml.call_method0("nvmlInit")?;
let handle = pynvml.call_method1("nvmlDeviceGetHandleByIndex", (0,))?;
let info = pynvml.call_method1("nvmlDeviceGetMemoryInfo", (handle,))?;
let used: u64 = info.getattr("used")?.extract()?;
let total: u64 = info.getattr("total")?.extract()?;
Ok((used, total))
})
}
Model Loading and Unloading
impl PythonBridge {
pub fn load_models(&mut self) -> Result<()> {
Python::with_gil(|py| {
let embedders = PyModule::import(py, "embedders")?;
embedders.getattr("load_all_models")?.call0()?;
Ok::<(), PyErr>(())
})?;
Ok(())
}
pub fn unload_models(&mut self) -> Result<()> {
Python::with_gil(|py| {
let embedders = PyModule::import(py, "embedders")?;
embedders.getattr("unload_all_models")?.call0()?;
Ok::<(), PyErr>(())
})?;
Ok(())
}
}
Error Handling
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PythonError {
#[error("Python import error: {0}")]
Import(String),
#[error("Python execution error: {0}")]
Execution(String),
#[error("Type conversion error: {0}")]
TypeConversion(String),
}
impl From<PyErr> for PythonError {
fn from(err: PyErr) -> Self {
Python::with_gil(|py| {
PythonError::Execution(err.to_string())
})
}
}
Guidelines
- Always use
Python::with_gil() for Python access
- Use
spawn_blocking for async integration
- Use
py.allow_threads() for parallel Rust work
- Handle Windows path prefixes (\?)
- Add module paths to sys.path before import
- Extract types safely with
.ok() and .unwrap_or_default()
- Unload models to free GPU memory when done
Examples
See hercules-local-algo/src/python/mod.rs for complete implementation.