Skip to content

Commit 54e02cd

Browse files
authored
Add endian adaptors and allow setting default stream byte order (#19)
1 parent c2c5a72 commit 54e02cd

File tree

9 files changed

+460
-62
lines changed

9 files changed

+460
-62
lines changed

README.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ auto serialise(const UserPacket& packet) {
5959
return buffer;
6060
}
6161
```
62-
By default, Hexi will try to serialise basic structures such as our `UserPacket` if they meet requirements for being safe to directly copy the bytes. Now, for reasons of portability, it's not recommended that you do things this way unless you're positive that the data layout is identical on the system that wrote the data. Not to worry, this is easily solved. Plus, we didn't do any error handling. All in good time.
62+
By default, Hexi will try to serialise basic structures such as our `UserPacket` if they meet requirements for being safe to directly copy the bytes. Now, for reasons of portability, it's not recommended that you do things this way unless you're positive that the data layout is identical on the system that wrote the data. Not to worry, this is easily solved. Plus, we didn't do any error or endianness handling. All in good time.
6363
6464
<img src="docs/assets/frog-remember.png" alt="Remember these two classes, if nothing else!">
6565
@@ -146,15 +146,15 @@ struct UserPacket {
146146
std::string username;
147147
uint64_t timestamp;
148148
uint8_t has_optional_field;
149-
uint32_t optional_field; // pretend this is big endian in the protocol
149+
uint32_t optional_field; // pretend this is big-endian in the protocol
150150

151151
// deserialise
152152
auto& operator>>(auto& stream) {
153153
stream >> user_id >> username >> timestamp >> has_optional_field;
154154

155155
if (has_optional_field) {
156-
stream >> optional_field;
157-
hexi::endian::big_to_native_inplace(optional_field);
156+
// fetch explicitly as big-endian value
157+
stream >> hexi::endian::from_big(optional_field);
158158
}
159159

160160
// we can manually trigger an error if something went wrong
@@ -167,7 +167,8 @@ struct UserPacket {
167167
stream << user_id << username << timestamp << has_optional_field;
168168

169169
if (has_optional_field) {
170-
stream << hexi::endian::native_to_big(optional_field);
170+
// write explicitly as big-endian value
171+
stream << hexi::endian::to_big(optional_field);
171172
}
172173

173174
return stream;
@@ -194,7 +195,14 @@ void read() {
194195

195196
auto handle_user_packet(std::span<const char> buffer) {
196197
hexi::buffer_adaptor adaptor(buffer);
197-
hexi::binary_stream stream(adaptor);
198+
199+
/**
200+
* hexi::endian::little tells the stream to convert to/from
201+
* little-endian unless told otherwise by using the endian
202+
* adaptors. If no argument is provided, it does not perform
203+
* any conversions by default.
204+
*/
205+
hexi::binary_stream stream(adaptor, hexi::endian::little);
198206

199207
UserPacket packet;
200208
stream >> packet;
@@ -208,10 +216,21 @@ auto handle_user_packet(std::span<const char> buffer) {
208216
}
209217
```
210218
211-
Because `binary_stream` is a template, it's easiest to allow the compiler to perform type deduction magic.
219+
This example is fully portable and is even independent of the platform byte order. By specifying the endianness of the stream, it'll automagically convert all endian-sensitive data to the requested byte order.
220+
The default argument is `hexi::endian::native`, which will perform no conversions, while `hexi::endian::big` and `hexi::endian::little` will perform conversions if required.
221+
222+
If your protocol contains mixed endianness, you can use the endian adaptors to specify the desired byte order when streaming
223+
the data, as shown in the above example.
224+
225+
Best of all, because this is handled by templates, there is zero runtime cost if no conversion is required
226+
(i.e. that is the native byte order matches the requested byte order) and constant values can be converted
227+
at compile-time. For example, specifying `hexi::endian::little` on a little-endian platform will generate zero
228+
code.
229+
230+
`docs/examples/endian.cpp` provides examples for byte order handling functionality.
212231
213-
If you want the function bodies to be in a source file, it's recommended that you provide your own `using` alias for your `binary_stream` type.
214-
The alternative is to use the polymorphic equivalents, `pmc::buffer_adaptor` and `pmc::binary_stream`, which allow you to change the underlying buffer type at runtime but at the cost of virtual call overhead and lacking some functionality that doesn't mesh well with polymorphism.
232+
As for the serialisation functions, if you want the function bodies to be in a source file, it's recommended that you provide your own `using` alias for your `binary_stream` type.
233+
The alternative is to use the polymorphic equivalents, `pmc::buffer_adaptor` and `pmc::binary_stream`, which allow you to change the underlying buffer type at runtime but at the potential cost of virtual call overhead (devirtualisation not withstanding) and lacking some functionality that doesn't mesh well with polymorphism.
215234
216235
How you structure your code is up to you, this is just one way of doing it.
217236

docs/examples/endian.cpp

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,54 @@
77
int main() {
88
std::vector<char> buffer;
99
hexi::buffer_adaptor adaptor(buffer);
10-
hexi::binary_stream stream(adaptor);
10+
hexi::binary_stream stream(adaptor); // performs no conversions by default
11+
12+
{
13+
hexi::binary_stream be_stream(adaptor, hexi::endian::big); // will convert to/from big endian by default
14+
hexi::binary_stream be_stream(adaptor, hexi::endian::little); // will convert to/from little endian by default
15+
hexi::binary_stream be_stream(adaptor, hexi::endian::native); // default, will not convert by default
16+
}
1117

12-
{ // serialise foo & bar as big/little endian
18+
{ // serialise foo & bar as big/little endian with put
1319
const std::uint64_t foo = 100;
1420
const std::uint32_t bar = 200;
15-
stream.put<hexi::endian::conversion::native_to_big>(foo);
16-
stream.put<hexi::endian::conversion::native_to_little>(bar);
21+
stream.put<hexi::endian::to_big>(foo);
22+
stream.put<hexi::endian::to_little>(bar);
1723
}
1824

1925
{ // deserialise foo & bar as big/little endian
2026
std::uint64_t foo = 0;
2127

2228
// write to existing variable or return result
23-
stream.get<hexi::endian::conversion::big_to_native>(foo);
29+
stream.get<hexi::endian::to_big>(foo);
2430
std::ignore = stream.get<std::uint32_t, hexi::endian::conversion::little_to_native>();
2531
}
2632

2733
{ // stream integers as various endian combinations
28-
stream << hexi::endian::native_to_big(9000);
29-
stream << hexi::endian::big_to_native(9001); // over 9000
34+
stream << hexi::endian::to_big(9000);
35+
stream << hexi::endian::to_little(9001); // over 9000
3036
stream << hexi::endian::native_to_little(9002);
3137
stream << hexi::endian::little_to_native(9003);
3238
}
3339

40+
{ // retrieve stream integers as big or little endian
41+
std::uint64_t foo;
42+
stream >> hexi::endian::from_big(foo);
43+
stream >> hexi::endian::from_little(foo);
44+
}
45+
3446
{ // convert endianness inplace
3547
int foo = 10;
3648
hexi::endian::native_to_big_inplace(foo);
3749
hexi::endian::big_to_native_inplace(foo);
3850
hexi::endian::little_to_native_inplace(foo);
3951
hexi::endian::native_to_little_inplace(foo);
4052
}
53+
54+
{ // retrieve converted value
55+
auto foo = hexi::endian::native_to_big_inplace(1);
56+
auto bar = hexi::endian::big_to_native_inplace(2);
57+
auto baz = hexi::endian::little_to_native_inplace(3);
58+
auto qux = hexi::endian::native_to_little(4);
59+
}
4160
}

include/hexi/binary_stream.h

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ using namespace detail;
3636
} \
3737
}
3838

