
How to write better game libraries - LucaSas
https://handmade.network/wiki/7138-how_to_write_better_game_libraries
======
NohatCoder
It always baffles me when a library comes with its own arcane build code.
Instructions like "Requires Python" invariably turn out to actually mean
"Requires hours of debugging".

~~~
ginko
IMO the ideal way to get your library used is to generate a single .h and .c
file.

~~~
FreeFull
Some libraries don't even ship a .c file, all the code is in the header

~~~
anon4242
The downside of that is that all code is in the header. As in you will compile
all the library code whenever you include the header. As third-party library
code is something you probably seldom change yourself, you don't want your
build to spend time building it all the time.

Sure some of those libs may have some define you need to set in one of your .c
files so the implementation will go there but then whenever you change _that_
file it will result in a recompile of the third-party lib. So then you go and
create a separate .c file _just_ for the lib and we're back to why the lib
author didn't do that in the first place?

No, I think what sqlite does is pretty neat. If you're doing development in
the sqlite code-base you have lots of files (so you can manage it) but if you
just want to use sqlite, you have the amalgamation which is one .c and one .h.

------
jandeboevrie
This is bad advice on the c++ part. If you don't use RAII, then you could've
just not done the c++ lib (and have just a c lib). Exceptions, raii and smart
pointers are what makes modern c++ so pleasant, next to templates.

~~~
LessDmesg
> modern C++

> pleasant

Nice joke. Hour-long compilations, type-unsafe templates with unreadable
errors, and circular references are considered pleasant now?

~~~
je42
\- reasonable organized code doesn't take hours. even template heavy code is
quite fast these days

\- templates are type safe. it is just the error messages are not helpful a
lot of times. C++20's concepts should help

\- circular references. that's a thing in C as well. as soon as you need
interact with your libraries user to allocate/deallocate resources.

C++ has a large surface to interact with. Yes you can shot yourself in your
foot. However, the more you know you can use the features to your advantage
instead of shotting yourself in the foot. At the point you gain more than you
lose.

~~~
eru
Templates are duck typing at C++ compile time (which is their runtime). Yes,
concepts should help.

'Modules via copy-and-paste' (= #include) is what I would hurl at C++ these
days. Though they are working on that as well, I heard. Though
[https://vector-of-bool.github.io/2019/01/27/modules-doa.html](https://vector-
of-bool.github.io/2019/01/27/modules-doa.html) doesn't sound hopeful.

~~~
rumanator
> Templates are duck typing at C++ compile time (which is their runtime).

That description only applies of you venture into the dark realm of template
metaprogramming, and anyone who ventures into those lands is wise enough to
understand that the tool is not the one to blame if one decides to abuse a
feature to apply it in a way it was not (initially) designed to be used.

~~~
LessDmesg
You must be trolling... Templates in C++ ARE for metarogramming. The fact that
they work as a glorified text substitution (and they are only coming around to
fix them in 2020) leaves no apology for the tool.

~~~
rumanator
> You must be trolling... Templates in C++ ARE for metarogramming.

That's an ignorant statement both wrt generic programming (you know, the whole
purpose of C++ templates) as well as the history of C++, in particularly how
C++ template metaprogramming was discovered by accident after C++ templates
were already implemented, widely used (see the STL), and standardized.

------
saagarjha
I don't see it mentioned, but it's important to note that you don't have to
write your library in the same language that you expose to your users. You can
write everything in C++/Rust/Python and expose something compatible with the C
ABI and people will be able to use it (provided they have your toolchain…)

~~~
pjmlp
Fully agree with you.

Anyone providing a library written in pure C, better be serious about security
and prove that they have taken all the required steps to handle memory
corruption and UB exploits.

We really need more liability on software development.

~~~
erikbye
Do note where this article resides. Casey Muratori (of Handmade Hero, a game
dev tutorial project) is a self-proclaimed anything-not-C hater. A community
has formed around his project, and has, of course, adopted his hard-line
stance.

~~~
pjmlp
Interesting, thanks for the hint.

~~~
abnercoimbre
The earlier comment was misinformed. Handmade Network is inspired by the
series, yes, but stands on its own and embraces new languages and very
different projects. See the Handmade conference[0] for examples.

