You are right in a library-demographical sense, but not in a fundamental sense. There is a 3rd way. Have a look at the CTL I linked to (downvoted..maybe I should have explained more?).
Once you give up the closed source/prebuilt binary library idea and embrace the C++-like header library idea and write implemenations in terms of "assumed macro/inline function" definitions, the problem becomes straightforward with no performance issue and different ergonomics issues than you probably think.
It's a more "manual instantiation" than C++ templates or generics in other languages where just refering to them works, but most of C is quite manual. So, it fits the headspace & the hard parts of data structures/meddlesome hands remain factored out. Since you parameterize your files/code with #define/#include, you have to name your parameters which can make the instantiating client code more obvious than C++ templates with many arguments. OTOH, there is no/poor type checking of these parameters.
I had a look, and it feels like template programming but with even worse guarantees.
Having a type declaration dependent on #define P whether it is plain old data or not, and needing to know what that means, is not the kind of ergonomics I'd want. That requires learning a whole new paradigm to ensure I am not doing wrong things.
In my mind it is so big an extension of the C language, that it leaves the C headspace and becomes its own headspace.
Yeah. It's not for everyone. I think "different ergonomic issues" may cover that and I did mention the type checking already. :-)
It is a smaller learning curve from pure C than "all of Rust" or even "all of C++/STL". You got the basic idea in short order (that may be for ill as well as good..I was never trying to make a normative claim).
It's doable, but not very easy.