Skip to content

Commit f68353a

Browse files
authored
Merge pull request #7 from KredeGC/dev
Dev
2 parents 3b4be20 + 1fb4f2b commit f68353a

File tree

15 files changed

+499
-74
lines changed

15 files changed

+499
-74
lines changed

.github/workflows/docs.yml

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
name: Docs
22

33
on:
4+
push:
5+
branches: [ master ]
6+
paths: '**/*.md'
47
workflow_dispatch:
5-
inputs:
6-
tagName:
7-
description: 'Test'
8-
type: boolean
98

109
jobs:
11-
Docs:
10+
Build:
1211
runs-on: ubuntu-latest
1312
permissions:
1413
actions: read
15-
contents: write
16-
pull-requests: write
14+
contents: read
1715
steps:
1816
- name: Checkout
1917
uses: actions/checkout@v3
@@ -24,14 +22,29 @@ jobs:
2422
path: doxygen-awesome-css
2523
- name: Install doxygen
2624
run: sudo apt-get update && sudo apt-get install doxygen graphviz -y
27-
- name: Generate Doxygen Documentation
25+
- name: Generate Doxygen documentation
2826
run: doxygen Doxyfile
2927
- name: Copy LICENSE
3028
run: cp doxygen-awesome-css/LICENSE docs/html/LICENSE
3129
- name: Create .nojekyll
3230
run: touch docs/html/.nojekyll
33-
- name: Deploy Documentation to branch
34-
uses: JamesIves/github-pages-deploy-action@v4
31+
- name: Upload documentation
32+
uses: actions/upload-pages-artifact@v1
3533
with:
36-
branch: docs
37-
folder: docs/html
34+
path: docs/html
35+
Deploy:
36+
needs: Build
37+
runs-on: ubuntu-latest
38+
environment:
39+
name: github-pages
40+
url: ${{ steps.deployment.outputs.page_url }}
41+
permissions:
42+
actions: read
43+
contents: read
44+
pages: write
45+
id-token: write
46+
name: Deploy
47+
steps:
48+
- name: Deploy documentation
49+
id: deployment
50+
uses: actions/deploy-pages@v1

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
paths-ignore:
99
- '**/*.md'
1010
- '**/*.gitignore'
11+
- '**/Doxyfile'
1112

1213
jobs:
1314
Build:

