Design Con 2015

APIdeology: Application Programming Interface best practices

Trenton Henry, Silicon Laboratories

September 14, 2011

Trenton Henry, Silicon LaboratoriesSeptember 14, 2011

The public interface of a software component, i.e., an element such as a header file that’s readily visible to the developer, is often the software’s most important feature. Best practices in the development and maintenance of application programming interfaces (APIs), or what we might cleverly call “APIdeology,” dictate that software interfaces do not change once they have been released, with the exception of adding new entry points. Therefore, it is very important to design concise, extensible software interfaces that can be adapted to survive in an evolving hardware/software ecosystem.

APIs tend to follow several general patterns. For designers seeking to choose an appropriate API pattern, it’s helpful to understand the ways in which the choice of pattern, or style, drives the implementation of the software component and the usage model it forces on its clients.

Consider, for example, an API for setting color values for a liquid crystal display (LCD) library. For simplicity, let’s ignore details such as whether the LCD controller is integrated into the MCU or attached via some external bus, as well as power supply and timing considerations. In this idealized scenario, there is a means of plotting a pixel at some coordinate, and any pixel so plotted appears on the display in the "current drawing color." That color is specified by four values: red, green, blue and alpha.

The following formula represents one way to define an API to specify the drawing color:

typedef enum { RED, GREEN, BLUE, ALPHA, RGBA_V, RGB_V } keyword;

bool setFloatValue(keyword key, float value);

Depending on the intended application, there may be several ways to specify a color. Sometimes floating point values may be appropriate, while at other times scaled integer or short values may be desirable.

bool setIntValue(keyword key, int value);

Sometimes it makes sense to pass all of the color values independently, as above. But sometimes it is more appropriate to pass a vector (i.e., a one-dimensional array) of values.

bool setFloatVector(keyword key, float* value);

bool setIntVector(keyword key, float* value);

The number of items contained in a vector parameter is implicit in the keyword. For example, RGB_V expects 3 elements, while RGBA_V expects 4.

Although this API has only a small number of functions, it can be used for more than just setting the values of color components. This API can actually be used to set the values of anything for which a keyword has been defined.

This style is flexible and potentially usable in a wide variety of situations. But that flexibility comes with some complexity. The interface influences the implementation, encouraging each function to employ a switch on the keyword parameter, providing a case for each keyword. More keywords imply more cases in the switch, and thus larger functions.

Furthermore, it is possible that an unrecognized keyword may be passed to a function. Even though the ”keyword” type has an enumerated (“enum”) type definition, most compilers treat enums as signed integers and will not issue a warning if some other compatible value is passed.

This situation implies a need to return some "invalid keyword" error indicator to the caller. Depending on the complexity of an API, the number and type of error that needs to be reported can vary. In this particular case, a simple Boolean indicating success, or failure due to an invalid keyword, is sufficient.

In addition to influencing the implementation, this API design choice imposes a constraint onto the caller as well. The client code calling these functions is obligated to check the returned result to ensure that no inadvertent error has occurred.

There are, of course, alternative API styles that can perform the same task but with different tradeoffs. For example:

void setColor4f(float r, float g, float b, float a);

void setColor3f(float r, float g, float b);

void setColor4i(int r, int g, int b, int a);

void setColor3i(int r, int g, int b);

void setColor4fv(float* v);

void setColor3fv(float* v);

void setColor4iv(int* v);

void setColor3iv(int* v);

This API can accomplish the same effect as the previous API, but it goes about it in a different manner. Previously the function name specified only the type of value, relying on a keyword parameter to indicate exactly which value is affected.

Like the previous example, this second API provides parameter type checking at compile time and provides flexibility to the caller by allowing parameters to be passed individually or as vectors. Its function names are somewhat more meaningful, though, conveying not only the purpose of the function, but also the number and type of arguments expected.

The implementation of each function can be small, simple and efficient. The number and type of each argument is fixed by the API, and thus there is no need for parameter validation and no need to return any sort of error code. The functions of this API cannot fail.

But the API does require a number of very similar functions for each allowed parameter type and parameter passing style. The functions that accept vector parameters have to assume that the pointer they receive is indeed a pointer to the correct number of elements of the appropriate type.

