Skip to content

Commit f8babe6

Browse files
committed
Update to version 1.0.1; rename dynamic_buffer header and add backward compatibility shim
1 parent 98a31ee commit f8babe6

7 files changed

Lines changed: 193 additions & 168 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## v1.0.1 - 2026-06-17
4+
5+
- Renamed canonical header from `slick/dynamic_buffer.h` to `slick/dynamic_buffer.hpp`. The old .h path is kept as a backward-compatibility shim that re-exports the new header and emits a compiler warning directing users to update their includes.
6+
37
## v1.0.0 - 2026-06-16
48

59
### Added

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.10)
22

33
project(slick-dynamic-buffer
4-
VERSION 1.0.0
4+
VERSION 1.0.1
55
DESCRIPTION "Header-only Boost.Asio DynamicBuffer adapter for slick buffer backends (stream_buffer, producer_buffer) - zero-copy fan-out of received network bytes to lock-free consumers"
66
LANGUAGES CXX)
77

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Supported backends (any type satisfying `slick::buffer_backend`):
1919
| `slick::stream_buffer_multiplexer::producer_buffer` | Fans into an MPMC shared queue so multiplexer consumers receive the record |
2020

2121
The library has **no hard dependency** on either backend — bring your own and include the
22-
relevant header alongside `<slick/dynamic_buffer.h>`.
22+
relevant header alongside `<slick/dynamic_buffer.hpp>`.
2323

2424
## How it works
2525

@@ -64,10 +64,10 @@ from the underlying buffer.
6464
Header-only. Add the `include` directory to your include path:
6565

6666
```cpp
67-
#include <slick/dynamic_buffer.h>
67+
#include <slick/dynamic_buffer.hpp>
6868
// also include your chosen backend:
69-
#include <slick/stream_buffer.h> // for stream_buffer backend
70-
#include <slick/stream_buffer_multiplexer.h> // for producer_buffer backend
69+
#include <slick/stream_buffer.hpp> // for stream_buffer backend
70+
#include <slick/stream_buffer_multiplexer.hpp> // for producer_buffer backend
7171
```
7272

