cpointersstructlinkeropaque-pointers

How does linking work in C with regards to opaque pointers?


So, I've been having a bit of confusion regarding linking of various things. For this question I'm going to focus on opaque pointers.

I'll illustrate my confusion with an example. Let's say I have these three files:

main.c

#include <stdio.h>
#include "obj.h"            //this directive is replaced with the code in obj.h

int main()
{
    myobj = make_obj();
    setid(myobj, 6);

    int i = getid(myobj);
    printf("ID: %i\n",i);

    getchar();
    return 0;
}

obj.c

#include <stdlib.h>

struct obj{
    int id;
};

struct obj *make_obj(void){
    return calloc(1, sizeof(struct obj));
};

void setid(struct obj *o, int i){
    o->id = i;
};

int getid(struct obj *o){
    return o->id;
};

obj.h

struct obj;

struct obj *make_obj(void);

void setid(struct obj *o, int i);

int getid(struct obj *o);

struct obj *myobj;

Because of the preprocessor directives, these would essentially become two files:

(I know technically stdio.h and stdlib.h would have their code replace the preprocessor directives, but I didn't bother to replace them for the sake of readability)

main.c

#include <stdio.h>

//obj.h
struct obj;
struct obj *make_obj(void);
void setid(struct obj *o, int i);
int getid(struct obj *o);
struct obj *myobj;

int main()
{
    myobj = make_obj();
    setid(myobj, 6);

    int i = getid(myobj);
    printf("ID: %i\n",i);

    getchar();
    return 0;
}

obj.c

#include <stdlib.h>

struct obj{
    int id;
};

struct obj *make_obj(void){
    return calloc(1, sizeof(struct obj));
};

void setid(struct obj *o, int i){
    o->id = i;
};

int getid(struct obj *o){
    return o->id;
};

Now here's where I get a bit confused. If I try to make a struct obj in main.c, I get an incomplete type error, even though main.c has the declaration struct obj;.

Even if I change the code up to use extern, It sill won't compile:

main.c

#include <stdio.h>

extern struct obj;

int main()
{
    struct obj myobj;
    myobj.id = 5;

    int i = myobj.id;
    printf("ID: %i\n",i);

    getchar();
    return 0;
}

obj.c

#include <stdlib.h>

struct obj{
    int id;
};

So far as I can tell, main.c and obj.c do not communicate structs (unlike functions or variables for some which just need a declaration in the other file).

So, main.c has no link with struct obj types, but for some reason, in the previous example, it was able to create a pointer to one just fine struct obj *myobj;. How, why? I feel like I'm missing some vital piece of information. What are the rules regarding what can or can't go from one .c file to another?

ADDENDUM

To address the possible duplicate, I must emphasize, I'm not asking what an opaque pointer is but how it functions with regards to files linking.


Solution

  • Converting comments into a semi-coherent answer.

    The problems with the second main.c arise because it does not have the details of struct obj; it knows that the type exists, but it knows nothing about what it contains. You can create and use pointers to struct obj; you cannot dereference those pointers, not even to copy the structure, let alone access data within the structure, because it is not known how big it is. That's why you have the functions in obj.c. They provide the services you need — object allocation, release, access to and modification of the contents (except that the object release is missing; maybe free(obj); is OK, but it's best to provide a 'destructor').

    Note that obj.c should include obj.h to ensure consistency between obj.c and main.c — even if you use opaque pointers.

    I'm not 100% what you mean by 'ensuring consistency'; what does that entail and why is it important?

    At the moment, you could have struct obj *make_obj(int initializer) { … } in obj.c, but because you don't include obj.h in obj.c, the compiler can't tell you that your code in main.c will call it without the initializer — leading to quasi-random (indeterminate) values being used to 'initialize' the structure. If you include obj.h in obj.c, the discrepancy between the declaration in the header and the definition in the source file will be reported by the compiler and the code won't compile. The code in main.c wouldn't compile either — once the header is fixed. The header files are the 'glue' that hold the system together, ensuring consistency between the function definition and the places that use the function (references). The declaration in the header ensures that they're all consistent.

    Also, I thought the whole reason why pointers are type-specific was because the pointers need the size which can vary depending on the type. How can a pointer be to something of unknown size?

    As to why you can have pointers to types without knowing all the details, it is an important feature of C that provides for the interworking of separately compiled modules. All pointers to structures (of any type) must have the same size and alignment requirements. You can specify that the structure type exists by simply saying struct WhatEver; where appropriate. That's usually at file scope, not inside a function; there are complex rules for defining (or possibly redefining) structure types inside functions. And you can then use pointers to that type without more information for the compiler.

    Without the detailed body of the structure (struct WhatEver { … };, where the braces and the content in between them are crucial), you cannot access what's in the structure, or create variables of type struct WhatEver — but you can create pointers (struct WhatEver *ptr = NULL;). This is important for 'type safety'. Avoid void * as a universal pointer type when you can, and you usually can avoid it — not always, but usually.

    Oh okay, so the obj.h in obj.c is a means of ensuring the prototype being used matches the definition, by causing an error message if they don't.

    Yes.

    I'm still not entirely following in terms of all pointers having the same size and alignment. Wouldn't the size and alignment of a struct be unique to that particular struct?

    The structures are all different, but the pointers to them are all the same size.

    And the pointers can be the same size because struct pointers can't be dereferenced, so they don't need specific sizes?

    If the compiler knows the details of the structure (there's a definition of the structure type with the { … } part present), then the pointer can be dereferenced (and variables of the structure type can be defined, as well as pointers to it, of course). If the compiler doesn't know the details, you can only define (and use) pointers to the type.

    Also, out of curiosity, why would one avoid void * as a universal pointer?

    You avoid void * because you lose all type safety. If you have the declaration:

    extern void *delicate_and_dangerous(void *vptr);
    

    then the compiler can't complain if you write the calls:

    bool *bptr = delicate_and_dangerous(stdin);
    struct AnyThing *aptr = delicate_and_dangerous(argv[1]);
    

    If you have the declaration:

    extern struct SpecialCase *delicate_and_dangerous(struct UnusualDevice *udptr);
    

    then the compiler will tell you when you call it with a wrong pointer type, such as stdin (a FILE *) or argv[1] (a char * if you're in main()), etc. or if you assign to the wrong type of pointer variable.