Implementing Interfaces and Polymorphism in C Language

Core InsightAlthough C language does not support classes and virtual functions, by cleverly combining function pointers with structures, we can fully implement interface abstraction and polymorphism similar to C++. This is not only a technical skill but also the best way to deeply understand the underlying mechanisms of object-oriented programming.

1. Why Implement Polymorphism in C?

As an embedded engineer, I often face scenarios where I need to write drivers for multiple different sensors (DHT11, DS18B20, BMP280). If I write a separate set of code for each sensor, it not only leads to repetitive work but also requires the upper application to write different calling logic for different sensors.

In C++, we can define a base class for sensors, and each specific sensor can inherit from this base class, achieving polymorphism through virtual functions. But in a pure C environment (such as embedded bare metal or Linux kernel modules), we have no classes, no inheritance, and no virtual functions. What should we do?

💡 Practical NeedWe need a mechanism: to define a unified interface so that different implementations can be called in the same way. This is the essence of polymorphism— “the same interface, different implementations”.

2. Function Pointers: The Cornerstone of Polymorphism

2.1 Basics of Function Pointers

In C language, function pointers allow us to pass and call functions as variables. This is the key technology for implementing polymorphism.

/* Define function pointer type */
typedef int (*SensorReadFunc)(void* ctx, float* value);

/* DHT11 read function */
int dht11_read(void* ctx, float* value) {
    // DHT11 specific reading logic
    *value = 25.5f;
    return 0;
}

/* DS18B20 read function */
int ds18b20_read(void* ctx, float* value) {
    // DS18B20 specific reading logic
    *value = 26.3f;
    return 0;
}

/* Unified call */
void process_sensor(SensorReadFunc read_func, void* ctx) {
    float value;
    read_func(ctx, &value);  // Polymorphic call!
    printf("Temperature: %.1f\n", value);
}

2.2 Function Pointer Table: Simulating Virtual Function Table

The virtual function mechanism in C++ is fundamentally implemented through a virtual function table (vtable). We can explicitly create a similar structure in C:

/* Sensor operation interface - similar to C++ pure virtual function interface */
typedef struct {
    int (*init)(void* ctx);
    int (*read)(void* ctx, float* value);
    int (*write)(void* ctx, float value);
    void (*destroy)(void* ctx);
} SensorOps;

/* Sensor base class structure */
typedef struct {
    const char* name;           /* Sensor name */
    void* context;              /* Private data pointer */
    const SensorOps* ops;       /* Operation interface (virtual function table)*/
} Sensor;

/* Unified API - polymorphic call */
int sensor_read(Sensor* sensor, float* value) {
    if (sensor && sensor->ops && sensor->ops->read) {
        return sensor->ops->read(sensor->context, value);
    }
    return -1;
}

Key Understanding<span>SensorOps</span> structure is our “virtual function table”, containing a set of function pointers. Different sensors implement different sets of functions, but through the unified <span>sensor_read()</span> interface call, polymorphism is achieved.

3. Complete Practical Case: Device Driver Interface Design

Let’s design a more practical case: a generic communication bus interface. In embedded systems, we often need to support various communication methods such as SPI, I2C, and UART, but we want the upper-level code to call them in a unified way.

3.1 Interface Definition (comm_interface.h)

#ifndef COMM_INTERFACE_H
#define COMM_INTERFACE_H

#include <stdint.h>
#include <stddef.h>

/* Communication device operation interface (virtual function table)*/
typedef struct CommOps {
    int (*init)(void* ctx);
    int (*send)(void* ctx, const uint8_t* data, size_t len);
    int (*recv)(void* ctx, uint8_t* buffer, size_t len);
    int (*deinit)(void* ctx);
} CommOps;

/* Communication device base class */
typedef struct CommDevice {
    const char* name;           /* Device name: SPI/I2C/UART */
    void* context;              /* Private data */
    const CommOps* ops;         /* Operation interface */
} CommDevice;

/* Unified API interface (similar to virtual function call)*/
int comm_init(CommDevice* dev);
int comm_send(CommDevice* dev, const uint8_t* data, size_t len);
int comm_recv(CommDevice* dev, uint8_t* buffer, size_t len);
int comm_deinit(CommDevice* dev);

#endif /* COMM_INTERFACE_H */

3.2 Interface Implementation (comm_interface.c)

#include "comm_interface.h"

/* Initialize device */
int comm_init(CommDevice* dev) {
    if (!dev || !dev->ops || !dev->ops->init) {
        return -1;
    }
    return dev->ops->init(dev->context);
}

