A few years ago, I had the oportunity to design and guide the architecture of a fairly large software system developed by a team of engineers. The code implemented simulation models and test generators for a family of logic components and included software modules written in C, Verilog, Specman and a few other things. The code base was used internally, and was also released to customers. As a moderately large software project developed under enormous schedule pressure, I think we succeeded in producing a well-organized, clean product.
Organization up front was the key to our productivity. We had in place guidelines for where modules lived and their. We employed standardized names for common operations. The result was that those of us on the team could step into someone else’s role when necesssary and understand what they were working on, because a common style had been observed. Of course, there was still plenty of room for individuality and ingenuity in solving problems, but little energy had been wasted on inventing things like naming schemes or .
One of the things that was interesting about the C-based portion of this project was that we needed it to be widely portable to a variety of operating systems and simulation environments. In the end it ran on AIX, Solaris, HPUX and Linux on 32 and 64 bit machines. It operated as a library under Verilog, MESA, VHDL and Specman and also operated standalone. We isolated it from the pecularities of memory management and file IO for each of these systems so that porting could be simplified. UP-front planning of the interfaces that would isolate this system from its environment made porting a relatively small effort, localized to a few files.
In the project mentioned above, we established project-wide conventions, and also established language-specific ones. Recently, in the development of a new project, I laid out some guidelines for the organization of its C-based portion. Most of these “best practices” are things I observed from others in my years developing software systems of various types. Some are from specific “C”-based experts, and a few are simply idiosyncratic. ALL are debatable! Many of them anticipate porting the code collection to new environments. All of it promotes team-based productivity through regularity.
Here is what I wrote down.
Programming Guidelines for SLAM code PREFIX AND CAPITALIZATION - All file names and externally visible symbols begin with "slam_". - External function names use underscores and lower case. (CamelCase is to be avoided.) Example: slam_buf_new(n) PACKAGES - The header file for a package is named "slam_pkg.h" - If a package has state and requires initialization it has a "slam_pkg_init()" function. - A package should be able to be initialized multiple times with no ill effects. COLLECTIONS - Collections like hash tables, buffers and tuples are supported. Where possible, these will use similar names and argument lists for consistency. - New: Collections are allocated with a "_new" function, which may require arguments describing the collection. c = slam_col_new(...) - Free: A collection is freed with the "_free" function. This function will free the storage associated with the collection but not the items. slam_col_free(c) A deep free may be supplied and if so, will be named slam_col_free_deep(c) - Copy: A copy of a collection is creaetd with the "_copy" function. This function will copy the collection container and the elements that are held by value. It is not a "deep copy." If a deep copy function is provided, it is named "_copy_deep". - Length: The length or size of a collection is returned with "_len" function. len = slam_col_len(c) - Indexed access: if the collection supports indexing with integers, "0" refers to the first element and "len-1" to the last. Also, in the style of Python, "-1" refers to the last element and "-len" refers to the first. Indexed access functions are "_get" and "_put". The order of the arguments is: x = slam_col_get(col, index, data) x = slam_col_put(col, index, &data) - Insert/Append: The function "_ins" implies making space for a new value so that it may be put at the "index" or "key" specified. [The insert/append functions modify mutable collections (buf, hash), and return new copies for immutable collections (tuple).] Examples: INSERT x = slam_hash_ins(hash, key, value) - make space for key and link to value x = slam_buf_ins(buf, 11, value) - expand the buffer and put value at index 11 APPEND x = slam_buf_app(buf, -1, value) - put new value after end of current buffer - Delete The function "_del" removes an item from a collection and possibly reduces the length of the collection. An item is identified for deleting by an integer index that is returned by some function that placed the item there, or announced its location. - Return Values: collection functions (except "_free") should return a success code. "-1" is failure. Non-negative values are success, and may have a meaning. x = slam_hash_get: returns the index where the key was found x = slam_buf_ins: returns the index where the value was inserted x = slam_buf_app: returns the index where the value was appended (The index may be used with the appropriate "_del" function.) - Build/Parse (comprehensions) The "_build" function may use varargs to take a format string to build an entire collection from a list of arguments. The "_parse" function may be used to extract elements from a collection. VARIABLES AND THROWAWAY VARIABLES - Use short names like "i" and "s" for things like integers and strings that are examined in the next line or so and forgotten. - Use short name "x" for variable whose value is simply thrown away or overwritten. - In the future: use "xfoo" to name throwaway variables. ERROR/WARNING MESSAGES - Use prefix "+++ SLAM_PKG" for messages that are meaningful mostly to the SLAM programmer. It is appropriate to use fprintf(stderr) for these messages as they may be necessary for low-level debugging. It is also acceptable to use "slam_printf()" - Use prefix "*** SLAM_PKG" for messsages that are intended for users. These messages should almost always be printed using "slam_printf()" so that they may be redirected to logs or output media. - Normal user-level messages should NOT use printf/fprintf, but should use "slam_printf()" at all times. - Use prefix "@@@" for messages from Verilog code examples. PROGRESS MESSAGES - Messages that are intended for debugging are selectively controlled on a package or feature basis with a "_verbosity" flag. The "_verbosity" flag's default value may be overridden with an associated "_VERBOSITY" environment variable whose presence indicates that its values should override the default. - Progress messages MUST be disable-able through a "_VERBOSITY" flag. - The "_VERBOSITY" environment variable should be examined no more than ONCE during program execution. MEMORY ALLOCATION - Avoid calling malloc/realloc/free and other functions that allocate memory directly. Instead use the functions in "slam_malloc.h". slam_malloc() slam_free() slam_strdup() - because this allocates memory This makes it easier to port the SLAM collection to other memory management systems. PARAMETER PASSING - ANSI C: it is ok to assume that structs may be passed by value and that the compiler knows how to copy the chunk. TYPE NAMES AND STRUCTS AND POINTERS - Use typedef and the following conventions: struct slam_foo_s - is the struct name typedef struct slam_foo_s *slam_foo_p - is the pointer to a struct typedef struct slam_foo_s slam_foo_t - is the typedef'd name of the struct - Keep structure members simple and use normal-sized types (int, double) instead of space-optimal types (short int, float). This will make it easier to port the code and to inspect data structures from other language systems. - Avoid bit-aligned structures. - Use unions appropriately to handle types whose sizes may be different - VOID*: It is ok to assume that an "int", "char" or "char*" fits into a "void*". It is ok to assume that any pointer fits into a "void*". It is NOT ok to put a "double" or "float" into a "void*". - Use the "typemarker" technique to label structs with what they are for debugging purposes and to assert that they are what they are. UNIT-TEST - A package should include a short self-test of its functionality. This is called its "unit test." - The unit test is enabled through the flag "#ifdef UNITTEST" - For a package named "slam_foo" its unittest executable is named "slam_foo_unittest" - If a package supports multiple unit tests, they are controlled with flags UNITTEST, UNITTEST0, UNITTEST1, etc. with associated executables "slam_foo_unittest", "slam_foo_unittest0", "slam_foo_unittest1", etc. DEBUG AND ASSERT - Use the "assert()" macro liberally and check consistencies everywhere. - Use the "#ifdef DEBUG" flag liberally, but assume that it is either ON for all SLAM code, or OFF for all SLAM code. I.e., that it is not controlled on a module-by-module basis. - Use a module-level "#define SLAM_MODULE_DEBUG" flag to control module-level debugging. ABORT - Use the "abort()" function to terminate execution immediately. COMPILATION - all code must compile clean silently RE-ENTRANT CODE - avoid using static variables to allow a group of functions to work together. Instead, require the programmer to allocate and pass state variables explicitly. SUPPORT FOR 64-BIT - To do.