Chapter 3. C Language Projects

This chapter will show you how to use MakeKit to configure and build a C language project.

Building Programs

Programs are built using the mk_program function, which requires at a minimum a list of source files and a program name. The program name is passed as the PROGRAM parameter, and the list of sources files is passed as the SOURCES parameter. Consider Example 3.1, “Using mk_program for a simple "Hello, World" program hello, it's source file main.c, and the MakeKitBuild file to build it.

Example 3.1. Using mk_program

main.c
#include <stdio.h>

int main(int argc, char** argv)
{
    printf("Hello, world!\n");
}
MakeKitBuild
make()
{
    mk_program \
        PROGRAM=hello \
        SOURCES="main.c"
}

By default, the program will be installed in the usual binary directory (e.g. /usr/local/bin). The install location can be changed with the INSTALLDIR parameter to mk_program.

Finding Header Files

A C project with a complex layout will typically need to look for headers from multiple directories within the source tree. Using the INCLUDEDIRS parameter to mk_program, you can specify a list of directories where the compiler should look for header files. Adding a header file include/values.h to our small example project and modifying MakeKitBuild to find it yields Example 3.2, “Using INCLUDEDIRS.

Example 3.2. Using INCLUDEDIRS

include/values.h
#define MESSAGE "Hello, World!"
main.c
#include <stdio.h>
#include "values.h"

int main(int argc, char** argv)
{
    printf("%s\n", MESSAGE);
}
MakeKitBuild
make()
{
    mk_program \
        PROGRAM=hello \
        SOURCES="main.c" \
        INCLUDEDIRS="include"
}

Linking To Libraries

To link your program to other libraries, specify them as a list via the LIBDEPS parameter. Each library should be specified exactly as you would with -l to the C compiler (without the lib prefix). See Example 3.3, “Using LIBDEPS, which links to libm to use the sqrt() function.

Example 3.3. Using LIBDEPS

main.c
#include <stdio.h>
#include <math.h>
#include "values.h"

int main(int argc, char** argv)
{
    printf("%s\n", MESSAGE);
    printf("The square root of 2.0 is %f\n", sqrt(2.0));
}
MakeKitBuild
make()
{
    mk_program \
        PROGRAM=hello \
        SOURCES="main.c" \
        INCLUDEDIRS="include" \
        LIBDEPS="m"
}

Building Libraries

MakeKit allows you to build two types of libraries:

Shared libraries

A shared library is a collection of address-relocatable code which is combined with the program that links against it at startup by the system dynamic linker. Shared libraries are built with the mk_library function.

Dynamically loadable objects

A dynamically loadable object is a blob of code which is explicitly loaded and executed by a program through functions like dlopen(). Dynamically loadable objects are typically used to implement plugins. Although on many UNIX systems dynamically loadable objects and shared libraries are the same type of file, some systems distinguish between the two. For example, Mac OS X distinguishes between .dylib (shared libraries) and .bundle or .so (dynamically loadable objects). To build a dynamically loadable object, use the mk_dlo function, which takes nearly identical parameters to mk_library.

As with building programs, building a library requires a library name, passed as the LIB parameter, and a list of sources, passed as the SOURCES parameter. The library name should be specified without a filename extension or the lib prefix. Consider Example 3.4, “Using mk_library, which expands on previous examples by building a small library and linking it into the program hello.

Example 3.4. Using mk_library

include/example.h
external unsigned long factorial(unsigned long i);
main.c
#include <stdio.h>
#include <math.h>
#include "values.h"
#include "example.h"

int main(int argc, char** argv)
{
    printf("%s\n", MESSAGE);
    printf("The square root of 2.0 is %f\n", sqrt(2.0));
    printf("5! is %lu\n", factorial(5));
}
example.c
#include "example.h"

unsigned long factorial(unsigned long i)
{
    return i == 1 ? 1 : i * factorial(i - 1);
}
MakeKitBuild
make()
{
    mk_library \
        LIB=example \
        SOURCES="example.c" \
        INCLUDEDIRS="include"

    mk_program \
        PROGRAM=hello \
	SOURCES="main.c" \
        INCLUDEDIRS="include" \
        LIBDEPS="m example"
}

As you can see, mk_library accepts many of the same parameters as mk_program, including INCLUDEDIRS. As with programs, libraries can specify dependencies on other shared libraries with LIBDEPS.

Note

