Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
STB: Single-file public domain libraries for C/C++ (github.com/nothings)
85 points by seansh on Jan 6, 2024 | hide | past | favorite | 35 comments


Before the usual confusion starts flooding the comment section:

STB-style is different from traditional C++ style 'header-only libraries' in that the implementation is not parsed and compiled each time the header is included, instead the implementation code sits inside a big ifdef/endif block which is only activated at a single include point across the whole project (meaning the implementation is compiled only once in a complete project rebuild, every other include only parses the API declarations at the top of the header and skips the implementation).

This means that STB-style libs don't suffer from the compile time explosion we are seeing for typical header-only C++ libraries (such as the C++ stdlib headers).

You still pay the raw IO cost for skipping the ifdef/endif section of course, but this comes down to a couple of seconds for including the file tens-of-thousands of times (which is rarely the case even in large projects since such libraries tend to be isolated in small areas of the projects, such as a single subsystem).


I don't really get the benefit tbh. I always end up with a stb_image.c file which only defines the necessary macro and includes stb_image.h. Wouldn't it be simpler if the project just provided a stb_image.c and an stb_image.h? It would have better compile-time performance, the header would have more readable API docs (which is important since the only API docs are in the headers), and it would be less work to integrate.


Yeah, definitely debatable. IME the difference between a single STB-style header and a single .h/.c pair isn't really all that relevant in practice (not enough to waste much time discussing the pros and cons at least - especially the build time consequences are usually overblown, I benchmarked my own header libraries and it comes down to a couple of seconds at most for including the header in thousands of C source files - with the implementation skipped of course).

The actual problem are libraries with hundreds or thousands of source files in dozens of directories and a CMakeLists.txt file from hell.

A good C/C++ library should be easy to drop into a project, trivial to integrate with any build system (or no build system at all), and no need for a package manager. If configuration is necessary it should happen with a small number of defines.

...one arguable advantage of STB-style headers is that you can include all header-library implementations of a project in a single source file, which might actually speed up overall build time because this benefits from the same advantages as unity/jumbo builds (especially in case of C++ code when stdlib headers need to be included). But for C code - where the stdlib headers are just a couple of hundred lines of simple declarations instead of tens-of-thousands of lines of gnarly template code - the difference is also negligible.

PS: another advantage of STB-style headers is that you can build even non-trivial projects into a single source file and compile that with `cc main.c -o mytool`, e.g. no need for a build system at all (of course the same is possible by including .c files into the main.c file, even if that might look a bit icky).


> The actual problem are libraries with hundreds or thousands of source files in dozens of directories and a CMakeLists.txt file from hell.

Why is that a problem? If you have a large library, surely it's better to properly break it up into separate files?!

> no need for a package manager.

Well, yeah if you don't want even a build system, large projects in general become really difficult to maintain anyway. The solution seems to be like every programming language created after 1989? Have a package manager and make it irrelevant if the library contains 1 file or 1000. What's the reason C resists so much using a package manager, even when similarly low level languages like Rust and Zig have them and make them first class citizens?


> Why is that a problem? If you have a large library, surely it's better to properly break it up into separate files?!

That's at best good for developing the library (even that is debatable though), but definitely not when just using the source-distribution of the library in a project.

> What's the reason C resists so much using a package manager...

One important problem is that there are so many package managers and build systems for C and C++ projects. So the choice for a library author is either to support all package managers and build systems (not really a realistic option), or none specifically (much better but requires that the library source distribution is designed for simple integration into projects without esoteric build requirements).

Requiring a specific package manager and build system is hostile to users preferring other tools.

The problem doesn't lie with C or C++ users, but with the respective language committees saying "dependency management and build system standardization isn't our problem".


> If you have a large library, surely it's better to properly break it up into separate files?!

Sqlite solves this dilemma by providing both: regular source code repository with separate files for development and an "amalgamation" with everything in a single .c file. The latter makes it really easy to integrate.


Zstd too. IOW the single-file header (or .c/.h) is a build artifact, like a .dll.


If there are multiple such libraries, I will probably put them into a single `deps.c` and forget. No need for individual `<library>.c`. Yes, upgrading one of dependencies will recompile `deps.c`, but that should be infrequent.


> Why not C99? stdint.h, declare-anywhere, etc.

> I still use MSVC 6 (1998) as my IDE because it has better human factors for me than later versions of MSVC.

This is really amusing to me. Does anyone know if the author has ever given more detail on why?

Also stb is a really great library to look at for those starting C development.


I used to be a VC6 holdout. It was the last version of Visual Studio that was written in native code and had a distinct Visual C++ subset as a product. After that, they rewrote the IDE in .NET, making it much slower and breaking a bunch of stuff. If you run VC6 on a modern computer, it's blazingly fast with a compact install, and doesn't have a bunch of non-C++ nonsense getting in the way.

Of course, you also have a C++ compiler from 1998 that doesn't use correct for loop scoping, barely works with templates, can't understand any C++11 or later constructs, barfs a page of angle-bracket meatloaf diagnostics whenever an error occurs in the STL, has a single-threaded build, and only works with a Windows XP era version of the Windows SDK. I gave it up around Visual Studio 2005 when the IDE became tolerable again.


You also have 1998 tooling for debugging, C++ code analysis, refactoring and 6 generations behind of VS improvements, including perf.