/* Send data - polymorphic call */
int comm_send(CommDevice* dev, const uint8_t* data, size_t len) {
    if (!dev || !dev->ops || !dev->ops->send) {
        return -1;
    }
    /* Call different implementations based on dev->ops */
    return dev->ops->send(dev->context, data, len);
}

/* Receive data - polymorphic call */
int comm_recv(CommDevice* dev, uint8_t* buffer, size_t len) {
    if (!dev || !dev->ops || !dev->ops->recv) {
        return -1;
    }
    return dev->ops->recv(dev->context, buffer, len);
}

/* Destroy device */
int comm_deinit(CommDevice* dev) {
    if (!dev || !dev->ops || !dev->ops->deinit) {
        return -1;
    }
    return dev->ops->deinit(dev->context);
}

3.3 SPI Specific Implementation (spi_driver.c)

#include "comm_interface.h"
#include <stdio.h>
#include <stdlib.h>

/* SPI private data structure */
typedef struct {
    uint8_t spi_bus;      /* SPI bus number */
    uint8_t chip_select;  /* Chip select pin */
    uint32_t speed;       /* Clock frequency */
} SPIContext;

/* SPI initialization */
static int spi_init(void* ctx) {
    SPIContext* spi = (SPIContext*)ctx;
    printf("SPI Initialization: Bus %d, Chip Select %d, Speed %d Hz\n",
           spi->spi_bus, spi->chip_select, spi->speed);
    /* Actual SPI hardware initialization code... */
    return 0;
}

/* SPI send */
static int spi_send(void* ctx, const uint8_t* data, size_t len) {
    printf("SPI Sending %zu bytes of data\n", len);
    /* Actual SPI sending code... */
    return (int)len;
}

/* SPI receive */
static int spi_recv(void* ctx, uint8_t* buffer, size_t len) {
    printf("SPI Receiving %zu bytes of data\n", len);
    /* Actual SPI receiving code... */
    return (int)len;
}

/* SPI destroy */
static int spi_deinit(void* ctx) {
    printf("SPI Closing\n");
    return 0;
}

/* SPI operation interface implementation (virtual function table)*/
static const CommOps spi_ops = {
    .init = spi_init,
    .send = spi_send,
    .recv = spi_recv,
    .deinit = spi_deinit
};

/* Create SPI device instance (factory function)*/
CommDevice* create_spi_device(uint8_t bus, uint8_t cs, uint32_t speed) {
    /* Allocate device structure */
    CommDevice* dev = (CommDevice*)malloc(sizeof(CommDevice));
    if (!dev) return NULL;

    /* Allocate private data */
    SPIContext* ctx = (SPIContext*)malloc(sizeof(SPIContext));
    if (!ctx) {
        free(dev);
        return NULL;
    }

    /* Initialize private data */
    ctx->spi_bus = bus;
    ctx->chip_select = cs;
    ctx->speed = speed;

    /* Set device properties */
    dev->name = "SPI";
    dev->context = ctx;
    dev->ops = &spi_ops;  /* Bind operation interface */

    return dev;
}

3.4 I2C Specific Implementation (i2c_driver.c)

#include "comm_interface.h"
#include <stdio.h>
#include <stdlib.h>

/* I2C private data structure */
typedef struct {
    uint8_t i2c_bus;       /* I2C bus number */
    uint8_t slave_addr;    /* Slave address */
} I2CContext;

/* I2C initialization */
static int i2c_init(void* ctx) {
    I2CContext* i2c = (I2CContext*)ctx;
    printf("I2C Initialization: Bus %d, Slave Address 0x%02X\n",
           i2c->i2c_bus, i2c->slave_addr);
    return 0;
}

/* I2C send */
static int i2c_send(void* ctx, const uint8_t* data, size_t len) {
    printf("I2C Sending %zu bytes of data\n", len);
    return (int)len;
}

/* I2C receive */
static int i2c_recv(void* ctx, uint8_t* buffer, size_t len) {
    printf("I2C Receiving %zu bytes of data\n", len);
    return (int)len;
}

/* I2C destroy */
static int i2c_deinit(void* ctx) {
    printf("I2C Closing\n");
    return 0;
}

/* I2C operation interface implementation */
static const CommOps i2c_ops = {
    .init = i2c_init,
    .send = i2c_send,
    .recv = i2c_recv,
    .deinit = i2c_deinit
};