Some systems do not allow inter-dependencies between shared libraries. On those systems, MakeKit will propagate dependencies forward to any program that links against them. The resulting program may explicitly depend on libraries from which it consumes no symbols.

Our new library was linked into hello the same way as the system library libm. Note that the order of the two function invocations in MakeKitBuild is significant. Because mk_program was called after mk_library, it knows that libexample is a library we build ourselves rather than pre-existing on the system, and it adds appropriate dependencies to the generated Makefile.

Groups

When a library or program grows large enough, it is often prudent to break its source code up across multiple subdirectories to enhance organizational clarity and separation of concerns. Rather than maintaining a long list of source files from disparate subdirectories in your invocation of mk_library or mk_program, you can use the mk_group function to group together a set of source files and compiler flags into a logical unit which can then be added in to your program or library. This feature is an analogue of "convenience libraries" available in GNU libtool.

Imagine that the currently diminuitive libexample is destined to become a vast utility library with both math and string manipulation functions. In anticipation of this, we break it into two subdirectories, math and string. We will build each subdirectory as an object group and then combine the two into libexample using the GROUPS parameter to mk_library. This yields Example 3.5, “Using mk_group.

Example 3.5. Using mk_group

include/example.h
/* Math functions */
external unsigned long factorial(unsigned long i);
/* String functions */
external void reverse_string(char* str);
math/factorial.c
#include "example.h"

unsigned long factorial(unsigned long i)
{
    return i == 1 ? 1 : i * factorial(i - 1);
}
math/MakeKitBuild
make()
{
    mk_group \
        GROUP=math \
        SOURCES="*.c" \
        INCLUDEDIRS="../include"
}
string/reverse.c
#include <string.h>
#include "example.h"

unsigned void reverse_string(char* str)
{
    char* front = NULL;
    char* back = NULL;
    char tmp = 0;

    for (front = str, back = str + strlen(str) - 1;
         back > front;
         front++, back--)
    {
        tmp = *front;
        *front = *back;
        *back = tmp;
    }
}
string/MakeKitBuild
make()
{
    mk_group \
        GROUP=string \
        SOURCES="*.c" \
        INCLUDEDIRS="../include"
}
main.c
#include <stdio.h>
#include <math.h>
#include "values.h"
#include "example.h"

int main(int argc, char** argv)
{
    char reverse[] = MESSAGE;

    reverse_string(reverse);

    printf("%s\n", MESSAGE);
    printf("Reversed: %s\n", reverse);
    printf("The square root of 2.0 is %f\n", sqrt(2.0));
    printf("5! is %lu\n", factorial(5));
}
MakeKitBuild
SUBDIRS="math string"

make()
{
    mk_library \
        LIB=example \
        GROUPS="math/math string/string"

    mk_program \
        PROGRAM=hello \
	SOURCES="main.c" \
        INCLUDEDIRS="include" \
        LIBDEPS="m example"
}

The mk_group function accepts nearly the same set of optional parameters as mk_library; in fact, you can even use GROUPDEPS to combine object groups together hierachically.

This particular example also demonstrates a useful shortcut enabled by keeping source files neatly separated into subdirectories: filename patterns. The invocations of mk_group in the math and string subdirectories can include all C source files with *.c. In general, MakeKit supports filename patterns whenever specifying source files.

Important

Filename patterns only work against files that exist at the time that configure is run. Files that are created by a build rule will not be included.

Pre-compiled Headers

Pre-compiled headers help reduce build times for large projects by avoiding parsing the same header files multiple times. C++ templates in particular tend to result in large header files, and pre-compiling them can be a substantial win for build times. MakeKit makes it easy to use pre-compiled headers with both gcc and clang.

The easiest way to use a pre-compiled header is by passing PCH=header to mk_program, mk_library, mk_dlo, or mk_group. The pre-compiled header will be included before all other headers via the -include parameter to the compiler. If header is also included by source files, it should use a standard include guard.

Pre-compiled headers are highly sensitive to language (C or C++) and various compiler settings (e.g. PIC versus non-PIC code). Using this method will ensure that the pre-compiled header is built with compatible settings and for the correct language, but does not allow sharing the result between build targets.

You can use mk_pch to produce an explicit pre-compiled header. Capture the value of result and pass it as PCH to subsequent build targets. It is up to you to ensure that it is built with compatible settings. The following non-exhaustive list of parameters can affect the result:

PIC

Must be no (the default) for programs and static libraries, and yes for libraries and dynamically-loadable objects.

COMPILER