7373
### Using CMake FetchContent
@@ -94,8 +94,8 @@ target_link_libraries(your_target PRIVATE
9494
### With `slick::stream_buffer` (SPMC)
9595

9696
```cpp
97-
#include <slick/dynamic_buffer.h>
98-
#include <slick/stream_buffer.h>
97+
#include <slick/dynamic_buffer.hpp>
98+
#include <slick/stream_buffer.hpp>
9999

100100
// 64 MB data ring, 64K message records; named -> shared memory, nullptr -> local
101101
slick::stream_buffer stream(1ull << 26, 1u << 16, "market_data");
@@ -131,8 +131,8 @@ for (;;) {
131131
### With `slick::stream_buffer_multiplexer::producer_buffer` (MPMC)
132132

133133
```cpp
134-
#include <slick/dynamic_buffer.h>
135-
#include <slick/stream_buffer_multiplexer.h>
134+
#include <slick/dynamic_buffer.hpp>
135+
#include <slick/stream_buffer_multiplexer.hpp>
136136

137137
slick::stream_buffer_multiplexer mux(1024);
138138
auto pb = mux.add_producer(0, 1ull << 26, 1u << 16); // shared_ptr<producer_buffer>

include/slick/dynamic_buffer.h

Lines changed: 8 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -11,155 +11,11 @@
1111

1212
#pragma once
1313

14-
#include <boost/asio/buffer.hpp>
15-
16-
#include <concepts>
17-
#include <cstddef>
18-
#include <cstdint>
19-
#include <limits>
20-
#include <memory>
21-
#include <stdexcept>
22-
#include <utility>
23-
24-
namespace slick {
25-
26-
/**
27-
* @brief Named requirements (C++20 concept) for a buffer backend usable with dynamic_buffer.
28-
*
29-
* Satisfied by slick::stream_buffer and
30-
* slick::stream_buffer_multiplexer::producer_buffer. The backend must be
31-
* single-producer: all prepare/commit/consume/discard calls for one instance
32-
* must come from a single thread.
33-
*/
34-
template<typename T>
35-
concept buffer_backend = requires(T& b, const T& cb, std::size_t n) {
36-
{ b.prepare(n) } -> std::same_as<std::pair<uint8_t*, std::size_t>>;
37-
{ b.commit(n) };
38-
{ b.consume(n) };
39-
{ b.discard() };
40-
{ cb.data() } -> std::same_as<const uint8_t*>;
41-
{ cb.size() } -> std::convertible_to<std::size_t>;
42-
{ cb.capacity() } -> std::convertible_to<std::size_t>;
43-
};
44-
45-
/**
46-
* @brief Boost.Asio DynamicBuffer_v1 adapter over a slick buffer backend.
47-
*
48-
* A drop-in replacement for beast::flat_buffer: pass it to boost::asio/boost::beast
49-
* read operations so received network bytes are written directly into the backend ring —
50-
* no copy is needed to hand the data to other threads or processes. When the application
51-
* has a complete package in the readable area, calling consume(n) publishes those n bytes
52-
* to consumers as one message record (it does not discard them).
53-
*
54-
* Supported backends:
55-
* - slick::stream_buffer — SPMC ring; consumers call stream_buffer.read(cursor)
56-
* - slick::stream_buffer_multiplexer::producer_buffer — fans into the shared MPMC queue
57-
*
58-
* This is a cheap copyable handle: asio composed operations copy DynamicBuffer_v1 objects
59-
* by value, so the adapter stores a std::shared_ptr to the backend. Two constructors are
60-
* provided: a shared-ownership one (pass shared_ptr<BufferT>, e.g. from add_producer())
61-
* and a non-owning reference one (pass BufferT&, caller must ensure the backend outlives
62-
* all copies).
63-
*
64-
* Note: each consume(n) call publishes exactly one record. If a protocol layer consumes
65-
* incrementally (e.g. the beast HTTP parser), records correspond to those increments; call
66-
* consume() yourself on message boundaries when you need strict package framing.
67-
*/
68-
template<buffer_backend BufferT>
69-
class dynamic_buffer {
70-
public:
71-
/// The type used to represent the readable bytes as a single contiguous buffer
72-
using const_buffers_type = boost::asio::const_buffer;
73-
/// The type used to represent the writable bytes as a single contiguous buffer
74-
using mutable_buffers_type = boost::asio::mutable_buffer;
75-
76-
/**
77-
* @brief Shared-ownership constructor — participates in the backend's ref count.
78-
* @param ptr Shared pointer to the backend; the backend stays alive as long as
79-
* this adapter (or any copy) holds a reference.
80-
* @param max_size Optional cap on size() + prepared bytes; clamped to backend capacity.
81-
*/
82-
explicit dynamic_buffer(
83-
std::shared_ptr<BufferT> ptr,
84-
std::size_t max_size = (std::numeric_limits<std::size_t>::max)()) noexcept
85-
: buffer_(std::move(ptr))
86-
, max_size_(max_size < buffer_->capacity()
87-
? max_size
88-
: static_cast<std::size_t>(buffer_->capacity()))
89-
{
90-
}
91-
92-
/**
93-
* @brief Non-owning reference constructor — backward-compatible with local backends.
94-
* @param buffer The backend; must outlive this adapter and all copies of it.
95-
* @param max_size Optional cap on size() + prepared bytes; clamped to buffer.capacity().
96-
* beast/asio read operations use max_size() to limit how much they read.
97-
*
98-
* Internally wraps the reference in a shared_ptr with a null deleter so copies are
99-
* cheap, but lifetime management remains the caller's responsibility.
100-
*/
101-
explicit dynamic_buffer(
102-
BufferT& buffer,
103-
std::size_t max_size = (std::numeric_limits<std::size_t>::max)())
104-
: dynamic_buffer(std::shared_ptr<BufferT>(&buffer, [](BufferT*){}), max_size)
105-
{
106-
}
107-
108-
/// The number of readable (committed but not consumed) bytes
109-
std::size_t size() const noexcept {
110-
const std::size_t n = buffer_->size();
111-
return n < max_size_ ? n : max_size_;
112-
}
113-
114-
/// The maximum sum of readable and writable bytes
115-
std::size_t max_size() const noexcept { return max_size_; }
116-
117-
/// The maximum sum of readable and writable bytes that can be held without relocation
118-
std::size_t capacity() const noexcept { return static_cast<std::size_t>(buffer_->capacity()); }
119-
120-
/// The readable bytes as a single contiguous buffer
121-
const_buffers_type data() const noexcept {
122-
return { buffer_->data(), size() };
123-
}
124-
125-
/**
126-
* @brief Get a writable region of n bytes at the end of the readable area.
127-
* @throws std::length_error if size() + n exceeds max_size().
128-
*/
129-
mutable_buffers_type prepare(std::size_t n) {
130-
if (buffer_->size() + n > max_size_) {
131-
throw std::length_error("dynamic_buffer too long");
132-
}
133-
auto [ptr, sz] = buffer_->prepare(n);
134-
return { ptr, sz };
135-
}
136-
137-
/// Move n bytes from the writable area to the readable area
138-
void commit(std::size_t n) noexcept { buffer_->commit(n); }
139-
140-
/// Publish the first n readable bytes to consumers as one message record.
141-
/// Returns the record as consumers will see it (asio/beast callers may ignore it).
142-
/// The return type is deduced from BufferT::consume() — published_record for slick backends.
143-
auto consume(std::size_t n) noexcept { return buffer_->consume(n); }
144-
145-
/// Discard the readable bytes and any prepared region without publishing them,
146-
/// matching beast::flat_buffer::clear(). Use after a connection drops mid-message
147-
/// so the partial bytes are not prepended to the next connection's data. This
148-
/// does not create a new record; older published records still follow the normal
149-
/// lossy overwrite semantics.
150-
void clear() noexcept { buffer_->discard(); }
151-
152-
/// Access the underlying backend (e.g. for in-process consumers calling read()).
153-
BufferT& buffer() noexcept { return *buffer_; }
154-
const BufferT& buffer() const noexcept { return *buffer_; }
155-
156-
/// Shared-ownership handle to the backend, so it can safely outlive this adapter.
157-
std::shared_ptr<BufferT> buffer_ptr() noexcept { return buffer_; }
158-
std::shared_ptr<const BufferT> buffer_ptr() const noexcept { return buffer_; }
159-
160-
private:
161-
std::shared_ptr<BufferT> buffer_;
162-
std::size_t max_size_;
163-
};
164-
165-
} // namespace slick
14+
// Backward-compatibility shim. The canonical header is <slick/dynamic_buffer.hpp>.
15+
#pragma once
16+
#ifdef _MSC_VER
17+
# pragma message("warning: <slick/dynamic_buffer.h> is deprecated; use <slick/dynamic_buffer.hpp>")
18+
#else
19+
# warning "<slick/dynamic_buffer.h> is deprecated; use <slick/dynamic_buffer.hpp>"
20+
#endif
21+
#include "dynamic_buffer.hpp"

include/slick/dynamic_buffer.hpp

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/********************************************************************************
2+
* Copyright (c) 2026 Slick Quant LLC
3+
* All rights reserved
4+
*
5+
* This file is part of the slick-dynamic-buffer. Redistribution and use in source
6+
* and binary forms, with or without modification, are permitted exclusively
7+
* under the terms of the MIT license which is available at
8+
* https://github.com/SlickQuant/slick-dynamic-buffer/blob/main/LICENSE
9+
*
10+
********************************************************************************/
11+
12+
#pragma once
13+
14+
#include <boost/asio/buffer.hpp>
15+
16+
#include <concepts>
17+
#include <cstddef>
18+
#include <cstdint>
19+
#include <limits>
20+
#include <memory>
21+
#include <stdexcept>
22+
#include <utility>
23+
24+
namespace slick {
25+
26+
/**
27+
* @brief Named requirements (C++20 concept) for a buffer backend usable with dynamic_buffer.
28+
*
29+
* Satisfied by slick::stream_buffer and
30+
* slick::stream_buffer_multiplexer::producer_buffer. The backend must be
31+
* single-producer: all prepare/commit/consume/discard calls for one instance
32+
* must come from a single thread.
33+
*/
34+
template<typename T>
35+
concept buffer_backend = requires(T& b, const T& cb, std::size_t n) {
36+
{ b.prepare(n) } -> std::same_as<std::pair<uint8_t*, std::size_t>>;
37+
{ b.commit(n) };
38+
{ b.consume(n) };
39+
{ b.discard() };
40+
{ cb.data() } -> std::same_as<const uint8_t*>;
41+
{ cb.size() } -> std::convertible_to<std::size_t>;
42+
{ cb.capacity() } -> std::convertible_to<std::size_t>;
43+
};
44+
45+
/**
46+
* @brief Boost.Asio DynamicBuffer_v1 adapter over a slick buffer backend.
47+
*
48+
* A drop-in replacement for beast::flat_buffer: pass it to boost::asio/boost::beast
49+
* read operations so received network bytes are written directly into the backend ring —
50+
* no copy is needed to hand the data to other threads or processes. When the application
51+
* has a complete package in the readable area, calling consume(n) publishes those n bytes
52+
* to consumers as one message record (it does not discard them).
53+
*
54+
* Supported backends:
55+
* - slick::stream_buffer — SPMC ring; consumers call stream_buffer.read(cursor)
56+
* - slick::stream_buffer_multiplexer::producer_buffer — fans into the shared MPMC queue
57+
*
58+
* This is a cheap copyable handle: asio composed operations copy DynamicBuffer_v1 objects
59+
* by value, so the adapter stores a std::shared_ptr to the backend. Two constructors are
60+
* provided: a shared-ownership one (pass shared_ptr<BufferT>, e.g. from add_producer())
61+
* and a non-owning reference one (pass BufferT&, caller must ensure the backend outlives
62+
* all copies).
63+
*
64+
* Note: each consume(n) call publishes exactly one record. If a protocol layer consumes
65+
* incrementally (e.g. the beast HTTP parser), records correspond to those increments; call
66+
* consume() yourself on message boundaries when you need strict package framing.
67+
*/
68+
template<buffer_backend BufferT>
69+
class dynamic_buffer {
70+
public:
71+
/// The type used to represent the readable bytes as a single contiguous buffer
72+
using const_buffers_type = boost::asio::const_buffer;
73+
/// The type used to represent the writable bytes as a single contiguous buffer
74+
using mutable_buffers_type = boost::asio::mutable_buffer;
75+
76+
/**
77+
* @brief Shared-ownership constructor — participates in the backend's ref count.
78+
* @param ptr Shared pointer to the backend; the backend stays alive as long as
79+
* this adapter (or any copy) holds a reference.
80+
* @param max_size Optional cap on size() + prepared bytes; clamped to backend capacity.
81+
*/
82+
explicit dynamic_buffer(
83+
std::shared_ptr<BufferT> ptr,
84+
std::size_t max_size = (std::numeric_limits<std::size_t>::max)()) noexcept
85+
: buffer_(std::move(ptr))
86+
, max_size_(max_size < buffer_->capacity()
87+
? max_size
88+
: static_cast<std::size_t>(buffer_->capacity()))
89+
{
90+
}
91+
92+
/**
93+
* @brief Non-owning reference constructor — backward-compatible with local backends.
94+
* @param buffer The backend; must outlive this adapter and all copies of it.
95+
* @param max_size Optional cap on size() + prepared bytes; clamped to buffer.capacity().
96+
* beast/asio read operations use max_size() to limit how much they read.
97+
*
98+
* Internally wraps the reference in a shared_ptr with a null deleter so copies are
99+
* cheap, but lifetime management remains the caller's responsibility.
100+
*/
101+
explicit dynamic_buffer(
102+
BufferT& buffer,
103+
std::size_t max_size = (std::numeric_limits<std::size_t>::max)())
104+
: dynamic_buffer(std::shared_ptr<BufferT>(&buffer, [](BufferT*){}), max_size)
105+
{
106+
}
107+
108+
/// The number of readable (committed but not consumed) bytes
109+
std::size_t size() const noexcept {
110+
const std::size_t n = buffer_->size();
111+
return n < max_size_ ? n : max_size_;
112+
}
113+
114+
/// The maximum sum of readable and writable bytes
115+
std::size_t max_size() const noexcept { return max_size_; }
116+
117+
/// The maximum sum of readable and writable bytes that can be held without relocation
118+
std::size_t capacity() const noexcept { return static_cast<std::size_t>(buffer_->capacity()); }
119+
120+
/// The readable bytes as a single contiguous buffer
121+
const_buffers_type data() const noexcept {
122+
return { buffer_->data(), size() };
123+
}
124+
125+
/**
126+
* @brief Get a writable region of n bytes at the end of the readable area.
127+
* @throws std::length_error if size() + n exceeds max_size().
128+
*/
129+
mutable_buffers_type prepare(std::size_t n) {
130+
if (buffer_->size() + n > max_size_) {
131+
throw std::length_error("dynamic_buffer too long");
132+
}
133+
auto [ptr, sz] = buffer_->prepare(n);
134+
return { ptr, sz };
135+
}
136+
137+
/// Move n bytes from the writable area to the readable area
138+
void commit(std::size_t n) noexcept { buffer_->commit(n); }
139+
140+
/// Publish the first n readable bytes to consumers as one message record.
141+
/// Returns the record as consumers will see it (asio/beast callers may ignore it).
142+
/// The return type is deduced from BufferT::consume() — published_record for slick backends.
143+
auto consume(std::size_t n) noexcept { return buffer_->consume(n); }
144+
145+
/// Discard the readable bytes and any prepared region without publishing them,
146+
/// matching beast::flat_buffer::clear(). Use after a connection drops mid-message
147+
/// so the partial bytes are not prepended to the next connection's data. This
148+
/// does not create a new record; older published records still follow the normal
149+
/// lossy overwrite semantics.
150+
void clear() noexcept { buffer_->discard(); }
151+
152+
/// Access the underlying backend (e.g. for in-process consumers calling read()).
153+
BufferT& buffer() noexcept { return *buffer_; }
154+
const BufferT& buffer() const noexcept { return *buffer_; }
155+
156+
/// Shared-ownership handle to the backend, so it can safely outlive this adapter.
157+
std::shared_ptr<BufferT> buffer_ptr() noexcept { return buffer_; }
158+
std::shared_ptr<const BufferT> buffer_ptr() const noexcept { return buffer_; }
159+
160+
private:
161+
std::shared_ptr<BufferT> buffer_;
162+
std::size_t max_size_;
163+
};
164+
165+
} // namespace slick

0 commit comments

Comments
 (0)