python: skeleton pybind11 bindings (issue #6 task #197)

Каркас Python-пакета `cuframes`:
- python/pyproject.toml — scikit-build-core конфиг
- python/CMakeLists.txt — pybind11 module через FetchContent
- python/src/_native.cpp — module entry, error таксономия,
  enum mirrors (PixelFormat, SubscriberMode), version
- python/cuframes/__init__.py — re-export публичного API
- python/tests/test_smoke.py — smoke tests без real subscribe
- python/README.md — статус + build instructions
- CMakeLists.txt — подключение python/ при BUILD_PYTHON_BINDINGS=ON

Реальный subscriber/frame wrapper в следующих коммитах
(tasks #198-#202).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:59:04 +01:00
parent 655649f4d8
commit a7da4ea728
8 changed files with 406 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
// cuframes Python bindings — pybind11 entry point.
//
// Этот файл — skeleton модуля. Полноценные обёртки subscriber/frame появятся
// в следующих коммитах (см. issue gx/cuframes#6, tasks #198-#202).
//
// Контракт thread-safety: см. docs/python.md (т.б.д. в task #202).
#include <pybind11/pybind11.h>
#include <stdexcept>
#include "cuframes/cuframes.h"
namespace py = pybind11;
namespace {
// ─────────────────────────────────────────────────────────────────────────────
// Error taxonomy — Python exceptions, соответствующие cuframes_error_t.
//
// Принцип: каждая категория ошибок которая требует разной обработки в
// downstream'е (reconnect vs retry vs fatal) → отдельный exception class.
// Это решает требование из architect review: «detector должен уметь
// reconnect-loop по publisher-gone, не падать».
// ─────────────────────────────────────────────────────────────────────────────
struct CuframesExceptions {
py::object base;
py::object publisher_gone; // CUFRAMES_ERR_DISCONNECTED, _NOT_FOUND
py::object frame_timeout; // CUFRAMES_ERR_TIMEOUT, _WOULD_BLOCK
py::object device_lost; // CUFRAMES_ERR_CUDA
py::object shm_error; // CUFRAMES_ERR_IO
py::object protocol_mismatch; // CUFRAMES_ERR_PROTOCOL
py::object invalid_argument; // CUFRAMES_ERR_INVALID_ARG
py::object out_of_memory; // CUFRAMES_ERR_OUT_OF_MEMORY
py::object internal; // CUFRAMES_ERR_INTERNAL, прочее
};
CuframesExceptions g_exc;
// Маппинг cuframes_error_t → подходящий Python exception class.
py::object exception_for(int err) {
switch (err) {
case CUFRAMES_ERR_NOT_FOUND:
case CUFRAMES_ERR_DISCONNECTED:
return g_exc.publisher_gone;
case CUFRAMES_ERR_TIMEOUT:
case CUFRAMES_ERR_WOULD_BLOCK:
return g_exc.frame_timeout;
case CUFRAMES_ERR_CUDA:
return g_exc.device_lost;
case CUFRAMES_ERR_IO:
return g_exc.shm_error;
case CUFRAMES_ERR_PROTOCOL:
return g_exc.protocol_mismatch;
case CUFRAMES_ERR_INVALID_ARG:
return g_exc.invalid_argument;
case CUFRAMES_ERR_OUT_OF_MEMORY:
return g_exc.out_of_memory;
default:
return g_exc.internal;
}
}
// Бросает подходящий exception если err != CUFRAMES_OK.
void check(int err, const char* operation = nullptr) {
if (err == CUFRAMES_OK) return;
const char* msg = cuframes_strerror(err);
std::string what = operation
? std::string(operation) + ": " + msg + " (code=" + std::to_string(err) + ")"
: std::string(msg) + " (code=" + std::to_string(err) + ")";
PyErr_SetString(exception_for(err).ptr(), what.c_str());
throw py::error_already_set();
}
} // namespace
PYBIND11_MODULE(_native, m) {
m.doc() = "cuframes — zero-copy CUDA frame sharing (native bindings)";
// ── Версия ──────────────────────────────────────────────────────────
m.def("version_string", []() {
return std::string(cuframes_version_string());
}, "Runtime version of libcuframes (MAJOR.MINOR.PATCH).");
m.def("protocol_version", []() {
return static_cast<uint32_t>(cuframes_protocol_version());
}, "Wire-protocol version. Subscribers с разной версией не подключатся.");
m.attr("__binding_version__") = CUFRAMES_PY_BINDING_VERSION;
// ── Error taxonomy ──────────────────────────────────────────────────
// Иерархия:
// CuframesError (base)
// ├── CuframesPublisherGone
// ├── CuframesFrameTimeout
// ├── CuframesDeviceLost
// ├── CuframesShmError
// ├── CuframesProtocolMismatch
// ├── CuframesInvalidArgument
// ├── CuframesOutOfMemory
// └── CuframesInternal
//
// Downstream code может ловить либо конкретный subtype, либо CuframesError
// как catch-all.
g_exc.base = py::exception<std::runtime_error>(m, "CuframesError").attr("__class__");
auto make_subexc = [&m](const char* name) {
return py::exception<std::runtime_error>(m, name, g_exc.base.ptr()).attr("__class__");
};
g_exc.publisher_gone = make_subexc("CuframesPublisherGone");
g_exc.frame_timeout = make_subexc("CuframesFrameTimeout");
g_exc.device_lost = make_subexc("CuframesDeviceLost");
g_exc.shm_error = make_subexc("CuframesShmError");
g_exc.protocol_mismatch = make_subexc("CuframesProtocolMismatch");
g_exc.invalid_argument = make_subexc("CuframesInvalidArgument");
g_exc.out_of_memory = make_subexc("CuframesOutOfMemory");
g_exc.internal = make_subexc("CuframesInternal");
// ── Pixel formats (enum mirror) ─────────────────────────────────────
py::enum_<cuframes_format_t>(m, "PixelFormat")
.value("NV12", CUFRAMES_FORMAT_NV12)
.value("YUV420P", CUFRAMES_FORMAT_YUV420P)
.value("RGB", CUFRAMES_FORMAT_RGB)
.value("BGR", CUFRAMES_FORMAT_BGR)
.value("RGBA", CUFRAMES_FORMAT_RGBA)
.value("GRAYSCALE", CUFRAMES_FORMAT_GRAYSCALE);
py::enum_<cuframes_subscriber_mode_t>(m, "SubscriberMode")
.value("NEWEST_ONLY", CUFRAMES_MODE_NEWEST_ONLY)
.value("STRICT_ORDER", CUFRAMES_MODE_STRICT_ORDER);
// TODO(task #198): CuframesSubscriber + CuframesFrame classes.
// TODO(task #199): DLPack export.
// TODO(task #200): Health/stats properties (потребует расширения C API).
// TODO(task #201): Per-subscriber CUDA stream + thread-safety contract.
}