Early 2021 Mega Update
The last four years of my coding life have been incredibly stressful. In this time I’ve:
- Started a job at AMD in the GPU compiler team.
- Left a job at AMD for Unity in the Burst compiler team.
- Wrapped up my involvement as a creator of Vulkan and SPIR-V.
- Learnt the C# and Rust programming languages.
You couple this with the absolute trainwreck that was 2020 (COVID yay) and now it looks like 2021 might be mostly a write-off too, and the fact that my dog Benji has progressively worsening epilepsy that is horrific to witness - it is safe to say that I’m probably at the highest level of stress I’ve ever been under.
I do realise that I’m in a very fortunate position nonetheless though - I have a good and stable job at Unity, I live on a remote island with a big garden and lots of places I can run while maintaining an easy social distancing. I know a lot of people have it much worse - very much a first-world-problem level of stress.
But one thing that has been nagging me for, and I admit with a good dose of shame, this entire time was that the bug count on my open source C libraries had creeped up to a level of real annoyance.
These issues could be broke up into a few categories:
- Later Clang/GCC would add new warnings that my libraries would fail on (I like
to ensure that all the libraries can compile with the full shebang of
-Werror -Wall -Wextra
). - I didn’t support clang-cl.exe on Windows at all (my libraries assumed that you were using any of the Visual Studio compilers from 2008 to 2019).
- There were a raft of general bugs people had found.
- And a few quality of life feature requests thrown in there too.
So in this early part of 2021 I’ve went on a pretty severe gutting of my libraries to fix the bugs, implement the features, test the compilers that I didn’t previously test, and generally just get my issue list as close to zero as possible.
The libraries that I put all the effort into are the following six:
- ๐งช utest.h - a single header unit testing framework that supports C and C++.
- โฑ๏ธ ubench.h - a single header unit benchmarking framework that supports C and C++.
- ๐ utf8.h - a single header UTF-8 string supporting header for C and C++.
- ๐๏ธ json.h - a single header JSON parsing and writing header that supports JSON5 and simplified-JSON, and works with C and C++.
- ๐ subprocess.h - a single header process spawning, joining, and interacting with library for C and C++.
- #๏ธโฃ hashmap.h - a single header hashmap implementation for C and C++.
GitHub Actions⌗
To fix that I wasn’t testing the latest Clang and GCC I decided to finally pull the trigger on using GitHub Actions instead of my old Travis pipelines. Travis was starting to have some integration issues (I don’t know the details, they just suddenly stopped appearing on the PRs when I pushed changes), and since I knew I wanted to end up on GitHub Actions anyway I used this as the reason to just do it.
GitHub Actions was a little daunting at first, but once you get into it the yml format is very powerful. For example here is the workflow of my subprocess.h library.
The most interesting parts are how I upgraded to the latest clang/gcc:
- name: Setup dependencies
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get install -y gcc-10 g++-10 clang-10
And then how I used these configurations:
- name: Configure CMake with GCC
shell: bash
if: matrix.compiler == 'gcc'
working-directory: ${{github.workspace}}/build
run: cmake $GITHUB_WORKSPACE/test -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10
- name: Configure CMake with Clang (Ubuntu)
shell: bash
if: (matrix.compiler == 'clang') && startsWith(matrix.os, 'ubuntu')
working-directory: ${{github.workspace}}/build
run: cmake $GITHUB_WORKSPACE/test -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_C_COMPILER=clang-10 -DCMAKE_CXX_COMPILER=clang++-10
- name: Configure CMake with Clang (Windows)
shell: bash
if: (matrix.compiler == 'clang') && startsWith(matrix.os, 'windows')
working-directory: ${{github.workspace}}/build
run: cmake $GITHUB_WORKSPACE/test -DCMAKE_BUILD_TYPE=${{ matrix.type }} -T ClangCL
My build matrix is pretty extensive too:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
type: [Debug, RelWithDebInfo, MinSizeRel, Release]
compiler: [default, clang, gcc]
exclude:
- {os: "macOS-latest", compiler: "clang"}
- {os: "windows-latest", compiler: "gcc"}
- {os: "macOS-latest", compiler: "gcc"}
- {os: "ubuntu-latest", compiler: "default"}
- {os: "ubuntu-latest", compiler: "default"}
runs-on: ${{ matrix.os }}
And all in all this lets me build for the three desktop platforms, using five different compiler toolchains.
Even though this let me ditch Travis I couldn’t ditch appveyor in the process. I would have liked to have a single CI integration as that’d have made maintaining everything easier, but GitHub Actions does not allow you to test with older Visual Studio installations.
Quite a few of my users love the older Visual Studio’s and won’t upgrade under any circumstances, and so I’ve kept appveyor around to ensure this path is tested.
utest.h⌗
In utest.h I removed the
#include <winbase.h>
entirely. This meant that the huge headers that were
pulled in as part of including utest.h on windows were no longer required, which
sped up compile time on the Windows platform.
utf8.h⌗
In utf8.h I added some new helper
functions utf8makevalid
and utf8rcodepoint
- allowing you to sanitize a
potentially invalid UTF-8 string by replacing malformed codepoints with an ASCII
replacement, and allowing you to reverse iterate through the string.
I also fixed a long standing bug whereby the Greek Capital Theta symbol ‘ฯด’ does not have a uniform mapping from upper to lower case, or vice-versa. This means that if you did the dance of doing upper -> lower -> upper you could have a different string than you started with (but the fix means I correctly handle that letter’s encoding).
json.h⌗
In json.h I added all the fun JSON tests from the JSONTestSuite project, and fixed any and all the corner case bugs this uncovered. Mostly I was incorrectly finding an end-of-file and assuming the JSON to be valid (without checking that strings/objects/arrays were actually closed correctly). I also took the opportunity to add clang sanitizer tests as since json.h uses a single allocation to handle the entire JSON DOM, there was a greater chance of trashing problems. Luckily there are zero issues with the library under its current testing load - nice!
I also added a new function json_extract_value
which lets you pull out a value
from a JSON DOM into a new allocation. This is useful if you want to get a bit
of a DOM and do something with just that (you could free the original allocation
for instance).
subprocess.h⌗
In subprocess.h I fixed two fun issues:
- On Linux if you did not specify the
subprocess_option_inherit_environment
option then you had to have a qualified path to the executable (either a full path with the starting ‘/’, or a relative path like ‘./’). - I also added a new entry-point
subprocess_create_ex
that lets users specify a custom environment to spawn the subprocess with.
hashmap.h⌗
In hashmap.h I fixed a bug where if you had three entries that mapped to the same hash, and therefore used the linear probe part of hashing to find a location. If you removed the middle entry, and then tried to reinsert the third, it’d wrongly make two copies of that entry in the hash table. The test for this shows the problem nicely:
struct hashmap_s hashmap;
int x = 42;
int y = 13;
int z = -53;
ASSERT_EQ(0, hashmap_create(4, &hashmap));
// These all hash to the same value.
ASSERT_EQ(0, hashmap_put(&hashmap, "000", 3, &x));
ASSERT_EQ(0, hashmap_put(&hashmap, "002", 3, &y));
ASSERT_EQ(0, hashmap_put(&hashmap, "007", 3, &z));
ASSERT_EQ(3u, hashmap_num_entries(&hashmap));
// Now we remove the middle value.
ASSERT_EQ(0, hashmap_remove(&hashmap, "002", 3));
ASSERT_EQ(2u, hashmap_num_entries(&hashmap));
// And now attempt to insert the last value again. There was a bug where this
// would insert a new entry incorrectly instead of resolving to the previous
// entry.
ASSERT_EQ(0, hashmap_put(&hashmap, "007", 3, &z));
ASSERT_EQ(2u, hashmap_num_entries(&hashmap));
hashmap_destroy(&hashmap);
I also added a new function hashmap_remove_and_return_key
which lets you
remove and item and also return the key. Remember that hashmap.h doesn’t copy
the memory backing the key, so if you had to allocate memory for the key and
wanted to get the key back so that you could free this memory - this is the
function for you!
GitHub Sponsorship⌗
First off - I don’t really need the money from GitHub sponsorship. I’ve got a good job at Unity and my personal projects in C/C++ and Rust are simple labours of love.
But a few people have asked if I would add a sponsorship option to my GitHub anyway, because they like my libraries and use them.
So I’ve added a sponsorship option to my GitHub for anyone that wants to help me justify the time to keep my libraries updated, bug free, and great.