39-
template<byte_oriented buf_type, std::derived_from<except_tag> exceptions = allow_throw_t>
39+
template<
40+
byte_oriented buf_type,
41+
std::derived_from<except_tag> exceptions = allow_throw_t,
42+
std::derived_from<endian::storage_tag> endianness = endian::as_native_t
43+
>
4044
class binary_stream final {
4145
public:
4246
using size_type = typename buf_type::size_type;
@@ -109,11 +113,23 @@ class binary_stream final {
109113
: buffer_(source),
110114
read_limit_(read_limit) {};
111115

116+
explicit binary_stream(buf_type& source, exceptions)
117+
: binary_stream(source, 0) {}
118+
119+
explicit binary_stream(buf_type& source, endianness)
120+
: binary_stream(source, 0) {}
121+
122+
explicit binary_stream(buf_type& source, exceptions, endianness)
123+
: binary_stream(source, 0) {}
124+
112125
explicit binary_stream(buf_type& source, size_type read_limit, exceptions)
113126
: binary_stream(source, read_limit) {}
114127

115-
explicit binary_stream(buf_type& source, exceptions)
116-
: binary_stream(source, 0) {}
128+
explicit binary_stream(buf_type& source, size_type read_limit, endianness)
129+
: binary_stream(source, read_limit) {}
130+
131+
explicit binary_stream(buf_type& source, size_type read_limit, exceptions, endianness)
132+
: binary_stream(source, read_limit) {}
117133

118134
binary_stream(binary_stream&& rhs) noexcept
119135
: buffer_(rhs.buffer_),
@@ -136,9 +152,22 @@ class binary_stream final {
136152
return data.operator<<(*this);
137153
}
138154

155+
template<std::derived_from<endian::adaptor_in_tag_t> endian_func>
156+
binary_stream& operator<<(endian_func adaptor) requires writeable<buf_type> {
157+
const auto converted = adaptor.convert();
158+
write(&converted, sizeof(converted));
159+
return *this;
160+
}
161+
162+
binary_stream& operator<<(const arithmetic auto& data) requires writeable<buf_type> {
163+
const auto converted = endian::storage_in(data, endianness{});
164+
write(&converted, sizeof(converted));
165+
return *this;
166+
}
167+
139168
template<pod T>
140-
requires (!has_shl_override<T, binary_stream>)
141-
binary_stream& operator <<(const T& data) requires writeable<buf_type> {
169+
requires (!has_shl_override<T, binary_stream> && !arithmetic<T>)
170+
binary_stream& operator<<(const T& data) requires writeable<buf_type> {
142171
write(&data, sizeof(T));
143172
return *this;
144173
}
@@ -221,10 +250,10 @@ class binary_stream final {
221250
*
222251
* @param data The element to be written to the stream.
223252
*/
224-
template<endian::conversion conversion>
225-
void put(const arithmetic auto& data) requires writeable<buf_type> {
226-
const auto swapped = endian::convert<conversion>(data);
227-
write(&swapped, sizeof(data));
253+
template<std::derived_from<endian::adaptor_out_tag_t> endian_func>
254+
void put(const endian_func& adaptor) requires writeable<buf_type> {
255+
const auto swapped = adaptor.convert();
256+
write(&swapped, sizeof(swapped));
228257
}
229258

230259
/**
@@ -357,8 +386,23 @@ class binary_stream final {
357386
return data.operator>>(*this);
358387
}
359388

389+
template<std::derived_from<endian::adaptor_out_tag_t> endian_func>
390+
binary_stream& operator>>(endian_func adaptor) requires writeable<buf_type> {
391+
STREAM_READ_BOUNDS_ENFORCE(sizeof(adaptor.value), *this);
392+
buffer_.read(&adaptor.value, sizeof(adaptor.value));
393+
adaptor.value = adaptor.convert();
394+
return *this;
395+
}
396+
397+
binary_stream& operator>>(arithmetic auto& data) requires writeable<buf_type> {
398+
STREAM_READ_BOUNDS_ENFORCE(sizeof(data), *this);
399+
buffer_.read(&data, sizeof(data));
400+
endian::storage_out(data, endianness{});
401+
return *this;
402+
}
403+
360404
template<pod T>
361-
requires (!has_shr_override<T, binary_stream>)
405+
requires (!has_shr_override<T, binary_stream> && !arithmetic<T>)
362406
binary_stream& operator>>(T& data) {
363407
STREAM_READ_BOUNDS_ENFORCE(sizeof(data), *this);
364408
buffer_.read(&data, sizeof(data));
@@ -394,11 +438,11 @@ class binary_stream final {
394438
*
395439
* @param The destination for the read value.
396440
*/
397-
template<endian::conversion conversion>
398-
void get(arithmetic auto& dest) {
399-
STREAM_READ_BOUNDS_ENFORCE(sizeof(dest), void());
400-
buffer_.read(&dest, sizeof(dest));
401-
dest = endian::convert<conversion>(dest);
441+
template<std::derived_from<endian::adaptor_out_tag_t> endian_func>
442+
void get(endian_func& adaptor) {
443+
STREAM_READ_BOUNDS_ENFORCE(sizeof(adaptor.value), void());
444+
buffer_.read(&adaptor.value, sizeof(adaptor));
445+
adaptor.value = adaptor.convert();
402446
}
403447

404448
/**

include/hexi/endian.h

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,57 @@ constexpr auto convert(arithmetic auto value) -> decltype(value) {
131131
};
132132
}
133133

134+
struct adaptor_in_tag_t {};
135+
struct adaptor_out_tag_t {};
136+
137+
#define ENDIAN_ADAPTOR(name, func, tag, ref) \
138+
template<arithmetic T> \
139+
struct name final : tag { \
140+
T ref value; \
141+
name(T ref t) : value(t) {} \
142+
auto convert() -> T { \
143+
return func(value); \
144+
} \
145+
};
146+
147+
#define ENDIAN_ADAPTOR_OUT(name, func) ENDIAN_ADAPTOR(name, func, adaptor_out_tag_t, &)
148+
#define ENDIAN_ADAPTOR_IN(name, func) ENDIAN_ADAPTOR(name, func, adaptor_in_tag_t, )
149+
150+
ENDIAN_ADAPTOR_IN(to_big, native_to_big)
151+
ENDIAN_ADAPTOR_IN(to_little, native_to_little)
152+
ENDIAN_ADAPTOR_OUT(from_big, big_to_native)
153+
ENDIAN_ADAPTOR_OUT(from_little, little_to_native)
154+
155+
struct storage_tag {};
156+
struct as_big_t final : storage_tag {};
157+
struct as_little_t final : storage_tag {};
158+
struct as_native_t final : storage_tag {};
159+
160+
constexpr static as_big_t big {};
161+
constexpr static as_little_t little {};
162+
constexpr static as_native_t native {};
163+
164+
inline auto storage_in(const arithmetic auto& value, as_native_t) {
165+
return value;
166+
}
167+
168+
inline auto storage_in(const arithmetic auto& value, as_little_t) {
169+
return native_to_little(value);
170+
}
171+
172+
inline auto storage_in(const arithmetic auto& value, as_big_t) {
173+
return native_to_big(value);
174+
}
175+
176+
inline void storage_out(arithmetic auto& value, as_native_t) {}
177+
178+
inline void storage_out(arithmetic auto& value, as_little_t) {
179+
return little_to_native_inplace(value);
180+
}
181+
182+
inline void storage_out(arithmetic auto& value, as_big_t) {
183+
return big_to_native_inplace(value);
184+
}
185+
186+
134187
} // endian, hexi

include/hexi/pmc/binary_stream_reader.h

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ class binary_stream_reader : virtual public stream_base {
128128
return data.operator>>(*this);
129129
}
130130

131+
template<std::derived_from<endian::adaptor_out_tag_t> endian_func>
132+
binary_stream_reader& operator>>(endian_func adaptor) {
133+
enforce_read_bounds(sizeof(adaptor.value));
134+
buffer_.read(&adaptor.value, sizeof(adaptor.value));
135+
adaptor.value = adaptor.convert();
136+
return *this;
137+
}
138+
131139
template<pod T>
132140
requires (!has_shr_override<T, binary_stream_reader>)
133141
binary_stream_reader& operator>>(T& data) {
@@ -233,11 +241,11 @@ class binary_stream_reader : virtual public stream_base {
233241
*
234242
* @param The destination for the read value.
235243
*/
236-
template<endian::conversion conversion>
237-
void get(arithmetic auto& dest) {
238-
enforce_read_bounds(sizeof(dest));
239-
buffer_.read(&dest, sizeof(dest));
240-
dest = endian::convert<conversion>(dest);
244+
template<std::derived_from<endian::adaptor_out_tag_t> endian_func>
245+
void get(endian_func& adaptor) {
246+
enforce_read_bounds(sizeof(adaptor.value));
247+
buffer_.read(&adaptor.value, sizeof(adaptor.value));
248+
adaptor.value = adaptor.convert();
241249
}
242250

243251
/**

include/hexi/pmc/binary_stream_writer.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ class binary_stream_writer : virtual public stream_base {
5555
return data.operator<<(*this);
5656
}
5757

58+
template<std::derived_from<endian::adaptor_in_tag_t> endian_func>
59+
binary_stream_writer& operator<<(endian_func adaptor) {
60+
const auto converted = adaptor.convert();
61+
buffer_.write(&converted, sizeof(converted));
62+
total_write_ += sizeof(converted);
63+
return *this;
64+
}
65+
5866
template<pod T>
5967
requires (!has_shl_override<T, binary_stream_writer>)
6068
binary_stream_writer& operator<<(const T& data) {
@@ -142,9 +150,9 @@ class binary_stream_writer : virtual public stream_base {
142150
*
143151
* @param data The element to be written to the stream.
144152
*/
145-
template<endian::conversion conversion>
146-
void put(const arithmetic auto& data) {
147-
const auto swapped = endian::convert<conversion>(data);
153+
template<std::derived_from<endian::adaptor_out_tag_t> endian_func>
154+
void put(const endian_func& adaptor) {
155+
const auto swapped = adaptor.convert();
148156
write(&swapped, sizeof(swapped));
149157
}
150158

0 commit comments

Comments
 (0)