Skip to content

Commit

Permalink
Merge pull request #7 from KredeGC/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
KredeGC authored Mar 25, 2023
2 parents 3b4be20 + 1fb4f2b commit f68353a
Show file tree
Hide file tree
Showing 15 changed files with 499 additions and 74 deletions.
37 changes: 25 additions & 12 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
name: Docs

on:
push:
branches: [ master ]
paths: '**/*.md'
workflow_dispatch:
inputs:
tagName:
description: 'Test'
type: boolean

jobs:
Docs:
Build:
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
pull-requests: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v3
Expand All @@ -24,14 +22,29 @@ jobs:
path: doxygen-awesome-css
- name: Install doxygen
run: sudo apt-get update && sudo apt-get install doxygen graphviz -y
- name: Generate Doxygen Documentation
- name: Generate Doxygen documentation
run: doxygen Doxyfile
- name: Copy LICENSE
run: cp doxygen-awesome-css/LICENSE docs/html/LICENSE
- name: Create .nojekyll
run: touch docs/html/.nojekyll
- name: Deploy Documentation to branch
uses: JamesIves/github-pages-deploy-action@v4
- name: Upload documentation
uses: actions/upload-pages-artifact@v1
with:
branch: docs
folder: docs/html
path: docs/html
Deploy:
needs: Build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
actions: read
contents: read
pages: write
id-token: write
name: Deploy
steps:
- name: Deploy documentation
id: deployment
uses: actions/deploy-pages@v1
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
paths-ignore:
- '**/*.md'
- '**/*.gitignore'
- '**/Doxyfile'

