23 August, 2020

If you have a C API, how do you pass handles to it in a typesafe manner?

Problem

It is fairly common to see C APIs that use an opaque handle to an underlying resource. The idea is to hide the details of the implementation from the caller. This makes a lot of sense, because you want to provide the caller with functionality, but you don’t want them to make arbitrary changes to objects that you own.

But how do you make your handles typesafe?

Don’t do this…

It is tempting, when creating such handles, to typedef them to void*. To illustrate this, let’s say you have a thing called a Gadget, and you want to expose a C API on it. You might do something like this:

typedef void* Gadget;

Gadget CreateGadget();
void ShowGadget(Gadget gadget);
void HideGadget(Gadget gadget);
void DestroyGadget(Gadget gadget);

At first glance, it looks like you’ve achieved your goal. The caller can only get to the underlying resource (a gadget) via the Gadget API.

They can create a gadget with CreateGadget(), manipulate it with ShowGadget() and HideGadget(), and dispose of it with DestroyGadget(). But they can’t do anything else with it, because the gadget is opaque. So far, so good.

But then you introduce a new resource, the Gizmo, and use the same technique to expose a C API on it.

typedef void* Gizmo;

Gizmo CreateGizmo();
void FireGizmo(Gizmo gizmo);
void ReloadGizmo(Gizmo gizmo);
void DestroyGizmo(Gizmo gizmo);

Now users of your API can access gadgets via the Gadget API, and gizmos via the Gizmo API.

Why is this a problem?

What stops users from passing a Gadget to the Gizmo API, or vice versa?

A typedef is just an alias, nothing more.

For example, what happens when they try to compile this?

void ThisIsNotFine()
{
    Gadget g = CreateGadget();  // Create a gadget.
    ...
    DestroyGizmo(g);            // We tried to destroy a Gizmo, but we passed it a Gadget.
}

This code compiles. As far as the compiler is concerned, the code is valid because a void* was passed to a function that expected a void*. This is because a typedef is just an alias, nothing more, so it sees Gadget and Gizmo as the same.

If a user of your API makes a typo (perhaps aided by a “helpful” editor) and accidentally destroys a Gizmo when they meant to destroy a Gadget, then the compiler won’t bat an eyelid.

And at runtime, all bets are off. If your user is very lucky then their code will crash early and spectacularly, and they’ll figure out what went wrong. If they’re unlucky, then it’ll give the appearance of working, only for their program to fail much later in a completely different part of the code.

Solution

Wrap each handle in its own struct

The solution is to ensure that the compiler sees your handle types as different types. One way of doing this is to wrap each handle in its own struct.

Ensure that the compiler sees your handle types as different types.

Here’s what Gadget would look like with this approach.

typedef struct { void* handle; } Gadget;

Gadget CreateGadget();
void ShowGadget(Gadget gadget);
void HideGadget(Gadget gadget);
void DestroyGadget(Gadget gadget);

And here’s Gizmo.

typedef struct { void* handle; } Gizmo;

Gizmo CreateGizmo();
void FireGizmo(Gizmo gizmo);
void ReloadGizmo(Gizmo gizmo);
void DestroyGizmo(Gizmo gizmo);

Now what happens when the user of your API makes a typo and tries to destroy a Gizmo when they meant to destroy a Gadget?

void ThisIsNotFine()
{
    Gadget g = CreateGadget();
    DestroyGizmo(g);
}

Well, the Gadget and Gizmo types might be structurally identical, but they are not the same type. The problem will be caught at compile time. In other words, the code won’t compile.

error C2664: 'void DestroyGizmo(Gizmo)': cannot convert argument 1 from 'Gadget' to 'Gizmo'
message : No user-defined-conversion operator available that can perform this conversion, or the
          operator cannot be called
message : see declaration of 'DestroyGizmo'

Not only that, but the compiler will almost certainly give a helpful error message (such as the one above) stating that you can’t turn a Gadget into a Gizmo.