Design Patterns in C: Adapter Pattern

7 min

The adapter pattern solves a common problem: you have some code that expects one interface, and some other code that provides a different interface, and you need them to work together without modifying either. The adapter sits in the middle, translating between the two.

In object-oriented languages, this usually involves classes and inheritance. In C, we achieve the same thing with function pointers and structs. The principle is identical — only the implementation differs.

A Practical Scenario

Let’s say you’re building an audio application. You’ve defined a clean interface for audio sources:

// audio_source.h
#ifndef AUDIO_SOURCE_H
#define AUDIO_SOURCE_H

#include <stddef.h>

// Your application's audio source interface
typedef struct AudioSource {
    // Read samples into buffer, return number of samples read
    size_t (*read_samples)(struct AudioSource *self, float *buffer, size_t count);

    // Get the sample rate
    int (*get_sample_rate)(struct AudioSource *self);

    // Clean up resources
    void (*destroy)(struct AudioSource *self);

    // Pointer to implementation-specific data
    void *impl_data;
} AudioSource;

#endif

Your audio pipeline expects AudioSource objects. It calls read_samples() to pull audio data, and everything works nicely.

NOTE

In C, a function’s name is actually a pointer to where that function’s code lives in memory. When you write printf(“hello”), you’re calling the function via its address. Function pointers let you store that address in a variable, pass it around, and call whatever function it happens to point to. The struct in the above example stores three function pointers: read_samples, get_sample_rate, and destroy; which means each AudioSource instance can have its own set of functions wired up. When you create an MP3 adapter, you point these at your MP3-specific functions. When you create a WAV adapter, you point them at WAV-specific functions. The calling code doesn’t know or care which functions are actually being called; it just calls source->read_samples(source, buffer, count) and the right thing happens.

The impl_data field is the other half of the puzzle. It’s a void *, which means it can point to anything. Each adapter uses this to store whatever internal state it needs — the MP3 adapter stores a pointer to its MP3Decoder, the WAV adapter might store file handle and format information, and so on. When one of the function pointers is called, it casts impl_data back to the appropriate type and uses it.

This is how you get polymorphism in C: a generic struct that presents a uniform interface, combined with a void pointer that lets each implementation carry its own specific data. The consumer of the interface just sees AudioSource and calls methods on it; the implementation details are hidden behind those function pointers and that void pointer.

Now you want to add support for a third-party MP3 decoding library. The problem is that this library has its own interface that looks nothing like yours:

// Hypothetical third-party MP3 library
typedef struct {
    void *internal_state;
    int channels;
    int sample_rate;
} MP3Decoder;

// Their functions
MP3Decoder *mp3_open(const char *filename);
int mp3_decode_frame(MP3Decoder *dec, short *pcm_out, int max_samples);
void mp3_close(MP3Decoder *dec);

The library uses short samples instead of float. It has different function names. The structure is completely different. You can’t pass an MP3Decoder to code expecting an AudioSource.

You could modify your audio pipeline to handle MP3Decoder as a special case, but that’s messy. You could modify the third-party library, but you don’t control it. The adapter pattern gives you a third option: wrap the incompatible interface to make it compatible.

Building the Adapter

The adapter needs to present the AudioSource interface while internally using the MP3Decoder. Here’s how:

// mp3_adapter.c
#include <stdlib.h>
#include "audio_source.h"
#include "mp3_library.h"  // The third-party library

// Internal structure holding the adapted decoder
typedef struct {
    MP3Decoder *decoder;
} MP3AdapterData;

// Adapter implementation of read_samples
static size_t mp3_adapter_read(AudioSource *self, float *buffer, size_t count) {
    MP3AdapterData *data = (MP3AdapterData *)self->impl_data;

    // Temporary buffer for the library's short samples
    short *temp = malloc(count * sizeof(short));
    if (!temp) return 0;

    // Call the third-party library
    int samples_read = mp3_decode_frame(data->decoder, temp, (int)count);

    // Convert from short (-32768 to 32767) to float (-1.0 to 1.0)
    for (int i = 0; i < samples_read; i++) {
        buffer[i] = temp[i] / 32768.0f;
    }

    free(temp);
    return (size_t)samples_read;
}

// Adapter implementation of get_sample_rate
static int mp3_adapter_get_sample_rate(AudioSource *self) {
    MP3AdapterData *data = (MP3AdapterData *)self->impl_data;
    return data->decoder->sample_rate;
}

// Adapter implementation of destroy
static void mp3_adapter_destroy(AudioSource *self) {
    MP3AdapterData *data = (MP3AdapterData *)self->impl_data;
    mp3_close(data->decoder);  // Clean up the third-party decoder
    free(data);                 // Clean up our adapter data
    free(self);                 // Clean up the AudioSource struct
}