[0] [https://handmade-seattle.com](https://handmade-seattle.com)

------
rehasu
The problem for game libraries in general is that game dev lives in c++ world
and c++ is awful for libraries. Most c++ devs I know would rather start
writing a program by defining string than by learning to handle proper library
management tooling. I'm not an expert so I'm not sure if it's just a culture
thing or if there are inherent features in the language that make library
usage hard, but yeah. That's that.

~~~
novok
I wish the C++ world would adopt bazel eventually as their standard tooling
base.

~~~
rumanator
> I wish the C++ world would adopt bazel

Why do you believe picking a specific build system is relevant wrt libraries,
particularly a build system whose main selling point is build speed.

------
flohofwoe
As a warning: While C99 is a really nice language - especially over C89 -
please note that the Visual Studio C compiler still does not support the full
C99 specification, and it is not possible at all to compile C99 as C++ code
(which sometimes is useful).

I usually recommend to write library-level code in the "subset of C99 which
compiles both in C and C++ mode on GCC, clang and MSVC". This is basically a
version of C that's somewhere C89 and C99 (basically a "C95").

Another quite valid option is to define a pure C-API first, but implement the
"inner library code" in a simple C++ (just be careful with "modern C++", since
this usually results in increased compilation time and binary size).

~~~
LucaSas
Thanks for the comment, I might steal your "subset of C99 which compiles both
in C and C++ mode on GCC, clang and MSVC" quote and put it in the article if
that's ok ;)

~~~
matheusmoreira
Is there a benefit to building C code with a C++ compiler?

~~~
flohofwoe
C++ has some stricter type checking by default, but most of this can be
achieved by raising the warning level when compiled as C.

Nevertheless I received enough requests to make my C libraries 'C++ compliant'
that I gave in :)

------
drjeats
Nice article :)

Might be worth touching on error callbacks/logging as an error handling
strategy.

Sometimes an error is not recoverable in the sense that the calling code can't
really do anything about it, but the library should attempt to make progress
anyway instead of halting the entire program.

By allowing users to specify an error callback, this means they can log
errors, capture stack traces, assert, or whatever.

This isn't that helpful for smaller libraries with smaller-scoped processes,
but if it's something like a renderer or interactive audio lib, those often
just need to be given a bunch of frame time to do work with the complex input
you've prepped and fed to it, and trying to propagate error codes up out of
that simulation step would both contort the inner code and not be as helpful
as an error callback.

With an error callback you can assert, set breakpoints, or do whatever. But
more importantly, by default you can have it just log so when you inevitably
in a bug it doesn't prevent everyone else from getting work done while it
keeps asserting until you fix your shit.

This will be a less common need than the other standard error reporting
mechanisms, but is important to get right if your library has these complex
internal preconditions that you want to make visible to the client when
violated.

------
grawprog
Honestly, for me all I would like is the ability to include/import one or more
files as needed, helpfully separated by basic functionality, without needing
to include the entire library, have a bare minimum of dependencies, hopefully,
module independent, the ability to build painlessly for different platforms
with a minimum of code changes and a painless build process for the library
itself, ideally one I can include in my own build process without a lot of
effort.

I've used some libraries that do some of these things, but I can't think of
any that does all of them.

------
yoz-y
> Not everyone wants to use C++ (some prefer C).

This could be very well flipped over. If I have the source available, I'd much
rather deal with C++ bindings.

~~~
LucaSas
Hi, I mention in the article that "It is easier in general for a C++ user to
use a C library than it is for a C user to use a C++ library." which is where
the advice comes from.

~~~
yoz-y
I agree. But "wants" is a term indicating personal preference. I understand
that C is best suited for maximum portability and performance. But as a C++
developer I prefer libraries that come with C++ interfaces (and you do mention
that it is good to provide a C++ wrapper as well).

~~~
LucaSas
Thanks a lot for your feedback. I decided to remove that since I think you are
right about indicating personal preference. I do not want this article to be a
list of preferences but rather an analysis of the considerations that popular
C libraries make (eg: stb, sokol, etc) and that new authors should also reason
about.

------
rienbdj
Why not a C++ library with an optional C API?

~~~
LessDmesg
Because C++ is one of the most complex languages in the world, hence a pain to
interop with. C FFIs, on the other hand, are ubiquitous.

