#![allow(clippy::useless_conversion)]
use std::sync::OnceLock;
use bytes::Bytes;
use pyo3::create_exception;
use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use s4_codec_rs::{cpu_gzip, cpu_zstd, ChunkManifest, Codec, CodecError, CodecKind};
use tokio::runtime::{Builder, Runtime};
create_exception!(s4_codec, S4Error, PyValueError);
create_exception!(s4_codec, S4CrcMismatchError, S4Error);
create_exception!(s4_codec, S4SizeMismatchError, S4Error);
create_exception!(s4_codec, S4CodecMismatchError, S4Error);
create_exception!(s4_codec, S4UnregisteredCodecError, S4Error);
create_exception!(s4_codec, S4ManifestSizeExceedsLimitError, S4Error);
create_exception!(s4_codec, S4ManifestSizeMismatchError, S4Error);
create_exception!(s4_codec, S4BackendError, PyRuntimeError);
create_exception!(s4_codec, S4IoError, PyIOError);
fn runtime() -> &'static Runtime {
static RT: OnceLock<Runtime> = OnceLock::new();
RT.get_or_init(|| {
Builder::new_multi_thread()
.enable_all()
.thread_name("s4-codec-py")
.build()
.expect("failed to start tokio runtime for s4_codec python binding")
})
}
fn codec_err_to_py(e: CodecError) -> PyErr {
use s4_codec_rs::CodecError::*;
match e {
SizeMismatch { expected, got } => {
S4SizeMismatchError::new_err(format!("size mismatch: expected {expected}, got {got}"))
}
CrcMismatch { expected, got } => S4CrcMismatchError::new_err(format!(
"crc32c mismatch: expected {expected:#010x}, got {got:#010x}"
)),
CodecMismatch { expected, got } => S4CodecMismatchError::new_err(format!(
"codec mismatch: expected {expected:?}, got {got:?}"
)),
UnregisteredCodec(k) => {
S4UnregisteredCodecError::new_err(format!("codec {k:?} not registered"))
}
ManifestSizeExceedsLimit { requested, limit } => S4ManifestSizeExceedsLimitError::new_err(
format!("manifest claims {requested} bytes but limit is {limit}"),
),
ManifestSizeMismatch { manifest, actual } => S4ManifestSizeMismatchError::new_err(format!(
"manifest claims {manifest} bytes but body is {actual}"
)),
Backend(msg) => S4BackendError::new_err(format!("backend: {msg}")),
Io(e) => S4IoError::new_err(format!("io: {e}")),
TruncatedStream { expected, got } => S4Error::new_err(format!(
"stream truncated: expected {expected} input bytes, got {got}"
)),
OverlengthStream { expected, got } => S4Error::new_err(format!(
"stream over-length: expected {expected} input bytes, got at least {got}"
)),
Join(e) => S4BackendError::new_err(format!("backend (worker join): {e}")),
}
}
fn manifest_from_parts(
kind: CodecKind,
payload_len: u64,
original_size: u64,
crc32c: u32,
) -> ChunkManifest {
ChunkManifest {
codec: kind,
original_size,
compressed_size: payload_len,
crc32c,
}
}
fn block_on<F, T>(py: Python<'_>, fut: F) -> T
where
F: std::future::Future<Output = T> + Send,
T: Send,
{
py.allow_threads(|| runtime().block_on(fut))
}
#[pyclass(name = "CpuZstd", module = "s4_codec")]
struct PyCpuZstd {
inner: cpu_zstd::CpuZstd,
}
#[pymethods]
impl PyCpuZstd {
#[new]
#[pyo3(signature = (level = 3))]
fn new(level: i32) -> Self {
Self {
inner: cpu_zstd::CpuZstd::new(level),
}
}
fn compress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
let input = Bytes::copy_from_slice(data.as_bytes());
let codec = self.inner.clone();
let (out, manifest) =
block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
Ok((
PyBytes::new(py, &out),
manifest.original_size,
manifest.crc32c,
))
}
fn decompress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
original_size: u64,
crc32c: u32,
) -> PyResult<Bound<'py, PyBytes>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let manifest = manifest_from_parts(
CodecKind::CpuZstd,
input.len() as u64,
original_size,
crc32c,
);
let codec = self.inner.clone();
let out = block_on(py, async move { codec.decompress(input, &manifest).await })
.map_err(codec_err_to_py)?;
Ok(PyBytes::new(py, &out))
}
fn __repr__(&self) -> String {
format!("CpuZstd(level={})", cpu_zstd::CpuZstd::DEFAULT_LEVEL)
}
}
#[pyclass(name = "CpuGzip", module = "s4_codec")]
struct PyCpuGzip {
inner: cpu_gzip::CpuGzip,
}
#[pymethods]
impl PyCpuGzip {
#[new]
#[pyo3(signature = (level = 6))]
fn new(level: u32) -> Self {
Self {
inner: cpu_gzip::CpuGzip::new(level),
}
}
fn compress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
) -> PyResult<(Bound<'py, PyBytes>, u64, u32)> {
let input = Bytes::copy_from_slice(data.as_bytes());
let codec = self.inner.clone();
let (out, manifest) =
block_on(py, async move { codec.compress(input).await }).map_err(codec_err_to_py)?;
Ok((
PyBytes::new(py, &out),
manifest.original_size,
manifest.crc32c,
))
}
fn decompress<'py>(
&self,
py: Python<'py>,
data: &Bound<'py, PyBytes>,
original_size: u64,
crc32c: u32,
) -> PyResult<Bound<'py, PyBytes>> {
let input = Bytes::copy_from_slice(data.as_bytes());
let manifest = manifest_from_parts(
CodecKind::CpuGzip,
input.len() as u64,
original_size,
crc32c,
);
let codec = self.inner.clone();
let out = block_on(py, async move { codec.decompress(input, &manifest).await })
.map_err(codec_err_to_py)?;
Ok(PyBytes::new(py, &out))
}
fn __repr__(&self) -> String {
format!("CpuGzip(level={})", cpu_gzip::CpuGzip::DEFAULT_LEVEL)
}
}
#[pyfunction]
fn gpu_available() -> bool {
s4_codec_rs::nvcomp::is_gpu_available()
}
#[pymodule]
fn s4_codec(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCpuZstd>()?;
m.add_class::<PyCpuGzip>()?;
m.add_function(wrap_pyfunction!(gpu_available, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("S4Error", py.get_type::<S4Error>())?;
m.add("S4CrcMismatchError", py.get_type::<S4CrcMismatchError>())?;
m.add("S4SizeMismatchError", py.get_type::<S4SizeMismatchError>())?;
m.add(
"S4CodecMismatchError",
py.get_type::<S4CodecMismatchError>(),
)?;
m.add(
"S4UnregisteredCodecError",
py.get_type::<S4UnregisteredCodecError>(),
)?;
m.add(
"S4ManifestSizeExceedsLimitError",
py.get_type::<S4ManifestSizeExceedsLimitError>(),
)?;
m.add(
"S4ManifestSizeMismatchError",
py.get_type::<S4ManifestSizeMismatchError>(),
)?;
m.add("S4BackendError", py.get_type::<S4BackendError>())?;
m.add("S4IoError", py.get_type::<S4IoError>())?;
Ok(())
}