README.md

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ Based on [Glenn Fiedler's articles](https://gafferongames.com/post/reading_and_w
3232
* [Bounded float - bounded_range](#bounded-float---bounded_range)
3333
* [Quaternion - smallest_three\<Q, BitsPerElement\>](#quaternion---smallest_threeq-bitsperelement)
3434
* [Serialization Examples](#serialization-examples)
35+
* [Extensibility](#extensibility)
36+
* [Adding new serializables types](#adding-new-serializables-types)
37+
* [Unified serialization](#unified-serialization)
38+
* [Partial trait specializations](#partial-trait-specializations)
39+
* [Trait deduction](#trait-deduction)
3540
* [Building and running tests](#building-and-running-tests)
3641
* [3rd party](#3rd-party)
3742
* [License](#license)
@@ -50,20 +55,21 @@ The source and header files inside the `src/` directory are only tests and shoul
5055
# Usage
5156
The library has a global header file ([`bitstream/bitstream.h`](https://github.com/KredeGC/BitStream/tree/master/include/bitstream/bitstream.h)) which includes every other header file in the library.
5257

53-
If you only need certain features, you can simply include the files you need.
58+
If you only need certain features you can instead opt to just include the files you need.
5459
The files are stored in categories:
5560
* [`quantization/`](https://github.com/KredeGC/BitStream/tree/master/include/bitstream/quantization/) - Files relating to quantizing floats and quaternions into fewer bits
5661
* [`stream/`](https://github.com/KredeGC/BitStream/tree/master/include/bitstream/stream/) - Files relating to streams that read and write bits
5762
* [`traits/`](https://github.com/KredeGC/BitStream/tree/master/include/bitstream/traits/) - Files relating to various serialization traits, like serializble strings, integrals etc.
5863

5964
An important aspect of the serialiaztion is performance, since the library is meant to be used in a tight loop, like with networking.
6065
This is why most operations don't use exceptions, but instead return true or false depending on whether the operation was a success.
61-
It's important to check these return values after every operation, especially when reading.
66+
It's important to check these return values after every operation, especially when reading from an unknown source.
6267
You can check it manually or use the `BS_ASSERT(x)` macro for this, if you want your function to return false on failure.
6368

6469
It is also possible to dynamically put a break point or trap when a bitstream would have otherwise returned false. This can be great for debugging custom serialization code, but should generally be left out of production code. Simply `#define BS_DEBUG_BREAK` before including any of the library header files if you want to break when an operation fails.
6570

66-
For more examples of usage, see the [Serialization Examples](#serialization-examples) below.
71+
For more concrete examples of usage, see the [Serialization Examples](#serialization-examples) below.
72+
If you need to add your own serializable types you should also look at the [Extensibility](#extensibility) section.
6773
You can also look at the unit tests to get a better idea about what you can expect from the library.
6874

6975
# Documentation
@@ -348,11 +354,15 @@ These examples can also be seen in [`src/test/examples_test.cpp`](https://github
348354
# Extensibility
349355
The library is made with extensibility in mind.
350356
The `bit_writer` and `bit_reader` use a template trait specialization of the given type to deduce how to serialize and deserialize the object.
357+
The only requirements of the trait is that it has (or can deduce) 2 static functions which take a bit_writer& and a bit_reader& respectively as their first argument.
358+
The 2 functions must also return a bool indicating whether the serialization was a success or not, but can otherwise take any number of additional arguments.
359+
360+
## Adding new serializables types
351361
The general structure of a trait looks like the following:
352362

353363
```cpp
354364
template<>
355-
struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
365+
struct serialize_traits<TRAIT_TYPE> // The type to use when referencing this specific trait
356366
{
357367
// Will be called when writing the object to a stream
358368
static bool serialize(bit_writer& stream, ...)
@@ -364,36 +374,81 @@ struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
364374
};
365375
```
366376
367-
Note that `TRAIT_TYPE` does not necessarily have to be part of the serialize function definitions.
368-
It is purely used to specify which trait to use when serializing, since it cannot be deduced from the arguments.<br/>
369-
To use the trait above to serialize an object you need to explicitly specify it:
370-
```cpp
371-
bool status = writer.serialize<TRAIT_TYPE>(...);
372-
```
373-
374-
The specialization can also be unified with templating, if writing and reading look similar:
377+
## Unified serialization
378+
The serialization can also be unified with templating, if writing and reading look similar.
379+
If some parts of the serialization process don't match entirely you can query the `Stream::reading` or `Stream::writing` and branch depending on the value.
380+
An example of this can be seen below:
375381
```cpp
376382
template<>
377383
struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
378384
{
379385
// Will be called when writing or reading the object to a stream
380386
template<typename Stream>
381387
static bool serialize(Stream& stream, ...)
382-
{ ... }
388+
{
389+
// Some code that looks the same for writing and reading
390+
391+
if constexpr (Stream::writing) {
392+
// Code that should only be run when writing
393+
}
394+
395+
// A variable that differs if the stream is writing or reading
396+
int value = Stream::reading ? 0 : 1;
397+
398+
...
399+
}
383400
};
384401
```
385402

403+
## Partial trait specializations
386404
The specialization can also be templated to work with a number of types.
387-
It also works with `enable_if`:
405+
It also works with `enable_if` as the second argument:
388406
```cpp
389-
// This trait will be used by any integral pointer type (char*, uint16_t* etc.)
407+
// This trait will be used by any non-const integral pointer type (char*, uint16_t* etc.)
390408
template<typename T>
391-
struct serialize_traits<T*, typename std::enable_if_t<std::is_integral_v<T>>>
409+
struct serialize_traits<T*, typename std::enable_if_t<std::is_integral_v<T> && !std::is_const_v<T>>>
392410
{ ... };
393-
// An example which would use the above trait
411+
// An example which will use the above trait
394412
bool status = writer.serialize<int16_t*>(...);
413+
// An example which won't use it (and won't compile)
414+
bool status = writer.serialize<const int16_t*>(...);
415+
```
416+
417+
Note that `TRAIT_TYPE` does not necessarily have to be part of the serialize function definitions.
418+
It can just be used to specify which trait to use when serializing, if it cannot be deduced from the arguments.<br/>
419+
Below is an example where we serialize an object by explicitly defining the trait type:
420+
```cpp
421+
bool status = writer.serialize<TRAIT_TYPE>(...);
422+
```
423+
424+
## Trait deduction
425+
When calling the `serialize` function on a `bit_writer` or `bit_reader`, the trait can sometimes be deduced instead of being explicitly declared.
426+
This can only be done if the type of the second argument in the `static bool serialize(...)` function is (roughly) the same as the trait type.
427+
An example of the structure for an implicit trait can be seen below:
428+
```cpp
429+
template<>
430+
struct serialize_traits<TRAIT_TYPE> // The type to use when referencing this specific trait
431+
{
432+
// The second argument is the same as TRAIT_TYPE (const and lvalue references are removed when deducing)
433+
static bool serialize(bit_writer& stream, const TRAIT_TYPE&, ...)
434+
{ ... }
435+
436+
// The second argument is the same as TRAIT_TYPE (lvalue is removed)
437+
static bool serialize(bit_reader& stream, TRAIT_TYPE&, ...)
438+
{ ... }
439+
};
395440
```
396441
442+
The above trait could then be used when implicitly serializing an object of type `TRAIT_TYPE`:
443+
```cpp
444+
TRAIT_TYPE value;
445+
bool status = writer.serialize(value, ...);
446+
```
447+
448+
It doesn't work on all types, and there is some guesswork involved relating to const qualifiers.
449+
E.g. a trait of type `char` is treated the same as `const char&` and thus the call would be ambiguous if both had a trait specialization.
450+
In case of ambiguity you will still be able to declare the trait explicitly when calling the `serialize` function.
451+
397452
More concrete examples of traits can be found in the [`traits/`](https://github.com/KredeGC/BitStream/tree/master/include/bitstream/traits/) directory.
398453

399454
# Building and running tests

include/bitstream/bitstream.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// Traits
1515
#include "traits/array_traits.h"
1616
#include "traits/bool_trait.h"
17+
#include "traits/enum_trait.h"
1718
#include "traits/integral_traits.h"
1819
#include "traits/quantization_traits.h"
1920
#include "traits/string_traits.h"

include/bitstream/stream/bit_reader.h

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "../utility/assert.h"
33
#include "../utility/crc.h"
44
#include "../utility/endian.h"
5+
#include "../utility/meta.h"
56

67
#include "byte_buffer.h"
78
#include "serialize_traits.h"
@@ -15,6 +16,7 @@ namespace bitstream
1516
{
1617
/**
1718
* @brief A stream for reading objects from a tightly packed buffer
19+
* @note Does not take ownership of the buffer
1820
*/
1921
class bit_reader
2022
{
@@ -38,7 +40,7 @@ namespace bitstream
3840
* @param bytes The byte array to read from. Should be 4-byte aligned if possible. The size of the array must be a multiple of 4
3941
* @param num_bytes The maximum number of bytes that we can read
4042
*/
41-
bit_reader(const void* bytes, uint32_t num_bytes) noexcept :
43+
explicit bit_reader(const void* bytes, uint32_t num_bytes) noexcept :
4244
m_Buffer(static_cast<const uint32_t*>(bytes)),
4345
m_NumBitsRead(0),
4446
m_TotalBits(num_bytes * 8),
@@ -138,21 +140,24 @@ namespace bitstream
138140
*/
139141
bool serialize_checksum(uint32_t protocol_version) noexcept
140142
{
143+
BS_ASSERT(m_NumBitsRead == 0);
144+
145+
BS_ASSERT(can_serialize_bits(32U));
146+
141147
uint32_t num_bytes = (m_TotalBits - 1U) / 8U + 1U;
142148

143149
// Read the checksum
144-
uint32_t checksum;
145-
std::memcpy(&checksum, m_Buffer, sizeof(uint32_t));
150+
uint32_t checksum = *m_Buffer;
146151

147152
// Copy protocol version to buffer
148153
uint32_t* buffer = const_cast<uint32_t*>(m_Buffer); // Somewhat of a hack, but it's faster to change the checksum twice than allocate memory for it
149-
std::memcpy(buffer, &protocol_version, sizeof(uint32_t));
154+
*buffer = protocol_version;
150155

151156
// Generate checksum to compare against
152157
uint32_t generated_checksum = utility::crc_uint32(reinterpret_cast<uint8_t*>(buffer), num_bytes);
153158

154159
// Write the checksum back, just in case
155-
std::memcpy(buffer, &checksum, sizeof(uint32_t));
160+
*buffer = checksum;
156161

157162
// Advance the reader by the size of the checksum (32 bits / 1 word)
158163
m_WordIndex++;
@@ -173,30 +178,23 @@ namespace bitstream
173178

174179
BS_ASSERT(num_bytes * 8U >= m_NumBitsRead);
175180

176-
// Align with word size
177-
uint32_t remainder = m_NumBitsRead % 32;
178-
if (remainder != 0)
179-
{
180-
uint32_t zero;
181-
bool status = serialize_bits(zero, 32 - remainder);
182-
183-
BS_ASSERT(status && zero == 0);
184-
}
181+
uint32_t offset = m_NumBitsRead / 32;
182+
uint32_t zero;
185183

186184
// Test for zeros in padding
187-
for (uint32_t i = m_WordIndex; i < num_bytes / 4; i++)
185+
for (uint32_t i = offset; i < num_bytes / 4; i++)
188186
{
189-
uint32_t zero = 0;
190187
bool status = serialize_bits(zero, 32);
191188

192189
BS_ASSERT(status && zero == 0);
193190
}
194191

192+
uint32_t remainder = num_bytes * 8U - m_NumBitsRead;
193+
195194
// Test the last word more carefully, as it may have data
196-
if (num_bytes % 4 != 0)
195+
if (remainder % 32U != 0U)
197196
{
198-
uint32_t zero = 0;
199-
bool status = serialize_bits(zero, (num_bytes % 4) * 8);
197+
bool status = serialize_bits(zero, remainder);
200198

201199
BS_ASSERT(status && zero == 0);
202200
}
@@ -312,18 +310,40 @@ namespace bitstream
312310
}
313311

314312
/**
315-
* @brief Reads from the buffer, using the given `Trait`.
313+
* @brief Reads from the buffer, using the given @p Trait.
314+
* @note The Trait type in this function must always be explicitly declared
316315
* @tparam Trait A template specialization of serialize_trait<>
317316
* @tparam ...Args The types of the arguments to pass to the serialize function
318317
* @param ...args The arguments to pass to the serialize function
319318
* @return Whether successful or not
320319
*/
321320
template<typename Trait, typename... Args>
322-
bool serialize(Args&&... args) noexcept(noexcept(serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...)))
321+
bool serialize(Args&&... args) noexcept(utility::is_noexcept_serialize_v<Trait, bit_reader, Args...>)
323322
{
323+
static_assert(utility::has_serialize_v<Trait, bit_reader, Args...>, "Could not find serializable trait for the given type. Remember to specialize serializable_traits<> with the given type");
324+
324325
return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
325326
}
326327

328+
/**
329+
* @brief Reads from the buffer, by trying to deduce the trait.
330+
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
331+
* If the trait cannot be deduced it will not compile.
332+
* @tparam Trait A template specialization of serialize_trait<>
333+
* @tparam ...Args The types of the arguments to pass to the serialize function
334+
* @param ...args The arguments to pass to the serialize function
335+
* @return Whether successful or not
336+
*/
337+
template<typename Trait, typename... Args>
338+
bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_noexcept_serialize_v<utility::deduce_trait_t<Trait, bit_reader, Args...>, bit_reader, Trait, Args...>)
339+
{
340+
using deduce_t = utility::deduce_trait_t<Trait, bit_reader, Args...>;
341+
342+
static_assert(utility::has_serialize_v<deduce_t, bit_reader, Trait, Args...>, "Could not deduce serializable trait for the given arguments. Remember to specialize serializable_traits<> with the given type");
343+
344+
return serialize_traits<deduce_t>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
345+
}
346+
327347
private:
328348
const uint32_t* m_Buffer;
329349
uint32_t m_NumBitsRead;

0 commit comments

Comments
 (0)