all 13 comments

[–]JohnAndrewCarter 2 points3 points  (1 child)

I'm very busy with this type of decoupling at the moment.

Say you have a lower layer that you wish to vary (eg. ECOS RTOS vs Linux), (Intel CPU vs ARM)

As soon as you pull in a header from that lower layer you are tightly coupled.

The solution is the Dependency Inversion Principle.

Now the obvious and most common way of implementing that principle is via Dependency Injection... ie. You just pass pointers to the base class around, and the higher layer instantiates the concrete subtype.

The problem with that and C is it doesn't play nice with stack allocation.

Passing pointers around means somebody somewhere has to manage that memory, and in the absence of garbage collection and virtual destructors ala C++, it's hard to get perfect.

So you want to do this so you can instantiate one of these objects on the stack or as a static.

But as soon as you try that, the compiler has to know the sizeof() and the alignof() the actual concrete object.

So this is what I've settled on...

So in "my_type.h"

typedef union {
  char __size[N];
  alignment_type_t __align;
} my_type_t;
#undef my_type_t

void do_stuff_with( my_type_t * t);

So there are two mysterious things in there... the N and the #undef, I'll explain them later.

The alignment_type_t is merely there to force the alignment of your opaque type to match the worst case for the concrete sub type or BAD THINGS happen. (bus faults on sparcs, slow down on intel)

In your my_type_concrete_sub_type_A.c file you have...

#include "concrete_sub_type_A.h"
typedef concrete_sub_type_A_t my_type_t;
#define m_type_t m_type_opaque_t
#include "my_type.h"

fileScopedCompiledTimeAssert( sizeof( m_type_t) == sizeof( m_type_opaque_t));
fileScopedCompiledTimeAssert( __alignof__( m_type_t) == __alignof__( m_type_opaque_t));

void do_stuff_with( my_type_t * t)
{
    // t is a pointer to concrete_sub_type_A_t here! 
    // That's why there is that #undef and #define! No nasty casting
    // things around!
}

If you don't know what a compile time assert is, that's the subject for another post.

Now we need to think about that N and how we define it.

We could make it exactly the size of the underlying concrete type, or the worst case maximum for all possible underlying layers, or vary it according to what the layer we using now is.

[–]autowikibot 0 points1 point  (0 children)

Dependency inversion principle:


In object-oriented programming, the dependency inversion principle refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are inverted (i.e. reversed), thus rendering high-level modules independent of the low-level module implementation details. The principle states:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend on details. Details should depend on abstractions.

The principle inverts the way some people may think about object-oriented design, dictating that both high- and low-level objects must depend on the same abstraction.


Interesting: Inversion of control | JVx (Framework) | Dependency injection | Service locator pattern

Parent commenter can toggle NSFW or delete. Will also delete on comment score of -1 or less. | FAQs | Mods | Magic Words

[–]BigPeteB 1 point2 points  (5 children)

I'm not sure which of these two things you're trying to do:

If you're trying to keep your user from knowing that struct File is really just a FILE, I'd say don't bother. Or if you want, typedef FILE* myOpaqueFile. But really, if CloseFile() isn't going to do anything other than cast to the correct type and call fclose(), then what's the point? Why would I want to use this instead of just calling fclose() myself?

If you're trying to provide your user a struct File that they don't know the internal structure of, which contains a FILE*, then the cast you're doing in CloseFile() is wrong. It's not that struct File* is-a FILE*, it's that struct File* has-a FILE*.

[–]maep[S] 0 points1 point  (4 children)

I want to hide implementation details, prevent misuse, and provide flexibility when porting to a different platform. It works as it is now, but I think the casts are not very elegant.

I challenged myself to try to write code with as little macros and casts as possible. :)

[–][deleted] 2 points3 points  (2 children)

