1

I want to create an API in C. My goal is to implement abstractions to access and mutate struct variables that are defined in the API.

API's header file:

#ifndef API_H
#define API_H

struct s_accessor {
        struct s* s_ptr;
};

void api_init_func(struct s_accessor *foo);
void api_mutate_func(struct s_accessor *foo, int x);
void api_print_func(struct s_accessor *foo);

#endif

API' implementation file:

#include <stdio.h>
#include "api.h"

struct s { 
        int internal; 
        int other_stuff;
};

void api_init_func(struct s_accessor* foo) {
        foo->s_ptr = NULL;
}

void api_print_func(struct s_accessor *foo)
{
        printf("Value of member 'internal' = %d\n", foo->s_ptr->internal);
}

void api_mutate_func(struct s_accessor *foo, int x)
{
        struct s bar;
        foo->s_ptr = &bar;
        foo->s_ptr->internal = x;
}

Client-side program that uses the API:

#include <stdio.h>
#include "api.h"


int main()
{
        struct s_accessor foo;
        api_init_func(&foo);  // set s_ptr to NULL

        api_mutate_func(&foo, 123); // change value of member 'internal' of an instance of struct s

        api_print_func(&foo);   // print member of struct s
}

I have the following questions regarding my code:

  1. Is there a direct (non-hackish) way to hide the implementation of my API?

  2. Is this the proper way to create abstractions for the client-side to use my API? If not, how can I improve this to make it better?

Ronak Sharma
  • 109
  • 6
  • if you talking about abstraction you need to talk about encapsulation also, when talking about incapsulation the struct should be exsposed only inthe c file (incomplete type) – Adam Jul 06 '20 at 22:15
  • 1
    why to use extern? you dont need to use it in this case at all. just define the functions signatures without the extern – Adam Jul 06 '20 at 22:16
  • From a convenience perspective it's nice to have a `typedef` to skip the `struct` part when using this thing. – tadman Jul 06 '20 at 22:19
  • 1
    @Adam Thanks for the suggestion. I have removed extern from my header file – Ronak Sharma Jul 06 '20 at 23:08
  • 1
    @tadman Thanks for the suggestion! – Ronak Sharma Jul 06 '20 at 23:08

2 Answers2

2

this is the right way to do abstarction and encapsulation in C applications. use the Incomplete Types in C Language for hiding structure details. You can define structures, unions, and enumerations without listing their members (or values, in the case of enumerations). Doing so results in an incomplete type. You can't declare variables of incomplete types, but you can work with pointer to those types

constness in c lang in evrey function espicialy those that you are exposing in api, that do not change the pointer or the structure data pointed by pointer, better and shall be const pointer. this will ensure (somehow :-) you still can change it in c) to the api user that you are not changing structure data. you can also protect the datat and the address by double const the pointer, seee below:

#ifndef API_H
#define API_H

typedef struct s_accessor s_accessor, *p_s_accessor;

void api_init_func(p_s_accessor p_foo);
void api_mutate_func(p_s_accessor p_foo, int x);
void api_print_func(const p_s_accessor const p_foo);

#endif

in the api.c you can complete the structure type:

struct s { 
        int internal; 
        int other_stuff;
};

