ECE 329 Project Guide
Introduction
Projects in ECE 329 require a considerable amount of programming using the C programming language with the Linux operating system. C is a standard systems implementation language and Linux provides a rich environment for software development. During the course of these projects you will be asked to implement a number of packages that are representative of the kinds of code used to implement systems. In most cases this code will be developed in a simulated environment, which is to say you will not be implementing the kernel of an actual operating system, but you will get the experience of dealing with many of the issues involved without the complexity inherent in developing a kernel.
Reporting Requirements
Each project will have a due date coinciding with a regularly scheduled class period. On that due date at the beginning of class you are expected to turn in your project report. Reports turned in after this time are considered late. Electronic submission must accompany the hard copy submission and must be completed before the end of the business day on the day the project is due.
All project reports must be typewritten and must include the following information in the order listed:
Cover page: your name; the class number and section; the semester; the project number; the due date; brief overview of the project goals; brief status of the project.
Design overview: at most one page narrative describing your approach to solving the problem.
Test overview: at most one page narrative describing how you convinced yourself your code was operating correctly.
Copy of your project code.
Copy of your test harness code.
Output of your test harness.
Your C code will be a major portion of each project's grade and will be evaluated on readability and design as well as functional correctness. Code should be divided into header files (.h files) with macro definitions, type definitions, external definitions, and function prototypes; and source files (.c files) with global variable definitions and function definitions. Where practical, functions and variables with logically distinct functions should be implemented as separate modules (with a .h and .c file). Your code must be neatly indented and your identifiers appropriately named using some clear and consistent coding standard.
Each file must start with a block comment that lists the file name, the author's name, the course number, section, and semester, the project number (do not turn in the same code in subsequent projects even if it is reused), and a brief description of the file contents. Each variable should have a in-line comment identifying its purpose and each function should have a short block comment describing its function and identifying any side effects. Function code should be liberally salted with in-line comments that guide the reader through the code. Comments be pertinent and informative and contain content beyond that of the code. Gratuitous non-commenting will be penalized.
Development Environment
There are a number of tools available under Linux that you should use in the course of these projects. I will not directly test your use of these, but they will greatly improve your programming productivity, they will be of use in future classes, and they are simply things you should know before you leave here. There are several ways to get information on these tools, including looking at the online manual (man pages) or the online docs (which may be in PS or PDF or other formats). The tools you should use are:
rcs or cvs - source code revision control
make - compile scripts with dependency analysis
gdb - symbolic debugger
gcc - C compiler
RCS is the "revision control system." Using it you can "check-in" and "check-out" versions of each of your program files. Each time you check-in a new version RCS keeps a record of all the previous versions. This is really cool when you discover you have screwed up and made some horrible change to your code that you'd like to have undone. It is even MORE useful when there is more than one person writing the code (as will be the case in future classes). CVS is the more modern version of RCS. You should probably use it instead (but I never have, so I can't tell you much about it).
Make is a program that calls the compiler to recompile your programs. It does this by looking at the modify date of the source files and object files and then recompiling those where the source is newer than the object. In its most basic form you really only need to tell make what objects your program is built from and what headers your various objects depend on. Then whenever you want to recompile you just run "make" and it does it for you. Make is horribly complicated for doing things like building whole sets of tools in nested directories and on many different kinds of systems and stuff, but the basics are fairly simple and you should start using it with all of your programming projects.
Gdb allows you to run your program, stop in a arbitrary places, examine the contents of memory, and lots of other things. There is a nice GUI front end for it that makes it even easier to use (though you can get by with the text-only version in a pinch). You should have used gdb in ECE 272 so this should not be a big stretch for you. You should find gdb to be even MORE useful for C code than for assembly!
The C compiler has a lot of important options, including the -g option that allows the debugger to work with your code and the -W options that allow you to turn on warnings. Turning on warning can be a good tool for finding some hard to spot bugs, but it can also be rather frustrating as it will often complain about a lot of things you don't care too much about. Learning to quite the warnings (by writing your code just right) is a good habit to get into - so you should try to work with the warnings turned on as much as you can.
There are a number of editors you can use and it does not matter which you used in these projects. If you use "vi" then you should know that the vi implementation on Linux (called "vim") has a color mode that can be used to highlight different parts of your code in different colors. This can be very nice especially after a long night of coding when typo's and things can really get you.
The course instructor can point you to sources of information about all of these tools, and most if it is documented on the web. There are other tools that you might find useful as well, but these are some of the most basic that any programmer should be aware of, so try to use this semester as your opportunity to get started using them.
Testing Your Code
It is your responsibility to test your code. In the real world, if you are called upon to write a software module you often will not get an extensive test plan to go with the specifications. Later, after your code is integrated, if your code turns out to be broken, it looks bad on you, and possibly on your company. Thus you need to pay attention to how your code is tested.
Since this is not a class on software engineering I do expect any sort of formal treatment of testing, but I do expect some reasonable effort to be expended in devising a test plan. You should make sure you test obvious boundary conditions including initial conditions (for example if a structure is empty), or extreme conditions (for example is a structure is full), situations where structures are uninitialized and potential error conditions.
The exact details of your test plan will depend on the nature of the project and details of your design. For example, if you were implementing a queue you should consider:
uninitialized queues
empty queues
full queues (depending on your design)
passing bad parameters to various functions
repeatedly inserting and deleting to see if it eventually fails
repeatedly insert until you run out of memory (be careful about this - try not to lock up the machine)
If you implement with dynamic allocation (malloc) you should test what happens when malloc fails (hint - write your own malloc - for testing purposes - that keeps a counter before calling the REAL malloc and then fails by returning a NULL pointer after a certain number of calls). If you use an array you should check for overrunning or underrunning the array bounds.
In short you should try to consider all of the things someone might do when calling your code and test that, and then consider your implementation and all of the potential pitfalls - and test for that. Write no more than one page about HOW you tested your code and include this in your report.
You must also submit a test harness which consists of a sample client program that calls your project code and executes the tests described in your test plan.
Coding Style
The projects in this class will generally consist of developing one or more modules of code in C where a module is a group of functions, data types, and possibly global data structures that provide a service to client code. You should assume that client code will be written in a separate file and will include a header file (written by you) that defines any types or constants and declares all visible functions and global variables. It is preferred that opaque types are used where appropriate to enforce good programming practice but this is not strictly required. Most projects will build on previous projects, thus your code may become a client of code previously written. Each new module must be implemented in one or more files of its own.
Later projects may require multiple modules be implemented as part of a single project. Further, some services may naturally want to be divided into multiple sub-modules as defined by you, the programmer. Each module should be written into its own file. I much prefer several short files of code than one huge file of code. The point is to put closely related code together in a file. In these cases additional header files may be needed to define types, functions, and variables that are shared between modules. These headers need not be included by potential client codes. The exact nature of these modules may not be specified by the project but are part of the design of your solution. They should, of course, be well documented in your report. A well designed piece of code will subdivide a complex problem into smaller more manageable problems, and these will tend to naturally form independent sub-modules. I expect you to design it that way and code it that way.
In all programming assignments you should consider a number of design issues that make for "better" code such as:
running time efficiency
memory requirements
limits imposed by static memory allocation
generality of data structures
multiple concurrent use of data structures
proper error handling
In general, your code should be as fast as possible, use as little memory as possible, have no size limits imposed by your code, should be able to operate with any reasonable type of data, and you should be able to manage multiple data structures - not just a single one. Errors should not result in a program crashing, but should be reported to the caller of the function in question. Of course, these goals are often at odds with one another, so you will have to make choices that trade off these issues in your design.
Opaque Types
Several projects require you to code opaque types. An opaque Type is analygous to a class in java or C++ that has only private members - variables of this type can only be passed into or out of functions written specifically for that type (which are analygous to methods). In C, we implement opaque types with a void pointer type. A void pointer is a pointer that cannot be dereferenced because the type it points to is not known. A void pointer can be assigned to or from any other pointer type without producing an error or warning in the compiler. In this case, a function can be written that returns a void pointer, but when the function returns, the value returned is actually a pointer to the type desired. When programming an opaque type, there will be TWO types defined. One is the void pointer type which will be used by clients of the type, and other other is the true type which will only be used internally. The void pointer type is defined in the header file for the module and the internal type is defined either in the module's C file or in another internal header file if more than one C file must access the internal type (which should not be needed in ECE 329). All functions that take the new type as an argument will use the void pointer type for that argument and all functions that return the new type will return the void pointer type. For return values, the actual type can be returned in a return statement. For arguments, the argument must either be assigned to a local variable of the internal type, or type cast to the internal type when needed.
Listed below is an example of a module for implementing an opaque type "foo" which consts of two files foo.h and foo.c. Any client of this module would include foo.h.
// foo.h
// this is the client's type: foo
typedef void *foo;
// function that returns a foo
foo new_foo(int a);
// function that takes a foo as an argument
int foo_add(foo f, int a);
// foo.c
// be sure to include the header file
#include "foo.h"
// this is the internal type: foo_p
typedef struct foo { int a; int b; } *foo_p;
foo new_foo(int a)
{
foo_p fp = (foo_p)malloc(sizeof(struct foo));
fp->a = a;
fp->b = 0;
return fp; // don't need a cast here
}
int foo_add(foo f, int a)
{
foo_p fp = f; // need this assignment
fp->b = fp->a;
fp->a += a;
return fp->b;
}
To use the opaque type, a client would #include "foo.h", declare variables of type foo, and call the functions new_foo() and foo_add().
Using Make
Make reads a file in the current directory named "Makefile" and processes the rules found in it. A make rule consists of a dependency, and a script to execute if the dependency indicates a file is out of date. GNU make has a number of built-in rules that can do most of the work for you. The main thing that is usually needed is a description of how to link the various files of your project into an executable, and a set of dependencies that represent include files.
Suppose your project has C code in three files: foo.c bar.c and main.c and you have two include files: foo.h which is included in foo.c and main.c and bar.h which is included in bar.c and main.c. Then, your Makefile should look as follows:
CC=gcc
OBJS=foo.o bar.o main.o
main : $(OBJS)
$(CC) -o main $(OBJS)
foo.o : foo.h
bar.o : bar.h
main.o : foo.h bar.h
The first line tells make to use the gcc compiler (not needed under Linux). Capitalization is important.
The second line says these are the object files that are used to build the program. The program's name is "main" and the 4th and 5th lines say it depends on the objects and give the command for linking it using the compiler. The 7th, 8th, and 9th lines describe the include file dependencies.
The identifiers CC and OBJS are make variables - anywhere they are found in the Makefile inside a $( ... ) they are replaced by their value as shown on the line where they are defined. You may create more as needed to make writing the Makefile easier (or avoid them if you want). The CC variable is used by the built-in rules, and so may be important if gcc is not the default compiler.
There is online documentation at:
http://parlweb.parl.clemson.edu/~walt/ece329.html
Using RCS
To use RCS you need four things:
Create a directory named "RCS" in the directory with your source code
Use "ci" to "check-in" files
Use "co" to "check-out" files
Use "rcs" to do special stuff like break locks or merge versions
The "co" program has a few flags, the most important being the "-l" flag that checks out a file with a "lock". The "ci" program also has a "-l" flag that checks in a locked version, but checks the new version back out with a lock so you can continue to change it. The "ci" program also has a "-u" flag that will check in a locked file and check it back out without a lock.
A typical session with RCS looks like this
> mkdir RCS
> ci *.c *.h
> co RCS/*
> co -l myfile.c
> vi myfile.c
> ci -u myfile.c
This sequence first creates the RCS directory. Next all of the C source and header files are checked in. Next, everything in the RCS directory is checked out. When we want to edit a file, we first check it out with a lock as shown, edit it (as with vi) and then check it back in and unlock it (with the -u flag).
In practice you can edit a file multiple times before you check it back in. Any time you want to save the current state of a given file you can save it but keep the lock as follows:
> co -l somefile.c
> vi somefile.c
> ci -l somefile.c
> vi somefile.c
> ci -l somefile.c
> vi somefile.c
> ci -u somefile.c
In this case the file is edited and each version is saved until finally the lock is released. Obviously you cannot check in a file you do not have checked out with a lock. RCS will automatically set read-only permissions on unlocked files so you cannot accidentally edit them.
RCS will ask for a log entry each time you update a locked file by checking it in. This will be automatically added to a comment block in your file by putting the following at the top of your file:
//
// $Log$
//
Each time you check in a locked file RCS will add your log entry at this point, giving you a record of the changes you have made along with the version number.
You can check out old revisions of a file by specifying the revision number when you check out the file as follows:
> co -r 1.8 myfile.c
checks out revision 1.8 of the requested file. Each time a file is checked in the minor revision number is automatically incremented by one. You can force RCS to use a given revision number by specifying it when you check in the file as follows:
> ci -r 2.0 myfile.c
checks in revision 2.0 of the file. This is the only way to change the major revision number. After this command each check in will be for 2.1, 2.2, 2.3, and so on.
RCS has even more powerful features. The best way to learn about them is to read the man pages for ci, co, and rcs and look for online documentation of RCS.
Using GDB
Using gdb is beyond the scope of this guide. You should have used gdb in ECE 272 and you can learn more by looking at a book that covers gdb available at most better book stores.
There is online documentation at:
http://parlweb.parl.clemson.edu/~walt/ece329.html
To use gdb you must supply a "-g" flag to the compiler when you compile. Some of the most important commands in gdb are:
run - to run your program
where - to show where your program is currently executing
break - to set breakpoints to stop your program when you need to
print - to display the contents of variables
There are dozens of other commands that you can you as well, but these are the most important.
Turning In Projects
To turn in your project you need to copy some files into a specific directory in the ECE272 lab before the stated deadline. For each project the directory would be:
/users/walt/ece329/projectX/<your user id>/
You need to put the following files into this directory:
C code files for the current project
C header files for the current project
C code files from previous projects used in the current project
C header files from previous projects used in the current project
C code to test the current project
Makefile to compile and link the test program
Ascii text documentation for the current project
Your Makefile must have rules defined to compile and link your test program and rules to RUN your test program. The default target should compile and link, and the target "test" should run your test. The following is an example Makefile that should work for most of the projects:
CC=gcc
CFLAGS=-g
PROGRAM=testmsg
OBJS=testmsg.o msg.o sema4.o thread.o queue.o
$(PROGRAM) : $(OBJS)
$(CC) -o $(PROGRAM) $(OBJS)
test : $(PROGRAM)
testmsg -f input_data_file
testmsg.o : msg.h
msg.o : sema4.h thread.h queue.h
sema4.o : thread.h queue.h
thread.o : queue.h
In this Makefile, the test program is named "testmsg" and when it is run it expects a flag "-f" followed by a filename. The details of your test program will vary. To use this Makefile, edit the definition of PROGRAM and OBJS to fit your needs and edit the script for running your test program (just below the line that starts with "test : "). You are free to modify this Makefile or make your own as you see fit, as long as it serves the purpose required here.
Grading of projects
The following criteria and their weights will be applied when grading ECE 329 projects:
Functionality
compiles cleanly 5%
test program runs without crashing 10%
test program appears to run correctly 10%
matches given specification 10%
Coding
block comments 10%
in-line comments 10%
reasonable identifiers 5%
reasonable structure 10%
Documentation
problem statement (cover page) 10%
design statement 10%
test plan 10%
Other
extras - up to 5% bonus