Practical C Issues:! Preprocessor Directives, Multi-file Development, Makefiles CS449 Fall 2017
Multi-file Development
Multi-file Development Why break code into multiple source files? Parallel development involving multiple authors Quicker compilation (only compile modified file) Modularity (can reuse object file / library) Encapsulation (easier to read / maintain) Use smallest scope to enforce encapsulation Avoids polluting global namespace Minimizes scope of code to find all uses of symbol But sometimes scopes must cross file boundaries... Then same symbol must be declared in all relevant source files
Block / Function Scope Scope: Local (function or block of code) Lifetime: Automatic (duration of function) Static (duration of program) void foo( ) { } int x; // automatic static int y; // static x = y =
Internal Linkage Scope Scope: File Lifetime: Static (duration of program) static int x; void foo( ) { x = } Void bar( ) { x = }
External Linkage Scope Scope: Program (multiple files) Lifetime: Static (duration of program) extern used to declare vars in other files File A File B int x; extern int x; void foo() { } void foo(); B declares x and foo defined in A
Multi-file Example int x = 0; a.c b.c #include <stdio.h> int f(int y) { return x+y; } /* Declarations for symbols defined in other files */ extern int x; int f(int); int main() { x = 1; printf("%d", f(2)); return 0; }
Multi-file Example thoth$ gcc c a.c b.c thoth$ gcc a.o b.o thoth$./a.out 3
Pitfall: Missing Definitions a.c // Missing x definition b.c #include <stdio.h> int f(int y) { return y; } /* Declarations for symbols defined in other files */ extern int x; int f(int); int main() { x = 1; printf("%d", f(2)); } return 0;
Pitfall: Missing Definitions thoth$ gcc c a.c b.c thoth$ gcc a.o b.o./b.o: In function `main': b.c:(.text+0x6): undefined reference to `x' collect2: ld returned 1 exit status Linker cannot find x in symbol table when it tries to resolve x in x = 1; inside b.c
Pitfall: Missing Declarations int x = 0; a.c b.c #include <stdio.h> int f(int y) { return x+y; } /* Missing declarations for x and int f(int) */ int main() { x = 1; printf("%d", f(2)); } return 0;
Pitfall: Missing Declarations thoth$ gcc c a.c b.c./b.c: In function main :./b.c:5: error: x undeclared Compiler must know there is an externally defined x and its type is int to generate code
Declarations and Definitions Definitions: Put into individual C (.c) files Function definitions: ends up in text section of.o file Variable definitions: ends up in data section of.o file One symbol must be defined in only one C file Declarations: Put into header (.h) files è #included in any C file using the symbols Function declarations Variables declarations (using extern) Type definitions (e.g. struct definitions) #defines
Declarations and Definitions mymalloc.h // Declarations void *my_malloc(int size); mymalloc.c // Definitions static void *head; void my_free(void *ptr); Why wasn t head declared in mymalloc.h? void *my_malloc(int size) {... } void my_free(void *ptr) {... }
Including a Header File Insert header In each C file using my_malloc, my_free: main.c #include mymalloc.h // insert header int main() { my_malloc( ); } foo.c #include mymalloc.h // insert header int foo() { my_free( ); }
Compiling Multiple Files Compiling: gcc mymalloc.c main.c foo.c Why not also compile mymalloc.h? A. Header does not define any symbols Contains only declarations to help compiler Nothing to generate an object file out of (Code / data segment would be empty) Hence nothing to compile
Makefiles:! Easy Multi-file Development
Makefile A script interpreted by the GNU Make utility to build projects containing multiple files Design Goal: on a source / header file modification, perform the smallest set of actions required By expressing what files depend upon others Composed of a collection of rules which look like target: dependencies action That is a single <tab> before action, not spaces! Script invoked by: make <target> If no target given, first target in script built by default
Makefile malloctest: mymalloc.o mallocdriver.o gcc o malloctest mymalloc.o mallocdriver.o mymalloc.o: mymalloc.c mymalloc.h gcc c mymalloc.c mallocdriver.o: mallocdriver.c mymalloc.h gcc c mallocdriver.c clean: rm f *.o malloctest
Dependency Graph malloctest mymalloc.o mallocdriver.o mymalloc.c mymalloc.h mallocdriver.c
Build from scratch Using a Makefile thoth $ ls Makefile mallocdrv.c mymalloc.c mymalloc.h thoth $ make gcc -c mymalloc.c gcc -c mallocdrv.c gcc -o malloctest mymalloc.o mallocdrv.o thoth $ make make: `malloctest' is up to date. Partial build after modifying mymalloc.c thoth $ touch mymalloc.c thoth $ make gcc -c mymalloc.c gcc -o malloctest mymalloc.o mallocdrv.o
Defining Variables in Makefiles Works like macros (text replacement) Syntax: <name> :=... or <name> =... Example: Instead of: malloctest: mymalloc.o mallocdriver.o gcc o malloctest mymalloc.o mallocdriver.o Can do: OBJECTS = mymalloc.o mallocdriver.o malloctest: $(OBJECTS) gcc o malloctest $(OBJECTS)
Automatic Variables $@: The file name of the target. E.g.: malloctest: $(OBJECTS) gcc o $@ $(OBJECTS) $<: The name of the first prerequisite. E.g.: mymalloc.o: mymalloc.c mymalloc.h gcc c $< $^: The names of all prerequisites. E.g.: malloctest: $(OBJECTS) gcc o $@ $^
Pattern Matching Character % is a wildcard for any string Example: %.o: %.c gcc c $< -o $@ What it means: For all targets matching <some string>.o Dependency is <that string>.c Action is gcc c <that string>.c o <that string>.o Rule is used to produce any.o file from.c file
Concise Makefile malloctest: mymalloc.o mallocdriver.o gcc -o $@ $^ %.o: %.c gcc -c $< -o $@ mymalloc.o: mymalloc.h mallocdriver.o: mymalloc.h clean: rm -f *.o malloctest
Make Utility Options Usage: make [-f makefile] [options] [targets] -f makefile: Makefile to interpret targets: Targets to be built Options: <name> = <value>: Define a variable. -C <dir>: Change to directory <dir> before building. -n: Dry run. Just print commands and don t execute. -d: Debug mode. Print verbose information.
Preprocecessor Directives:! Easy Manipulation of Source Code
#include Copies the contents of specified file into current file #include < >: file in standard include location Usually /usr/include #include : file in current directory or specified paths Paths specified using the I option If main.c has following includes: #include <stdio.h> #include myheader.h and is compiled using gcc I ~/local/include main.c stdio.h under /usr/include myheader.h under current directory or ~/local/include
#define Defines macros Macro: rule that specifies textual replacement of one string for another Often used to assign names to constants #define PI 3.1415926535 #define MAX 10 float f = PI; for(i=0;i<max;i++)
#define Good macros are generic (do not make assumptions about inputs) Good: #define MAX(a,b) (a > b)? a : b Only assumes a can be compared to b Not so good: #define SWAP(a,b) {int t=a; a=b; b=t;} Makes assumption that types are int Better #define SWAP(T,a,b) {T t=a; a=b; b=t;}
#if #if <condition known to preprocessor> // Some code #endif Preprocessor emits code between #if directive and #endif directive to the compiler only if condition is true Condition evaluated at preprocessing time (cf. C if statement is evaluated at execution time) What does preprocessor know? Constants (0, 1, 2, Linux, x86, ) Values of #defined variables Arithmetic (+, -, *, /, >, <, ==, &&,, )
Example #include <stdio.h> int main() { } #if 0 printf( This is not compiled\n ); I can doodle here when I am bored. #endif printf( This is compiled\n ); return 0;
Example 2 #include <stdio.h> #define LIBRARY_VERSION 7 int main() { } #if LIBRARY_VERSION >= 5 some_function_included_in_version_5(); printf( This is compiled\n ); #endif return 0;
#else #if <condition1> // Emitted if condition1 is true #elif <condition2> // Emitted if condition1 is false and condition2 is true #else // Emitted if neither is true #endif
#if defined #if defined Checks to see if a macro has been defined, but doesn t care about the value A defined macro might expand to nothing, but is still considered defined
Example #include <stdio.h> #define DEBUG int main() { } #if defined DEBUG printf( Some debug output\n ); #endif return 0;
#undef Undefines a macro: #include <stdio.h> #define DEBUG int main() { } #if defined DEBUG #endif printf( This is printed\n ); #undef DEBUG #if defined DEBUG printf( This is not\n ); #endif return 0;
Shortcuts for #if defined #if defined #ifdef #if!defined #ifndef
Uses of #if directive Enable / disable code specific to a library, OS, CPU, etc. Turn on / off different features of program Debugging: #ifdef DEBUG printf( ) #endif More flexible debugging //easier to modify functionality of PrintDebug later #ifdef DEBUG #define PrintDebug(args ) fprintf(stderr, args) #else #define PrintDebug(args ) #endif
Using #if to #include! Header Only Once Including same header twice can lead to compile errors Redefinition of the same struct type, etc.. Easy to stumble into with multiple levels of nested headers #ifndef _MYHEADER_H_ #define _MYHEADER_H_ Declarations only to be included once #endif
Command Line Defined Macros D: Defines variables from command line gcc o test DVERSION=5 test.c gcc o test DDEBUG test.c Allows tailoring of source code to specific versions and features from command line
Pre-Defined Macros Macro FILE LINE DATE TIME STDC GNUC VERSION unix i386 Meaning Current compiled file Current line number Current date Current time Defined if compiler supports ANSI C Defined if compiler is GNU C compiler Version of GNU C compiler Defined if OS is a UNIX compliant system Defined if CPU has x86 instruction set Many other macros
Other Preprocessor Details # - places quotes around macro argument #define CALL(f) { printf(#f); f(); } CALL(foo) { printf( foo ); foo(); } ## - concatenates two things in macro #define CALL(f) f ## _debug () CALL(foo) foo_debug() #error: Emit error message and exit #warning: Emit warning message #pragma: Change behavior of compiler
Error Direc4ve Example #include <stdio.h> #ifndef i386 #error "Needs x86 architecture." #endif int main() { // Some x86 specific code return 0; } >> gcc./error.c./error.c:3:2: error: #error "Needs x86 architecture. >> gcc m32./error.c Tests whether hardware plagorm is i386 (x86) and displays error Ini4ally fails because default compila4on target is x86_64 (and i386 is not defined) -m32 op4on changes target to x86, implicitly defining i386 #warning: allows compila4on but with warning message
Concatena4on Example #include <stdio.h> #ifdef i386 #define CALL(f) f ## _x86 () #else #define CALL(f) f ## _x86_64() #endif void foo_x86() { } printf( x86 binary executable\n"); void foo_x86_64() { } printf( x86_64 binary executable\n"); int main() { } CALL(foo); return 0; >> gcc./concat.c >>./a.out x86_64 binary executable >> gcc m32./concat.c >>./a.out x86 binary executable Calls different func4ons depending on target architecture When i386 macro is undefined: CALL(foo) calls foo_x86_64() When i386 macro is defined: CALL(foo) calls foo_x86() -m32 op4on implicitly passes -D i386 to preprocessor
Pragma Example #include <stdio.h> #pragma message "Compiling " FILE " using " VERSION int main() { } return 0; >> gcc./pragma.c./pragma.c:3: note: #pragma message: Compiling./pragma.c using 4.4.7 20120313 (Red Hat 4.4.7-4) Prints diagnos4c message during compila4on of file Note two pre-defined macros: FILE and VERSION Many more pragmas To control struct padding (e.g. #pragma pack(1) ) To control code op4miza4ons (e.g. #pragma omp parallel )