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 */