/* Create I2C device instance */
CommDevice* create_i2c_device(uint8_t bus, uint8_t addr) {
    CommDevice* dev = (CommDevice*)malloc(sizeof(CommDevice));
    if (!dev) return NULL;

    I2CContext* ctx = (I2CContext*)malloc(sizeof(I2CContext));
    if (!ctx) {
        free(dev);
        return NULL;
    }

    ctx->i2c_bus = bus;
    ctx->slave_addr = addr;

    dev->name = "I2C";
    dev->context = ctx;
    dev->ops = &i2c_ops;

    return dev;
}

3.5 Usage Example (main.c)

#include "comm_interface.h"
#include <stdio.h>
#include <stdlib.h>

/* External declaration of factory functions */
extern CommDevice* create_spi_device(uint8_t bus, uint8_t cs, uint32_t speed);
extern CommDevice* create_i2c_device(uint8_t bus, uint8_t addr);

/* Generic device operation function - the power of polymorphism!*/
void device_test(CommDevice* dev) {
    uint8_t tx_data[] = {0x01, 0x02, 0x03};
    uint8_t rx_data[10];

    printf("\n===== Testing Device: %s =====\n", dev->name);

    /* Unified API call, automatically dispatched to different implementations */
    comm_init(dev);
    comm_send(dev, tx_data, sizeof(tx_data));
    comm_recv(dev, rx_data, sizeof(rx_data));
    comm_deinit(dev);
}

int main(void) {
    /* Create SPI device */
    CommDevice* spi_dev = create_spi_device(1, 10, 1000000);

    /* Create I2C device */
    CommDevice* i2c_dev = create_i2c_device(0, 0x48);

    /* Test different devices with the same function - polymorphism!*/
    device_test(spi_dev);
    device_test(i2c_dev);

    /* Clean up resources */
    free(spi_dev->context);
    free(spi_dev);
    free(i2c_dev->context);
    free(i2c_dev);

    return 0;
}

Run Result:

===== Testing Device: SPI =====
SPI Initialization: Bus 1, Chip Select 10, Speed 1000000 Hz
SPI Sending 3 bytes of data
SPI Receiving 10 bytes of data
SPI Closing

===== Testing Device: I2C =====
I2C Initialization: Bus 0, Slave Address 0x48
I2C Sending 3 bytes of data
I2C Receiving 10 bytes of data
I2C Closing

Perfect Manifestation of PolymorphismNote the <span>device_test()</span> function: it is completely unaware of whether the passed device is SPI or I2C, it simply calls the unified API. However, at runtime, based on the different <span>ops</span> bound to the device, the correct implementation function is automatically called. This is polymorphism!

4. Comparison with C++ Implementation

Let’s see how the same functionality is implemented in C++ and the relationship between the two:

4.1 C++ Version Implementation

// Abstract base class for communication devices (interface)
class CommDevice {
public:
    virtual ~CommDevice() {}
    virtual int init() = 0;          // Pure virtual function
    virtual int send(const uint8_t* data, size_t len) = 0;
    virtual int recv(uint8_t* buffer, size_t len) = 0;
    virtual int deinit() = 0;
};

// SPI device implementation class
class SPIDevice : public CommDevice {
private:
    uint8_t spi_bus;
    uint8_t chip_select;
    uint32_t speed;

public:
    SPIDevice(uint8_t bus, uint8_t cs, uint32_t spd)
        : spi_bus(bus), chip_select(cs), speed(spd) {}

    int init() override {
        printf("SPI Initialization...\n");
        return 0;
    }

    int send(const uint8_t* data, size_t len) override {
        printf("SPI Sending data\n");
        return len;
    }

    int recv(uint8_t* buffer, size_t len) override {
        printf("SPI Receiving data\n");
        return len;
    }

    int deinit() override {
        printf("SPI Closing\n");
        return 0;
    }
};

// Polymorphic call
void device_test(CommDevice* dev) {
    dev->init();    // Virtual function call, runtime binding
    dev->send(...);
    dev->recv(...);
    dev->deinit();
}

4.2 Comparison of the Two

Feature C Language Implementation C++ Implementation
Interface Definition Function pointer structure (manually defined) Abstract base class (compiler supported)
Virtual Function Table Explicitly create ops structure Compiler automatically generates vtable
Polymorphic Call Manually call ops->func() Automatically dispatched through virtual functions
Type Safety Weak typing, requires void* conversion Strong type checking
Memory Management Manual malloc/free Constructor/destructor automatically managed
Code Complexity Requires more boilerplate code Simpler and more elegant syntax
Runtime Overhead One indirect call One indirect call (same)
Applicable Scenarios Embedded bare metal, kernel drivers Application layer, environments with C++ support

