Last updated: 13 Oct 2019 (history since 12 Sep 2019 at end of document) The importance of correct C function prototypes and Makefile dependencies * Introduction Earlier today (when I originally wrote this) I was working on a small C project of mine and I ran into an odd problem. Typically when I create a new Makefile I add a 'make depend' to it; however, I forgot to do that for this one and in turn I ran into a seemingly really odd problem: unexpected, unpredictable results of a function call. It didn't take much time before I remembered that the file where the function (prototype and definition) change (that I had made) was called had not in fact been updated; this made it obvious I didn't implement 'make depend'. In turn, this inspired me to write about C function prototypes, problems with prototypes and Makefile dependencies. Do note that even if you do implement `make depend' if you forget to update it (that is run `make depend') when necessary you can run into this problem as well. As a bonus I have a special Makefile for when you have several different independent programs in the same directory but want to have a `make depend' for them all as well as running `make' to compile them all. * Function prototypes In C (this is a somewhat simplified explanation) you have function definitions (implementation) and also function prototypes (the header of the function which says what the return type is, the name of the function and what arguments it expects). The prototype allows for you to call the function before the compiler sees the actual definition (if it isn't found then you'll get a linker error and the build will fail). * K&R C (pre-ANSI) function prototypes limitations (return type, argument list) Originally, in K&R C, if you left off the return type the function would implicitly return 'int' as far as the compiler was concerned (I'm not 100% certain if the compiler would return whatever is returned as an int but I would guess it did). In addition, it was not permitted to specify arguments in the prototype (the function definition including the arguments had a different syntax also) which meant the compiler could not detect errors that is a mismatch between the prototype and the actual function. The following table explains this: Prototype Return type --------- ----------- function(); int char function(); char In the ANSI/C89/C90 standard the following holds true: if you wanted the compiler to check arguments for a function which takes no arguments (an empty list) you needed to specify 'void' as in: int function(void); (otherwise you can pass any number of arguments to 'function') * Compiler function prototype/definition can still happen Despite the improvements in function prototypes the problem still exists: I can think of two sets of conditions where a function can be called with the wrong parameters and return type (which needless to say is unpredictable which can be and should be considered disastrous). The following two files demonstrates the first condition: $ cat func.c #include int function(char *); int main() { int s = 0; s = function(NULL); printf("%d\n", s); return 0; } $ cat function.c void function(void) {} $ gcc -c func.c function.c $ gcc func.o function.o $ ./a.out 4195632 The problems: 'function' returns nothing (i.e. void return type) and takes no arguments (void); but in func.c (where it is called) it is prototyped to return 'int' and takes a 'char *'. I initialise 's' to 0 first so that it has an expected value initially; it might otherwise be said by some that of course 's' is garbage: you didn't initially initialise it to anything (originally I did not initialise it but that suggestion is in error: the code shouldn't compile at all except that the prototype told the compiler it does return an int when it actually doesn't; the same applies for parameters it takes because 'function' technically expects no parameters at all). After this I assign to 's' the result of 'function(NULL);' (which technically returns nothing but the compiler doesn't know this) and then print it only to get the result of '4195632'; despite the fact the function doesn't return any value at all 's' is assigned to a garbage int. I'm not sure the gcc option '-Wnested-externs' catches all cases but it does help resolve some. There is a similar source of this problem: if you change the function definition (and the optional prototype in the relevant header file) but do not compile (because the source file hasn't changed) the file that calls it and then link the final binary then it will happily be called as it was last defined. * The best solution: `make' dependencies Whilst `make' can do much more than building programs (from compiling the source code to objects to creating the final executable) it is a common example use. It is quite an advanced piece of software which allows for a lot of different possibilities. In a simple Makefile all the source code files are compiled each time; you can make it so only files that haven't been built (is the object file older than the source file? Clock skewing can indeed be a problem for make although I've not encountered that problem in a long time and it is easy enough to fix generally in any case). That functionality is extremely useful for large projects. But you can go a step further: what if you #include a file in your project and that header file has been modified? What happens to the files that #include it? Should they be rebuilt? Yes they should. All sorts of problems can happen if they don't; but if you don't compile them all or your Makefile doesn't make use of dependencies properly (or you forget to update them like I did) you can run into the problem described above. Makefiles can get pretty complex and although I do have a general way to generate dependencies I feel I would have to explain Makefiles in general and I'm not qualified to do so. But I can give one hint: gcc has the following options which generate the dependencies; how you implement it will vary and as I suggested already I won't go into that: -M Instead of outputting the result of preprocessing, output a rule suitable for make describing the dependencies of the main source file. The preprocessor outputs one make rule containing the object file name for that source file, a colon, and the names of all the included files, including those coming from -include or -imacros command-line options. Unless specified explicitly (with -MT or -MQ), the object file name consists of the name of the source file with any suffix replaced with object file suffix and with any leading directory parts removed. If there are many included files then the rule is split into several lines using \-newline. The rule has no commands. This option does not suppress the preprocessor's debug output, such as -dM. To avoid mixing such debug output with the dependency rules you should explicitly specify the dependency output file with -MF, or use an environment variable like DEPENDENCIES_OUTPUT. Debug output will still be sent to the regular output stream as normal. Passing -M to the driver implies -E, and suppresses warnings with an implicit -w. -MM Like -M but do not mention header files that are found in system header directories, nor header files that are included, directly or indirectly, from such a header. It so happens there are other related options but I feel that is sufficient for now. If you would like to look at the special Makefile I referred to you can find it here: https://texts.xexyl.net/docs/source/Makefile (I have comments in there explaining it as well as requesting that the header remains intact although you're free to to use it wherever and whenever; you probably shouldn't mess with the Makefile itself however unless you know what you're doing or you're doing some experiment). Furthermore as of 13 Oct 2019 there is a helper script that (re)generates .gitignore which can be found at: https://texts.xexyl.net/docs/source/make-gitignore.sh. # Important notes about the Makefile to remember Understand that it's very easy to make a Makefile that compiles all source files in the directory. The key difference to this Makefile is that it has a make dependency target and this is - as the article discussed - very important. Second thing to remember is that this will NOT work for any program that requires more than one source file in order to compile and link into a binary. Finally as discussed in the Makefile as well as the README.MAKE file this Makefile does NOT detect the correct linker options. To add these you have to add the options to the LDFLAGS in the Makefile (or specify at the make invocation). This might look like: make LDFLAGS="-lm" Or otherwise you can add set LDFLAGS in the Makefile to be -lm (you do not need quotes there - the only requirement is that it's on one line or the end of all but the last line are escaped with a \ or otherwise use the append feature to variables in Makefiles). You can download the tarball of all the relevant files at: https://texts.xexyl.net/docs/source/c-deps.tar.bz2 # Document history (since 12 Sep 2019) * 13 Oct 2019 - Automagic Makefile dependency target now (re)generates .gitignore file (uses helper script located at https://texts.xexyl.net/docs/source/make-gitignore.sh). - Add clarification about how it is very easy to make a Makefile that attempts to compile all source files in the directory but which will lead to problems as well the fact that it will NOT work for programs that require multiple object files. - Add some comments on library linking. * 12 Sep 2019 - Clean up Makefile so that features of make itself are used for compiling the individual source files as well as removing the binary files. Now works under macOS as well as Linux.