// Factory function: create an AudioSource from an MP3 file
AudioSource *audio_source_from_mp3(const char *filename) {
    // Open the MP3 using the third-party library
    MP3Decoder *decoder = mp3_open(filename);
    if (!decoder) return NULL;

    // Allocate our adapter data
    MP3AdapterData *data = malloc(sizeof(MP3AdapterData));
    if (!data) {
        mp3_close(decoder);
        return NULL;
    }
    data->decoder = decoder;

    // Allocate and populate the AudioSource interface
    AudioSource *source = malloc(sizeof(AudioSource));
    if (!source) {
        mp3_close(decoder);
        free(data);
        return NULL;
    }

    // Wire up the function pointers
    source->read_samples = mp3_adapter_read;
    source->get_sample_rate = mp3_adapter_get_sample_rate;
    source->destroy = mp3_adapter_destroy;
    source->impl_data = data;

    return source;
}

Now your audio pipeline can use MP3 files without knowing anything about the MP3 library:

// main.c
#include "audio_source.h"

// Declared in mp3_adapter.c
AudioSource *audio_source_from_mp3(const char *filename);

void process_audio(AudioSource *source) {
    float buffer[1024];
    size_t samples;

    while ((samples = source->read_samples(source, buffer, 1024)) > 0) {
        // Process the audio...
    }
}

int main(void) {
    AudioSource *source = audio_source_from_mp3("song.mp3");
    if (source) {
        process_audio(source);  // Works with any AudioSource
        source->destroy(source);
    }
    return 0;
}

What Makes This an Adapter

The key elements are clear if you look at what each piece is doing:

  • The target interface is AudioSource. This is what your existing code expects. You don’t want to change it.

  • The adaptee is MP3Decoder from the third-party library. This is what you want to use, but it doesn’t match your interface.

  • The adapter is the combination of MP3AdapterData and the mp3_adapter_* functions. It implements the target interface while internally delegating to the adaptee. The translation logic (converting short to float, mapping function calls etc.) lives here.

  • The factory function audio_source_from_mp3() constructs the adapter and wires everything together. Client code just calls this and gets back an AudioSource it can use normally.

Adding More Adapters

The real power shows when you add more formats. Each one gets its own adapter:

// Different factory functions, same return type
AudioSource *audio_source_from_mp3(const char *filename);
AudioSource *audio_source_from_wav(const char *filename);
AudioSource *audio_source_from_flac(const char *filename);
AudioSource *audio_source_from_ogg(const char *filename);

Each adapter wraps a different underlying library, handling whatever quirks that library has. Your audio processing code doesn’t care, it just sees AudioSource objects and calls read_samples().

void play_file(const char *filename) {
    AudioSource *source = NULL;

    // Pick the right adapter based on file extension
    if (ends_with(filename, ".mp3")) {
        source = audio_source_from_mp3(filename);
    } else if (ends_with(filename, ".wav")) {
        source = audio_source_from_wav(filename);
    } else if (ends_with(filename, ".flac")) {
        source = audio_source_from_flac(filename);
    }

    if (source) {
        process_audio(source);  // Same code handles all formats
        source->destroy(source);
    }
}

When to Use This Pattern

The adapter pattern is useful when you’re integrating third-party libraries that have their own conventions, maintaining backward compatibility with old interfaces while using new implementations, writing plugins or modular systems where components need a uniform interface, or testing code by swapping in mock implementations that conform to the expected interface.

There’s also a maintenance benefit: the adapter isolates your code from changes in the underlying system. If the MP3 library releases a new version with a different API, maybe mp3_decode_frame becomes mp3_read_pcm with different parameters, you only need to update the adapter. Every part of your codebase that uses AudioSource remains untouched, because it never knew about the MP3 library in the first place. The adapter acts as a buffer between your code and external dependencies, containing the blast radius of changes.

A Simpler Alternative

This pattern does add a layer of indirection, which has a small cost. Don’t use it when a simple wrapper function would suffice, or when you control both sides of the interface and can just make them match directly.

// Simple wrapper, no function pointers
size_t read_mp3_samples(MP3Decoder *dec, float *buffer, size_t count) {
    short temp[count];
    int read = mp3_decode_frame(dec, temp, count);
    for (int i = 0; i < read; i++) {
        buffer[i] = temp[i] / 32768.0f;
    }
    return read;
}

This translates between interfaces without the struct-and-function-pointer machinery. Use the full adapter pattern when you need to treat different implementations uniformly through a common interface; use simple wrappers when you just need to bridge a single gap.

As always, thanks for joining me on this journey into embedded development!