Technique: Encapsulation and Information Hiding in C

Encapsulation is usually associated with object-oriented programming languages, and information hiding is not straightforward to achieve in C. However, these two principles can still be applied by creating interfaces that work on “opaque” structures.

The first step is to forward-declare a structure in a header file. This structure will NOT have a definition in the header file, so users cannot declare this type directly in their code. They can only work with pointers to this type.

// Opaque circular buffer structure
typedef struct circular_buf_t circular_buf_t;

The definition for this structure will be kept inside of the corresponding .c file. This will ensure that we can change the structure definition as needed, without requiring end users to update their code. The users only need to know that this type of structure exists.

We often provide a corresponding “handle” type that is mapped as a pointer to this opaque structure, a void *, or a uintptr_t. This is a better indication that the user cannot directly dereference the type.

// Handle type, the way users interact with the API
typedef circular_buf_t* cbuf_handle_t;
Note

If not using a pointer to the structure as the handle type, then inside of the implementation you would cast to the appropriate type.

You could also completely eliminate the struct forward definition and instead only supply a generic void * or uintptr_t handle type.

All public interfaces will be declared in the header file and must operate on the handle type. All private interfaces and data will be defined in the corresponding .c file so they are not visible to consumers of the header.

When working with opaque types, an initialization function is often required in order to generate a valid handle.

/// Pass in a storage buffer and size 
/// Returns a circular buffer handle 
cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size);
Note

Recall that since our opaque struct has no public definition in the .h file, users cannot declare these structures on their own. If you do not want to utilize dynamic memory allocation (common in embedded software), you can instead statically pre-allocate a pool of objects inside of your .c file. Be sure to assert or return a NULL value if the assignment fails because all pre-allocated objects are in use. Also indicate to users that they should check for failure.

That handle is then passed as an input parameter to all public functions associated with the opaque type:

/// Reset the circular buffer to empty, head == tail
void circular_buf_reset(cbuf_handle_t me);

/// Put version 1 continues to add data if the buffer is full
/// Old data is overwritten
void circular_buf_put(cbuf_handle_t me, uint8_t data);

A “destructor” is usually required in order to free any previously allocated data:

/// Free a circular buffer structure. 
/// Does not free data buffer; owner is responsible for that 
void circular_buf_free(cbuf_handle_t me);

Summary

  • Forward-declare structures or provide handle typedefs to users
  • Declare an initialization function that can generate an “object” of the appropriate type and return the handle to the user
  • Declare a destructor that can free any memory allocated to the “object” with the associated handle
  • All public interfaces are declared in the header file and operate on the handle type
  • All private interfaces and data is declared within the .c file (without extern!) so that external modules cannot access them

References

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.