However not all is lost, for those that are deep into COM, the tooling has hardly changed, it is still mostly ATL as in 1998 (WRL, WIL, C++/WinRT still rely on the same VS 6 tooling).


> https://www.youtube.com/watch?v=3BYKiOHdCNg

My attempted summary: it's mostly a matter of the window layout in VC6 giving the most real estate to Just Code, and also the hotkeys for common workflows (e.g. show/hide debug output) are simple.

Commentary: Modern MSVC tends to be more difficult to customize, whereas e.g. Jetbrains IDEs are more straightforward to customize in such a way as stb would be familiar with -- but, both MSVC and Jetbrains suffer from such awful performance (e.g. input latency) that it detracts from their usefulness as IDEs.


Sean is part of the old guard of developers which (similar to Rob Pike) eschews modern conveniences like syntax highlighting because they find it distracting (I'm paraphrasing here). And I think it's a bit obnoxious to amuse yourself at the expense of the grandfathers of computing just because they enjoy doing things in a slightly archaic way.


stb_image is a stable in gamedev art pipelines.

“I just want to load an image. Thank you. Moving on…”


I love it for doing small experiments in C++ (the kind where everything is a few hundred lines in a single file) with images too.


I’m a C noob, so why aren’t header only files the norm? Seems a lot easier to deal with then a split header and c file


There are a number of reasons, some already presented. One more: you are (you are supposed to at least) much more likely to change the implementation of a library than its interface. You don't want to have all project files which need the interface be recompiled every time you change the implementation.


Because if your program is split into many files (as is the norm for non- trivial projects) and several of these files #include the same header, then the functions defined in such header are instantiated/compiled several times. You end up with longer compilation times and code duplication.


True. Except, it's trivial to mitigate this: one only needs to wrap the whole library under one giant #ifndef. Like here, for example: https://github.com/sheredom/utf8.h/blob/b7ed0a28eb92803c81d6...


No, this doesn't help at all. It will only stop the header from being parsed twice if it's #included twice in the same .c file, not from being parsed twice if it's #included from two different .c files.


You'd still have to parse & compile the contents of the header for each translation unit, instead of only once.


In STB-style headers the implementation is inside a separate ifdef/endif block which is only activated at a single include point in the whole project. At all other include points the compiler will only parse the public API declarations and skip the implementation block (which is so much faster than parsing and compiling the implementation that the difference to a header which only contains the API declaration doesn't matter much even in large projects).

For instance here is the start of the implementation block in stb_image.h, above that is just "cheap" public API declarations:

https://github.com/nothings/stb/blob/f4a71b13373436a2866c5d6...


While I'm a big fan of keeping declarations and implementations as close as possible, I find that the need to explicitly #define STB_SOMETHING_IMPLEMENTATION before including the header somehow spoils the simplicity of the solution. At this point, I'd rather use a single file that acts as either a .h or a .c based on __INCLUDE_LEVEL__ (a GCC extension, not sure if available for MSC).


I wish there was the same thing for opengl3.3

tinyengine is great but not supported for windows yet



there's glad (https://github.com/Dav1dde/glad) which you can use as a single .c file + .h header that defines OpenGL stuff or a single header-only file. I use it on all of my OpenGL projects!


glad is not an engine like tinyengine


Plenty of Opengl stuff available on vcpkg,

https://vcpkg.io/en/packages


Header only libraries are great, and these little gems floating around are super easy to use, until you hit an unsupported (by them) feature, or something that now you have to do (like for year you've been using it for reading, but now you need to write the file, or interleave it, or whatnot).

And then you realize you need the official lib - libpng, etc.

E.g. it's not so simple.


The same is true for any lib. There is no guarantee even the largest lib will have everything you ever need. That's where new code comes from.


That's not true. There are several established and well supported libraries that actually work as expected. Then there are other, optimized versions of them.

The elephant in the room is the missing common build system, so folks engage in what's EASIEST - include just a header and you are done. This is awesome for initial development, gives you wings, but once your software has been deployed over and over - that header only library starts to show it's weakness - because it's header-only there are certain limitations - especially around how to handle singletons, initialization, and few other critical things.... And then compilation.

Some header-only libraries, realize that and simply asks the user to at least do this once somewhere:

#define SOME_HEADER_ONLY_LIB_IMPL 1 #include <some_header_only_lib.h>

and above gets code that needs to be compiled in exactly one compilation unit, and not multiple.

This is actually not so bad, if it was considered more... but it gets hairy when you have library that needs to use that header only library, and now up the tree of deps the top-most program has to do the above, and then it gets super messy.

Again, it's super easy, but not simple (long term). I've seen it happen with code that I work, I've also written tons of header-only libs - they are exciting!


Well, yeah. Unless things have changed, the stb image library is explicitly not for parsing arbitrary image files.

> Primarily of interest to game developers and other people who can avoid problematic images and only need the trivial interface


That's exactly the point of the lib. It's easy to use for common use cases. If it saved you time at the beginning, saved you from build system nonsense, learning convoluted apis and other crap for a year. That's a big win.


Thankfully, now with C++ modules, I can package them and don't bother with them being parsed all over the place.


There is stb_image_write.h, with the function stbi_write_png().




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: