Class Implementation Design

All classes and files in the CATS package library follow a predictable naming convention to make their development and use simpler. A class name in the CATS library, referred to generically as CatsClass, always begins with "Cats", followed by the class' functional description using distinct capitalized words. These distinct words are mapped to underscore-separated tokens in the corresponding source and header filenames of the class implementation. So, for example, the class CatsDelayLine is implemented in the source file cats_delay_line.c and header file cats_delay_line.h, respectively.

CATS Package Library Classes

As is typical of object-oriented C, the header file associated with CatsClass includes headers of other classes referenced in the header, specifies external function prototypes, and declares a class structure that may contain data declarations, declarations of other classes, and embedded method prototypes. Referring again to the CatsDelayLine example, the corresponding header file cats_delay_line.h has the following form:

…

typedef struct CatsDelayLinePrivate_str *CatsDelayLinePrivate;
typedef struct CatsDelayLine_str *CatsDelayLine;

CatsDelayLine CatsDelayLine_create(int bufferSize);

struct CatsDelayLine_str {
  CatsDelayLinePrivate private;

  CatsDelayLine (*destroy)(CatsDelayLine this);
  int (*readable)(CatsDelayLine this);
  int (*read)(CatsDelayLine this, char *b, int off, int len);
  int (*write)(CatsDelayLine this, char *b, int off, int len);
  void (*flush)(CatsDelayLine this);
  int (*getBufferSize)(CatsDelayLine this);
};

…
Note first that there is a typedef that defines a handle to the class structure, allowing us to refer to CatsDelayLine by name in code. Likewise, a handle is defined to refer to CatsDelayLine's private data. Private data handles in the CATS package library are always named CatsClassPrivate.

Another common feature of all CATS package classes is the constructor CatsClass_create, which (as you might expect) creates an instance of CatsClass. We will refer to an instance of CatsClass as catsObject[1]. In addition to creating an instance, constructors are expected to perform whatever global initialization is necessary on the basis of input arguments, and to return a handle to the instance or NULL if the constructor fails.

Given an instance catsObject, note that all elements of catsObject, including methods, are accessed via standard C structure pointer dereference, e.g.

CatsDelayLine delayLine;

delayLine = CatsDelayLine_create(0);
delayLine->destroy(delayLine);
instantiates a CatsDelayLine object with a buffer size of zero, then immediately destroys it. This code segment illustrates two important features of classes in the CATS package library:

Finally, consider an implementation of a class such as CatsDelayLine through excerpts of its source file cats_delay_line.c:

…

static CatsDelayLine CatsDelayLine_destroy(CatsDelayLine this);
static int CatsDelayLine_readable(CatsDelayLine this);

…

struct CatsDelayLinePrivate_str {
  CatsRingBuffer buffer;
};

…

CatsDelayLine CatsDelayLine_create(int bufferSize)
{
  CatsDelayLine this=NULL;

  if ((this = (CatsDelayLine) calloc(1,sizeof(struct CatsDelayLine_str)))
      != NULL) {
    CatsDelayLinePrivate private=NULL;
    char zero=0;

    this->destroy = CatsDelayLine_destroy;
    this->readable = CatsDelayLine_readable;

…

    if ((private = this->private =
         (CatsDelayLinePrivate)
	 calloc(1,sizeof(struct CatsDelayLinePrivate_str))) == NULL)
      return(this->destroy(this));

    if ((private->buffer = CatsRingBuffer_create(bufferSize)) == NULL)
      return(this->destroy(this));

…

  return this;
}

static CatsDelayLine CatsDelayLine_destroy(CatsDelayLine this)
{
  if (this->private != NULL) {
    CatsDelayLinePrivate private=this->private;
    if (private->buffer != NULL)
      private->buffer->destroy(private->buffer);
    free(this->private);
  }
  free(this);
  return NULL;
}

static int CatsDelayLine_readable(CatsDelayLine this)
{
  return this->private->buffer->readable(this->private->buffer);
}
The constructor allocates a class structure, allocates its private data structure, populates itself with any aggregate classes it owns, destroying itself if any part of those operations fail.

Note that class methods (both private and public) in the CATS package library are implemented statically according to the naming convention CatsClass_method, and public methods are assigned to the CatsClass structure's method pointers, thus ensuring that methods are only accessible through an instance of CatsClass. Note also that the private data structure is declared in the class' source file, thus preventing (as you might expect) its access outside the context of the class source file. The destructor "tears down" an instance of the class, freeing any memory allocated by the class.

Interface Classes

As discussed earlier, interfaces comprise a special case of classes in the CATS package library. While the Java™ terminology "interface" is used to indicate that these classes contain no data of their own and are solely provided as a template to be implemented by other classes, they are in fact abstract classes since object-oriented C does not support interfaces. This distinction is clear from the context.

An example of an interface class is the CatsEventListener interface class. As the name implies, this interface provides the means for a class to register to receive event notifications from another class, without the notifying class being aware of the specific class of the listener. Consider the header file cats_event_listener.h:

…

#include "cats_event.h"

CatsEventListener CatsEventListener_create(void *source);

struct CatsEventListener_str {
  CatsEventListenerPrivate private;
  CatsEventListener (*destroy)(CatsEventListener this);
  void *(*getSource)(CatsEventListener this);
  void (*eventGenerated)(CatsEventListener this, CatsEvent event);
};

…
Note that all interface constructors take a single parameter, source, which is the handle of the object implementing the interface in question. As discussed earlier, this handle is required to provide the call context of interface functions, and is stored in the private data of an interface instance. At construction time, all interface functions are initialized to NULL - the implementing class is expected to assign appropriate function pointers to the interface class structure.

Plug-in Classes

In Java™ parlance, plug-in classes correspond to classes that realize interface classes - as the name implies, these interface classes allow the implementing class to be plugged in to other classes in a generic manner. The implementation of a plug-in class is best illustrated by example. Consider the following excerpt from the CatsController class, which implements a CatsEventListener instance to listen to events from a CatsCapture object it creates:

…

static void CatsController_captureEventGenerated(CatsEventListener listener,
						 CatsEvent event);

…

struct CatsControllerPrivate_str {

…

  CatsEventListener captureListener;

…

}

…

CatsController CatsController_create(int argc, char **argv, CatsServer server)
{

…

    if ((capture = private->capture = CatsCapture_create(this)) == NULL)
      return(this->destroy(this));

…

    if ((private->captureListener = CatsEventListener_create(this)) == NULL)
      return(this->destroy(this));
    private->captureListener->eventGenerated =
      CatsController_captureEventGenerated;
    capture->addListener(capture,private->captureListener);

…

}

…

static void CatsController_captureEventGenerated(CatsEventListener listener,
						 CatsEvent event)
{
  CatsController this=listener->getSource(listener);

  switch (event->eventType) {
  case CATS_CAPTURE_EVENT:
    switch(event->eventID) {
    case CATS_RECORD_STARTED:
      this->private->recordStarted = GAL_TRUE;
      break;
    case CATS_RECORD_STOPPED:
      this->private->recordStarted = GAL_FALSE;
      break;
    case CATS_CAPTURE_LINE_OPENED:
      this->private->captureLineOpen = GAL_TRUE;
      break;
    case CATS_CAPTURE_LINE_CLOSED:
      this->private->captureLineOpen = GAL_FALSE;
      break;
    }
    break;
  case CATS_VAD_EVENT:
    switch(event->eventID) {
    case CATS_VAD_STARTED:
      this->private->vadStarted = GAL_TRUE;
      break;
    case CATS_VAD_STOPPED:
      this->private->vadStarted = GAL_FALSE;
      break;
    }
    break;
  }
}

…
The activity in the constructor provides an example of how to create and use an interface object. The function CatsController_captureEventGenerated illustrates how to write an interface method. Since the interface object is passed as a parameter of the method, the first step of the implementing function is to extract the stored call context from the interface object using its getSource method. After that point, all methods and data in the source object, including private data, are available to the implementation.

Finally, it is worth noting that object-oriented C requires interface objects to be exposed by implementing classes before they can be used. When such interface objects are stored as private data, this requires the implementation of a "getter" method in the class.

Notes

[1]

As in Java™, the suggested convention of class instances is to begin their names with lower case letters, to avoid confusion with class names.