The fundamental modular unit in C is the source file. Compiled source files are the basic units which can be linked together to form executable programs.
IPW tries to maintain a one-to-one relationship between source files and functions. This eliminates the possibility that an executable program will contain unused modules, since only identifiers that are explicitly referenced will be loaded. Of course, source files may also contain an arbitrary number of top-level static identifiers, since they are invisible during the linking process.
Source files are named after the most significant global function they contain. This makes it easier for maintainers to find the source file associated with a particular identifier.
There are some general rules for deciding how to modularize a particular
programming problem. First of all, the overall problem should be divided into
levels of abstraction. Functions at one level should ideally call only
functions at the same or next lower level. Global data structures should
likewise be allotted to particular levels, and not accessed by functions
outside those levels. A good example of this model is the IPW scheme
for image header I/O, in which the various layers (header-specific,
hdrio
, and uio
) each have associated global
data structures, and each call only the next lower layer:
layer | functions | data structure |
---|---|---|
BIH | bihread, bihwrite | BIH_T **_bih[] |
hdrio | hrname, hgetrec, hwprmb, hputrec | HDRIO_T _hdriocb[] |
uio | ubof, ugets, uputs | UIO_T _uiocb[] |
Code should not be duplicated if there is any possibility of sharing it. If you find yourself doing the same thing in two or more functions, it's worth it to generalize that action into a separate function: there will be fewer lines of code to keep track of, fewer duplicated fixes to make if the code proves buggy, and a smaller overall program.
A general principle for deciding what actions should be encapsulated in a single function is that a function should be fully explicable by a simple sentence; i.e., one verb and one object. To the extent that a function deviates from this criterion, it should be considered a candidate for:
Functions should be kept small enough to be easily comprehended. A source file should be small enough that you would not worry about losing track of the order of the pages in a printed version. Deep nesting of blocks, or many blocks containing block-scoped local variables, are also indications that a function may be too large.
Ideally, all communication between a function and the "outside world" should be via either the formal arguments, or the function's return value. However, it is also desirable to minimize the number of arguments in a function. As a practical matter, beyond about 5 arguments, it becomes much more difficult for the programmer to remember the order of the arguments, and what each argument does. Functional grouping of the arguments can mitigate this somewhat (e.g., input arguments come first, then output arguments), as can other regularities in argument order (e.g., all IPW I/O routines accept arguments in the order: file descriptor, buffer, count).
IPW allows functions to communicate via global data structures, in lieu of arguments that would otherwise convey the following:
The resulting simplification of function calling sequences has proven worthwhile; however, this use of globals requires some discipline on the part of the programmer, since there is nothing in C to prevent any function from modifying a global to which it has access. All functions that explicitly access or modify global variables MUST identify those variables in their documentation.
The meaning of an argument should be invariant; i.e., "op-code" arguments which alter the interpretation of other arguments should be avoided. The one permissible exception to this is an argument which signals whether additional arguments follow, in afunction which accepts a variable number of arguments.
The type void
should always be used for functions which return
no value.
Functions should not require a special calling sequence to be initialized;
instead, they should rely on either compile-time initialization of local
static
, or run-time initialization triggered by a local
static
flag.