CLib: Header-only C library that implements the most important classes from GLib(github.com)
github.com
CLib: Header-only C library that implements the most important classes from GLib
https://github.com/aheck/clib
9 comments
> It will simply force me to make a `.c` file for every `.h` and define it there.
You can do that of course but you'd be missing the point of how it is meant to be used. Your project already has at least a single .c file anyway, so why make a separate .c file just so you can put `XXX_IMPL` in there instead of putting it in the .c file that is going to be using the library itself?
You can do that of course but you'd be missing the point of how it is meant to be used. Your project already has at least a single .c file anyway, so why make a separate .c file just so you can put `XXX_IMPL` in there instead of putting it in the .c file that is going to be using the library itself?
Because:
- I like my object files (.o) to only contain what they are supposed to contain and nothing else
- I don't want to have to keep track of where did I put the `XXX_IMPL`
No I'm not missing the point of how it is meant to be used, I'm criticizing that use case as a very poor one, I'd even say it's an anti-pattern IMHO.Right, but those would be your choices, it wouldn't be the library itself forcing you to do that, it would be your personal preferences. That isn't a fault of the library.
By not making a choice, the library is actually making the worst possible decision.
It is bloating my ABI if I'm not careful, and it makes distributing more cumbersome when it could/should be straightforward.
That's just not how you are supposed to distribute a C library, this is a poor design that clearly states "I don't know or don't care how to package my code".
It is bloating my ABI if I'm not careful, and it makes distributing more cumbersome when it could/should be straightforward.
That's just not how you are supposed to distribute a C library, this is a poor design that clearly states "I don't know or don't care how to package my code".
There really is no "correct way" to distribute C libraries and at this point it seems you're nitpicking just to nitpick. Imgui uses the same method, no one ever complained. If you do require obj files to not be "bloated" (why?) you can always do it the C way of having separate C/H files.
There is, just like C has been doing in its 50 years of history, header only libraries are a consequence of new generations pretending C is something like Python.
imgui distribute .c/.h files, not .h files with .c embedded in them.
What I'm criticizing is the extra step of me having to write the .c files anyway to avoid bloating the obj files, which should have been done by the library in the first place.
What I'm criticizing is the extra step of me having to write the .c files anyway to avoid bloating the obj files, which should have been done by the library in the first place.
As others have pointed out, you don't need to write extra .c files, and you won't bloat the obj files. This paradigm is fairly common, see, e.g.:
https://github.com/lieff/minimp3
https://github.com/Immediate-Mode-UI/Nuklear
https://github.com/mattiasgustavsson/libs/blob/main/docs/htt...
https://github.com/lieff/minimp3
https://github.com/Immediate-Mode-UI/Nuklear
https://github.com/mattiasgustavsson/libs/blob/main/docs/htt...
The library did make a choice - header-only C libraries are a choice. And the "how you are supposed to distribute a C library" is clearly just your own opinion you are trying to pass as objective truth. There are many header-only C libraries out there written by many experienced programmers that contradict your opinions.
TBH this is really just bikeshedding, not real issues. IMO even a tab-vs-spaces argument would have more merit as that has real practical effects (appearance of the code in tools like terminal or sites that have forced tabs like code review sites or github).
TBH this is really just bikeshedding, not real issues. IMO even a tab-vs-spaces argument would have more merit as that has real practical effects (appearance of the code in tools like terminal or sites that have forced tabs like code review sites or github).
There are billions of flies who eat shit. Therefore, shit should be edible?
The "numbers argument" is a fallacious one.
No, "how you are supposed to distribute a C library" is not my opinion, but the experience/observations of the last 50 years of engineering. Why would we even have a .so/.a/.lib/.dll format for libraries?
As I said in a sibling comment, those are not header-only libraries. It's poorly packaged .c/.h in a single file that we happened to name with the .h extension.
It's not a header, it's a header+implementation. And it's bad practice, and any decent C developer will tell you so.
The "numbers argument" is a fallacious one.
No, "how you are supposed to distribute a C library" is not my opinion, but the experience/observations of the last 50 years of engineering. Why would we even have a .so/.a/.lib/.dll format for libraries?
As I said in a sibling comment, those are not header-only libraries. It's poorly packaged .c/.h in a single file that we happened to name with the .h extension.
It's not a header, it's a header+implementation. And it's bad practice, and any decent C developer will tell you so.
> It is bloating my ABI if I'm not careful
How so? The private implementation functions are usually declared static, only the public API functions are visible to the linker. Some single-file libraries also allow to declare the entire API as static (via a config define) so that none of the library API functions are visible to the linker.
How so? The private implementation functions are usually declared static, only the public API functions are visible to the linker. Some single-file libraries also allow to declare the entire API as static (via a config define) so that none of the library API functions are visible to the linker.
Making a separate C file prevents you from having to recompile the single-header lib every time a change is made to the file with the IMPL in it.
Also compilation of a single file is a lot worse when it's really big, compared to several smaller files that can use multiple cores.
Also compilation of a single file is a lot worse when it's really big, compared to several smaller files that can use multiple cores.
Well, everything is in a header, so 'header only' is sorta correct - but the name is also problematic because that's what 'header only' C++ libraries (with the implementation in inline methods) are often called too, and those do actually suffer from downsides, such as increased compilation time.
A more correct name is 'STB-style single-file libraries' and it's really just a distribution format, nothing to get all worked up about - the differences to a single .h/.c pair are mostly negligible.
A common way to integrate such libraries into a non-trivial project is not to create one .c file for each header, but instead to create one .c file for a whole group of related headers (up to all external single-file dependencies of a project in a single implementation file), this may actually decrease compilation time the same way that unity/jumbo builds do, and since external dependencies don't change, this is also not a problem for incremental builds.
Another advantage: at least the original STB libraries (stb_image.h etc...) are often used in small command line tools, those can simply include the implementation in the "main.c" source, resulting in a project with dependencies that's still compiled as a single source file:
A more correct name is 'STB-style single-file libraries' and it's really just a distribution format, nothing to get all worked up about - the differences to a single .h/.c pair are mostly negligible.
A common way to integrate such libraries into a non-trivial project is not to create one .c file for each header, but instead to create one .c file for a whole group of related headers (up to all external single-file dependencies of a project in a single implementation file), this may actually decrease compilation time the same way that unity/jumbo builds do, and since external dependencies don't change, this is also not a problem for incremental builds.
Another advantage: at least the original STB libraries (stb_image.h etc...) are often used in small command line tools, those can simply include the implementation in the "main.c" source, resulting in a project with dependencies that's still compiled as a single source file:
cc -O3 mytool.c -o mytool
...e.g. no build system needed at all.I know that the header-only C and C++ libraries have been catching on for years now, but I don't feel like the trade-offs are meaningful enough for me personally.
I already do something that some people seem to frown upon, which is ship the exact source code of the dependencies that my projects use when writing in C or C++, thus "pinning" them, so that I know you're able to build out of the box without having to download all n number of dependencies, set up their paths, etc. And also, so I know that the dependencies don't disappear, which for some projects I've worked on, they have. And the entire site they came from.
It's also not how I understand the intention of either language. Implementation in .c and .cpp files, interfaces in headers.
I already do something that some people seem to frown upon, which is ship the exact source code of the dependencies that my projects use when writing in C or C++, thus "pinning" them, so that I know you're able to build out of the box without having to download all n number of dependencies, set up their paths, etc. And also, so I know that the dependencies don't disappear, which for some projects I've worked on, they have. And the entire site they came from.
It's also not how I understand the intention of either language. Implementation in .c and .cpp files, interfaces in headers.
Unfortunately you can't please everyone, but I think your approach is the safer one, and it's ok to put your foot down, it's your project after all. The people who would care so much about external dependencies may not be the ones you want on your side anyways. The only middle ground I can even think of would be something like using git submodule/subtree/subrepo where the user can choose to either not even clone those parts (or somehow exclude them altogether) in order to use the system libs, or be able to update the dependencies themselves if they don't want to stay on your known-working versions.
Your approach makes a lot of sense. I wish more people would do that.
This idiom used to trigger a panic is interesting. Is it also used in the original GLib?
fprintf(stderr, "BUG: _g_hash_table_find_free_slot: Failed to find an empty slot.");
int *ptr = NULL;
*ptr = 0;
I'd have thought this was somewhat dangerous, as dereferencing a NULL pointer is undefined behaviour, so that the compiler is technically free to eliminate '*ptr = 0' (with the result that the code will continue executing).Back when GLib was written, compilers didn't do anything fancy with code like this, so this was a completely reliable way to trigger a segmentation violation. As always, even though the standard says you should not do that, people write to the actual implementation.
Nowadays, I would expect the entire branch to be marked unreachable, i.e. the whole block would be deleted and if actually reached, nothing would happen.
Nowadays, I would expect the entire branch to be marked unreachable, i.e. the whole block would be deleted and if actually reached, nothing would happen.
FWIW from a quick testing both GCC and Clang generate a segfault with full optimizations as long as this isn't the only code in the program (e.g. in my test i put this in a separate function that printf'd some message before doing the assignment).
Also GCC does generate the segfault even if this is the only code (in main) but Clang writes code that exits the program with some error code instead of segfaulting in all optimization levels except 0 (with -O0 it generates the segfault like GCC).
Also GCC does generate the segfault even if this is the only code (in main) but Clang writes code that exits the program with some error code instead of segfaulting in all optimization levels except 0 (with -O0 it generates the segfault like GCC).
abort() exists for a reason.
Especially given that it could just call abort(), which has instead a well-defined behavior.
This is/was sometimes used as a debugger trap, i remember a coworker of mine having a t-shirt once that was like "KEEP CALM AND (void*)0 = (void)0" :-P
This most likely is an unexpected failure. Indicating bug in algorithm or memory corruption. Like "Failed to find empty slot, while logic flow says I should have."
That's definitely the intent of the code, but I don't think it's correct to do it this way, as the compiler can just optimise out the null pointer dereference.
Thanks for pointing this out, I just replaced it with exit(1).
Yeah, you're right. That's undefined behaviour, which is a compiler free for all in terms of unwanted effects.
I think this is unique to CLib, not from GLib.
I think this is unique to CLib, not from GLib.
Why instantly crash when out of memory instead of letting caller decide what to do?
Also, some GString methods that have string length argument do not append NUL terminator, assuming that the input string is terminated at length+1. This could lead to fun buffer overrun if the string is to be used by functions expecting C-strings. I guess they are to be used when strlen is known by caller, but nevertheless, some documentation would help.
Also, some GString methods that have string length argument do not append NUL terminator, assuming that the input string is terminated at length+1. This could lead to fun buffer overrun if the string is to be used by functions expecting C-strings. I guess they are to be used when strlen is known by caller, but nevertheless, some documentation would help.
You're right, calling exit when out of memory is at least controversial if not even bad. I still do it because the goal is to be source compatible with GLib. And this is how GLib handles out of memory, unfortunately.
https://libsoup.org/glib/glib-Memory-Allocation.html#g-mallo...
https://stackoverflow.com/questions/16974254/glib-handle-out...
Thanks for pointing out the null termination issue. This was actually a bug and I just fixed it in g_string_append_len. The other _len methods look fine to me.
Since CLib aims to be source compatible with GLib the documentation for the respective classes of GLib should do it:
https://libsoup.org/glib/glib-Strings.html
https://libsoup.org/glib/glib-Hash-Tables.html
https://libsoup.org/glib/glib-Doubly-Linked-Lists.html
https://libsoup.org/glib/glib-Memory-Allocation.html#g-mallo...
https://stackoverflow.com/questions/16974254/glib-handle-out...
Thanks for pointing out the null termination issue. This was actually a bug and I just fixed it in g_string_append_len. The other _len methods look fine to me.
Since CLib aims to be source compatible with GLib the documentation for the respective classes of GLib should do it:
https://libsoup.org/glib/glib-Strings.html
https://libsoup.org/glib/glib-Hash-Tables.html
https://libsoup.org/glib/glib-Doubly-Linked-Lists.html
I had no idea GLib did that. Their string implementation seems to be the best one to use, too, for those looking for a capacity, length, and data implementation. I'm a bit disappointed to know this now. But good on you for being strictly compatible; I feel like many people would fudge the implementation.
Thanks for sharing this.
Thanks for sharing this.
Yeah, the GLib documentation is not very good at pointing this out, unfortunately.
I'm also thinking about maybe creating a slightly incompatible version which might use "c_" as a prefix instead of "g_". There would also need to be some changes to the interface and not only the return values because the methods that do a realloc internally only return a pointer to the object itself (e.g. GString*) for chaining and therefore cannot be used to signal an error condition.
But not sure, yet if this is a good idea or just creates confusion.
For the moment I just want to provide the (in my opinion) by far most important classes of GLib because I always missed something like that in the C standard library. This is also why I chose to implement it header-only. It should be as easy as possbile for people to use them.
The out of memory handling is very unfortunate and you should probably not use this code in the middle of a database transaction or something similar but I think for most user level code and situations where you would handle out of memory with exit anyway it is ok as it is.
I'm also thinking about maybe creating a slightly incompatible version which might use "c_" as a prefix instead of "g_". There would also need to be some changes to the interface and not only the return values because the methods that do a realloc internally only return a pointer to the object itself (e.g. GString*) for chaining and therefore cannot be used to signal an error condition.
But not sure, yet if this is a good idea or just creates confusion.
For the moment I just want to provide the (in my opinion) by far most important classes of GLib because I always missed something like that in the C standard library. This is also why I chose to implement it header-only. It should be as easy as possbile for people to use them.
The out of memory handling is very unfortunate and you should probably not use this code in the middle of a database transaction or something similar but I think for most user level code and situations where you would handle out of memory with exit anyway it is ok as it is.
When I tried GLib and GTK4 I was surprised how much the former reminded me of Core Foundation and the later Cocoa. Unfortunately, as much as I like C, the GObject system was less pleasant and felt like a poor mans Objective-C. I wonder why the GNOME team, and Linux crew in general, never adopted the language? I understand shunning C++, but Objective-C is C with a thin OO layer.
They experimented with Vala, but I don’t know much of it today. Maybe check it out!
https://en.wikipedia.org/wiki/Vala_(programming_language)
https://en.wikipedia.org/wiki/Vala_(programming_language)
I don't like C++ but even C++ would be a better choice than the GObject system IMO. GObject is abusing C for what C is not designed for. The result is an inefficient and complex system that makes C++98 look simpler.
See Gtkmm.
From gstring.h:
> Copyright 1988-90 by Robert B. Stout dba MicroFirm > * > * Released to public domain, 1991
However the header and readme state MIT license. You can't just release public domain code under a new license right?
> Copyright 1988-90 by Robert B. Stout dba MicroFirm > * > * Released to public domain, 1991
However the header and readme state MIT license. You can't just release public domain code under a new license right?
The point of public domain code is that you can do anything with it that you want. You could even release it under the GPL.
This is a derivative so yes, you can (just like you can release derivatives of MIT-licensed code[1] under a license that doesn't let the recipient receive the source code, or conversely under a copyleft license).
[1] "derivatives" include compiled binaries
[1] "derivatives" include compiled binaries
Genuine question - what do you think "public domain" means?
The problem is more that "public domain" is a concept that is not universally accepted in a legal sense, examples are Europe and in particular Germany. Case in point, Sqlite had to come up with a license agreement to make it usable for companies in those countries.
Public domain exists everywhere, but the concept of releasing something into the public domain by choice doesn't, so in those jurisdictions something only officially enters the public domain some time after either the author's death or publication.
The mental model of GPL cancer has infected the concept of public domain
I was expecting ghashtable to be the longest but what a surprise
None of the functions seem to be marked static or inline. Isn't this an instant link error once you include the same header in two different translation units?
Static is probably safer in C (as opposed to C++), as it's valid to have a function with the same name defined as inline within one translation unit and elsewhere with external linkage. The compiler is then free to resolve the relevant function calls to either the inline version or the external version (!)
https://www.ibm.com/docs/en/ssw_ibm_i_73/rzarg/inline_linkag...
https://www.ibm.com/docs/en/ssw_ibm_i_73/rzarg/inline_linkag...
Seems like there’s #ifdef _CLIB_IMPL to control that, but I’m still missing the point.
The point is unwillingness to learn how to work with compiler toolchains and package repos, while imposing longer compile times on everyone.
Oh come on, stop with the FUD :)
Unlike typical C++ "header only" libraries, the compilation time increase for STB-style "single-file libraries" is completely negligible because the implementation is also just compiled once for the entire project. Skipping an ifdef-block happens at IO speed, and this is very fast (at least since we left floppy disks behind). We also don't advice people to not comment their code (skipping ifdef is about as fast as skipping comments), or use one-character variable and function names to speed up compilation ;)
Unlike typical C++ "header only" libraries, the compilation time increase for STB-style "single-file libraries" is completely negligible because the implementation is also just compiled once for the entire project. Skipping an ifdef-block happens at IO speed, and this is very fast (at least since we left floppy disks behind). We also don't advice people to not comment their code (skipping ifdef is about as fast as skipping comments), or use one-character variable and function names to speed up compilation ;)
Back in 1999, I used to wait 1h for building our CRM server solution from scratch, written in a mix of C and TCL, while taking advantage of ClearMake optimization to share object files across the build machines.
So nope, no FUD, rather production deployment experience.
Naturally if one plans to use C as a scripting language, in tiny projects, maybe it doesn't matter.
So nope, no FUD, rather production deployment experience.
Naturally if one plans to use C as a scripting language, in tiny projects, maybe it doesn't matter.
Could you show an example of how you'd package this code?
Just like any classic C library with a static and dynamic versions, using pkg-config to get the respective build flags.
If targeting only Linux workloads, use something like checkinstall to generate all lib-xyz-dev flavours.
If targeting cross platform, settle on one of conan, vcpkg or cmake packages.
If targeting only Linux workloads, use something like checkinstall to generate all lib-xyz-dev flavours.
If targeting cross platform, settle on one of conan, vcpkg or cmake packages.
> Just like any classic C library ... using pkg-config
A "classic C library" cannot assume the existence of pkg-config (or checkinstall, or conan, or vcpkg, or cmake).
It can assume the existence of the preprocessor.
A "classic C library" cannot assume the existence of pkg-config (or checkinstall, or conan, or vcpkg, or cmake).
It can assume the existence of the preprocessor.
Read the rest of the comment...
EDIT: Since you modified the comment, it can at very least assume the existence of a compiler, a linker and make.
EDIT: Since you modified the comment, it can at very least assume the existence of a compiler, a linker and make.
No, it really can't. Nothing about C guarantees that the implementation will provide `make` (let alone that it will be POSIX make or BSD make or GNU make). Nor (going back to your original comment: https://news.ycombinator.com/item?id=34244452) is it guaranteed to be hosted on a system with dynamic linking.
The OP library has a design such that the only build system you need is the preprocessor. That is a perfectly valid design decision, especially since this is explicitly for lighter-weight uses where GLib would be excessive. If you don't like it, don't use it.
EDIT: Clang, for example, does not provide a `make` or guarantee a `make` exists. It will happily co-exist with BSD make on FreeBSD, or GNU make on macOS, or no make at all. I assume you concede on pkg-config, etc.
The OP library has a design such that the only build system you need is the preprocessor. That is a perfectly valid design decision, especially since this is explicitly for lighter-weight uses where GLib would be excessive. If you don't like it, don't use it.
EDIT: Clang, for example, does not provide a `make` or guarantee a `make` exists. It will happily co-exist with BSD make on FreeBSD, or GNU make on macOS, or no make at all. I assume you concede on pkg-config, etc.
Who said dynamic linking is required?!?
Another example of lack of understanding how C toolchains work.
Since the days C got outside Bell Labs, every single C compiler has provided those tools.
Ignoring 50 years of C practices in library distribution is exactly that, willful ignorance.
Another example of lack of understanding how C toolchains work.
Since the days C got outside Bell Labs, every single C compiler has provided those tools.
Ignoring 50 years of C practices in library distribution is exactly that, willful ignorance.
[deleted]
[deleted]
[deleted]
https://github.com/ocornut/imgui
> Just copy paste the source code in your source tree
And/Or make a simple CMakeLists.txt like this:
> Just copy paste the source code in your source tree
And/Or make a simple CMakeLists.txt like this:
cmake_minimum_required(VERSION 3.22.0)
project(mylib C)
file(GLOB_RECURSE MYLIB_SOURCES
${PROJECT_SOURCE_DIR}/src/*.c
)
file(GLOB_RECURSE MYLIB_HEADERS
${PROJECT_SOURCE_DIR}/include/*.h
)
add_library(${PROJECT_NAME} STATIC ${MYLIB_HEADERS} ${MYLIB_SOURCES})
target_include_directories(${PROJECT_NAME}
PRIVATE "${PROJECT_SOURCE_DIR}/include"
)That's not really any different from how this library works. CLib just chooses to avoid the necessity of adding additional makefile definitions at the cost of making the preprocessor fast-forward through a bit more text. As it's a small amount of text anyway, I doubt it makes much difference. (The implementation code only gets parsed once, even though it's scanned multiple times by the preprocessor.) It seems a bit unfair to assume that the author has an "unwillingness to learn how to work with compiler toolchains and package repos" just because they chose to make this tradeoff.
Also, OP referred to 'package repos', which obviously aren't involved in your example.
Also, OP referred to 'package repos', which obviously aren't involved in your example.
If the library was instead giving me a .c and a .h, I would simply need to make that CMakeLists.txt, or a Makefile, or whatever build system I use.
Now, the library gives me a .h and a `#define XXX_IMPL`. So to avoid making the mistake of defining XXX_IMPL more than once, I'll have to make a .c myself for each file of the library, and then I still need to write the CMakeLists.txt/Makefile/whatever.
This is just wasting my time for no good reasons.
I linked the imgui repository because that's what they do, they give you the .c/.h and you decide how to build it. You just have to copy/paste, no time wasted.
Now, the library gives me a .h and a `#define XXX_IMPL`. So to avoid making the mistake of defining XXX_IMPL more than once, I'll have to make a .c myself for each file of the library, and then I still need to write the CMakeLists.txt/Makefile/whatever.
This is just wasting my time for no good reasons.
I linked the imgui repository because that's what they do, they give you the .c/.h and you decide how to build it. You just have to copy/paste, no time wasted.
>So to avoid making the mistake of defining XXX_IMPL more than once, I'll have to make a .c myself for each file of the library
Hmm, I guess I wouldn’t worry about that and I’d just define XXX_IMPL in one of the existing .c files that imports the library. If I make a mistake and define it in multiple .c files then I’ll get an easy-to-interpret compiler error and it will be easy to fix the mistake. I imagine that’s how the author envisions the library being used. But yeah, if you don’t want to do it that way for some reason, then there’s no particular advantage to this style. I’m not sure that I hate it any more than I hate every other hack for distributing C libraries in the absence of a proper module system.
Hmm, I guess I wouldn’t worry about that and I’d just define XXX_IMPL in one of the existing .c files that imports the library. If I make a mistake and define it in multiple .c files then I’ll get an easy-to-interpret compiler error and it will be easy to fix the mistake. I imagine that’s how the author envisions the library being used. But yeah, if you don’t want to do it that way for some reason, then there’s no particular advantage to this style. I’m not sure that I hate it any more than I hate every other hack for distributing C libraries in the absence of a proper module system.
Using file globbing isn't exactly 'best practice' for CMakeLists.txt files though ;)
(and while Dear ImGui is definitely manageable, you still need to decide what to include in your project, for instance: why add imgui_demo.cpp)
(and while Dear ImGui is definitely manageable, you still need to decide what to include in your project, for instance: why add imgui_demo.cpp)
You only #define _CLIB_IMPL 1 in one translation unit when compiling.
Ah, OK. I was confused how that was supposedly used. I suppose header-only C libs are somewhat a pain, as inline does not work the same way as in C++, and static may cause code bloat when inlining does not happen in multiple TUs.
C Header-only libraries do not exist. It's just `#include "foo.c"` with extra steps.
`#include "foo.c"` would repeat the implementation in multiple object files which is both wasteful and, if not using static, create conflicting symbols.
You missed the "with extra steps".
#define XXX_IMPL
#include "xxx.h"
And #include "xxx.c"
Are exactly the same trash.No they are not, the former allows you to include the header file in multiple .c files, the latter does not.
Is it binary compatible with glib?
No, because AIUI, header only libraries don't really have an ABI. They get compiled into the application, as opposed to building a separate library that you linked against.
> header only libraries don't really have an ABI
This one is not really a header only library. It becomes a source file when you define a macro, to compile along your other source files.
If you build a shared library with it, it makes sense to set the default symbol visibility to be hidden, so it wouldn't clash with actual glib pulled in from some other shared library. But you can't easily do the same if you build a static library.
And if you expose any function with GHashTable, GList or GString in its signature, you really want them to be binary compatible with actual Glib. It's not true that header-only libraries don't have ABIs.
This one is not really a header only library. It becomes a source file when you define a macro, to compile along your other source files.
If you build a shared library with it, it makes sense to set the default symbol visibility to be hidden, so it wouldn't clash with actual glib pulled in from some other shared library. But you can't easily do the same if you build a static library.
And if you expose any function with GHashTable, GList or GString in its signature, you really want them to be binary compatible with actual Glib. It's not true that header-only libraries don't have ABIs.
It will simply force me to make a `.c` file for every `.h` and define it there.
Why not simply distribute the .c/.h like imgui does, or lua, or many many others? I'm already copy/pasting your .h files, I would not mind copy/pasting .c files and adding them to my build system, since that's what I'm gonna do anyway to make sure i do not define `XXX_IMPL` multiple times.
Header-only libraries truly only work in C++ when it's all templates anyway.