2020-09-09

Extensible C APIs

The problem we want to solve is how to add functionality to an existing C API in an elegant way. You may have seen the practice to add one extra parameter at the end of a function call, reserved for future use. It must always be 0 in the first version of the API. The implementation should enforce it to be 0 to avoid unpleasant surprises in the future.

uint32_t do_work(int32_t a, uint8_t b, void* rfu);

Later, when additional requirements make it necessary to vary how "work is done" and the variation needs one additional parameter, uint16_t c. It is added in the second version of the API.

struct do_work_extensionA {
    uint16_t c;
    void* rfu;
};
uint32_t do_work(int32_t a, uint8_t b, do_work_extensionA* extA);

When extA is 0 do_work() should behave exactly like the first version to be backwards compatible. Existing users can keep their code as it is until they need the new functionality.

You may have spotted the pattern, and to be sure we will add a third version of the API with yet more parameters.

struct do_work_extensionB {
    uint8_t* d;
    uint32_t e;
    void* rfu;
};
struct do_work_extensionA {
    uint16_t c;
    do_work_extensionB* extB;
};
uint32_t do_work(int32_t a, uint8_t b, do_work_extensionA* extA);

An alternative to the extension scheme described above is to use multiple entry points.

uint32_t do_work(int32_t a, uint8_t b);
uint32_t do_work_extA(int32_t a, uint8_t b, uint16_t c);
uint32_t do_work_extB(int32_t a, uint8_t b, uint16_t c, uint8_t* d, uint32_t e);

You decide which method suits you best.

No comments:

Debugging with Popper