Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
299 lines (245 sloc) 13.9 KB
\chapter{Exercise 19: A Simple Object System}
I learned C before I learned Object Oriented Programming, so it helped me to
build an OOP system in C to understand the basics of what OOP meant. You are
probably the kind of person who learned an OOP language before you learned C,
so this kind of bridge might help you as well. In this exercise, you will
build a simple object system, but also learn more about the C Pre-Processor or
CPP.
This exercise will build a simple game where you kill a Minotaur in a small
little castle. Nothing fancy, just four rooms and a bad guy. This project
will also be a multi-file project, and look more like a real C software
project than your previous ones. This is why I'm introducing the CPP here
because you need it to start using multiple files in your own software.
\section{How The CPP Works}
The C Pre-Processor is a template processing system. It's a highly
targeted one that helps make C easier to work with, but it does
this by having a syntax aware templating mechanism. Traditionally
people just used the CPP to store constants and make "macros" to simplify
repetitive coding. In modern C you'll actually use the CPP as a code
generator to create templated pieces of code.
How the CPP works is you give it one file, usually a .c file, and it
processes various bits of text starting with the \verb|#| (octothorpe\footnote{A.K.A. pound, hash, mesh, number symbol, pick whatever makes you happy})
character. When it encounters one of these it performs a specific replacement
on the text of the input file. It's main advantage though is it can
\emph{include} other files, and then augment its list of macros
based on that file's contents.
A quick way to see what the CPP does is take the last exercise and
run this:
\begin{Verbatim}
cpp ex18.c | less
\end{Verbatim}
It will be a huge amount of output, but scroll through it and you'll see the
contents of the other files you included with \verb|#include|. Scroll down
to the original code and you can see how the \program{cpp} is altering the
source based on various \verb|#define| macros in the header files.
The C compiler is so tightly integrated with \program{cpp} that it just
runs this for you and understands how it works intimately. In modern C,
the \program{cpp} system is so integral to C's function that you might
as well just consider it to be part of the language.
In the remaining sections, we'll be using more of the CPP syntax and explaining
it as we go.
\section{The Prototype Object System}
The OOP system we'll create is a simple "prototype" style object system more
like JavaScript. Instead of classes, you start with prototypes that have
fields set, and then use those as the basis of creating other object instances.
This "classless" design is much easier to implement and work with than a
traditional class based one.
\subsection{The Object Header File}
I want to put the data types and function declarations into a separate
header file named \file{object.h}. This is standard C practice and it
lets you ship binary libraries but still let the programmer compile against
it. In this file I have several advanced CPP techniques I'm going to
quickly describe and then have you see in action later:
\begin{code}{object.h}
<< d['code/object.h|pyg|l'] >>
\end{code}
Taking a look at this file, you can see we have a few new pieces of
syntax you haven't encountered before:
\begin{description}
\item[\#ifndef] You've seen a \verb|#define| for making simple constants, but
the CPP can also do logic and remove sections of code. This \verb|#ifndef| is
"if not defined" and checks if there's already a \verb|#define _object_h| and
if there is it skips all of this code. I do this so that we can include
this file any time we want and not worry about it defining things multiple
times.
\item[\#define] With the above \verb|#ifndef| shielding this file from we then
add the \ident{\_object\_h} define so that any attempts to include it later
cause the above to skip.
\item[\#define NEW(T,N)] This makes a macro, and it works like a template function
that spits out the code on the right, whenever you write use the macro
on the left. This one is simply making a short version of the normal way
we'll call \func{Object\_new} and avoids potential errors with calling it
wrong. The way the macro works is the \ident{T} and \ident{N} parameters
to \ident{NEW} are "injected" into the line of code on the right. The
syntax \verb|T##Proto| says to "concat Base at the end of T", so if you had
\verb|NEW(Room, "Hello.")| then it'd make \ident{RoomProto} there.
\item[\#define \_(N)] This macro is a bit of "syntactic sugar" for the object
system and basically helps you write \verb|obj->proto.blah| as simply
\verb|obj->_(blah)|. It's not necessary, but it's a fun little trick
that I'll use later.
\end{description}
\subsection{The Object Source File}
The \file{object.h} file is declaring functions and data types that are defined
(created) in the \file{object.c}, so that's next:
\begin{code}{object.c}
<< d['code/object.c|pyg|l'] >>
\end{code}
There's really nothing new in this file, except one \emph{tiny} little trick.
The function \func{Object\_new} uses an aspect of how \ident{struct}s work
by putting the base prototype at the beginning of the struct. When you look
at the \file{ex19.h} header later, you'll see how I make the first field
in the struct an \ident{Object}. Since C puts the fields in a struct
in order, and since a pointer just points at a chunk of memory, I can
"cast" a pointer to anything I want. In this case, even though I'm taking
a potentially larger block of memory from \func{calloc}, I'm using a
\ident{Object} pointer to work with it.
I explain this a bit better when we write the \file{ex19.h} file since it's
easier to understand when you see it being used.
That creates your base object system, but you'll need a way to compile it
and link it into your \file{ex19.c} file to create a complete program. The
\file{object.c} file on its own doesn't have a \func{main} so it isn't
enough to make a full program. Here's a \file{Makefile} that will do this
based on the one you've been using:
\begin{code}{The Makefile}
\begin{lstlisting}
<< d['code/ex19.1.Makefile'] >>
\end{lstlisting}
\end{code}
This \file{Makefile} is doing nothing more than saying that \program{ex19}
depends on \ident{object.o}. Remember how \program{make} knows how to build
different kinds of files by their extensions? Doing this tells make the
following:
\begin{enumerate}
\item When I say run \program{make} the default \ident{all} should just build
\program{ex19}.
\item When you build \program{ex19}, you need to also build \file{object.o}
and include it in the build.
\item \program{make} can't see anything in the file for \file{object.o}, but it does
see an \file{object.c} file, and it knows how to turn a \file{.c} into
a \file{.o}, so it does that.
\item Once it has \file{object.o} built it then runs the correct compile
command to build \program{ex19} from \file{ex19.c} and \file{object.o}.
\end{enumerate}
\section{The Game Implementation}
Once you have those files you just need to implement the actual game
using the object system, and first step is putting all the data types
and function declarations in a \file{ex19.h} file:
\begin{code}{ex19.h}
<< d['code/ex19.h|pyg|l'] >>
\end{code}
That sets up three new Objects you'll be using: \ident{Monster}, \ident{Room},
and \ident{Map}.
Taking a look at \file{object.c:52} you can see where I use a pointer
\verb|Object *el = calloc(size, 1)|. Go back and look at the \func{NEW}
macro in \file{object.h} and you can see that it is getting the \func{sizeof}
another struct, say \ident{Room}, and I allocate that much. However, because
I've pointed a \ident{Object} pointer at this block of memory, and because
I put an \ident{Object proto} field at the from of \ident{Room}, I'm able
to treat a \ident{Room} like it's an \ident{Object}.
The way to break this down is like so:
\begin{enumerate}
\item I call \verb|NEW(Room, "Hello.")| which the CPP expands as a macro
into \verb|Object_new(sizeof(Room), RoomProto, "Hello.")|.
\item This runs, and inside \func{Object\_new} I allocate a piece of memory
that's \ident{Room} in size, \emph{but} point a \ident{Object *el} pointer
at it.
\item Since C puts the \ident{Room.proto} field first, that means the \ident{el}
pointer is really only pointing at enough of the block of memory to
see a full \ident{Object} struct. It has no idea that it's even called
\ident{proto}.
\item It then uses this \ident{Object *el} pointer to set the contents of
the piece of memory correctly with \verb|*el = proto;|. Remember that
you can copy structs, and that \ident{*el} means "the value of whatever el points
at", so this means "assign the proto struct to whatever el points at".
\item Now that this mystery struct is filled in with the right data from
\ident{proto}, the function can then call \ident{init} or \ident{destroy}
on the \ident{Object}, but the cool part is whoever called this function
can \emph{change} these out for whatever ones they want.
\end{enumerate}
And with that, we have a way to get this one function to construct new types,
and give them new functions to change their behavior. This may seem like
"hackery" but it's stock C and totally valid. In fact there's quite a few
standard system functions that work this same way, and we'll be using some of
them for converting addresses in network code.
With the function definitions and data structures written out I can now
actually implement the game with four rooms and a minotaur to beat up:
\begin{code}{ex19.c}
<< d['code/ex19.c|pyg|l'] >>
\end{code}
Honestly there isn't much in this that you haven't seen, and only you might
need to understand how I'm using the macros I made from the headers files.
Here's the important key things to study and understand:
\begin{enumerate}
\item Implementing a prototype involves creating its version of the
functions, and then creating a single struct ending in "Proto".
Look at \ident{MonsterProto}, \ident{RoomProto} and \ident{MapProto}.
\item Because of how \func{Object\_new} is implemented, if you don't set
a function in your prototype, then it will get the default implementation
created in \file{object.c}.
\item In \func{Map\_init} I create the little world, but more importantly
I use the \ident{NEW} macro from \file{object.h} to build all of the
objects. To get this concept in your head, try replacing the \ident{NEW}
usage with direct \ident{Object\_new} calls to see how it's being
translated.
\item Working with these objects involves calling functions on them, and the
\ident{\_(N)} macro does this for me. If you look at the code
\verb|monster->_(attack)(monster, damage)| you see that I'm using the
macro, which gets replaced with \verb|monster->proto.attack(monster, damage)|.
Study this transformation again by rewriting these calls back to their
original. Also, if you get stuck then run \program{cpp} manually to see what it's
going to do.
\item I'm using two new functions \func{srand} and \func{rand}, which setup
a simple random number generator good enough for the game. I also use
\func{time} to initialize the random number generator. Research those.
\item I use a new function \func{getchar} that gets a single character
from the stdin. Research it.
\end{enumerate}
\section{What You Should See}
Here's me playing my own game:
\begin{code}{ex19 output}
\begin{lstlisting}
<< d['code/ex19.out'] >>
\end{lstlisting}
\end{code}
\section{Auditing The Game}
As an exercise for you I have left out all of the \func{assert} checks
I normally put into a piece of software. You've seen me use \func{assert} to
make sure a program is running correctly, but now I want you to go back
and do the following:
\begin{enumerate}
\item Look at each function you've defined, one file at a time.
\item At the top of each function, add \func{asserts} that make sure
the input parameters are correct. For example, in \func{Object\_new}
you want a \verb|assert(description != NULL)|.
\item Go through each line of the function, and find any functions
being called. Read the documentation (man page) for that function,
and confirm what it returns for an error. Add another assert to
check that the error didn't happen. For example, in \func{Object\_new}
you need one after the call to \func{calloc} that does \verb|assert(el != NULL)|.
\item If a function is expected to return a value, either make sure it returns
an error value (like NULL), or have an assert to make sure that the returned
variable isn't invalid. For example, in \func{Object\_new}, you need
to have \verb|assert(el != NULL)| again before the last return since
that part can never be NULL.
\item For every \ident{if-statement} you write, make sure there's an else
clause unless that if is an error check that causes an exit.
\item For every \ident{switch-statement} you write, make sure that there's
a \ident{default} case that handles anything you didn't anticipate.
\end{enumerate}
Take your time going through every line of the function and find any errors you
make. Remember that the point of this exercise is to stop being a "coder" and
switch your brain into being a "hacker". Try to see how you could break it,
then write code to prevent it or abort early if you can.
\section{Extra Credit}
\begin{enumerate}
\item Update the \file{Makefile} so that when you do \program{make clean}
it will also remove the \file{object.o} file.
\item Write a test script that works the game in different ways and augment
the \file{Makefile} so you can run \program{make test} and it'll thrash
the game with your script.
\item Add more rooms and monsters to the game.
\item Put the game mechanics into a third file, compile it to .o, and then
use that to write another little game. If you're doing it right
you should only have a new \ident{Map} and a \func{main} function
in the new game.
\end{enumerate}
Jump to Line
Something went wrong with that request. Please try again.