⚠️ Performance ComparisonThe runtime performance of both implementations is exactly the same! Both perform one indirect call through function pointers. The virtual functions in C++ are fundamentally implemented using a similar mechanism, but the compiler does this work for you.

4.3 C++ Virtual Function Table Revealed

Let’s take a closer look at what the C++ compiler actually does:

// C++ code
class Base {
public:
    virtual void func1() { printf("Base::func1\n"); }
    virtual void func2() { printf("Base::func2\n"); }
};

// Equivalent C code generated by the compiler (simplified)
typedef struct Base_VTable {
    void (*func1)(struct Base* this);
    void (*func2)(struct Base* this);
} Base_VTable;

typedef struct Base {
    Base_VTable* vptr;  // Virtual function table pointer (automatically added by compiler)
    // Other member variables...
} Base;

// Virtual function implementation
void Base_func1(Base* this) {
    printf("Base::func1\n");
}

// Virtual function table initialization
static Base_VTable base_vtable = {
    .func1 = Base_func1,
    .func2 = Base_func2
};

// Constructor will set vptr
void Base_construct(Base* obj) {
    obj->vptr = &base_vtable;  // Point to virtual function table
}

// Virtual function call
obj->vptr->func1(obj);  // Equivalent to C++'s obj->func1()

💡 Sudden RealizationDo you see it? The virtual function mechanism in C++ is essentially the function pointer table we manually implemented! C++ just automates this process and provides better syntax sugar. Understanding the implementation in C language truly helps you grasp the underlying principles of object-oriented programming.

5. Advanced Techniques: Applying Design Patterns in C

5.1 Factory Pattern

We have already used the factory function <span>create_spi_device()</span><span> in the previous examples. This is a typical application of the factory pattern:</span>

/* Device type enumeration */
typedef enum {
    DEVICE_TYPE_SPI,
    DEVICE_TYPE_I2C,
    DEVICE_TYPE_UART
} DeviceType;

/* Factory function - create different devices based on type */
CommDevice* create_device(DeviceType type, ...) {
    switch (type) {
        case DEVICE_TYPE_SPI:
            return create_spi_device(...);
        case DEVICE_TYPE_I2C:
            return create_i2c_device(...);
        case DEVICE_TYPE_UART:
            return create_uart_device(...);
        default:
            return NULL;
    }
}

5.2 Singleton Pattern

In embedded systems, some resources are globally unique (such as system clocks, unique displays). The singleton pattern can ensure that only one instance exists:

/* System clock singleton */
typedef struct {
    uint32_t frequency;
    uint8_t initialized;
} SystemClock;

/* Get singleton instance */
SystemClock* get_system_clock(void) {
    static SystemClock instance = {0};  // Static variable, initialized only once
    static uint8_t first_call = 1;

    if (first_call) {
        instance.frequency = 72000000;  // 72MHz
        instance.initialized = 1;
        first_call = 0;
    }

    return &instance
}

/* Usage */
SystemClock* clk = get_system_clock();
printf("System frequency: %u Hz\n", clk->frequency);
}

5.3 Observer Pattern (Callback Mechanism)

When an event occurs, notify all subscribers. This is very common in event-driven programming:

/* Event callback function type */
typedef void (*EventCallback)(void* user_data);

/* Event manager */
typedef struct {
    EventCallback callbacks[10];  /* Up to 10 subscribers */
    void* user_data[10];
    int count;
} EventManager;

/* Subscribe to event */
void event_subscribe(EventManager* mgr, EventCallback cb, void* data) {
    if (mgr->count < 10) {
        mgr->callbacks[mgr->count] = cb;
        mgr->user_data[mgr->count] = data;
        mgr->count++;
    }
}

/* Trigger event - notify all subscribers */
void event_trigger(EventManager* mgr) {
    for (int i = 0; i < mgr->count; i++) {
        if (mgr->callbacks[i]) {
            mgr->callbacks[i](mgr->user_data[i]);
        }
    }
}

6. Best Practices and Considerations

6.1 Memory Management

Common Pitfalls:

  • • Forgetting to check malloc return value
  • • Forgetting to free memory pointed to by context pointer
  • • Using already freed device objects

Correct Destroy Function:

void destroy_device(CommDevice* dev) {
    if (!dev) return;

    /* 1. Call deinit to clean up hardware resources */
    if (dev->ops && dev->ops->deinit) {
        dev->ops->deinit(dev->context);
    }

    /* 2. Free private data */
    if (dev->context) {
        free(dev->context);
        dev->context = NULL;  // Prevent dangling pointer
    }

    /* 3. Free the device structure itself */
    free(dev);
}

6.2 Type Safety

Using <span>void*</span><span> loses type checking, which can be enhanced by adding type identifiers:</span>

typedef struct {
    uint32_t magic;  /* Magic number for type validation */
    /* Other private data... */
} SPIContext;

#define SPI_MAGIC 0x53504930  /* 'SPI0' */

static int spi_init(void* ctx) {
    SPIContext* spi = (SPIContext*)ctx;

    /* Type check */
    if (spi->magic != SPI_MAGIC) {
        fprintf(stderr, "Error: Invalid SPI context\n");
        return -1;
    }

    /* Normal initialization... */
    return 0;
}

6.3 Thread Safety

In a multithreaded environment, shared resources need to be protected:

#include <pthread.h>

typedef struct {
    pthread_mutex_t lock;
    /* Other data... */
} ThreadSafeDevice;

int safe_send(ThreadSafeDevice* dev, const uint8_t* data, size_t len) {
    pthread_mutex_lock(&dev->lock);

    /* Critical section: send data */
    int result = /* Actual send operation */;

    pthread_mutex_unlock(&dev->lock);
    return result;
}

7. Practical Application Scenarios

Where are these techniques widely used?

  • Linux Kernel: VFS (Virtual File System), Network Protocol Stack, Device Driver Model
  • Embedded RTOS: Device Driver Framework, Message Queues, Task Scheduling
  • Open Source Libraries: libusb, libuv, SDL, etc.
  • Firmware Development: HAL Layer Abstraction, BSP Adaptation Layer

7.1 Linux Kernel Case: File Operation Interface

/* File operation structure in Linux kernel (simplified) */
struct file_operations {
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    int (*release)(struct inode *, struct file *);
    /* More operations... */
};

/* Different file systems implement different file_operations */
static struct file_operations ext4_file_operations = {
    .read = ext4_file_read,
    .write = ext4_file_write,
    .open = ext4_file_open,
    .release = ext4_file_release,
};

/* When a user calls read() system call, the kernel dispatches through file_operations polymorphically */
ssize_t vfs_read(struct file *file, char __user *buf, size_t count) {
    if (file->f_op && file->f_op->read)
        return file->f_op->read(file, buf, count, &file->f_pos);
    return -EINVAL;
}

8. Conclusion

Through this article, we have gained a deep understanding of how to implement interface abstraction and polymorphism in C language. The core points are summarized as follows:

🎯 Core Technology Summary

  1. 1. Function Pointer Table: Simulates C++ virtual function table, defines a unified interface
  2. 2. Structure Encapsulation: Contains function pointer table and private data, implementing a concept similar to “object”
  3. 3. Factory Function: Creates instances of specific implementations, hiding construction details
  4. 4. Unified API: Upper-level code calls through a consistent interface, with automatic polymorphic dispatch at the lower level

Applicable Scenarios

  • Embedded Bare Metal Development: No C++ support, requires modular design
  • Kernel Driver Development: Linux/RTOS kernel programming
  • Cross-Platform Libraries: Need to support unified interfaces across different platforms
  • Performance-Sensitive Scenarios: Avoid C++’s additional overhead (though the difference is minimal)
  • Not Suitable: Application layer development, prefer C++ when available

Learning Suggestions

  1. 1. Practice is Key: Start with small projects and gradually apply these techniques
  2. 2. Read Source Code: Study implementations of Linux kernel drivers and open-source libraries
  3. 3. Comparative Learning: Learn OOP in C++ simultaneously to understand the relationship between the two
  4. 4. Focus on Design: Design clear interfaces first, then implement specific functionalities

⚠️ Avoid Over-DesignNot all code needs such abstraction. For simple functionalities, implement directly. Only introduce these techniques when you need to support multiple implementations or when the code needs to be highly modular.

9. Reference Resources

  • • “Understanding the Linux Kernel” – Detailed introduction to object-oriented design in the Linux kernel
  • • “C Interfaces and Implementations” – David R. Hanson
  • Linux kernel source code: <span>include/linux/fs.h</span> (File system interface)
  • Linux kernel source code: <span>drivers/</span> directory (Device driver examples)

Conclusion“Understanding how C language implements polymorphism not only allows you to write better C code but also helps you truly understand how object-oriented mechanisms work in higher-level languages like C++ and Java. Mastery of underlying principles is a reflection of technical depth.”

If this article has helped you, feel free to like, share, and spread it to more friends!

If you have questions, feel free to leave comments for discussion, let’s improve together!

Leave a Comment