Vous êtes sur la page 1sur 4

e m be dde d.co m http://www.embedded.

co m/design/pro to typing-and-develo pment/4227354/APIdeo lo gy--Applicatio n-Pro gramming-Interfacebest-practices

APIdeology: Application Programming Interface best practices


Trenton Henry, Silicon Laboratories T he public interf ace of a sof tware component, i.e., an element such as a header f ile thats readily visible to the developer, is of ten the sof twares most important f eature. Best practices in the development and maintenance of application programming interf aces (APIs), or what we might cleverly call APIdeology, dictate that sof tware interf aces do not change once they have been released, with the exception of adding new entry points. T heref ore, it is very important to design concise, extensible sof tware interf aces that can be adapted to survive in an evolving hardware/sof tware ecosystem. APIs tend to f ollow several general patterns. For designers seeking to choose an appropriate API pattern, its helpf ul to understand the ways in which the choice of pattern, or style, drives the implementation of the sof tware component and the usage model it f orces on its clients. Consider, f or example, an API f or setting color values f or a liquid crystal display (LCD) library. For simplicity, lets 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." T hat color is specif ied by f our values: red, green, blue and alpha. T he f ollowing f ormula represents one way to def ine an API to specif y 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 specif y a color. Sometimes f loating 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); T he 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 f unctions, it can be used f or more than just setting the values of color components. T his API can actually be used to set the values of anything f or which a keyword has been def ined. T his style is f lexible and potentially usable in a wide variety of situations. But that f lexibility comes with some complexity. T he interf ace inf luences the implementation, encouraging each f unction to employ a switch on the keyword parameter, providing a case f or each keyword. More keywords imply more cases in the switch, and thus larger f unctions. Furthermore, it is possible that an unrecognized keyword may be passed to a f unction. Even though the

keyword type has an enumerated (enum) type def inition, most compilers treat enums as signed integers and will not issue a warning if some other compatible value is passed. T his 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 f ailure due to an invalid keyword, is suf f icient. In addition to inf luencing the implementation, this API design choice imposes a constraint onto the caller as well. T he client code calling these f unctions is obligated to check the returned result to ensure that no inadvertent error has occurred. T here are, of course, alternative API styles that can perf orm the same task but with dif f erent tradeof f s. 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); T his API can accomplish the same ef f ect as the previous API, but it goes about it in a dif f erent manner. Previously the f unction name specif ied only the type of value, relying on a keyword parameter to indicate exactly which value is af f ected. Like the previous example, this second API provides parameter type checking at compile time and provides f lexibility to the caller by allowing parameters to be passed individually or as vectors. Its f unction names are somewhat more meaningf ul, though, conveying not only the purpose of the f unction, but also the number and type of arguments expected. T he implementation of each f unction can be small, simple and ef f icient. T he number and type of each argument is f ixed by the API, and thus there is no need f or parameter validation and no need to return any sort of error code. T he f unctions of this API cannot f ail. But the API does require a number of very similar f unctions f or each allowed parameter type and parameter passing style. T he f unctions 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 f unction is somewhat of f set by the need to provide many such f unctions, this can become an advantage since sometimes a linker can eliminate f unctions that are never called. T he switch/case implementation encouraged by the previous API of f ers the linker less opportunity f or dead code elimination. T here are, of course, circumstances when it is not possible to design an API whose f unctions cannot possibly f ail. Consider the f ollowing example of "pass/f ail" style. bool someFunction(someArguments); if (NO == someFunction(theseArguments)) rslt = getError();

T his is simple and easily tested f or success or f ailure. But it may not be appropriate in some situations since it does not lend itself to f unctional composition such as f (g(x)). T he pass/f ail 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 f ollowing. s omeReturnType someFunction(someArguments); result = someFunction(args); if (NULL == result) handleError(); OR if (result < 0) handleError(); etc. T his 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 f orthcoming. Functions def ined 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 f or and handles that possibility. Another concern with the use of distinguished values to indicate an error is that there is of ten 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"? T hus this style of API also suf f ers f rom an inability to clearly indicate the cause of the error, and theref ore is also typically used in conjunction with a "get last error" mechanism. In this current example, handleError() would be responsible f or determining the precise nature of the error and behaving appropriately. T he "get last error" concept is of ten seen in the APIs of popular operating systems, such as Windows and Unix. T he pattern is typically something similar to the f ollowing. clearError(); value = fnT hatCanSetError(someParameters); result = getError(); T his style does not interf ere 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 f rom using this error reporting mechanism. One variation of this style is to have the f unction that may set an error clear any previous error on entry, and set a new one if it occurs. T his is a somewhat cumbersome and potentially conf using style, particularly when wrapping system calls on Unix platf orms, 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 f inal 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 f unctions as parameters, although doing so precludes checking the result

parameter that such f unctions return. An expression such as f(&f_error, g(&g_error, x)) f orces 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 f or reporting errors is typically the f irst parameter that it so always f alls in the same position even when used with f unctions accepting variadic arguments. However, it is necessary f or the called f unction to check the parameter f or NULL. Unf ortunately, it can only be assumed that passing NULL f or the error parameter indicates that the caller does not care about errors. T his potential disadvantage is of f set by the f act that this style is thread-saf e and f ully reentrant. It can be used in multi-threaded environments without requiring thread-local storage, and it can be used by f unctions invoked by interrupt handlers as well. In summary, the style used f or a given API inf luences the underlying implementation, potentially af f ecting dead code elimination, and imposes specif ic usage patterns on its clients. When designing an API, it is usef ul to compare the implementation and optimization opportunities af f orded 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.

Vous aimerez peut-être aussi