Working at a software vendor which provides an SDK for its products, I learned quite a bit about APIs and how to implement them. Despite the global move towards webservice interfaces to connect components of different languages, a clean C interface can still provide a superior alternative.

Some of the reasons why a C interface still is a good alternative are:

  • C interfaces are fast.
  • C interfaces are cross-language compatible.
  • C interfaces can have low footprint.

C APIs can have low footprint

A C API is a simple dll/so file. It's as large or as small as you want it to be. It's as cross-platform as you make it. You can cross-compile it on your dev machine and then deploy it on you tiny MIPS router box with 32MiB of RAM. Try that with a Java based SOAP framework. A C interface needs no sockets, no complicated SSL handshakes, no expensive XML/JSON/whatever parsing. It is just a simple call executed by the CPU.

C APIs are fast

Again, a C interface comes in the form of a dynamic library (dll or a so file). When the application loads a dynamic library, the dynamic linker puts the code of the image into the address space of the calling application. The calling application can then call the API functions via function pointers (either manually or through the GPT). Compared to the marshalling/unmarshalling that is required between webservice caller/callee this is multiple orders of magnitudes faster.

C APIs are cross-language compatible

While it's hard to call from one interpreted language into code from another, you can call from most interpreted languages directly into C.

In Java there is JNI which is a bit complicated but still does the job: The JVM execution thread stops running and jumps into your C code. Type conversion happens in the JNI glue code you have to write in C/C++. The JNI even allows you to call code from the JVM in C/C++ (although I don't consider this a good idea, except for data interchange at the interface).

In C# the compiler/runtime provides PInvoke and the DllImport attribute to do straight forward calls into dll functions.

In Python there are various ways to call C: You can write Python extensions manually, you can generate python extension code with Cython or you can call into given dlls with ctypes.

In Perl there also are various ways to call C. Google for Perl XS or Inline::C. While not having used them myself I guess it won't be too hard.

In Go there is Cgo.

In Ruby you can write Ruby extensions in C.

The list goes on.

And if you're fed up with writing these wrappers manually, you can try SWIG which will generate a binding with your code for various target languages, including the languages mentioned above.

Now to the contrary: If you have code in any of these high level / interpreted languages and try to call it from another of these languages you will go through big trouble as you will have to instantiate the corresponding virtual machine (be it the JVM, the CPython interpreter, the .NET runtime, ...) inside or outside your current process and then figure out a way to pass calls between them. Your best bet may be to create a C interface to your non-native code and then call that from your target language. No fun.

Best Practices For C APIs

Now when designing C APIs there are a few things you can do to keep your interface

  • easy to wrap in foreign languages and
  • binary stable, so users can replace the dll/so without re-linking (commonly called ABI stability).

Avoid structs

C comes with a few caveats which are non obvious even for experienced programmers. One of those is padding of structs. Say you have a struct

typedef struct color_t {
  char r;
  char g;
  char b;
} color;

From looking at it you could assume that sizeof(color) == 3. But that's not the case. The compiler is free to expand the size of the struct to basically any size it likes. On 32bit platforms it will usually be a multiple of 4 bytes but it depends on the specific compiler options and #pragma pack switches in the header.

Closely related to padding is alignment. To improve the performance of accessing the members of the struct, the compiler can chose to pad the individual data members in a struct so that each of the members is aligned to a specific boundary (again, mostly 4 bytes on 32bit and 8 bytes on 64bit platforms). So between color.r and color.g there may be 3 or more unused dummy bytes. (In reality, there probably won't be padding between char elements, but between elements of different size, there probably will.)

Now when you want to pass structs through a C interface, you have to make sure that both side agree to the same padding and alignment rules. This is a non-trivial problem.

So, my advice is to basically avoid structs in C interfaces. This will save you a lot of trouble. As a replacement, you can work with handles. Everytime you want your user to create a complex struct-like data transfer object, you should provide him with

color_handle make_color(char r, char g, char b);
void remove_color(color_handle);

functions. color_handle would be a typedef void* color_handle;. To access the individual members, you can provide additional char from_color_get_r(color_handle) functions. This will allow you to fully control the implementation details of the color struct and change it at any time as long as you keep the accessor functions stable. Also this nicely maps to an object orient layer that you might want to put above the C level interface in your target language.

Calling Conventions

Another non-obvious pitfall is the calling convention. Both sides of a C function call have to agree how they exchange parameters on the stack or through registers. The standard calling convention should be cdecl but especially on MS compilers there are some other. Your calling convention also depends on the target platform. Wikipedia has a good article on calling conventions. The point here is, you should controll the calling convention on the library implementation and the higher level language wrapper side so that both sides agree.

Memory Management

Heap memory is a delicate thing. As a mental model, one can think of the operating system as only providing the program with address space and mapped pages. Then the program must figure out a way to manage the pages for allocations of smaller objects via malloc/free or new/delete. This task is accomplished by the allocator, which is usually part of the standard C library. But your caller doesn't have to agree. He may want to use a different allocator for performance or reliability reasons. He might be using a completely different C standard library on his side. And at least on windows, every module in a program has it's own logical heap anyway. When a mismatch between malloc/free happens, disaster is bound to happen. There are two possible solutions to this problem:

  1. Let the user provide a malloc and free function pointer to the library, or
  2. use whatever allocator you choose but make sure that memory from inside the library is never deallocated outside and vice versa.

Solution number one only works with a new project or a very clean codebase where you can change the allocator with minimal amounts of work. I prefer the more practical variant two. In practice you will have to provide your user with make_handle(...) and remove_handle(...) functions for every object you might interchange. As handle type, you can choose void* or even int if you provide an internal handle-to-memory mapping. You might also want to typedef them on different dummy structs so that there won't be any type mismatch on the user side (see the java JNI headers for an excelent example).

typedef struct color_handle_t { int _; } *color_handle;
color_handle make_color(char r, char g, char b);
void remove_handle(color_handle c);

An additional advantage of this strategy is that you also

  • can check the validity of any handle before accessing it,
  • change the implementation of whatever the handle represents, and
  • you can manage handle lifetime and implement opaque reference counting.

You can think of a handle like the this pointer in a C++ class.

Introspection

Traditional C headers often define constants via the C preprocessor #define directive. These preprocessor defines are not accessible by the target language. Thus they have to be duplicated in the target language. This is duplication of knowledge violates the single-point-of-truth programming rule and is a code smell. I prefer to provide

int resolve_define_i(const char *define, int *out_value);
int resolve_define_s(const char *define, char *buffer, size_t *buffer_size);

functions which maps the defines given as C strings to their int/string value. This can be easily implemented in C using the original header defines and removes the burden from the wrapper implementation in the target language.

You might want to provide more introspection functions:

  • int get_version(); to query the version of the library (your API will change, prepare for that early on),
  • int get_function_signature(const char *function, char *buffer, size_t *buffer_size); to query parsable notations of function signatures.

Consistent Error Handling

This is not specific to C ABI stability but to component design in general: Provide your user with consistent error handling across all your APIs functions. For C interfaces the following rule has proven to be working:

All functions should:

  • either return an error code and affect a call-by-reference parameter to provide the output,
  • or return the output and provide an error code to a call-by-reference parameter.
  • Choose one pattern and apply it consistently throughout your API. No exceptions.

When designing a handle-based API, you can attach additional error messages to the handles when an error occurs.

Provide A Higher Level Wrapper

When you think you are done with your library/API I suggest you go on and try to build a first higher level wrapper. I suggest using Python ctypes as it is a good reference for understanding ABI level C wrappers. First provide a very thin function style target-level to C wrapper, then build an object oriented wrapper on top. You may notice a few things in your API that are not optimal. Go fix them. When you are done writing that wrapper, use it to build something (e.g. a simple command line tool). You might notice more issues with your API. Only when you're finished with this, you should consider your first version of the API stable and go forward to make it public.

There are more best practices for designing good C level APIs. I consider these to be the most essential. If you follow them, you're on a good way and most importantly: You can improve on it by providing new functions for satisfying new requirements.