Guidelines for Team Projects

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 naming conventions.  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 file organization.

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.

2 thoughts on “Guidelines for Team Projects”

  1. Reminds me of the conventions you helped establish on Cosmos all those years ago at CMU. Experience with that has served me well through the years.

    One thing that’s not often said about project conventions, that’s also helpful, is that when new staff begin contributing, you soon get a feel for how cooperative they are when there is need for pragmatic compromises. If they can’t be relied on to write code that looks Ok to the team, you know their chances of writing code that works Ok with the team, and you know it before they’ve screwed things up too badly.

  2. I think your insight about new staff is right on target. And by being aware of how a new team member joins the project, sometimes some coaching can help.

    And you’re absolutely right about the history of these conventions: many of them began with the Cosmos project!

Leave a Reply

Your email address will not be published. Required fields are marked *