From 2530057507d98199707e7eade48c8f349cc1e89f Mon Sep 17 00:00:00 2001 From: Evgeny Demchenko Date: Thu, 14 May 2026 23:23:35 +0100 Subject: [PATCH] hpp: C++ RAII wrapper (header-only, Step 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тонкий слой поверх C API: - cuframes::Error — exception при ошибках, code() для подробностей - cuframes::Publisher — RAII обёртка publisher'а (LIBRARY + EXTERNAL constructors) - cuframes::Subscriber + cuframes::FrameRef — RAII frame с автo-release - cuframes::AsyncSubscriber — с std::function callbacks - cuframes::Frame — read-only view (для callback'а) - cuframes::calc_frame_size(), now_ns() — utilities Smoke test (in dev container): $ g++ -std=c++17 ... -lcuframes -lcudart smoke.cpp $ ./smoke version: 0.1.0 FullHD NV12 frame: 3317760 bytes (pitch_y=2048, pitch_uv=2048) --- include/cuframes/cuframes.hpp | 326 ++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 include/cuframes/cuframes.hpp diff --git a/include/cuframes/cuframes.hpp b/include/cuframes/cuframes.hpp new file mode 100644 index 0000000..846688e --- /dev/null +++ b/include/cuframes/cuframes.hpp @@ -0,0 +1,326 @@ +/* cuframes C++ wrapper — header-only RAII. + * + * Тонкий слой поверх C API: классы с automatic resource cleanup, + * exceptions вместо int return codes, std::optional для next(). + * + * Linkage: header-only; нужен `-lcuframes` (shared) или + * `libcuframes_static.a` (static) при линковке user-кода. + * + * License: LGPL-2.1+ + */ + +#ifndef CUFRAMES_HPP +#define CUFRAMES_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuframes { + +/* ─── Exceptions ─────────────────────────────────────────────────────── */ + +class Error : public std::runtime_error { +public: + Error(int code, const std::string &context) + : std::runtime_error(context + ": " + cuframes_strerror(code)), + code_(code) {} + int code() const noexcept { return code_; } + +private: + int code_; +}; + +inline void check(int rc, const char *context) { + if (rc != CUFRAMES_OK) throw Error(rc, context); +} + +/* ─── Frame (read-only view, не owning) ──────────────────────────────── */ + +class Frame { +public: + explicit Frame(const cuframes_frame_t *f) : f_(f) {} + + void *cuda_ptr() const noexcept { return cuframes_frame_cuda_ptr(f_); } + cuframes_format_t format() const noexcept { return cuframes_frame_format(f_); } + int32_t width() const noexcept { + int32_t w = 0, h = 0; cuframes_frame_size(f_, &w, &h); return w; + } + int32_t height() const noexcept { + int32_t w = 0, h = 0; cuframes_frame_size(f_, &w, &h); return h; + } + int32_t pitch_y() const noexcept { return cuframes_frame_pitch_y(f_); } + int32_t pitch_uv() const noexcept { return cuframes_frame_pitch_uv(f_); } + uint64_t seq() const noexcept { return cuframes_frame_seq(f_); } + int64_t pts_ns() const noexcept { return cuframes_frame_pts_ns(f_); } + + /* Raw C handle (для interop с C APIs) */ + const cuframes_frame_t *raw() const noexcept { return f_; } + +private: + const cuframes_frame_t *f_; +}; + +/* ─── Publisher (LIBRARY ownership) ──────────────────────────────────── */ + +struct PublisherOptions { + std::string key; + int32_t width = 0; + int32_t height = 0; + cuframes_format_t format = CUFRAMES_FORMAT_NV12; + int32_t ring_size = 4; + cuframes_publisher_policy_t policy = CUFRAMES_POLICY_DROP_OLDEST; + int32_t consumer_ack_timeout_ms = 0; + int32_t cuda_device = 0; +}; + +class Publisher { +public: + explicit Publisher(const PublisherOptions &opt) { + cuframes_publisher_config_t cfg{}; + cfg.key = opt.key.c_str(); + cfg.width = opt.width; + cfg.height = opt.height; + cfg.format = opt.format; + cfg.ownership = CUFRAMES_OWNERSHIP_LIBRARY; + cfg.ring_size = opt.ring_size; + cfg.policy = opt.policy; + cfg.consumer_ack_timeout_ms = opt.consumer_ack_timeout_ms; + cfg.cuda_device = opt.cuda_device; + check(cuframes_publisher_create(&cfg, &pub_), "Publisher::create"); + } + + /* EXTERNAL ownership constructor */ + Publisher(const PublisherOptions &opt, + void *const *cuda_ptrs, int32_t ptr_count, size_t frame_size) { + cuframes_publisher_config_t cfg{}; + cfg.key = opt.key.c_str(); + cfg.width = opt.width; + cfg.height = opt.height; + cfg.format = opt.format; + cfg.ownership = CUFRAMES_OWNERSHIP_EXTERNAL; + cfg.policy = opt.policy; + cfg.consumer_ack_timeout_ms = opt.consumer_ack_timeout_ms; + cfg.cuda_device = opt.cuda_device; + check(cuframes_publisher_create_external(&cfg, cuda_ptrs, ptr_count, + frame_size, &pub_), + "Publisher::create_external"); + } + + ~Publisher() { + if (pub_) cuframes_publisher_destroy(pub_); + } + + Publisher(const Publisher &) = delete; + Publisher &operator=(const Publisher &) = delete; + Publisher(Publisher &&o) noexcept : pub_(o.pub_) { o.pub_ = nullptr; } + Publisher &operator=(Publisher &&o) noexcept { + if (this != &o) { + if (pub_) cuframes_publisher_destroy(pub_); + pub_ = o.pub_; + o.pub_ = nullptr; + } + return *this; + } + + /* LIBRARY mode: acquire pointer + publish */ + void *acquire() { + void *p = nullptr; + check(cuframes_publisher_acquire(pub_, &p), "Publisher::acquire"); + return p; + } + + /* `stream` — CUDA stream на котором писались данные */ + void publish(void *stream, int64_t pts_ns) { + check(cuframes_publisher_publish(pub_, stream, pts_ns), "Publisher::publish"); + } + + /* EXTERNAL mode */ + void publish_external(void *cuda_ptr, void *stream, int64_t pts_ns) { + check(cuframes_publisher_publish_external(pub_, cuda_ptr, stream, pts_ns), + "Publisher::publish_external"); + } + + cuframes_publisher_t *raw() noexcept { return pub_; } + +private: + cuframes_publisher_t *pub_ = nullptr; +}; + +/* ─── Subscriber (sync) ──────────────────────────────────────────────── */ + +struct SubscriberOptions { + std::string key; + std::string consumer_name; // empty = auto-generate + cuframes_subscriber_mode_t mode = CUFRAMES_MODE_NEWEST_ONLY; + int32_t cuda_device = 0; + int32_t connect_timeout_ms = 5000; +}; + +/* RAII-обёртка для одного frame'а; release при destruct */ +class FrameRef { +public: + FrameRef() noexcept = default; + FrameRef(cuframes_subscriber_t *sub, cuframes_frame_t *f) noexcept + : sub_(sub), f_(f) {} + + ~FrameRef() { + if (f_ && sub_) cuframes_subscriber_release(sub_, f_); + } + + FrameRef(const FrameRef &) = delete; + FrameRef &operator=(const FrameRef &) = delete; + FrameRef(FrameRef &&o) noexcept : sub_(o.sub_), f_(o.f_) { + o.sub_ = nullptr; + o.f_ = nullptr; + } + FrameRef &operator=(FrameRef &&o) noexcept { + if (this != &o) { + if (f_ && sub_) cuframes_subscriber_release(sub_, f_); + sub_ = o.sub_; + f_ = o.f_; + o.sub_ = nullptr; + o.f_ = nullptr; + } + return *this; + } + + explicit operator bool() const noexcept { return f_ != nullptr; } + + Frame view() const noexcept { return Frame(f_); } + + /* Accessor shortcuts */ + void *cuda_ptr() const noexcept { return cuframes_frame_cuda_ptr(f_); } + int32_t width() const noexcept { + int32_t w = 0, h = 0; cuframes_frame_size(f_, &w, &h); return w; + } + int32_t height() const noexcept { + int32_t w = 0, h = 0; cuframes_frame_size(f_, &w, &h); return h; + } + int32_t pitch_y() const noexcept { return cuframes_frame_pitch_y(f_); } + int32_t pitch_uv() const noexcept { return cuframes_frame_pitch_uv(f_); } + uint64_t seq() const noexcept { return cuframes_frame_seq(f_); } + int64_t pts_ns() const noexcept { return cuframes_frame_pts_ns(f_); } + +private: + cuframes_subscriber_t *sub_ = nullptr; + cuframes_frame_t *f_ = nullptr; +}; + +class Subscriber { +public: + explicit Subscriber(const SubscriberOptions &opt) { + cuframes_subscriber_config_t cfg{}; + cfg.key = opt.key.c_str(); + cfg.consumer_name = opt.consumer_name.empty() ? nullptr : opt.consumer_name.c_str(); + cfg.mode = opt.mode; + cfg.cuda_device = opt.cuda_device; + cfg.connect_timeout_ms = opt.connect_timeout_ms; + check(cuframes_subscriber_create(&cfg, &sub_), "Subscriber::create"); + } + + ~Subscriber() { + if (sub_) cuframes_subscriber_destroy(sub_); + } + + Subscriber(const Subscriber &) = delete; + Subscriber &operator=(const Subscriber &) = delete; + Subscriber(Subscriber &&o) noexcept : sub_(o.sub_) { o.sub_ = nullptr; } + Subscriber &operator=(Subscriber &&o) noexcept { + if (this != &o) { + if (sub_) cuframes_subscriber_destroy(sub_); + sub_ = o.sub_; + o.sub_ = nullptr; + } + return *this; + } + + /* Returns empty FrameRef если TIMEOUT/WOULD_BLOCK/DISCONNECTED. + * Иначе бросает Error. + * + * `stream` — ваш CUDA stream. Если nullptr — будет cudaEventSynchronize. */ + std::optional next(void *stream, int32_t timeout_ms = -1) { + cuframes_frame_t *f = nullptr; + int r = cuframes_subscriber_next(sub_, stream, &f, timeout_ms); + if (r == CUFRAMES_OK) return FrameRef(sub_, f); + if (r == CUFRAMES_ERR_TIMEOUT || r == CUFRAMES_ERR_WOULD_BLOCK || + r == CUFRAMES_ERR_DISCONNECTED) + return std::nullopt; + throw Error(r, "Subscriber::next"); + } + + cuframes_subscriber_t *raw() noexcept { return sub_; } + +private: + cuframes_subscriber_t *sub_ = nullptr; +}; + +/* ─── AsyncSubscriber ────────────────────────────────────────────────── */ + +class AsyncSubscriber { +public: + using OnFrame = std::function; + using OnError = std::function; + + AsyncSubscriber(const SubscriberOptions &opt, + OnFrame on_frame, + OnError on_error = {}) + : on_frame_(std::move(on_frame)), on_error_(std::move(on_error)) { + cuframes_subscriber_config_t cfg{}; + cfg.key = opt.key.c_str(); + cfg.consumer_name = opt.consumer_name.empty() ? nullptr : opt.consumer_name.c_str(); + cfg.mode = opt.mode; + cfg.cuda_device = opt.cuda_device; + cfg.connect_timeout_ms = opt.connect_timeout_ms; + check(cuframes_async_subscriber_create(&cfg, + &AsyncSubscriber::frame_trampoline, + &AsyncSubscriber::error_trampoline, + this, &sub_), + "AsyncSubscriber::create"); + } + + ~AsyncSubscriber() { + if (sub_) cuframes_async_subscriber_destroy(sub_); + } + + AsyncSubscriber(const AsyncSubscriber &) = delete; + AsyncSubscriber &operator=(const AsyncSubscriber &) = delete; + +private: + static void frame_trampoline(const cuframes_frame_t *f, void *ud) { + auto *self = static_cast(ud); + if (self->on_frame_) self->on_frame_(Frame(f)); + } + static void error_trampoline(int err, const char *msg, void *ud) { + auto *self = static_cast(ud); + if (self->on_error_) self->on_error_(err, msg ? msg : ""); + } + + cuframes_async_subscriber_t *sub_ = nullptr; + OnFrame on_frame_; + OnError on_error_; +}; + +/* ─── Утилиты ─────────────────────────────────────────────────────────── */ + +inline int64_t now_ns() { return cuframes_now_ns(); } + +inline size_t calc_frame_size(cuframes_format_t format, int32_t w, int32_t h, + int32_t *pitch_y = nullptr, + int32_t *pitch_uv = nullptr) { + size_t s = 0; + check(cuframes_calc_frame_size(format, w, h, &s, pitch_y, pitch_uv), + "calc_frame_size"); + return s; +} + +} // namespace cuframes + +#endif /* CUFRAMES_HPP */