I just recently started learning c and having hard time wrapping my head around third party libraries.
Coming from Node/npm I’m used to the idea that your dependencies gets bundled in your app. But its not like that on C.
you expect your users to install your app dependencies for you, and you have to figure out the path of your own dependencies.
cause they don’t live in one nice place like node_modules. and I haven’t even began to figure out how Im supposed to know what linker flag to use for the library I use.
the tutorials just give it to you without explaining where they get it from.
and most of the sources I’ve read almost always recommends dynamic linking cause static is heavy. which brings me back to this article that now recommends static linking and I agree with that.
much of the learning materials I’ve come across are just spoon fed to you without explaining why. And its so incredibly frustrating until you look at linux history, most of the assume knowledge now is based on baggage upon baggage of layers of history.
> you expect your users to install your app dependencies for you
Yes, you do. And that is a good thing.
> and you have to figure out the path of your own dependencies.
No, you don't. Once a build dependency is installed, the compiler and the linker find it automatically.
All of this is easier than it sounds. For example, say your program depends on libpng. Then your users will have to "apt get install libpng" (or whatever your distro does). Then on your C files you just write #include <png.h>, on the call to the linker you will have -lpng and everything will work correctly.
The C ecosystem does not favor dependencies whose api changes every month, so this will work with whatever version of the libraries you have. Of course, you are also free to bundle all the dependencies that you want with your code. And you can easily distribute static executables that will work everywhere.
Compiler and linker flags are so annoying that people built tools to make developers lives easier. They aren’t amazing tools, but they get the job done. In this case, you would run `pkg-config -cflags libfoo` and `pkg-config -libs libfoo` and use the results when you invoke the compiler and linker.
Hmmm. I'd rather say that some people who package libraries are very annoying! If a library needs specific compiler flags, or linker flags beyond "-lfoo", this means that somebody is doing something wrong. Out of incompetence or out of malice.
Well, I dunno. libpng needs -lpng16 and -lz. Are you saying that libpng should have a staticly–linked copy of zlib inside of it, so that if your application needs both libpng and zlib you will end up with both? That’s probably unpalatable to most people.
Where? I'm compiling the same program with exactly the same makefile on several linux distributions, openbsd, freebsd and macos and "-lpng" is all it needs.
This is for dynamic linking. For static linking I need to add -lz explicitly, you are right about that.
Linux and other Unix-likes have a standard for this: if you want to link against libfoo, you pass `-lfoo`. The standard linker knows to expand that to some variant of libfoo, whether static or dynamic, with additional versioning, etc. If libfoo isn't in a standard library search path (e.g. it's being built in a `./vendor/` directory), then you need to additionally tell the linker that with `-Lvendor -lfoo`.
There's also a lot of silly memorization involved (e.g. remembering that zlib is usually packaged as `libz` so you need `-lz`, remembering that the non-SSL parts of OpenSSL are `-lcrypto`, etc.). C programmers tend to build mental scar tissue around these things and forget how pointlessly complicated and friction-inducing they are.
Yea, I have been learning C recently too. I have found the experience quite a lot more enjoyable than I thought I would. I was originally motivated so that I wouldn't be "scared" to involve myself in working with C FFI calls in the various other languages I use.
I've found it just as much fun as Scheme, my favorite language to work in. If you think of Scheme as the minimum-usable implementation of lambda calculus, then C is like the minimum-usable implementation of a Turing machine. They're kind of dual to one another in a very fun way.
The one thing I'm really struggling with, like you, is understanding the C "ecosystem". Anything past Makefiles is so complex, especially for my uses. Using something header-only library like STB or a simple library like Raylib is super straightforward, but figuring out what, when, and how to use things like pkg-config, Cmake, Meson, or other tools is a real headache. Does anyone have any favorite resources?
I'd also appreciate something more detailed about modern C style, when and why I'd need a custom allocator, or anything else you wouldn't see in K&R or other introduction.
PS A lot of people say things like "libc" sucks, you shouldn't be using XYZ function from libc. What should I be using instead? I'm all for learning via implementing things myself, but surely that's not the recommended route for everything in C world.
> Anything past Makefiles is so complex, especially for my uses. (...) to use things like pkg-config, Cmake, Meson, or other tools is a real headache. Does anyone have any favorite resources?
Yes. The GNU make documentation. Just don't use cmake and similar stuff. You can typically rewrite your complex, non-working Cmakelists as simple, straightforward Makefiles.
The GNU Make documentation is excellent, and you can get t a long way with handwritten Make files, but when you move beyond a few translation units that have different needs, it be becomes much easier to maintain things with CMake.
Choose cmake, learn it well, and it will make learning other systems much easier. If you jump from one build system to the next and just get a cursory understanding you will be hopping around forever.
Yes, this is crap, and not useful on modern systems. The only reason many claim to not think so is because they’ve been forced to figure it out, and now want others to as well.
It works in conjunction with your distribution package manager, so in Fedora I install the dependencies I need using 'dnf install foo-devel' or if the program is already known to Fedora I can install all the dependencies with 'dnf builddep program'
You can use pkg-config for that. E.g. if you want to compile with glib-2.0, you can run `pkg-config --cflags glib-2.0`: -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -pthread (E.g. for compiling to object code, but not linking yet)
Add `--libs` to link against it, too: -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -pthread -lglib-2.0
With meson that is basically `dependency('glib-2.0')` that you add to your executable/library.
> you expect your users to install your app dependencies for you, and you have to figure out the path of your own dependencies.
If you use a good buildsystem, it will find those dependencies automagically or you can e.g. use wrap dependencies: https://mesonbuild.com/Wrap-dependency-system-manual.html Those do automatically download the dependencies and compile them for you
> You can use pkg-config for that. E.g. if you want to compile with glib-2.0, you can run `pkg-config --cflags glib-2.0
That works sometimes, but is there a general way to find out what to pass to pkg-config besides consulting StackOverflow (or now perhaps ChatGPT)? For example, my recent history looked something like:
sudo apt install libopencv-dev
pkg-config --libs opencv # did not work
pkg-config --libs OpenCV # did not work
pkg-config --libs opencv-core # did not work
pkg-config --libs opencv45 # did not work
pkg-config --libs OpenCV45 # did not work
pkg-config --libs opencv4.5 # did not work
pkg-config --libs OpenCV4.5 # did not work
pkg-config --libs opencv-4.5 # did not work
pkg-config --libs OpenCV-4.5 # did not work
pkg-config --libs opencv-4 # did not work
pkg-config --libs OpenCV-4 # did not work
pkg-config --libs opencv4 # success!
At least for me, pkg-config has auto completion (Fedora), so typing e.g. `pkg-config open<TAB>` would give probably give me some results. But I agree that's one weakness of pkg-config, that you have to guess the name a bit
For now I’d skip worrying about managing dependencies. Just use system libraries or put your dependencies somewhere you can easily find them. Packaging and shipping code to users is a problem for after you are familiar with the language.
Anyway, the typical C program doesn’t pull in a ton of dependencies.
While this might be true, generic containers, sane strings, RAII and the possibility of not having to deal with pointers makes C++ an easier language to work with.
You certainly need to worry about much less things long as performance is not a priority.
>So, explain to me why every C++ program always has its own String implementation?
*saner :)
>Then C++ is a disastrous choice. Almost anything is better.
True, C and C++ are most of the time not the right tool for the job nowadays. But, if it's just a project that you're working on in your free time, it wouldn't harm to write it in your favorite language. I write in C++ a lot even if I could do the same thing much easier in other languages.
This is only the case, because the C standard library is quite small and has many bad parts. You can use something like STC [1] to even the playing field.
The last sentence doesn't really follow from the previous one. C++, like other higher level languages, has complexity built in because it takes care of things that in C you have to take care of yourself. I can't imagine any serious C++ project becoming "less complex" to work on by rewriting it in C.
I personally found it very relieving when I switched from C++ to C. (and I also rewrote some stuff, it did not get more complex.) I find readability of modern C code much better than C++. In C you see what you get: no templates, overloading, virtual inheritance, namespaces, exceptions, references etc you need to keep processing in your head just to understand what is going on.
I don't doubt that can be true - but there are plenty of common operations like concatenating strings or converting between strings/numbers that are a good deal simpler to write and understand in C++ than they are in C. In particular there are far more ways to get them wrong in C!
Almost any C library function that deals with strings is going to be more complex to use due to the lack of automatic memory handling, necessary for any use case where the max. size isn't sufficiently well-known at compile time for it to sit on the stack.
I'd accept something like "atoi" or "strtol" is pretty simple to use (and hard to get wrong), but hardly win any awards for obviousness. And the less said about the need (or at least strong tendency) to use sscanf for more complex parsing (hexadecimals etc.), the better.
I started doing this a bit myself too, but soon found that I missed RAII too much.
Having things clean themselves up when going out of scope is very convenient, and means that the early return code style that I favour is much easier to implement.
Your last sentence was that "there are fewer things you need to worry about in C". Which doesn't follow from c++ being a more complex/powerful language.
Yes, it does although we seem to be coming at this from different perspectives.
The C++ language spec is just vast. There are many footguns when doing apparently safe things. Different code bases use different subsets of the language. It is immensely powerful.
C, by contrast, is just a much simpler language than C++. I'm not entirely sure what you are disagreeing with.
My experience is you can get to where you think you know C in a few days. But it may take decades to become aware of all its footguns, and much more time to learn how to program C in a way that avoids them -- if that can be done by humans at all. (Maybe our new AI overlords will figure it out.)
Nuke the site from orbit. It's the only way to be sure. Deprecate C for all greenfield development, and launch a big-science push to rewrite all the extant load-bearing C code in something else with safety guarantees (Rust=good, SPARK=better). If null pointers were a billion dollar mistake, then letting C become ubiquitous was a mistake that runs in the trillions.
i respect your opinion, but for example you can do some very common tasks using std::string that will be really difficult in c. and much of programming is dealing with strings.
I found it very easy. I was writing production C code in a few days. (Of course, I was an experienced asm programmer before that, and C was a natural.)
I write code very differently today. But that's not learning C, it's learning technique.
ok. a small challenge - read character input of undefined length, and then print the input out backwards. i guarantee the c++ code will be easier to write and understand than any c code.
It's unclear exactly what you mean by "backwards" (and some interpretations like "print UTF-8 characters in reverse order" are infamously literally impossible[0]), but here's a straightforward/naive version (bytes in each line):
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sysexits.h>
#define die(E,...) ({ \
fprintf(stderr,__VA_ARGS__); \
exit(E); })
void reverse(char* a,size_t z) {
for(; z>=2 ;a++,z-=2) {
char t=*a; *a=a[z-1]; a[z-1]=t; } }
int main(void) {
size_t z=16,i=0;
char *a=malloc(z), *p=0;
for(;;) {
for(;;) {
ssize_t n = read(0,a+i,z-i);
if(n<0) die(EX_IOERR,"input error: %m");
if(n+i==0) exit(0);
if(n==0) die(EX_DATAERR,"error: %zu bytes of garbage at end of file",i);
p = memchr(a,'\n', (i += n) );
if(p) break;
if(i > z/2) {
a = realloc(a, (z = z+z/2) );
if(!a) die(EX_OSERR,"realloc(%zu bytes): %m",z); } }
while(p) {
reverse(a,p-a);
fwrite(a,p-a+1,1,stdout);
memmove(a,p+1, (i -= p-a+1) );
p = memchr(a,'\n',i); } } }
> i guarantee the c++ code will be easier to write and understand than any c code.
Put up or shut up, as the saying goes. At the very least it ought to be easy to abstract out some of the memory allocation and file handling to use C++ builtins, and I'd be interested to see what other improvements you can manage.
Edit: 0: Exercise for the reader: consider the input "abXc", where 'X' is some as-yet-undefined codepoint that will, at some point in the future, be defined as either a combining diacritic, or a standalone character. If 'X' is a character, the output should be "cXba", but if it's a diacritic, the output should be "cbXa". The information about which (mutually-exclusive) output is correct does not exist yet.
You could check for ferror when getchar returns EOF. Otherwise very nice. I wrote almost exactly the same code before I saw yours. The only difference was that I used multiplication by 2 instead of left shift since the goal was easier understanding. gcc can perform this optimization (for unsigned integers).
Of course all bets are off when someone tries to reverse emoji soup with this, but I am not knowledgeable enough about this topic to claim whether a general solution is even possible.
> but I am not knowledgeable enough about this topic to claim whether a general solution is even possible.
The correct way to reverse a Unicode string is to blit it back to front.
Addendum: a real C programmer helped me write (i.e., wrote most of) the following program:
#include <skalibs/skalibs.h>
int main() {
stralloc sa = STRALLOC_ZERO;
slurp(&sa, 0);
stralloc_reverse(&sa);
if (allwrite(1, sa.s, sa.len) < sa.len) {
strerror_diefusys(1, "output everything");
}
}
In the event of an allocation failure, this program will output nothing; other than that, it's the same as my C program above. Perhaps the problem isn't C, but its standard library?
or in c++, simply call getline? i haven't traced thru your code, but i would not be surprised if there were errors in it, having written similar stuff myself.
compare with using std::string and std::getline - easy to use and battle-hardened.
How does getline handle the out-of-memory condition? How do I avoid leaking memory – is that handled by std::string's RAII destructor? To detect the end-of-file condition, there's something about a failbit, but I'm only four pages of documentation in, so I don't know what that is yet.
What would the equivalent code look like in C++, handling EOF and OOM conditions appropriately?
if memory is exhausted, then getline throws a bad_alloc exception. if end of fileis reached (which i am not sure your code deals with correctly) then getline returns a stream in a bad state. so something in outline like this (syntactically correct. std:: omitted for brevity, but probably not what you would really write):
string input;
try {
if ( cin.getline ( input ) ) {
// ok read input, do something with it
}
else {
// some sort of predictable error - look at state of cin to diagnose EOF
}
}
catch (...) {
/// something terrible happened - as hard to recover as it would be in c
}
That actually sounds like a fun way to explore different programming languages. Especially if you extend it to inputs that may take up more memory then you've got on your computer (let's say 100GB) and at least reasonably fast.
I suppose it'd kinda trivial if you read the input from a regular (seekable) file, but if you actually read the 100 GB from stdin then this could get quite complex.
finding good teaching examples is hard - here's one i prepared earlier https://latedev.wordpress.com/2011/07/28/writing-a-real-c-pr... but could not complete as i was struck down with terrible depression, and then my mum & dad needed looking after, and then......
perhaps i will start it up again, now it is just me, and not exactly happy, but not depressed.
> i guarantee the c++ code will be easier to write and understand than any c code.
That will depend on how you define "understand". For a superficial level of understanding, you might be right. But the C++ code will make use of many concepts that would take a while to explain, for example references, classes, inheritance, templates, operator overloading, STL, and probably a bunch of other things I forgot.
Coming from Node/npm I’m used to the idea that your dependencies gets bundled in your app. But its not like that on C.
you expect your users to install your app dependencies for you, and you have to figure out the path of your own dependencies.
cause they don’t live in one nice place like node_modules. and I haven’t even began to figure out how Im supposed to know what linker flag to use for the library I use.
the tutorials just give it to you without explaining where they get it from.
and most of the sources I’ve read almost always recommends dynamic linking cause static is heavy. which brings me back to this article that now recommends static linking and I agree with that.
much of the learning materials I’ve come across are just spoon fed to you without explaining why. And its so incredibly frustrating until you look at linux history, most of the assume knowledge now is based on baggage upon baggage of layers of history.