all auxilary functions should be static in api.c(limit the fucntions scope to api.c only!

minimise the includes in the api.h.

regarding question 1 idont think there is a way that you can hide the implementaion details!

Adam
  • 2,820
  • 1
  • 13
  • 33
  • `*p_s_accessor;` puff, I would argue that this [typedef pointer is usually considered bad style](https://stackoverflow.com/questions/3781932/is-typedefing-a-pointer-type-considered-bad-practice). Ex. `api_print_func` could take `const s_acecssor*`. As I am more of a linux kernel coding style-guy, subbjectively I would go with just `struct s_accessor *`, I see no need for a typedef here. – KamilCuk Jul 06 '20 at 22:25
  • @KamilCuk - typedef in 99% of situations make code more clear easy to read. regarding the typedef to pointer type i can agree with you. but on the same time c enable us to do so. – Adam Jul 06 '20 at 22:36
  • @KamilCuk Much of the Linux kernel coding style is a bunch of bunk, starting with 8 space hard tabs. – Kaz Jul 06 '20 at 22:41
  • @Adam Thanks for your answer! Will make changes to my code as you suggested – Ronak Sharma Jul 06 '20 at 23:20
  • @my pleasure. Thank you. – Adam Jul 06 '20 at 23:22
2

"Accessor" isn't a good terminology. This term is used in object oriented programming to denote a kind of method.

The structure type struct s_accessor is in fact something called a handle. It contains a pointer to the real object. A handle is a doubly indirect pointer: the application passes around pointers to handles, and the handles contain pointers to the objects.

An old adage says that "any problem in computer science can be solved by adding another layer of indirection", of which handles are a prime example. Handles allow objects to be moved from one address to another or to be replaced. Yet, to the application, the handle address represents the object, and so when the implementation object is relocated or replaced, as far as the application is concerned, it is still the same object.

With a handle we can do things like:

  • have a vector object that can grow

  • have OOP objects that can apparently change their class

  • relocate variable-length objects such as buffers and strings to compact their memory footprint

all without the object changing its memory address and thus identity. Because the handle stays the same when these changes occur, the application does not have to hunt down every copy of the object pointer and replace it with a new one; the handle effectively takes care of that in one place.

In spite of all of that, handles tend to be unusual in C API's, in particular lower-level ones. Given an API that does not use handles, you can whip up handles around it. Even if you think that the users of your object will benefit from handles, it may be good to split up the API into two: an internal one which only deals with s, and the external one with the struct s_handle.

If you're using threads, then handles require careful concurrent programming. So that is to say, even though from the application's point of view, you can change the handle-referenced object, which is convenient, it requires synchronization. Say we have a vector object referenced by a handle. Application code is working with it, so we can't just suddenly replace the vector with a pointer to a different one (in order to resize it). Another thread is just in the middle of working with the original pointer. The operations that access the vector or store values into it through the handle must be synchronized with the replacement operation. Even if all of that is done right, it's going to add a lot of overhead, and so then application people may notice some performance problems and ask for escape hatches in the API, like for some functions function to "pin" down a handle so that the object cannot move while an efficient operation works directly with the s object inside it.

For that reason, I would tend stay away from designing a handle API, and make that sort of thing the application's problem. It may well be easier for a multi-threaded application to just use a well-designed "just the s please" API correctly, than to write a completely thread-safe, robust, efficient struct s_handle layer.

  1. Is there a direct (non-hackish) way to hide the implementation of my API?

Basically the "rule #1" of hiding the implementation of an API in C is not to allow an init operation whereby the client application declares some memory and your API initializes it. That said, it is possible like this:

typedef struct opaque opaque_t;

#ifndef OPAQUE_IMPL

struct opaque {
  int dummy[42]; // big enough for all future extension
} opaque_t;

#endif

void opaque_init(opaque_t *o);

In this declaration, we have revealed nothing to the client, other than that our objects are buffers of memory that require int alignment, and are at least 42 int wide.

In actual fact, the objects are smaller; we have just added a reserve amount for future growth. We can make our actual object larger withotu having to re-compile the clients, as long as our object does not require more than int [42] bytes.

Why we have that #ifndef is that the implementation code will do something like this:

#define OPAQUE_IMPL // suppress the fake definition in the header #include "opaque.h"

// actual definition struct opaque { int whatever; char *name; };

This kind of thing plays it loose with the "law" of ISO C, because effectively the client and implementation are using a different definition of the struct opaque type.

Allowing clients to allocate the objects themselves yields certain efficiencies, because allocating objects in automatic storage (i.e. declaring them as local variables) can place them in the stack with very little overhead compared to dynamic memory allocation.

The more common approach for opaqueness is not to provide an init operation at all, only an operation for allocating a new object and destroying it:

typedef struct opaque opaque_t; // incomplete struct

opaque_t *opaque_create(/* args .... */);
void opaque_destroy(opaque_t *o);

Now the caller knows nothing, other than that an "opaque" object is represented as a pointer, the same pointer over its entire lifetime.

Total opaqueness may not be worth it for an API which is internal to an application or application framework. It's useful for an API that has external clients, like application developers in a different team or organization.

Ask yourself the question: would the client of this API, and its implementation, ever be shipped and upgraded separately? If the answer is no, then that diminishes the need for total opaqueness.

Kaz
  • 55,781
  • 9
  • 100
  • 149
  • Thanks a lot for your answer! Got to learn a lot from your answer especially about handles in general. Based on your suggestion, I won't write a `struct s_handle` layer because I don't want an unnecessary(because in C everything is open) overhead to my code. If I plan to write the `s` only API (without the `struct s_handle` layer), which additional functions do you think I should add to make it a well-designed API? Is adding a mutator function enough? – Ronak Sharma Jul 06 '20 at 23:27
  • @RonakSharma Try developing the API along with a some sample applications (serving as unit tests). The use of the API will reveal what functions are needed. Sometimes if you overly anticipate what is needed, you end up with unused API functions. This is also a design matter; put the code aside and make some sequence diagrams about how an application interacts with the API. – Kaz Jul 07 '20 at 04:08