4

Context

We have three files:

  • module.h: it holds the declaration of a structure,
  • module.c: it holds the definition of the structure,
  • main.c: it holds an instance of the structure.

The goal is to use a structure in main.c by using an API (module.h) and not directly by manipulating the structure members. It is why the definition of the structure is in module.c and not in module.h.

Code

module.h

#ifndef MODULE_H
#define MODULE_H

typedef struct test_struct test_struct;

void initialize_test_struct(int a, int b, test_struct * test_struct_handler);

#endif

module.c

#include "module.h"

struct test_struct
{
    int a;
    int b;
};

void initialize_test_struct(int a, int b, test_struct * test_struct_handler)
{
    test_struct_handler->a = a;
    test_struct_handler->b = b;
}

main.c

#include "module.h"

int main(void)
{
    test_struct my_struct;  // <- GCC error here
    test_struct * my_struct_handler = &my_struct;

    initialize_test_struct(1, 2, my_struct_handler);

    return 0;
}

Problem

If we compile those files with GCC, we will get the following error:

main.c:7:17: error: storage size of ‘my_struct’ isn’t known

Question

How can we force to use an API and so forbid to use directly a structure's members to manipulate a structure, the structure declaration and definition being in a different module than the main.c?

Mat.R
  • 174
  • 1
  • 13
  • 1
    You want to use an [opaque structure](https://stackoverflow.com/questions/3965279/opaque-c-structs-how-should-they-be-declared). – Andrew Henle Dec 27 '18 at 16:14

4 Answers4

4

Since the definition of test_struct is not visible to your main function, you cannot create an instance of this object nor can you access its members. You can however create a pointer to it. So you need a function in module.c that allocates memory for an instance and returns a pointer to it. You'll also need functions to read the members.

In module.h:

test_struct *allocate_test_struct();
int get_a(test_struct *p);
int get_b(test_struct *p);

In module.c:

test_struct *allocate_test_struct()
{
    test_struct *p = malloc(sizeof(test_struct));
    if (!p) {
        perror("malloc failed");
        exit(1);
    }
    return p;
}

int get_a(test_struct *p) 
{
    return p->a;
}

int get_b(test_struct *p)
{ 
    return p-b;
}

In main.c:

test_struct * my_struct_handler = allocate_test_struct()

initialize_test_struct(1, 2, my_struct_handler);

printf("%d\n", get_a(my_struct_handler));
printf("%d\n", get_b(my_struct_handler));

free(my_struct_handler);
dbush
  • 205,898
  • 23
  • 218
  • 273
  • It answers the question but could I ask if it is easy to avoid dynamic memory allocation? – Mat.R Dec 27 '18 at 16:27
  • 1
    @Mat.R If the definition is not visible, absolutely not. You can't create an object you don't know the size of. The only way around this would be if module.c contained a static array of these structures and you returned the address of one of those instead of calling `malloc`, and that lacks flexibility and requires managing the list. – dbush Dec 27 '18 at 16:28
  • Do you mean I cannot avoid to use dynamic memory allocation if I do not want to allow direct manipulation of structure's members? – Mat.R Dec 27 '18 at 16:30
  • You could have the c file return a pointer to a global or static object. But in this case you would not want the user to call free() on the returned pointer. – Shawn Dec 27 '18 at 16:31
  • 1
    @Mat.R Aside from using static members in module.c, pretty much. There's nothing wrong with using dynamic allocation, and lots of libraries do exactly this. – dbush Dec 27 '18 at 16:33
0

If we compile those files with GCC, we will get the following error:

main.c:7:17: error: storage size of ‘my_struct’ isn’t known

The reason why structures are inside the header files in the first place is so that the user source (here main.c) can access its members...

Your compiler does not know the address of the definition-less struct typedef struct test_struct test_struct; and so &my_struct won't give you its address!

And without an address, you can not get the size of any variable!


Explanation:

test_struct my_struct;

Here, you make a variable of an incomplete type and hence is not valid and doesn't have an address since its members are inaccessible...

test_struct * my_struct_handler = &my_struct;

And here you pass the address of my_struct which unnotably is not possible to gain (The structure is definition-less inside the header and the source is compiled so it can't be accessed either)...

So you use pointers in this case so that a temporary address can be assigned to the incomplete type:

/* Don't forget to change to
 *   'void initialize_test_struct(int a, int b, test_struct ** test_struct_handler)'
 *   in the header file!
 */
void initialize_test_struct(int a, int b, test_struct ** test_struct_handler)
{
    // Allocate an undefined address to the pointer...
    *test_struct_handler = malloc(sizeof(test_struct));
    (*test_struct_handler)->a = a;
    (*test_struct_handler)->b = b;
}

// The declarations have to be present inside the headers as well...
// A function that returns the pointer to the variables a and b respectively...
// These functions can readily change their values while returning them...
int * get_a_ref(test_struct * test_struct_handler)
{
    return &test_struct_handler->a;
}

int * get_b_ref(test_struct * test_struct_handler)
{
    return &test_struct_handler->b;
}

and use it in main like this:

#include <stdio.h>

#include "module.h"

int main(void)
{
    test_struct * my_struct_handler;

    // Here a address is malloc'd to the pointer and the value is assigned to it...
    initialize_test_struct(1, 2, &my_struct_handler);

    // Will change the value of 'a' and similar for b as well...
    // *get_a_ref(my_struct_handler) = 10; 

    printf("%d\n", *get_a_ref(my_struct_handler));
    printf("%d\n", *get_b_ref(my_struct_handler));

    return 0;
}

Just to remind you about the magic (and obscurity) of typedefs...

Community
  • 1
  • 1
Ruks
  • 3,886
  • 1
  • 10
  • 22
0

You cannot instantiate a test_struct directly with only the include file because the details are not known when the C file is processed in the compiler. The language will only let you initialize pointers to objects of unknown size, not the objects themselves. The size and other details of test_struct are only known by the compiler when processing module.c

To get around this, you need to have module.c allocate data and provide a pointer to it in the initialize call. This means you have to either have the initialize function return a pointer to a newly created object (one that was either malloc'd or a global or static object), or have the function accept a test_struct **:

void initialize_test_struct(int a, int b, test_struct ** test_struct_handler)
{
    *test_struct_handler = malloc(sizeof(test_struct));
    //Do rest of init.  You should also check return value of malloc
}

//Alternatively
test_struct * initialize_test_struct(int a, int b)
{
    test_struct *temp;
    temp = malloc(sizeof(test_struct);
    //Init members as needed
    return temp;
}

Normally in this situation the typedef is for a pointer to the opaque structure, and named to indicate that it's not the struct itself - 'typedef struct test_struct* test_struct_handle' could be appropriate, as the struct name itself is rather useless to users of the module (except to make pointers).

It's also good practice to:

  • Have accessor functions if you need them (which you do for your main file - see dbush's answer)
  • Have a 'de-init'/'free' function. The user does not necessarily know that malloc was used, and having a de-init function will make it possible to hide more implementation details.
Shawn
  • 621
  • 5
  • 10
0

First, what is it that you think clients of your library want to do that your API doesn’t support, and they’ll be tempted to manipulate the fields of your structure to accomplish? Consider extending the API.

One option is to force client code to get its structures from your factory function, rather than allocate them itself.

Another is to create a phony definition, perhaps containing only an array of char to establish a minimum size and alignment, so that the client code knows just enough to allocate an array of them, and the library module itself can cast the pointer and twiddle the bits.

Another is to put the definition in the header and add a comment saying that the fields are internal implementation details that might change.

Another is to port to an object-oriented language.

Davislor
  • 14,674
  • 2
  • 34
  • 49