Although the simplicity of the implementation of each function is somewhat offset by the need to provide many such functions, this can become an advantage since sometimes a linker can eliminate functions that are never called. The switch/case implementation encouraged by the previous API offers the linker less opportunity for dead code elimination.

There are, of course, circumstances when it is not possible to design an API whose functions cannot possibly fail. Consider the following example of "pass/fail" style.

bool someFunction(someArguments);

if (NO == someFunction(theseArguments))
             rslt = getError();

This is simple and easily tested for success or failure. But it may not be appropriate in some situations since it does not lend itself to functional composition such as f(g(x)).

The pass/fail style of API makes it easy to report that something has gone wrong, but it is not so easy to report precisely what has gone wrong. For that reason it is not uncommon to see this style combined with some variation of a "get last error" mechanism to help determine the actual cause of the error. As an alternative, consider the following.

someReturnType someFunction(someArguments);

result = someFunction(args);
if (NULL == result)
             handleError();

OR
if (result < 0)
              handleError();
etc.

This style relies on an ability to return distinguished values in order to indicate an error. For example, returning a NULL pointer or a negative number to indicate to the caller that the desired result is not forthcoming. Functions defined with this style can be combined, but only with care. For example, f(g(x)) where g(x) might return NULL necessitates that f() checks for and handles that possibility.

Another concern with the use of distinguished values to indicate an error is that there is often a greater number of possible error causes than there are available distinguished values of a given type. For example, does a -1 return value indicate "out of memory", "end of input", "disk quote exceeded"?

Thus this style of API also suffers from an inability to clearly indicate the cause of the error, and therefore is also typically used in conjunction with a "get last error" mechanism. In this current example, handleError() would be responsible for determining the precise nature of the error and behaving appropriately.

The "get last error" concept is often seen in the APIs of popular operating systems, such as Windows and Unix. The pattern is typically something similar to the following.

clearError();
value = fnThatCanSetError(someParameters);
result = getError();

This style does not interfere with the return types or arguments and can be used to determine "what" went wrong. However, the developer must take care since accidental injection of additional calls between the clearError() and the getError() can result in an earlier error being overwritten by a subsequent one. Furthermore, in a multithreaded environment, each thread must maintain its own "last error" variable, and interrupts must be precluded from using this error reporting mechanism.

One variation of this style is to have the function that may set an error clear any previous error on entry, and set a new one if it occurs. This is a somewhat cumbersome and potentially confusing style, particularly when wrapping system calls on Unix platforms, where it is not always consistent which system calls return distinguished values and which ones manipulate errno, etc. And it does not eliminate the potential to overwrite an earlier error indicator with a subsequent one.

One final example concerning error reporting uses an out-parameter to provide the cause of an error, should one occur.

someReturnType someFunction(resultType *result, someArguments);

Here it is possible to use functions as parameters, although doing so precludes checking the result parameter that such functions return. An expression such as f(&f_error, g(&g_error, x)) forces the caller to evaluate all of the possible out-parameters to determine if an error occurred somewhere within the combination.

For consistency, the parameter used for reporting errors is typically the first parameter that it so always falls in the same position even when used with functions accepting variadic arguments. However, it is necessary for the called function to check the parameter for NULL.

Unfortunately, it can only be assumed that passing NULL for the error parameter indicates that the caller does not care about errors. This potential disadvantage is offset by the fact that this style is thread-safe and fully reentrant. It can be used in multi-threaded environments without requiring thread-local storage, and it can be used by functions invoked by interrupt handlers as well.

In summary, the style used for a given API influences the underlying implementation, potentially affecting dead code elimination, and imposes specific usage patterns on its clients. When designing an API, it is useful to compare the implementation and optimization opportunities afforded by alternative API styles, as well as the usage model of the client code that invokes them.

Understanding “APIdeology” – the interworking of APIs and the constraints that each API places on the implementation and its clients -- allows the designer to produce results that are in alignment with the system design goals.

Trenton Henry is a staff systems engineer at Silicon Laboratories  focusing on microcontroller products. Prior to joining Silicon Labs, Henry served as a systems architect for MCCI Corporation, where he oversaw design and development of embedded firmware solutions for mobile platforms and special-purpose development tools. He previously served as senior architect for SigmaTel's multimedia player SoC products, and at Standard Microsystems Corporation as the firmware architect for a USB mass storage peripheral product line. He holds a B.A. in Computer Science from the University of Texas at Austin

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER