Home > OS >  C - Exposing 3rd party data structures in a library's public header file
C - Exposing 3rd party data structures in a library's public header file

Time:09-29

I'm writing a library that wraps around a REST API and representing some of the data requires use of data structures like hash maps that are not available in the C standard library so internally I'm using 3rd party libraries for their implementations. Now I'd like to expose them in a response struct, I've come up with a couple of solutions:

  1. Just expose the type as it is and add a dependency on the 3rd party lib and make the user call the appropriate API functions (Eg hashmap_lib_get(users, "key") to get a value):
// header.h (Include guards excluded)
#include <hashmap_lib.h>

struct api_response {
    int field1;
    char *field2;
    HASHMAP_LIB_HM *users; // Map of users to their email. (example)
};

Issue: Requires extra user intervention, even more problematic in case the library is implemented using generic macros.

  1. Expose the type as a void pointer inside the struct and write duplicated wrappers for all appropriate functions:
// header.h
struct api_response {
    int field1;
    char *field2;
    void *users; // Don't touch
};

char *api_hashmap_get(void *hm, const char *key);

The internal implementation of api_hashmap_get would just be return hashmap_lib_get((HASHMAP_LIB_HM *) hm, key);

Issue: Requires duplication of various function definitions and requires the use of void pointers.

How is this problem usually solved in libraries ? The first solution would make sense if both the libraries were "related" with each other (Eg using an eventing system such as libevent to work with the user's application), but in this case it's just about general data structures so it doesn't make sense to me since the user would be using other libs for the same type of data structures aswell.

CodePudding user response:

I would characterize the pros & cons differently.:

  • option 1 incorporates the third-party lib's API into your own's. There are both technical and legal reasons to approach that with caution.

    • On the technical side, you make your dependency on the third-party lib much harder to break.
    • Also, you tie your API to a specific version or range of versions of the third-party lib. The best way to deal with this is probably to vendor a copy of the other lib, but that brings its own issues.
    • On the legal side, there may be implications on your library's licensing, depending on the license of the third-party lib. For example, if the 3p lib is licensed under the LGPL, then just dynamically linking to it does not require your lib to be licensed under a GPL-compatible license, but incorporating part or all of its API into your own probably does introduce such a requirement.
  • option 2 involves providing your own API for manipulating the third-party data structures, and requires you to drop some type safety. On the other hand, it insulates your users from the 3p lib.

You remark in comments

I'd like sugestions about improving upon option 2 since it feels too verbose.

, but if you're not going to provide for users to access the third-party data structures via their native means then you have no alternative but to provide your own means. And if you want to isolate your clients from the third-party lib then that has to manifest as wrapper functions. You don't necessarily have to wrap the whole third-party API, however, nor to express your wrappers as direct analogues of third-party functions / macros.

But there is also this:

  • option 3: make your own structure opaque to your library's clients. That would require you to provide suitable functions for all the kinds of accesses you support, so even more work for you on that front, but it allows you to declare the structure members (for internal use only) however you think is most natural. Plus, it might feel more consistent for all the accesses to go through functions instead of just some of them. And this makes it easy for you to change the details of your structure without breaking your lib's clients.

    The client-facing header might look like this:

    struct api_response; // members not declared
    
    int api_get_field1(const struct api_response *resp);
    const char *api_get_field2(const struct api_response *resp);
    const char *api_get_user_email(const struct api_response *resp, const char *user);
    // ...
    

    That would be supplemented by an internal-only, non-distributed header providing (at least) the full definition of struct api_response.

  • Related