Since many header extensions are ambiguous, you should manually specify c or c++ to determine which compiler should be used to generate the pre-compiled header.

Configuration Tests

Most projects of any significant complexity will need to determine how and what source code to build based on the attributes of the host operating system, available libraries, and so forth. To this end, MakeKit provides an array of functions to use in configure() within your MakeKitBuild files.

The idioms used to configure projects with MakeKit are very similar to autoconf's. This was an intentional design decision to ease migration.

Most configuration tests result in #define statements being written into a config header which you can include in your source files to make use of the test results. You can set the current config header with mk_config_header. You can add #define statements to the current config header with mk_define:

Example 3.6. Using mk_config_header

configure()
{
    # Place #defines in include/config.h relative
    # to location of MakeKitBuild in source tree
    mk_config_header "include/config.h"

    # Define FOOBAR to 1
    mk_define FOOBAR 1
    
    # Define BUILD_TIME to current time as a string
    # using UNIX 'date' program
    mk_define BUILD_TIME "\"`date`\""
}

Test Basics

Most configuration checks do one or more of the following:

  • Display the test being performed to the user
  • Perform a test using the C compiler
  • Set a variable to the result of the test
  • Add a define to the config header with the result of the test
  • Cache the results to speed up subsequent runs of configure
  • Display the result of the test to the user

The names of variables and defines are usually based on the item in question (e.g. the name of the header or library), with a few transformations:

  • All lowercase letters are converted to uppercase
  • All * (asterisk) characters are converted to P.
  • All other characters are converted to _.

These rules are the same as those used in autoconf. Hereafter, a name derived by these rules will be referred to as a varname.

Most of the configuration checks have two forms, singular and plural. The plural form takes a list of items and performs the above steps for each item, and is the most common and convenient way of performing a check. The singular form takes only a single item has none of the side effects of the plural form. Instead, it sets the variable result to the result of the test, and returns 0 (logical true in shell script) if the test succeeded, and 1 (logical false) otherwise. This form is useful for writing more elaborate custom tests.

Many of the tests have 3 possible results: internal, external, and no. If the item being checked for is provided by the current project -- for example, checking for a library which is built in a preceding source directory -- the test will trivially succeed with internal. If the item was identified as being provided by the operating system or build environment, such as a system header, the result will be external. If the item was not found, the result will be no.

Checking For Headers

To check for the presence of a header on the system, use mk_check_headers. This function takes a list of header names and tests for the presence and usability of each one by attempting to compile a small program that includes it. For each header, it will do the following:

  • Set the variable HAVE_varname to the result of the test.
  • If the header was found, define HAVE_varname

Example 3.7. Using mk_check_headers

configure()
{
    mk_config_header "include/config.h"

    # Check for some basic header files
    mk_check_headers stdio.h stdlib.h string.h does-not-exist.h
    
    # On a reasonable system, the following variables will be set:
    #
    # HAVE_STDIO_H=external
    # HAVE_STDLIB_H=external
    # HAVE_STRING_H=external
    # HAVE_DOES_NOT_EXIST_H=no
    #
    # The following defines will be written into config.h:
    #
    # #define HAVE_STDIO_H
    # #define HAVE_STDLIB_H
    # #define HAVE_STRING_H
}

If these side effects are undesirable, you can use the singular form mk_check_header.

Checking For Libraries

To check for the presence of a library, use mk_check_libraries. For each library name specified, it will test for its presence and usability by attempting to compile and link a test program against it. For each library it will do the following:

  • Set the variable HAVE_LIB_varname to the result of the test.
  • If the test succeeds, export the variable LIB_varname with the name of the library exactly as passed to mk_check_libraries. Otherwise, the variable will be set as the empty string. This is useful for conditionally including the library in a LIBDEPS= list to mk_program and friends.
  • If the library was found, define HAVE_LIB_varname.

Library names should be specified the same way as when linking to them with mk_program, with no lib prefix or file extension.

Example 3.8. Using mk_check_headers

configure()
{
    mk_config_header "include/config.h"

    # Check for some common UNIX libraries
    mk_check_libraries m pthread does-not-exist
    
    # On a typical Linux system, the following variables will be set:
    #
    # HAVE_LIB_M=external
    # HAVE_LIB_PTHREAD=external
    # HAVE_LIB_DOES_NOT_EXIST=no
    # LIB_M=m
    # LIB_PTHREAD=pthread
    # LIB_DOES_NOT_EXIST=
    #
    # The following defines will be written into config.h:
    #
    # #define HAVE_LIB_M
    # #define HAVE_LIB_PTHREAD
}

