Makefiles

General programming information

In this section, we are going to look at the things which make up a program, using the make utility which helps to compile programs. The last section discusses standardisation.

Building an executable

You need to understand the steps which are taken on the way from C source code to an executable. These are all done automatically by your compiler, but your compiler actually takes steps:

And an executable is the result.

The parts of a program

Suppose you write a program main.c which runs in the background and does stuff on regular times. This program is completely in the background and has no terminal to which it can write error messages. So you write a couple of functions which open and close a logfile and write error messages in this logfile.

The regular procedure is then to write a so-called header file. In this header file (which ends with .h), all functions are declared. The purpose of log.h is to let the compiler (not the linker) check whether you use the functions in log.c in the right manner. Besides that, it is a central area to put your structs and typedefs in. It is also used by commercial developers to protect their code. They give you the header files and the compiled library.

So now we have the files main.c, log.c and log.h. Let us have a look at their layout (contents are not important at this point):

 /* main.c */
 #include <stdio.h>
 #include %lt;stdlib.h>
 #include "log.h"
 int main(void)
 {
       log_open("logfile.log");
       do
       {
             /* do stuff */
             if (error == TRUE)
             {
                   log_write("error doing stuff");
             }
       }
       while(1);
       return 0;
 }
 /* log.c */
 #include <stdio.h>
 #include <stdlib.h>
 #include "log.h"
 
 void log_open(const char* logfile_name)
 {
       /* open the log */
 }
 void log_write(const char* error_message)
 {
       /* append the error message to the logfile */
 }
 void log_close()
 {
       /* close this logfile */
 }
 /* log.h */
 #ifndef _LOG_H_
 #def _LOG_H_
 void log_open(const char*);
 
 void log_write(const char*);
 
 void log_close(void);
 
 #endif
        
      

The precompiler directives (the commands starting with a #) make sure the header file is only included once. The first line says "if the macro _LOG_H_ is not defined, do everything below until we encounter the #endif". The second line defines this macro _LOG_H_.

Makefiles

The utility make its job is to make compiling and linking easier and smarter.

Suppose a large program consisting of several source files is compiled every time a change is made. That would be kind of a waste, because unchanged source files do not need to be recompiled.

This is where make comes in: when you run it, it looks at the so-called makefile. This file describes what should be compiled. And when recompiling, make checks whether the source file of each objectfile has been changed. If not, then it will skip the compiling work. In this way, large projects do not need to be recompiled every time, in this way saving resources.

Now we will look at the makefile, which is best described by looking at using make with the Server class (see the Sockets section). Below is the makefile of the Server class (the real makefile is not numbered, this is just for easy referencing):

 1) WARN = -Wall -Wstrict-prototypes
 2) FLAGS = -ansi -pedantic -O2 $(WARN) -D_REENTRANT
 3) CC = gcc
 4) LIBS = -lpthread
 5) 
 6) all:               testServer
 7) 
 8) testServer:        testServer.o SimpleServer.o Server.o
 9)	$(CC) -o testServer testServer.o SimpleServer.o Server.o $(LIBS) $(FLAGS)
 10)
 11) testServer.o:      testServer.cpp
 12)	$(CC) -c testServer.cpp $(FLAGS)
 13)
 14) SimpleServer.o:    SimpleServer.cpp
 15)	$(CC) -c SimpleServer.cpp $(FLAGS)
 16)
 17) Server.o:          Server.cpp
 18) 	$(CC) -c Server.cpp $(FLAGS)
 19)
 20) clean:
 21) 	echo Cleaning up files . . .
 22) # WATCH OUT WITH THIS LINE!!
 23) 	rm testServer *.o core *.bak *.aux *.log -f
        
      

The first four lines set some variables. This makes changing the makefile for another platform easy. In the first two lines, flags are defined for compiling. The third line defines the compiler. The fourth line defines whether any non-standard libraries should be included.

The lines after the variables are called rules. Each rule has two lines. The first line lists which should be done before the second line is executed. This list forms the dependencies. Let us jump ahead and look at line 8. Here it says that in order to execute line 9, testServer.o, SimpleServer.o and Server.o should be present. In other words, testServer is dependent on testServer.o, SimpleServer.o and Server.o.

make looks for a rule to make testServer.o and jumps to line 11. In words, it says here: "in order to make testServer.o, the file testServer.cpp should be present". Well, it is. So make executes the code on line 12. After that, the other two dependencies of line 8 should be done. So lines 15 and 18 are executed. Then, when all dependencies listed on line 8 are done, line 9 is executed.

Note line 6. When make is executed without any arguments, the first line is executed. Here it just says: testServer. This tells make to jump to line 8.

Now note line 20: no dependency is listed. When make clean is typed, line 21 is executed. Handy for cleaning core dumps and old object files!

Very important: every line which should be executed (lines 9, 12, 15, 18, 21 and 23 in the example above) must begin with the Tab character!

Standards

When portability is an issue, it is important to adhere to the two standards ANSI/ISO C and POSIX. If you want to use a certain library function, check your man page for a certain standard. When the function does not comply to a standard, make a wrapper function to make porting easy.

ANSI C, ISO C

What is called ANSI C, is committed by the American National Standards Institute in standard ANSI X3.159-1989. The International Standardization Organisation has copied that standard as ISO/IEC 9899. In 1995 and 1996, ISO added three documents containing minor additions and corrections. When the term ANSI C is used in this toolkit, we also take into account these three recent ISO additions. The ANSI C standard commits syntax, some standard libraries and the compiler is subject to certain demands (for example, what should be an error and what should raise a warning). The most important thing for the programmer is that he may not use identifiers that are used in the standard libraries. To compile according to all mentioned demands, the gcc compiler accepts the flags -ansi and -pedantic.

POSIX

The IEEE POSIX family of standards is a superset of the ANSI C standard. Besides ANSI C, it commits demands referring to the more low-level functions. POSIX is divided in two parts: POSIX.1 (IEEE std 1003.1-1990) describes the process environment, files and directories, system databases, terminal I/O and archive formats. The later 1996 version adds realtime extensions and the pthreads system API. POSIX.2 (IEEE std 1003.2-1992) is called the POSIX Shell and Utilities Standard; it talks about the shell functionality and more than one hundred utilities which should be present and conform to certain demands. POSIX.1 is very important for compatibility; if a system call is not POSIX compliant, most of the time it's best to use a POSIX alternative. (POSIX stands for Portable Operating System Interface for Computer Environments).

Resources