~~~
izacus
The idea (and one that worked well in several projects I've worked on) is that
you use C++ as main language of the library and make outside bindings C.

It does require some discipline, but C++ has many portable constructs that
make life significantly easier when writing business logic.

------
Const-me
Some of these recommendations are awesome, others aren’t good, IMO.

> Always prefix your names to avoid name collisions

Solved by C++ namespaces.

> Use header guards instead of #pragma once.

#pragma once is supported by all mayor compilers, header guards can introduce
bugs, also #pragma once builds measurably faster:
[https://github.com/electronicarts/EASTL/blob/3.15.00/include...](https://github.com/electronicarts/EASTL/blob/3.15.00/include/EASTL/vector.h#L64-L66)

> Expose constants to the user using constexpr variables.

Modern C++ has strongly-typed scoped enums for such constants.

~~~
LucaSas
Hi, thanks for your feedback. I am the original author.

Regarding prefixes, I advice that you start by writing the library in C and
then wrap it in C++ for a variety of reasons that you might want to consider.
In C++ you should indeed always use namespace.

Header guards have the advantage over pragma once that they are standard and
you can also use them to check if a library is included. I might remove that
since maybe it's not that important and people might different views.

Regarding constants, I was referring to things such as numeric constants for
which you would constexpr in C++. Maybe I can be more explicit there. Thanks
for the feedback.

~~~
Const-me
I advise you start by writing the library in C++ and then wrap it in C if you
need C ABI of the library.

It’s very hard to write correct C code which does IO and supports
multithreading. Take a look at Microsoft’s implementation of fprintf, copy-
pasted from Windows 10 SDK: [https://gist.github.com/Const-
me/f1bb320969adde6c79694265ea6...](https://gist.github.com/Const-
me/f1bb320969adde6c79694265ea644b9d) They use RAII to set & revert the locale,
and to lock RAM buffer to avoid corruption by another threads. They use C++
lambda for exception handling. They even use C++ template to avoid code
duplication between printf and wprintf.

But these C++ shenanigans are not exposed to user, user calls their `printf`
(possibly in a code built by C compiler) and it just works.

~~~
huhtenberg
> _It’s very hard to write correct C code which does IO and supports
> multithreading_

This is just a random unsubstantiated statement. There's nothing particularly
"hard" about writing IO libraries that is language-specific. Multithreaded or
not.

~~~
Const-me
When you do IO and multithreading, functions often need to acquire and release
stuff: mutexes or other locks, resources, locales. Even more so in videogames,
e.g. OpenGL code often calls glMapBuffer / glUnmapBuffer many thousand times
each frame.

The hard part is making sure you release stuff every time you acquire stuff,
exactly once. C++ RAII makes it almost trivially simple, but standard C has
nothing comparable. When you only targeting gcc and clang can use
__attribute__(cleanup) in C, it helps but still it’s more limited and more
error prone compared to destructors.

~~~
abcd_f
> The * hard part * is making sure you release stuff every time, exactly once.

Oy vey... you can't be serious. That's rudimentary basics of using any API.

~~~
Const-me
It’s easy enough to correctly use a small API in a small program. It doesn’t
matter at all for short-living apps which clean up their resources by exiting
the process.

There’re also other programs in wide use, which need to reliably work for
hours, sometimes weeks. Some of them have huge amount of code they built from,
written by many people over many years. Combine that with large enough APIs
(some peripheral devices have hundreds of writeable registers of state; or
D3D11 exposes huge amount of very complicated state, only limited by VRAM
amount which is measured in gigabytes) and it’s very easy to make bugs in such
programs.

Leaks of memory, handles, sockets, and many other resource types e.g. GPU
ones. Deadlocks caused by locked mutexes, or threads which exit but forgot to
release something they needed to release. Unwanted changes to global or thread
state, both internal to the process and external (locales, formatting options,
console colors, process and thread priorities, current directory, environment
variables, CPU registers like FPU flags and interrupt masks, GPU render
states) caused by some code changing stuff but not reverting the changes back.
Unwanted state changes of custom peripheral devices, due to the same reason.

C++ RAII is not a silver bullet, but it does help a lot for all these things.

~~~
abcd_f
It's easy enough to implement correct API usage semantics in a program of any
size.

If you need to rely on RAII in order not to screw things up, then it's an
issue with the coding style or the application design. That's what needs
fixing. Not the language choice. You got it backwards.

~~~
Const-me
It’s a hard problem, and language + runtime support helps. That’s why C++ has
RAII, C# has IDisposable / using, Java has try-with, python has enter / exit /
with, golang has defer, and so on. The only reason C has nothing comparable,
it’s almost 50 years old now.

The problem is only tangentially related to API semantics. The problem is
mutating state. The state is not necessarily managed by an API, for instance
CPU registers aren’t, you modify them directly. Same with other global state
like I/O formatting options and locales, these things are just global
variables.

------
astrobe_
_Make_ is a much the lingua franca of build systems as C is for programming,
so there's no reason not to provide a (simple) makefile. The alternative they
suggest is worse anyway.

~~~
huhtenberg
Windows is the reason make is not a good choice. Visual Studio is a de-facto
standard for Windows development and it's not exactly makefile-oriented.

The lowest common denominator for build instructions that works everywhere is
literally "set your include paths like so and compile these sources".

~~~
astrobe_
How strange. I have tinkered with a couple of FOSS games of Windows and none
of them required VS (not to mention another couple of other non-game
projects). Maybe that's because they often are multi-platform projects so Make
is the best choice at the global level for them?

> The lowest common denominator for build instructions that works everywhere
> is literally "set your include paths like so and compile these sources".

Yes, but that's quite inconvenient and error-prone ; a typo in a -D flag is
very likely to not compile what you wanted.

 _Make_ is not a multi-megabyte program with a frack tonne of dependencies.
Having your users to install it in order to build your project is far from
being completely outrageous (unless for _cough_ microsoft _cough_ cults that
consider the command-line is evil).

------
Keyframe
And then the author provides example where there’s Dear IMGUI which is c++.

~~~
flohofwoe
Dear ImGui has a very C-style API though, it's just flat a function API
wrapped in a namespace, and about the only other C++ feature that's used in
the API is default function arguments.

For usage from C code, there's cimgui, which is an automatically generated
C-API wrapper:

[https://github.com/cimgui/cimgui](https://github.com/cimgui/cimgui)

------
killmov3s
We just need more single-file headers like nothings' stb libraries

------
fortran90
Since this article lays so much emphasis on C, I have an honest question to
everyone. What is a good way for a beginner to learn C in the current time?
The minefield of undefined behavior is really overwhelming to a beginner. Are
there any good resources that teach C the right way with good advice and best
practices to navigate the UB minefield?

~~~
huhtenberg
> _the UB minefield_

While UB quirks exist, they are WAY off the beaten path and it takes an effort
to run into them. Doubly so if you are just starting with the language.

Just treat C as a thin convenient layer over the hardware that expects you to
think and act responsibly in exchange for this nearly raw access.

~~~
Profan
In C (and C++) something as simple as forgetting to initialize a variable
lands you in UB-land already, nearly every person I knew when I was learning
myself (and still now) ran into these things _very quickly_.

Not to mention compilers make fun-times out of this by sometimes zeroing
memory in debug and then not doing so for release builds (Hi MSVC!)..

The "thin layer over hardware" idea is a thing of the past as soon as
optimizations come into the picture, and even then.

------
edem
Please add (C) to the title of the article. In this form, it is misleading
because most of the advice only applies to C.

~~~
kd5bjo
The target audience isn’t C programmers, but library authors that want their
library to be usable with multiple programming languages. Most of the advice
applies to C libraries because the article starts by advising you to write
your library in C, and gives several reasons.

------
nurettin
>> C is the lingua franca of programming

I don't have the exact numbers, but this seems debatable.

~~~
Reelin
It's the lingua franca in that it's the standard for interoperating. Every
high level language I've used has had _at least_ one C FFI readily available.

~~~
nurettin
We do have a mutual understanding on whether C foreign function interfaces are
available for other programming languages, which does enable languages to send
system requests (for systems which are written in C) or to create optimized C
code to parse documents or calculate formulas. I am aware that the concept of
making languages talk with C interfaces exist. And I made the above statement
knowing this.

------
shmerl
_> Write the library in Standard C99_

Why not write it in Rust, and provide C99 bindings? Using stock C for the
actual library doesn't sound like a good idea to me, when today there are
better alternatives.

And for sure avoid using Metal lock-in directly. Use Vulkan, which Apple
should have supported from the beginning. For lock-in targets, there are
translation options from Vulkan.

~~~
LucaSas
Hi, the reason for using C99 is that it is fully compatible with other
languages. If you write a library in Rust you might be tempted to use Rust
only feature which will then make it hard to wrap the library for other
languages. And if you don't use those features from Rust in your library
people will comment that your library isn't Rust enough. Same thing applies to
C++. To that extent my advice is to write C in C rather than C in Rust or C++.

Also if people don't have a Rust toolchain setup, compiling the library and
using it from source would be hard. In some cases integrating Rust in their
toolchain could be hard.

Regarding metal and vulkan I will edit the article to mention Vulkan there too
alongside metal. Thanks for your feedback.

~~~
shmerl
I suppose wrapping it for other languages can be harder, but should be doable
I think. The benefit of Rust though is using a much better language with rich
standard library.

As for toolchain, Rust can be set up basically anywhere llvm can, which is
quite a lot. There are rare cases where llvm wasn't ported yet to, but I don't
think they are enough to make C a compelling option in general, and I don't
think any of them are gaming related.

If you are in such case - then sure, but otherwise, I'd still prefer Rust.

~~~
LucaSas
I think the Rust standard library is actually a liability in this case, not an
advantage. The rust stdlib will essentially be an extra dependency and also
the rust standard library doesn't have allocators, so unless you are very
careful you might violate the principle of not doing allocations for the user.
Also, even if it did have allocators, passing allocators from to Rust via a C
interface would probably be awkward.

So if you do choose to make the implementation in another language, there are
extra considerations that you have to take into account.

