Designing an API
How do you design an API? My experience is that just like with most other issues concerning code formatting and standards, every programmer has their own set of preferences. Which of course means this post is entirely based on my personal views, and I will obviously assume you agree with the choices I make.
This is part two in my series of posts about building a profiling library. The the code for that post, the code for this and the next parts will be available through github, released to the public domain:
void profile_begin_block( const char* identifier );
Everything else we add might violate one or more of the rules, so we need to tread carefully. A reasonable feature of this system would be to be able to group blocks together in a frame (where the meaning of a “frame” would be open for interpretation by the user). Since the end of a frame automatically marks the beginning of the next frame, we only need one function (compact). The function will have no side effects and only fulfill the primary purpose of marking a group of blocks as belonging to a frame (orthogonal). We name the function according to the previous block functions and use a standard integer type to identify the frame number (consistent, contained).
void profile_end_frame( uint64_t counter );
Going forward, we also want a function to enable/disable profiling as well as functionality to insert generic log messages into the profiling data stream. It could also be useful to add notifications of mutex locks/unlocks as they control the flow and execution of threads. However, we’re now close to breaking the orthogonality rule as we could potentially achieve these goals by using the generic log message function with messages of certain pre-defined formats. But this is where the “specialized” rule kicks in. Having a generic log message function performing both the task of inserting a log message into the profile data as well as inserting notifications about mutex locks would make the API harder to remember and more bug-prone.
void profile_enable( int enable );
void profile_log( const char* message );
void profile_trylock( const char* name );
void profile_lock( const char* name );
void profile_unlock( const char* name );
Finally we need functions to initialize and shutdown the profiling backend, and a function to declare how to output the gathered date. In order to minimize the dependencies of the library and allow the user to output in application-specific ways, we’ll use a callback.
typedef void (*profile_write_fn)( void*, uint64_t );
void profile_initialize( char* identifier );
void profile_output( profile_write_fn writer );
As you might have guessed I’ve placed a few restrictions/demands on the user of the API. In order to stick with primitive data types and minimize the amount of memory management going on in the implementation of the library, the strings passed to the functions must be valid until the data has been flushed to the output stream.
That concludes this small exercise in API design. In the next part I’ll go through the implementation of the profiling library, dealing with the thread synchronization issues and the steps taken to minimize the overhead of the implementation. To be effective, the profiling code itself can only spend an order of magnitude less time than the code being profiled.