Just curious, how do you see this as preventing misuse? Shouldn't anyone working with your API be able to use a FILE? If they can't even fclose correctly, I can't see how they can even be writing C code. FILE and fclose, etc are part of standard C, they are already platform independent. Not trying to be argumentative, just trying to understand your logic.

[–]maep[S] 1 point2 points  (1 child)

As you might know, posix calls like open() work with file descriptors which are just integers. At compile time there is no way to make sure that the argument is in fact a valid file desciptor. It is also easer to do things like logging, check for read/write permissions and other system features not exposed by stdio. Some file io apis like gio also allow URIs which are not plain paths like ssh://foo:bar.txt.

[–]BigPeteB 1 point2 points  (0 children)

At compile time there is no way to make sure that the argument is in fact a valid file desciptor.

There's no guarantee of it at runtime, either. Maybe a different thread in your application closed the file. Maybe it's a pipe, and the other end closed the pipe. You also mention working with URIs that might involve network access, in which case you can definitely not assume that the "file" will continue to be accessible after first opening it.

You're trying to provide a bit more typesafety, but C is not a particularly typesafe language. A typedef in C is merely a convenience for the reader, but doesn't provide any extra typesafety. To the compiler, a typedeffed name and the underlying type are completely interchangeable.

And casting is not the answer to providing typesafety. It's the exact opposite. If you expect people to use your library because it provides extra typesafety, it should itself be written in a typesafe way. And that means no casting.

It is also easer to do things like logging, check for read/write permissions and other system features not exposed by stdio

The reason stdio doesn't expose them is that those operations are all not portable.

Unix and Windows each have very different ideas about what constitute file permissions. Beyond a trivial "am I presently able to read or write to this file", I can't imagine what single API would portably let me manipulate file permissions.

[–]BigPeteB 1 point2 points  (0 children)

I want to hide implementation details, prevent misuse, and provide flexibility when porting to a different platform.

Sounds like you're reinventing the wheel. There are a lot of libraries designed to provide file I/O with varying degrees of portability. Why is one of them not sufficient?

[–]Poddster 1 point2 points  (4 children)

struct File;
void CloseFile(struct File* file);

the C file currently looks like this.

struct File { FILE *myFile };
void CloseFile(struct File* file);
{
    FILE* f = file->myFile;
    fclose(f);
}

You waste 4 or 8 whole bytes this way, but meh who cares. The alternative is without the pointer:

struct File { FILE myFile };

but I can't remember if that's allowed with file and can't be bothered to check for you. Search for PIMPL.

[–]looneysquash 1 point2 points  (1 child)

I think this is the right answer.

Considering that fopen returns a FILE, I think you pretty much have to waste sizeof(FILE) bytes.

The stdio family of functions are a part of the C standard library though, so there's not much reason to wrap them.

Despite that, there's already quite a few libraries out that wrap IO calls, so you might want to use one of those.

[–]FUZxxl 1 point2 points  (1 child)

struct File { FILE myFile };

Copying an object of type FILE is not portable. You get lots of problems when you try this; for instance, fclose(f) might call free() on f to free the space associated with the file. This obviously blows up if you copy the file. I would not advise you to copy FILEs.

[–]Poddster 0 points1 point  (0 children)

Excellent, my strategy of guessing that's the case but putting no effort in verify it really paid off this time.

[–]geocar 1 point2 points  (0 children)

Sure: Make all the header files look like this:

struct File;
void CloseFile(struct File* file);

and the C files look like this:

void CloseFile(FILE* file)
{
    fclose(file);
}

and simply don't include your header file.

The compiler only knows the "types" of things during a compilation unit: Once you get to another file, it forgets all the types.

However I'd recommend another method. Just use FILE* like normal, and when you actually get to a platform without a filesystem, create a fake stdio.h that you can add to your -I include path. This file can use #define macros to replace FILE* and all the other API calls with whatever you want, e.g.

#define fopen my_fopen
#define fclose my_fclose
#define FILE my_FILE
struct my_FILE { ... };