hpp: C++ RAII wrapper (header-only, Step 7)

Тонкий слой поверх 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)
This commit is contained in:
2026-05-14 23:23:35 +01:00
parent 46c2b94939
commit 2530057507
+326
View File
@@ -0,0 +1,326 @@
/* cuframes C++ wrapper — header-only RAII.
*
* Тонкий слой поверх C API: классы с automatic resource cleanup,
* exceptions вместо int return codes, std::optional<Frame> для next().
*
* Linkage: header-only; нужен `-lcuframes` (shared) или
* `libcuframes_static.a` (static) при линковке user-кода.
*
* License: LGPL-2.1+
*/
#ifndef CUFRAMES_HPP
#define CUFRAMES_HPP
#include <cuframes/cuframes.h>
#include <chrono>
#include <cstring>
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
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<FrameRef> 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<void(const Frame &)>;
using OnError = std::function<void(int err, const std::string &msg)>;
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<AsyncSubscriber *>(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<AsyncSubscriber *>(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 */