C Objects
This article looks at the options for implementing objects in the C language from the C API user’s point of view. This is the first from a series of articles that looks at how to deal with such APIs, with emphasis on correctly using such APIs (which is sadly too often not the case).
Introduction
The C language does not have objects in the classical sense, but C language APIs commonly implement what looks like objects by providing sets of functions that together allow the user to construct, use and then destroy data structures.
The set of C language functions belonging logically to an object manage the object through a handle. The handle is usually some pointer to the object’s data.
There is a constructor function, which you need to call first. It initializes an object instance and returns the object handle. To use the object, you pass the object handle back to other functions, which use it to access the object data, in addition to the other arguments. To free resources at the end, you need to call a destructor function with the object handle. After that, the handle should not be used any more.
For a concrete example, here are four function declarations from stdio.h
:
1
2
3
4
5
6
FILE * fopen(const char * filename, const char * mode);
size_t fread(void * ptr, size_t size, size_t count, FILE * stream);
size_t fwrite(const void * ptr, size_t size, size_t count, FILE * stream);
int fclose(FILE * stream);
fopen
is the constructor function. It opens a file and returns a FILE
pointer as the object handle. If called again (e.g. to open another file), it
returns a pointer to a new FILE
instance. Use fread
and fwrite
to read or
write a buffer to the file. Pass them the appropriate FILE
pointer, as the
handle, in addition to the three arguments related to the buffer. Call fclose
at the end to free resources associated with a handle.
Documentation of fwrite
might include an example on how to use
it that shows a call sequence fopen
before fwrite
, then fclose
at the
end:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main ()
{
char buffer[] = { 'x' , 'y' , 'z' };
FILE * f = fopen("dst.bin", "wb");
fwrite(buffer, 1, sizeof(buffer), f);
fclose(f);
}
The example above is wrong: for example it ignores that fopen
might fail
to open the file and continues regardless.
Handles
In the example above, the constructor function fopen
returns a FILE *
. In
this case the handle is an explicit pointer. Implementation of say fwrite
probably accesses fields from the FILE
structure e.g. it would access the
_flag
member of the FILE
structure like stream->_flag
.
Some APIs use an opaque approach. For example in Windows CreateEvent
returns
a HANDLE
. It is still a pointer because HANDLE
is defined as void *
.
When you pass theHANDLE
to a function like SetEvent
, it presumably
reinterprets it like a pointer to a internal data type that CreateEvent
created.
1
2
3
4
typedef void * HANDLE;
HANDLE CreateEvent(...);
BOOL SetEvent(HANDLE h);
Sometimes the handle is even more opaque (not a pointer) or just plain not a
pointer. For example in POSIX, open
returns an int
:
1
int open(const char * pathname, int flags);
Handles usually have an invalid value. Often a null pointer indicates that the constructor failed.
1
2
3
4
5
FILE * f = fopen(...);
if (0 == f) // or better compare against nullptr
{
// handle error
}
Sometimes the invalid value might be different from null, and there could be
more than one invalid value for a handle type. For example CreateEvent
returns
NULL
in case of error. However CreateFile
returns in case
of error INVALID_HANDLE_VALUE
, which is -1
, even if the return type is the
same as CreateEvent
. Ensure you check for the right invalid value depending
on the constructor function.
That -1
requires a reinterpret_cast
sometimes. constexpr
does not like
reinterpret_cast
.
Also the POSIX open
returns -1
to indicate an error.
1
2
3
4
5
6
7
8
9
10
11
HANDLE e = CreateEvent(...);
if (NULL == e)
{
// handle error
}
// but
HANDLE f = CreateFile(...);
if (INVALID_HANDLE_VALUE == f)
{
// handle error
}
Construction
There might be more than one constructor for a handle type. Sometimes an
additional constructor returns essentially the same object type, but it might
have different or additional arguments. This is the case of CreateEventEx
versus CreateEvent
or freopen
versus fopen
.
In other cases there is an implicit hierarchy and another constructor is used to create a different type in the hierarchy.
1
2
3
4
5
6
7
8
HANDLE CreateEvent(...);
BOOL SetEvent(HANDLE h);
HANDLE CreateFile(...);
BOOL ReadFile(HANDLE h, ...);
DWORD WaitForSingleObject(HANDLE h, ...);
BOOL CloseHandle(HANDLE h);
The set of functions above suggests a hierarchy where file objects and event
objects are some sort of waitable objects. SetEvent
is for event objects
created with CreateEvent
. ReadFile
is for file objects created with
CreateFile
. WaitForSingleObject
is for waitable objects. CloseHandle
is
for all object types.
The object handle is not always returned, some APIs return an error code
and receive a pointer to the object handle e.g. fopen_s
:
1
2
3
4
5
6
7
8
errno_t fopen_s(FILE ** streamptr, const char * filename, const char * mode);
FILE * f;
errno_t result = fopen_s(&f, ...);
if (0 != result)
{
// handle error
}
For this style of constructor functions you need to understand what happens if you pass a unitialized value like in the example above and how the handle value could change if the constructor function returns an error.
Usage
After the object is successfully constructed it can be used by passing the
handle to other functions, usually as the first argument. That is similar to
C++ object member methods where the this
pointer is passed by the compiler as
a hidden first argument.
Functions like fread
and fwrite
, where the object handle is the last
argument, are the odd case.
Sometimes C API implementations check that the object handle is not a null
pointer and return some error code in that case instead of crashing the program
(as fwrite
would usually do). Don’t rely on this behaviour, don’t use the
object handle if the constructor fails.
Destruction
Failing to destruct objects returned by constructing functions leads to resource leaks (at least memory if nothing else).
If you check for a null pointer before destroying the object, remember that not
all constructors return a null pointer (see CreateFile
above).
Similar to the usage, the C API implementation might check that the object
handle is not a null pointer. Don’t rely on this behaviour (e.g. by not
performing the check yourself) unless you’re sure it’s safe. E.g. it is safe
to call delete
with a null pointer in C++.
And finally, not all handles need destructing. For example stdio.h
also defines:
1
2
3
FILE * stdin;
FILE * stdout;
FILE * stderr;
There are many scenarios where you might want to perform operations on these
handles (e.g. fread
on stdin
) without closing them (there are also
scenarios where you might want to close them).
Similarly for registry handles in Windows, there are special values like
HKEY_LOCAL_MACHINE
that you want to use as argument to APIs like
RegOpenKeyExW
, but not close HKEY_LOCAL_MACHINE
at the end of the usage
(only the handle provided back from RegOpenKeyExW
).
Summary
We’ve looked at the variations C APIs typically use to implement objects. In the following articles we’ll look at techniques to use the C APIs correctly and effectively.