jobs:
Build:
Expand Down
89 changes: 72 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Based on [Glenn Fiedler's articles](https://gafferongames.com/post/reading_and_w
* [Bounded float - bounded_range](#bounded-float---bounded_range)
* [Quaternion - smallest_three\<Q, BitsPerElement\>](#quaternion---smallest_threeq-bitsperelement)
* [Serialization Examples](#serialization-examples)
* [Extensibility](#extensibility)
* [Adding new serializables types](#adding-new-serializables-types)
* [Unified serialization](#unified-serialization)
* [Partial trait specializations](#partial-trait-specializations)
* [Trait deduction](#trait-deduction)
* [Building and running tests](#building-and-running-tests)
* [3rd party](#3rd-party)
* [License](#license)
Expand All @@ -50,20 +55,21 @@ The source and header files inside the `src/` directory are only tests and shoul
# Usage
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.

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

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

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.

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

# Documentation
Expand Down Expand Up @@ -348,11 +354,15 @@ These examples can also be seen in [`src/test/examples_test.cpp`](https://github
# Extensibility
The library is made with extensibility in mind.
The `bit_writer` and `bit_reader` use a template trait specialization of the given type to deduce how to serialize and deserialize the object.
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.
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.

## Adding new serializables types
The general structure of a trait looks like the following:

```cpp
template<>
struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
struct serialize_traits<TRAIT_TYPE> // The type to use when referencing this specific trait
{
// Will be called when writing the object to a stream
static bool serialize(bit_writer& stream, ...)
Expand All @@ -364,36 +374,81 @@ struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
};
```
Note that `TRAIT_TYPE` does not necessarily have to be part of the serialize function definitions.
It is purely used to specify which trait to use when serializing, since it cannot be deduced from the arguments.<br/>
To use the trait above to serialize an object you need to explicitly specify it:
```cpp
bool status = writer.serialize<TRAIT_TYPE>(...);
```

The specialization can also be unified with templating, if writing and reading look similar:
## Unified serialization
The serialization can also be unified with templating, if writing and reading look similar.
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.
An example of this can be seen below:
```cpp
template<>
struct serialize_traits<TRAIT_TYPE> // The type to use when serializing
{
// Will be called when writing or reading the object to a stream
template<typename Stream>
static bool serialize(Stream& stream, ...)
{ ... }
{
// Some code that looks the same for writing and reading
if constexpr (Stream::writing) {
// Code that should only be run when writing
}
// A variable that differs if the stream is writing or reading
int value = Stream::reading ? 0 : 1;
...
}
};
```

## Partial trait specializations
The specialization can also be templated to work with a number of types.
It also works with `enable_if`:
It also works with `enable_if` as the second argument:
```cpp
// This trait will be used by any integral pointer type (char*, uint16_t* etc.)
// This trait will be used by any non-const integral pointer type (char*, uint16_t* etc.)
template<typename T>
struct serialize_traits<T*, typename std::enable_if_t<std::is_integral_v<T>>>
struct serialize_traits<T*, typename std::enable_if_t<std::is_integral_v<T> && !std::is_const_v<T>>>
{ ... };
// An example which would use the above trait
// An example which will use the above trait
bool status = writer.serialize<int16_t*>(...);
// An example which won't use it (and won't compile)
bool status = writer.serialize<const int16_t*>(...);
```
Note that `TRAIT_TYPE` does not necessarily have to be part of the serialize function definitions.
It can just be used to specify which trait to use when serializing, if it cannot be deduced from the arguments.<br/>
Below is an example where we serialize an object by explicitly defining the trait type:
```cpp
bool status = writer.serialize<TRAIT_TYPE>(...);
```

## Trait deduction
When calling the `serialize` function on a `bit_writer` or `bit_reader`, the trait can sometimes be deduced instead of being explicitly declared.
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.
An example of the structure for an implicit trait can be seen below:
```cpp
template<>
struct serialize_traits<TRAIT_TYPE> // The type to use when referencing this specific trait
{
// The second argument is the same as TRAIT_TYPE (const and lvalue references are removed when deducing)
static bool serialize(bit_writer& stream, const TRAIT_TYPE&, ...)
{ ... }

// The second argument is the same as TRAIT_TYPE (lvalue is removed)
static bool serialize(bit_reader& stream, TRAIT_TYPE&, ...)
{ ... }
};
```
The above trait could then be used when implicitly serializing an object of type `TRAIT_TYPE`:
```cpp
TRAIT_TYPE value;
bool status = writer.serialize(value, ...);
```

It doesn't work on all types, and there is some guesswork involved relating to const qualifiers.
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.
In case of ambiguity you will still be able to declare the trait explicitly when calling the `serialize` function.

More concrete examples of traits can be found in the [`traits/`](https://github.com/KredeGC/BitStream/tree/master/include/bitstream/traits/) directory.

# Building and running tests
Expand Down
1 change: 1 addition & 0 deletions include/bitstream/bitstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// Traits
#include "traits/array_traits.h"
#include "traits/bool_trait.h"
#include "traits/enum_trait.h"
#include "traits/integral_traits.h"
#include "traits/quantization_traits.h"
#include "traits/string_traits.h"
62 changes: 41 additions & 21 deletions include/bitstream/stream/bit_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "../utility/assert.h"
#include "../utility/crc.h"
#include "../utility/endian.h"
#include "../utility/meta.h"

#include "byte_buffer.h"
#include "serialize_traits.h"
Expand All @@ -15,6 +16,7 @@ namespace bitstream
{
/**
* @brief A stream for reading objects from a tightly packed buffer
* @note Does not take ownership of the buffer
*/
class bit_reader
{
Expand All @@ -38,7 +40,7 @@ namespace bitstream
* @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
* @param num_bytes The maximum number of bytes that we can read
*/
bit_reader(const void* bytes, uint32_t num_bytes) noexcept :
explicit bit_reader(const void* bytes, uint32_t num_bytes) noexcept :
m_Buffer(static_cast<const uint32_t*>(bytes)),
m_NumBitsRead(0),
m_TotalBits(num_bytes * 8),
Expand Down Expand Up @@ -138,21 +140,24 @@ namespace bitstream
*/
bool serialize_checksum(uint32_t protocol_version) noexcept
{
BS_ASSERT(m_NumBitsRead == 0);

BS_ASSERT(can_serialize_bits(32U));

uint32_t num_bytes = (m_TotalBits - 1U) / 8U + 1U;

// Read the checksum
uint32_t checksum;
std::memcpy(&checksum, m_Buffer, sizeof(uint32_t));
uint32_t checksum = *m_Buffer;

// Copy protocol version to buffer
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
std::memcpy(buffer, &protocol_version, sizeof(uint32_t));
*buffer = protocol_version;

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

// Write the checksum back, just in case
std::memcpy(buffer, &checksum, sizeof(uint32_t));
*buffer = checksum;

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

BS_ASSERT(num_bytes * 8U >= m_NumBitsRead);

// Align with word size
uint32_t remainder = m_NumBitsRead % 32;
if (remainder != 0)
{
uint32_t zero;
bool status = serialize_bits(zero, 32 - remainder);

BS_ASSERT(status && zero == 0);
}
uint32_t offset = m_NumBitsRead / 32;
uint32_t zero;

// Test for zeros in padding
for (uint32_t i = m_WordIndex; i < num_bytes / 4; i++)
for (uint32_t i = offset; i < num_bytes / 4; i++)
{
uint32_t zero = 0;
bool status = serialize_bits(zero, 32);

BS_ASSERT(status && zero == 0);
}

uint32_t remainder = num_bytes * 8U - m_NumBitsRead;

// Test the last word more carefully, as it may have data
if (num_bytes % 4 != 0)
if (remainder % 32U != 0U)
{
uint32_t zero = 0;
bool status = serialize_bits(zero, (num_bytes % 4) * 8);
bool status = serialize_bits(zero, remainder);

BS_ASSERT(status && zero == 0);
}
Expand Down Expand Up @@ -312,18 +310,40 @@ namespace bitstream
}

/**
* @brief Reads from the buffer, using the given `Trait`.
* @brief Reads from the buffer, using the given @p Trait.
* @note The Trait type in this function must always be explicitly declared
* @tparam Trait A template specialization of serialize_trait<>
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param ...args The arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename Trait, typename... Args>
bool serialize(Args&&... args) noexcept(noexcept(serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...)))
bool serialize(Args&&... args) noexcept(utility::is_noexcept_serialize_v<Trait, bit_reader, Args...>)
{
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");

return serialize_traits<Trait>::serialize(*this, std::forward<Args>(args)...);
}

/**
* @brief Reads from the buffer, by trying to deduce the trait.
* @note The Trait type in this function is always implicit and will be deduced from the first argument if possible.
* If the trait cannot be deduced it will not compile.
* @tparam Trait A template specialization of serialize_trait<>
* @tparam ...Args The types of the arguments to pass to the serialize function
* @param ...args The arguments to pass to the serialize function
* @return Whether successful or not
*/
template<typename Trait, typename... Args>
bool serialize(Trait&& arg, Args&&... args) noexcept(utility::is_noexcept_serialize_v<utility::deduce_trait_t<Trait, bit_reader, Args...>, bit_reader, Trait, Args...>)
{
using deduce_t = utility::deduce_trait_t<Trait, bit_reader, Args...>;

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");

return serialize_traits<deduce_t>::serialize(*this, std::forward<Trait>(arg), std::forward<Args>(args)...);
}

private:
const uint32_t* m_Buffer;
uint32_t m_NumBitsRead;
Expand Down
Loading

0 comments on commit f68353a

Please sign in to comment.