If these side effects are undesirable, you can use the singular form mk_check_library.

Checking For Functions

mk_check_functions allows you to test for the availability of a list of functions by attempting to compile and link test programs that reference them. For each function it will then do the following:

  • Set the variable HAVE_varname to the result of the test. The possible results are yes and no.
  • If the function was found, define HAVE_varname.
  • If the function was found, define HAVE_DECL_varname to 1. Otherwise, define it to 0.

Unlike autoconf, this check ensures that a declaration of the function was available in addition to the function's symbol being present in a library. You can use HEADERDEPS= and LIBDEPS= parameters to mk_check_functions to specify header files and libraries that are necessary for the function to be found.

Each function can be specified as a simple name, or as a full function prototype in C syntax. In the second case, MakeKit will verify that the function exists and has a compatible prototype. Bear in mind that the resulting variable/define name will reflect any whitespace in your prototype.

Example 3.9. Using mk_check_functions

configure()
{
    mk_config_header "include/config.h"

    # Check for some functions in libm
    mk_check_functions \
        HEADERDEPS="math.h" \
        LIBDEPS="m" \
        sin cos "double sqrt(double)" does_not_exist
    
    # On a typical UNIX system, the following variables will be set:
    #
    # HAVE_SIN=yes
    # HAVE_COS=yes
    # HAVE_DOUBLE_SQRT__DOUBLE_=yes
    # HAVE_DOES_NOT_EXIST=no
    #
    # The following defines will be written into config.h:
    #
    # #define HAVE_SIN
    # #define HAVE_COS
    # #define HAVE_DOUBLE_SQRT__DOUBLE_
    # #define HAVE_DECL_SIN 1
    # #define HAVE_DECL_COS 1
    # #define HAVE_DECL_DOUBLE_SQRT__DOUBLE_ 1
    # #define HAVE_DECL_DOES_NOT_EXIST 0
}

If these side effects are undesirable, you can use the singular form mk_check_function.

Checking For Types

mk_check_types allows you to test for the availability of a list of types by attempting to compile test programs that reference them. For each type it will then do the following:

  • Set the variable HAVE_varname to the result of the test. The possible results are yes and no.
  • If the type was found, define HAVE_varname.

You can use a HEADERDEPS= parameter to mk_check_types to specify header files necessary to find the type definition.

Example 3.10. Using mk_check_types

configure()
{
    mk_config_header "include/config.h"

    # Check for some types in stdlib.h and inttypes.h
    mk_check_types \
        HEADERDEPS="stdlib.h inttypes.h" \
        size_t int64_t uint64_t does_not_exist_t
    
    # On a typical UNIX system, the following variables will be set:
    #
    # HAVE_SIZE_T=yes
    # HAVE_INT64_T=yes
    # HAVE_UINT64_T=yes
    # HAVE_DOES_NOT_EXIST_T=no
    #
    # The following defines will be written into config.h:
    #
    # #define HAVE_SIZE_T
    # #define HAVE_INT64_T
    # #define HAVE_UINT64_T
}

If these side effects are undesirable, you can use the singular form mk_check_type.

Checking For Sizes of Types

mk_check_sizeofs allows you to test for the sizes of a list of types. For each type it will then do the following:

  • Set the variable SIZEOF_varname to the result of the test.
  • Define SIZEOF_varname to the result of the test.

You can use a HEADERDEPS= parameter to mk_check_types to specify header files necessary to find the type definitions. Note that if this function fails to determine the size of a type it will abort configuration, so you might want to first check that the types exist with mk_check_types.

Example 3.11. Using mk_check_sizeofs

configure()
{
    mk_config_header "include/config.h"

    # Check for sizes of some types
    mk_check_types \
        HEADERDEPS="stdlib.h inttypes.h" \
        size_t int64_t "void*"
    
    # On a typical 32-bit UNIX system, the following variables will be set:
    #
    # SIZEOF_SIZE_T=4
    # SIZEOF_INT64_T=8
    # SIZEOF_VOIDP=4
    #
    # The following defines will be written into config.h:
    #
    # #define SIZEOF_SIZE_T 4
    # #define SIZEOF_INT64_T 8
    # #define SIZEOF_VOIDP 4
}

If these side effects are undesirable, you can use the singular form mk_check_sizeof.