class: center, middle ### COMP26020 Programming Languages and Paradigms Part 1: C Programming *** # Modular Compilation --- # Modular Compilation - Large programs: need to organise the code in several source files - [Redis's `src` folder](https://github.com/redis/redis/tree/8.2.2/src) - [Linux](https://github.com/torvalds/linux/tree/v7.3-rc7) kernel sources (64K+ C source files for v7.3!) -- - **How to break up the program code into different files representing well-isolated compilation units?** - Modular compilation, covered in this slide deck -- - Also, we can't run `gcc <64K source files>` each time we make a small change to one or a few source files - **How to efficiently automate incremental builds?** - Automated compilation, covered in the next slide deck --- class: inverse, middle, center # A Closer Look at the Compilation Phase --- name: phase # A Closer Look at the Compilation Phase
-- .center[Actually things are still a little bit more complicated 😛] --- # A Closer Look at the Compilation Phase
--- # A Closer Look at the Compilation Phase
--- class: inverse, middle, center # Breaking Down the Program into Modules --- # Breaking Down the Program into Modules - Consider a hypothetical server application working as follows:
--- name: breaking # Breaking Down the Program into Modules - Server implemented in 3 `.c` source files, also called **modules**: --- template: breaking
--- template: breaking
```bash gcc -c main.c -o main.o # compile main.c into main.o gcc -c parser.c -o parser.o # compile parser.c into parser.o gcc -c network.c -o network.o # compile network.c into network.o gcc main.o network.o parser.o -o prog # link the object files into prog ``` --- name: interractions # Breaking Down the Program into Modules - Each module (`.c` source file) exposes an **interface** that can be called from the other source files - Should be **as small as possible**, hiding internal code/data --- template: interractions .leftlargecol[ - Assume the following interactions: - Server starts, `main.c` calls `init_network()` implemented in `network.c` to initialise networking - `main.c` then runs the main server loop: - Calls `rcv_request()` implemented in `network.c` to wait for the next request - Request received? Calls `parse_req()` implemented in `parser.c` to process it - Rinse and repeat ] .rightsmallcol[
] --- # Header File for Modular Compilation - There is generally one header file per module, **declaring the interface it exposes** - Can be included in several `.c` files, so it **must only declare functions and variables, not define them** - Type definitions (`struct`, `typedef`, `enum`) are fine -- - For example `network.h`: .leftcol[ ```c #ifndef NETWORK_H // include guard to avoid #define NETWORK_H // multiple inclusion typedef struct { int id; char content[128]; } request; void init_network(void); int rcv_request(request *r); #endif /* NETWORK_H */ // ``` .codelink[
`16-modular-compilation/network.h`
] ] .rightcol[
] ??? - So we generally create one header file per module exporting an interface - We have the architecture of our program on the left, we'll have 1 header for network and 1 header for parser - In addition to the 3 source files, network.c, parser.c and main.c - These header files will be included in several source files, so it is important that they only declare functions and variables - In other words, function prototypes, struct/typedef/enum type definitions, and variable declarations using `extern` - They should contain no function or variable definitions: if a header defined, e.g., a global variable, each .c file including it would define it too, and linking would fail with a multiple definition error - We see for example `network.h` on the slide - It contains everything that the world outside the module network needs to know in order to use the network interface - So, the prototypes of the two functions of the interface - But also the definition of the struct type that is used as a parameter in one of these functions - Note the enclosing #ifndef NETWORK_H, it's called an include guard - It ensures the header's content appears only once per .c file, even when that file includes several headers that themselves include this header - Without it, the request typedef would be defined twice, which is a compilation error --- # Breaking Down the Program into Modules `network.c`: .leftcol[ ```c *#include "network.h" // static gives internal linkage: these // functions/globals are only visible // within this source file static void generate_request(request *r); static int request_counter = 0; void init_network(void) { /* init code here ... */ } int rcv_request(request *r) { generate_request(r); /* ... */ } static void generate_request(request *r) { /* ... */ } ``` .codelink[
`16-modular-compilation/network.c`
] ] .rightsmallcol[
] ??? - Now if we look at the implementation of the module network, that is the content of network.c, it looks like that - We need to include the corresponding header to get access to the struct definition - Including it also lets the compiler check that our function definitions match the prototypes in the header - We have a function and a global variable that are internal to the module and are not supposed to be used from outside; we can enforce that with the static keyword - And we have the implementation of the 2 functions that are exported by the network module --- # Breaking Down the Program into Modules .leftcol[ `parser.h`: ```c #ifndef PARSER_H #define PARSER_H /* needed for the definition of request: */ #include "network.h" void parse_req(request *r); #endif ``` .codelink[
`16-modular-compilation/parser.h`
]
] .rightcol[ `parser.c`: ```c #include
*#include "parser.h" static void internal1(request *r); static void internal2(request *r); void parse_req(request *r) { internal1(r); internal2(r); } static void internal1(request *r) { /* ... */ } static void internal2(request *r) { /* ... */ } ``` .codelink[
`16-modular-compilation/parser.c`
] ] ??? - If we look at parser.h and parser.c, it follows the same principles as for the network module - We have an external interface declared in the header - We also need the declaration of the struct so we include network.h in the parser header - And in the module implementation we include the parser header - We have a couple of internal functions, as well as the exported function --- # Breaking Down the Program into Modules .leftcol[ - `main.c`: ```c *#include "network.h" *#include "parser.h" int main(int argc, char **argv) { request req; /* call functions from network module */ init_network(); rcv_request(&req); /* call function from parser module */ parse_req(&req); /* ... */ } ``` .codelink[
`16-modular-compilation/main.c`
] ] .rightcol[
] ??? - Finally, main.c looks like that - It includes both headers to get access to the interface function prototypes - Note that parser.h also includes network.h, so network.h ends up included twice here: this is exactly where the include guard is needed - And within the main function the functions from the interface can be called - We can compile our program like this --- # Compile & Test ```sh # Compile the .c source files, in no particular order # (-c: compile only, don't link; -o: name of the output file): $ gcc -c main.c -o main.o $ gcc -c parser.c -o parser.o $ gcc -c network.c -o network.o # Link the final executable: $ gcc main.o parser.o network.o -o prog # Launch it $ ./prog ``` --- # Summary - **Modular Compilation:** breaking down a program's sources into multiple header `.h` files and source `.c` files - Proper source organisation is important for medium/large programs - Use header files to export only the interface meant to be used from outside the module ---- .center[Feedback form: https://bit.ly/3lInZ8h]
??? - Let's recap - Breaking down C code into several files - It is done with a combination of header files, containing declarations and included where needed - and source files, containing the implementation, in other words the definitions - In the next video, we will see how to automate the compilation of programs made of multiple source files