Modules, Separate Compilation, Make Files


  1. Modularizing a Program:

    Let's look at a program that adjusts grades.

    There are some parts of the program that are generic and could be separated out for possible reused.

    What are the parts that could be separated out?

  2. Broken Up Version:

    When we separate a set of code into a self-contained entity, we call this a module.


    Note: A module is also sometimes called a library, although the term library is used more for code that is in an already-compiled form.

    To create a C module for the grade functions, we need to create 2 new source code files: a .h file and a .c file. It is typical for the base name of those files to be the same, and to relate to the functionality of the module. Thus, we'll name them:

    grades.h grades.c
    Contains stuff people calling the grade functions need to know. Contains stuff people writing the grade functions need to know.

    Since this module only contains the grade functions, we also need a file for the main program--let's call it adjust.c.

    We've already broken our original program up into these files and made them available for you to download. There is also a copy of a data file (for testing) and a make file that we'll use shortly.

    Let's look at each file and discuss them in more detail...

    grades.h
    This is the interface to the module. People who use the module (i.e., in a main program or another module) will have to include this header file. It should only contain the public information necessary for people to use the module--mainly, this means it contains functional prototypes (for those functions in the module that users of the module are allowed to access). It may also contain things like constants and type definitions.
    grades.c
    This is the implementation of the module. Those who use the grades module must link their code with this code. It contains all the internals of the module, such as the definitions (i.e., bodies) of all the functions.
    adjust.c
    The main program and user of the grades module. Since it relies on the grades module, it includes the grades header:
    #include "grades.h"
    


    Note: When including a non-standard header file (like one you wrote), list its name in double quotes (like above). When including system header files, list their names in angle brackets, as in:
    #include <stdio.h>
    

    How does the implementation file grades.c get prototypes for PrintGrades() and AdjustGrades()?

  3. Wrapping Header Files:

    One thing is missing from our new grades module...The header should be wrapped to prevent multiple inclusion.

    Header files, like our header for the grades module (grades.h), include things like: constants, new types, and functional prototypes. This code should only be included into a .c file once; otherwise, the compiler may complain and refuse to compile the .c file.

    You may think that including the code from a header twice would be unlikely, in that we would rarely mistakenly do the following:

    #include "grades.h"
    ...
    #include "grades.h"
    

    However, multiple inclusion is often likely in the following form:

    #include "grades.h"
    ...
    #include "final.h"
    

    where final.h includes grades.h. In this case, you might get some compiler error like:

    In file included from somefile.c:17:
    final.h:12: redefinition of `something'
    final.h:12: `something' previously declared here
    

    I.e., it did not like the fact that the same thing was defined twice.

    To solve the problem of multiple inclusion, we wrap the header by doing the following... Put the whole header inside an if-not-defined construct, like:

    #ifndef GRADES_H
    #define GRADES_H
    ...
    CONTENTS OF grades.h
    ...
    #endif /* not defined GRADES_H */
    
    where GRADES_H is unique to the file grades.h.


    Aside: A typical practice to get a unique symbol (e.g., GRADES_H) is to take the file name of the header (e.g., grades.h) and:

    This usually gives a unique symbol.


    Being wrapped, the first time grades.h is included:

    #include "grades.h"  /* GRADES_H not defined, but once this */
                         /* include is done it will be.         */
    
    the symbol GRADES_H will not be defined and the code in the header will be compiled. Later, if the header is included again (directly or indirectly):
    ...
    #include "final.h"  /* This tries to include grades.h. */
    
    in the same source file, then the code will be skipped since GRADES_H is already defined (i.e., it got defined the first time we included grades.h).

    Go ahead and wrap grades.h (i.e., edit grades.h).

  4. Compiling a program made up of multiple parts:

    Now we would like to compile and link our program, which is made up of multiple files (header (.h) and source (.c) files).

    We can use gcc as usual, and just need to specify the source code files that make up the program...

    gcc -ansi -pedantic -Wall -o adjust adjust.c grades.c
    


    Note: We DO NOT list the header files. Since they are included into source files, they will get compiled. The -o adjust tells it to give the executable the name adjust instead of a.out.

    Compiling produces a problem:

    /var/tmp/ccy8x5x_1.o: In function `main':
    /var/tmp/ccy8x5x_1.o(.text+0x1e0): undefined reference to `PrintGrades'
    /var/tmp/ccy8x5x_1.o(.text+0x21c): undefined reference to `PrintGrades'
    

    It is complaining that there is something called PrintGrades() (used in main()) that is not defined. What kind of error is this?

    Fix the error by correcting the function name in grades.c. Then, recompile:

    gcc -ansi -pedantic -Wall -o adjust adjust.c grades.c
    

    We now have an executable adjust that can be run on the data file data1.

    adjust < data1
    

    It should run fine.


    Notice that after we made a change to the source code in adjust.c, we ended up recompiling everything with...

    gcc -ansi -pedantic -Wall -o adjust adjust.c grades.c
    

    This is wasteful because nothing in adjust.c changed, only grades.c, so why should adjust.c be recompiled?!

  5. Compiling source code separately:

    What we should have been doing instead is compiling source code to intermediate object files and then linking them together.

    First, recall that the program is made up of 3 source files:

    We want to compile each piece of the program separately and then link them together. This will allow us to only regenerate the parts of the program that are affected when we change some source code.

    When we just compile source code (without linking it together), it means that we take the .c files and generate intermediate object (.o) files.

    To just compile source code, use the -c flag with the compiler...

    gcc -ansi -pedantic -Wall -c adjust.c
    gcc -ansi -pedantic -Wall -c grades.c
    

    This will generate the object files adjust.o (for adjust.c) and grades.o (for grades.c). List your files to see the intermediate object (.o) files.

    Finally, to link the object files (.o) into an executable that we can run, we use the compiler again (although this time it will just pass the .o files on to the linking stage):

    gcc -ansi -pedantic -Wall -o adjust adjust.o grades.o
    

    Remember, we use of the -o flags to tell it to call the executable adjust instead of a.out.

  6. Dependency chart:

    Since we have just compiled the executable adjust separately on the commandline, we know exactly what commands are needed to generate the pieces that make up the executable.

    We would like to know under what circumstances we'll have to regenerate each of those pieces. For example, if any of the object (.o) files get changed, then they have to be relinked into a new executable. Nonetheless, that's not the only case.

    Let's start from our goal (i.e., to have an executable named adjust) and draw a dependency chart...

    The executable adjust is made up of the 2 object files, adjust.o and grades.o. Thus, adjust depends on those 2 files. To generate adjust from those files, we link them together.

            adjust
           /       \
         /    link   \
       /               \
    adjust.o       grades.o
    

    Next, adjust.o depends on adjust.c and grades.o depends on grades.c. You generate these object files by compiling the corresponding .c files.

            adjust
           /       \
         /    link   \
       /               \
    adjust.o       grades.o
      |                 |
      |     compile     |
      |                 |
    adjust.c       grades.c
    

    Finally, the 2 source code files depend on grades.h since they both include that file. This, however, is a slightly different dependency since the .c don't need to be generated like the .o files and the executable.

            adjust
           /       \
         /    link   \
       /               \
    adjust.o       grades.o
      |                 |
      |     compile     |
      |                 |
    adjust.c       grades.c
       \               /
         \  include  /
           \       /
           grades.h
    


    We've just listed all the dependencies for the executable adjust. The executable depends on some things directly (like the .o files) and some things indirectly (i.e., you need the .c files to make the .o files to make the executable).

    Here is the complete dependency chart...

    DEPENDENCY CHART FOR adjust EXECUTABLE
    
            adjust
           /       \
         /    link   \
       /               \
    adjust.o       grades.o
      |                 |
      |     compile     |
      |                 |
    adjust.c       grades.c
       \               /
         \  include  /
           \       /
           grades.h
    
    Dependencies go downward.
    How to "get" or generate files goes upward.
    

    With such a dependency chart, we can answer questions about what we need to regenerate if we change certain files.

    For example, suppose we change the file adjust.c... What has to be regenerated?

    Answer: First, recompile adjust.c to get adjust.o. Then, relink all the object files together again to get the executable adjust.

    Now, suppose we changed grades.h?


    Note that when either a .c file changes or a header file included by a .c file changes, we have to regenerate the corresponding .o file.

  7. Why Make Utility:

    Since it is tedious to recompile pieces of a program when something changes, people often use the make utility instead.

    Make needs a make file that encodes both the dependencies between files and the commands needed to generate files.

    When you run the make utility, it examines the modification times of files and determines what needs to be regenerated. Files that are older than the files they depend on must be regenerated. Regenerating one file may cause others to become old, so that several files end up being regenerated.


    Aside: Note the difference between the make utility (a program you run) and a make file (that tells the make utility how to compile/link a specific program).

    A make file may either be named Makefile (first letter uppercase) or makefile (all lowercase). We will prefer the uppercase form, Makefile.


    Aside: Make files can have other file names, but then you'll have to tell the make utility what the name of the make file is (using a commandline option).

    It makes things much easier if the make file is in the same directory as the source code files. You can create a make file in any text editor.

    Let's look at the make file for the executable adjust.

  8. Writing a Make File:

    For simple make files, there will be at least 2 parts:

    Keep in mind that make files use their own language, so DON'T expect things in them to look like C.

    Make files should have comments so that they are more readable. In make files, any line starting with a pound sign (#) is a comment, like:

    # Makefile for executable adjust
    

    Variables

    The first variable is for the compiler to use...
    CC = gcc
    

    In general, the form for setting a variable is:

    VARNAME = value
    

    (The space around the = is optional for our version of make, but adds to readability).

    The second variable says what flags to pass to the C compiler...

    CFLAGS = -ansi -pedantic -Wall -g
    

    Here, the variable includes the flag that adds debugging information.

    The 2 variables, CC and CFLAGS, are the ones that our version of make expects to be set for the C compiler and flags to the C compiler, respectively. You can also use your own variables, but make has a set of standard variables, like these, that it uses.

    In the next section, CC and CFLAGS are used in commands needed to compile/link certain files.


    NOTE: Make actually knows how to compile some things automatically, though it needs variables like CC to figure out which compiler to use. We ignore these automatic features and write these commands ourselves to help in learning make.

    Targets

    Now that we have the variables we need, we can deal with the targets, which are files that must be generated.

    For each target, there are typically 1 or 2 lines in a make file. Those lines specify:

    1. its dependencies (easy to determine from a dependency chart)
    2. and possibly a command to generate the target (easy to determine from knowledge of separate compilation).

    By default, the first target in the make file is the one that gets generated when you just say "make" at the commandline, so it should be the name of the executable, in our case, adjust.

    Target: Executable

    Let's examine the first target in our make file (i.e., for adjust)...

    For a target's dependency line, the target file name should be listed, followed by a colon (:) and the files it depends on:

    adjust: adjust.o grades.o
    

    For a target that is an executable, we just list the object files it depends on.

    On the next line, there should be a command that generates the target:

    <Tab>$(CC) $(CFLAGS) -o adjust adjust.o grades.o 
    


    Note: This line MUST start with a <Tab> (i.e., hit the Tab key)...it cannot start with a regular space!

    Note that the variables C and CFLAGS are used, which means that the command is really:

    gcc -ansi -pedantic -Wall -g -o adjust adjust.o grades.o
    


    Note: Using a variable in a make file involves using the dollar sign ($) and then the variable name in parentheses.

    Targets: Object Files

    Our next target is the object file, adjust.o.

    For a target that is an object file, we list all files it depends on. This means its corresponding .c file, plus any header files its .c file includes (i.e., include directly or indirectly, but not system header files like stdio.h that aren't going to change).

    So, the dependencies line looks like:

    adjust.o: adjust.c grades.h
    

    The command to generate this target is:

    <Tab>$(CC) $(CFLAGS) -c adjust.c
    

    Finally, the last object file, grades.o requires:

    grades.o: grades.c grades.h
    <Tab>$(CC) $(CFLAGS) -c grades.c 
    

    That's all for the make file!!! It just encodes the dependencies and the generation commands for 1 executable and its 2 object files. The complete make file looks like:

    # Makefile for executable adjust
    
    # *****************************************************
    # Parameters to control Makefile operation
    
    CC = gcc
    CFLAGS = -ansi -pedantic -Wall -g
    
    # ****************************************************
    # Entries to bring the executable up to date
    
    adjust: adjust.o grades.o
    <Tab>$(CC) $(CFLAGS) -o adjust adjust.o grades.o 
    
    adjust.o: adjust.c grades.h
    <Tab>$(CC) $(CFLAGS) -c adjust.c
    
    grades.o: grades.c grades.h
    <Tab>$(CC) $(CFLAGS) -c grades.c 
    


    Aside:
    In a make file, if we have to continue a line, we cannot just continue on the next line (like in C). We must end a line with a \ (backslash, not the forward slash) to tell make that the line continues. So, we could have written the last target as:
    grades.o: grades.c \
               grades.h
    <Tab>$(CC) \
            $(CFLAGS) -c \
    	grades.c 
    

    Only break lines where space would normally go.


    Also, we have done some extra work. Make already knows that a .o can depend on a corresponding .c file (among other things). It also knows how to compile a .c file into a .o (assuming the variables CC and CFLAGS have been set to the values you want--else it uses the default compiler). Make does not necessarily know which header files a .c file includes, however, so we needed to include that information ourselves. Therefore, the last target could have just been:

    grades.o: grades.h
    
    without any generation command needed after it.

    The reason we included the extra pieces of information is so that you learn how to write complete dependency and generation lines for targets.


  9. Using a Make File:

    Now that we have a make file that encodes the dependencies between the files that make up the executable, it is easy to compile that executable.

    Try using the make file we've given you. First, remove the old executable and object files:

    % rm adjust *.o
    
    Then, run make with the command:
    % make
    

    Remember that this will cause make to generate the first target in the make file, which is adjust.

    Since nothing has been compiled/linked, it will do:

    gcc -ansi -pedantic -Wall -g -c adjust.c
    gcc -ansi -pedantic -Wall -g -c grades.c 
    gcc -ansi -pedantic -Wall -g -o adjust adjust.o grades.o 
    

    I.e., compile the 2 .c files into object files, and then link the object files into an executable named adjust.

    (If you have trouble, make sure that the make file is named Makefile and is in the same directory as the source code files.)

    Now, type the make command again (let's explicitly tell it what target to generate this time) and you get:

    % make adjust
    make: `adjust' is up to date.
    

    Here, make tells you nothing has changed, so it has no work to do.

    Next, suppose we want the program to be able to handle at least 20 grades (instead of 10). Bring up adjust.c in an editor and change:

    #define MAX_GRADES  10
    
    so that it reads 20...
    #define MAX_GRADES  20
    

    After the change has been made, when we use make:

    % make
    gcc -ansi -pedantic -Wall -g -c adjust.c 
    gcc -ansi -pedantic -Wall -g -o adjust adjust.o grades.o 
    

    it doesn't bother regenerating grades.o, since the files it depends on have not changed.

    Finally, try changing grades.h and then re-making...

    % make
    gcc -ansi -pedantic -Wall -g -c adjust.c
    gcc -ansi -pedantic -Wall -g -c grades.c 
    gcc -ansi -pedantic -Wall -g -o adjust adjust.o grades.o 
    

    Since both .c files include that header, they both have to be recompiled and linked together.


BU CAS CS - Modules, Separate Compilation, Make Files
Copyright © 1993-2000 by Robert I. Pitts <rip at bu dot edu>. All Rights Reserved.