______________________________________________________________________ Using Good Coding Style for Fun and Grades ______________________________________________________________________ Feeling particularly evil this quarter (most likely riffing on a recent theme, so to speak), Dave will be requiring OS students to participate in some particularly mind bending projects. In order to keep student injury and fatalities to a minimum, we will be abandoning the hacker principle of "if it compiles, ship it", and instead adopting a more strict grading policy that now includes evaluation of form as well as function. In short, you will need to use good coding style if you want to do well in this class. ______________________________________________________________________ Modules and modularity ______________________________________________________________________ One of the most important parts of keeping things sane is breaking your code up into modules. Each module will consist of a .h and a .c file. From here we will refer to .c and .h files as implementation and header files, respectively. It is the header file's job to tell other modules how they are allowed to interact with the module, and define any data structures that are used to interact with the module. It is the implementation file's job to implement the functions declared in the header as well as hide any implementation details from other modules. These roles are discussed in more detail below. ____________________________________________________________ File names Every implementation file should have a corresponding header file with the same root name. For example, the "foo" module will consist of two files: "foo.c" and "foo.h". The only exception to this rule is if the .c file implements a function that is an entry point for an externally linked system (like an operating system). In this case, a header file might be helpful, but is not required. What this means in English is that if the .c file contains a main() function, you most likely will not need a header file. ____________________________________________________________ Dependencies Before we get into the meat of what a good .h and .c file should contain, we're going to define two kind of module dependencies: data dependencies and functional dependencies. Data dependencies will occur when you want to write a function that extends or embeds the functionality of another module, or uses a constant defined in the header of another module. For example, lets say that you are keeping list of process data in a generic list data structure, and want to define a function that removes dead processes from the list: 8< ------------------------------------------------------------ #ifndef __LIST_H__ #define __LIST_H__ /* __________________________________________________ list.h - Header file for the list module. Some Student $Id$ __________________________________________________ */ /* __________________________________________________ Data type definitions __________________________________________________ */ typedef struct _ListNode { void * data; struct _ListNode * next; } ListNode; /* __________________________________________________ */ typedef struct { ListNode * head; ListNode * tail; } List; /* __________________________________________________ Function prototypes __________________________________________________ */ ListNode * newListNode (void * data); void delListNode (ListNode * node); List * newList (); void delList (List * list); void addNode (List * list, ListNode * node); void removeNode (List * list, ListNode * node); /* __________________________________________________ End of list.h __________________________________________________ */ #endif 8< ------------------------------------------------------------ #ifndef __PROCQUEUE_H__ #define __PROCQUEUE_H__ /* __________________________________________________ procqueue.h - Header file for the process queue module. Some Student's Partner $Id$ __________________________________________________ */ /* __________________________________________________ Includes __________________________________________________ */ #include "list.h" /* __________________________________________________ Data type definitions __________________________________________________ */ typedef struct { List * procList; /* etc... */ } ProcQueue; /* __________________________________________________ Function prototypes __________________________________________________ */ ProcQueue * newProcQueue (); void delProcQueue (ProcQueue * queue); void addListToQueue (ProcQueue * queue, List * list); void killProcInQueue (int pid, ProcQueue * queue); /* __________________________________________________ End of procqueue.h __________________________________________________ */ #endif 8< ------------------------------------------------------------ There are two data type dependencies here: the process queue contains a list data structure, and the addListToQueue() function accepts a list argument. Functional dependencies occur when one module calls functions of another module. For example, lets say that the process module is used by the procqueue module, discussed above. However, for some strange reason (this is a lame, contrived example), we didn't have a data type dependency in the procqueue header. 8< ------------------------------------------------------------ #ifndef __PROCESS_H__ #define __PROCESS_H__ /* __________________________________________________ process.h - Header for the process module. Some Student $Id$ __________________________________________________ */ /* __________________________________________________ Data type definitions __________________________________________________ */ typedef struct { int pid; int fds[256]; void * someStupidIOBuffer; /* etc... */ } Process; /* __________________________________________________ Function prototypes __________________________________________________ */ Process * newProcess (int pid); void delProcess (Process * proc); void killProcess (Process * proc); /* etc... */ /* __________________________________________________ End of process.h __________________________________________________ */ #endif 8< ------------------------------------------------------------ /* __________________________________________________ procqueue.c - Implementation file for the procqueue module. Some Student and Some Student's Partner $Id$ __________________________________________________ */ /* __________________________________________________ Includes __________________________________________________ */ #include "procqueue.h" #include "process.h" /* __________________________________________________ Function definitions __________________________________________________ */ /* __________________________________________________ killProcInQueue() - Look for a process in the current queue by PID. If found, kill it and remove from queue. Returns -1 if the process was not in the queue, pid otherwise. __________________________________________________ */ int killProcInQueue (int pid, ProcQueue * queue) { int result = -1; ListNode * node; Process * proc; #ifdef __DEBUG__ debugOut2("killProcInQueue: %d, 0x%x", pid, (int)queue); #endif if ((NULL == queue) || (NULL == queue->procList)) { return -1; } node = queue->procList->head; while (NULL != node) { proc = (Process *)node->data; if (NULL != proc) { if (pid == proc->pid) { result = proc->pid; removeNode(queue->procList, node); killProcess(proc); delProcess(proc); delListNode(node); break; } } node = node->next; } return result; } /* __________________________________________________ newProcQueue() - Create a new, empty process queue. Returns pointer to newly allocated and initialized process queue. XXX - Shouldn't one of us fix this before the kernel is due? - Some Student __________________________________________________ */ ProcQueue * newProcQueue () { return NULL; } /* etc...*/ /* __________________________________________________ End of procqueue.c __________________________________________________ */ 8< ------------------------------------------------------------ Here we see that the killProcInQueue() function calls functions from both the list and process modules. This means that the implementation file has functional dependencies on the list and process modules. Notice that since the procqueue header already includes the list module header, the procqueue .c does not redundantly include list.h. ____________________________________________________________ Header file contents A header file must contain three things: comments, preprocessor directives, and function prototypes. A header file may optionally contain constant definitions using the #define preprocessor directive, include directives, and data type definitions. ____________________________________________________________ Implementation file contents An implementation file should contain several things: comments, include directives, and function definitions. Depending on how sneaky your module is, the implementation file may also contain local data type definitions, local data, local function prototypes, and local function definitions. ____________________________________________________________ Comments All files should have an opening and closing comment block. The opening comment block should contain at least the file name, a brief description of the file content and/or purpose, and who has worked on that file. In the examples above you may also notice the somewhat cryptic line containing "$Id$". This is a keyword that CVS will expand to identify the version of the current file, the identity of the last person to check it in, the date it was last checked in, and the time. This is purely optional, but useful since you can quickly use this to check the file version without using a CVS command. While ostensibly a hold over from the days of turned in printouts, every file should also include a closing header that again identifies the file name. Header files must contain comment blocks that separate header file content. Implementation files must also contain comment blocks separating implementation sections. Additionally, implementation files should also have comment blocks preceding each function implementation. The function comment block must have the function name, a brief description of the function purpose, and a description of function return values. Additional verbosity (such as a description of each input parameter, or developer notes) is nice, but optional. You should decide upon your comment layout style, but it should be consistent across the entire project. You should also consider using delimiter lines that identify the start and end of major comment blocks (as illustrated by the repeated underscores in the examples.) You should not turn in files that contain commented out code. The code you mean to save can be saved using CVS. Comments should be just that, comments, and not failed experiments. ____________________________________________________________ Preprocessor directives While C preprocessors are getting better, some may still crash if you have a circular include. To prevent this from happening, header files should always start with the following preprocessor directives: #ifndef __MODULENAME_H__ #define __MODULENAME_H__ Where "MODULENAME" is the module name in all caps (note there should be no spaces, but additional underscores will work). The header file should always end with an #endif directive. If you are going to define constants, they should be in all caps. If the constant is local to a module, don't put the constant in the header. __________________________________________________ Optional preprocessor directives You may use #ifdef and #endif to create debug output sections, but this is purely optional. When you check in your code for collection, these sections should be clearly spaced away from live code, and whatever macro definitions that include the debug output code should be excluded from the build process. The graders should see no debug output. ____________________________________________________________ Include directives You should limit your include directives to only include the bare minimum number of header files required to compile a given module. Furthermore, unless there is a data dependency (as illustrated above) in a module header, all module includes should appear in the .c file. The implementation file should always include its corresponding header file (to pick up function prototypes). Points will be taken off if you include a file that is not used by the module, or include a file in a header file that is not required by that header file. FIXME: Points may be taken off if you do not include a file in a header that has a data dependency (i.e. you rely on a special order of includes in the .c file in order to properly include the dependent module header). ____________________________________________________________ Data type definitions Data types should be structured in a sensible manner. Give serious consideration to using typedef, thus saving the hassle of typing "struct StructName" wherever "StructName" could suffice. ____________________________________________________________ Function prototypes and definitions Function prototypes should always match the function definitions, including argument names. Depending on how lazy your partner is, you may want to include a function description comment before a function prototype. If you do comment prototypes, you won't have to look at the implementation file comments (hardly) ever. ____________________________________________________________ Local data types, function prototypes, data, and functions Each of the sections local to a module implementation should follow the conventions discussed above. The primary difference is the following: everything except data type definitions get preceded with the "static" keyword. For an example of local data and functions, see below. In terms of order, following the include directives, your local data types should come first, then local data, then function prototypes, then function definitions. ______________________________________________________________________ Source layout and whatever ______________________________________________________________________ Most of this stuff is optional, but if you go way outside the norm of these guidelines, there will be penalties. ____________________________________________________________ A word about global data Avoid global data. You will have global data, but you should consider keeping it local to a module and using functions to access the data. Example: kerneldata.h and kerneldata.c, respectively: 8< ------------------------------------------------------------ ... typedef struct { /* etc... */ } MasterKernelRecordWithKitchenSink; ... MasterKernelRecordWithKitchenSink * getKernelData (); ... 8< ------------------------------------------------------------ ... static MasterKernelRecordWithKitchenSink * kernelPtr = NULL; ... static MasterKernelRecordWithKitchenSink * makeMeAKernel (); ... MasterKernelRecordWithKitchenSink * getKernelData () { if (NULL == kernelPtr) { kernelPtr = makeMeAKernel(); } return kernelPtr; } ... 8< ------------------------------------------------------------ ____________________________________________________________ Whitespace Whitespace can be used to greatly improve your code readability. In general, you should have one empty line between comment blocks, function definitions, and other large file elements. However, there can be too much of a good thing. If you want to space your files out, there should be no more than two empty lines at any point in your code. ____________________________________________________________ Indentation You should use consistent indentation in a module. This means that if you indent using three spaces, you will always indent three spaces (in that file anyway). If you indent with one tab, you will always indent with one tab. Nested block structures (discussed more below) should be indented one level higher than the containing structure (i.e. anywhere there is an opening curly brace you should indent one more level). ____________________________________________________________ Braces Use them. A lot. Even if there is just one stinking line of code in the block. Just make sure indentation is consistent and you chose one of three styles: Wacky Java fiend style if (0 != bleh) { freakOut(bleh); } Suave Pascal junkie style if (0 != bluh) { freakOut(bluh); } Stylish C guru style if (0 != bar) { freakOut(bar); } ____________________________________________________________ Underscores Pick one: capsBetweenWords or underscores_between_words. Avoid mixing them too much. ____________________________________________________________ Predicates If you are comparing equality between a value and a constant, put the constant on the left hand side. It will prevent you from spending days looking for errors like: if (myptr = NULL) { freakOut(myptr); } Which of course assigns NULL to your pointer variable and freaks out. If NULL was on the left hand side, the compiler would catch it since you can't assign to a constant. ______________________________________________________________________ The End ______________________________________________________________________