Sunday, July 31, 2022

C++ API Design Best Practices - Improving Performance


PASS INPUT ARGUMENTS BY CONST REFERENCE

  • Prefer const references over pointers to pass input parameters, that is, parameters that are not changed by the function. However, you should prefer pointers over non-const references for output parameters so that their mutability is clearly advertised to clients. This section now offers some additional performance reasons to prefer the use of const references to pass input arguments into a function.
  • By default, function arguments in C++ are passed “by value.” This means that the object being passed into the function is copied and then that copy is destroyed when the function returns. As a result, the original object that is passed into the method can never be modified. However, this involves the overhead of calling the object’s copy constructor and then the destructor. Instead you should pass a const reference to the object. This has the effect of only passing the pointer to the object, but also ensuring that the object is not modified by the method. This can be particularly important for embedded systems where you have a very limited stack size.

                        void SetValue(std::string str); // pass by value
                        void SetValue(std::string &str); // pass by reference
                        void SetValue(const std::string &str); // pass by const reference
  • Always prefer passing a non-mutable object as a const reference rather than passing it by value. This will avoid the memory and performance costs to create and destroy a temporary copy of the object and all of its members and inherited objects.
  • This rule only applies to objects. It is not necessary for built-in types, such as int, bool, float, double, or char, because these are small enough to fit in a CPU register. In addition, 
  • STL iterators and function objects are designed to be passed by value. However, for all other custom types, you should favor references or const references.
  • If you assume that the SetObjectByValue() and SetObjectByConstReference() methods both simply assign their argument to the mObject member variable, then the sequence of operations that get performed when each of these methods is called is as follows.
                        SetObjectByValue(object)
      1. std::string constructor
      2. MyObject copy constructor
      3. MyObject assignment operator
      4. MyObject destructor
      5. std::string destructor

                        SetObjectByConstReference(object)
      1. MyObject assignment operator
  • The situation becomes worse if MyObject is derived from some base class because then the copy constructor and destructor of each base class in the object hierarchy would also have to be called for the pass-by-value case.

 MINIMIZE #INCLUDE DEPENDENCIES

  • The time it takes to compile a large project can depend greatly on the number and depth of #include files. As such, one of the common techniques for decreasing build times is to try to reduce the number of #include statements in header files
  • Some APIs provide a single large header file that pulls in all of the classes and global definitions for the interface. This can seem like a convenient affordance for your clients, however, it only serves to increase the compile-time coupling between your clients’ code and your API, which means that even the most minimal use of your API must pull in every public symbol.
  • For example, the standard Win32 header windows.h pulls in well over 200,000 lines of code (under Visual Studio 9.0). Every .cpp file that includes this header effectively adds over 4 MB of extra code that needs to be loaded from around 90 separate files and compiled for every source file.

Forward Declarations

  • A header file, A, includes another header file, B, in order to pull in the declaration of a class, function, struct, enum, or other entity that is used in header A. The most common situation in an object-oriented program is that header A wants to pull in the declaration of one or more classes from header B. However, in many situations, header A does not actually need to include header B and can instead simply provide a forward declaration for the classes needed. A forward declaration can be used when
    1. The size of the class is not required. If you include the class as a member variable or subclass from it, then the compiler will need to know the size of the class.
    2. You do not reference any member methods of the class. Doing so would require knowing the method prototype: its argument and return types.
    3. You do not reference any member variables of the class; but you already know to never make those public (or protected).
  • use forward declarations if header A only refers to the name of classes from header B via pointers or references
                class B; // forward declaration
                class A
                {
                public:
                                void SetObject(const &B obj);
                private:
                                B *mObj;
                };
  • However, if you were to change the definition of class A so that that compiler needs to know theb actual size of class B, then you must include the actual declaration of class B, that is, you must #include its header. For example, if you store an actual copy of B inside of A.
                #include <B.h>
                class A
                {
                public:
                                void SetObject(const &B obj);
                private:
                                B mObj;
                };
  • As a rule of thumb, you should only need to #include the header file for a class if you use an object of that class as a data member in your own class or if you inherit from that class.

Redundant #include Guards

  • Another way to reduce the overhead of parsing too many include files is to add redundant preprocessor guards at the point of inclusion. For example, if you have an include file, bigfile.h, that looks like this

                #ifndef BIGFILE_H

                #define BIGFILE_H

                // lots and lots of code

                #endif

  • Then you might include this file from another header by doing the following

                #ifndef BIGFILE_H
                #include "bigfile.h"
                #endif
  • Consider adding redundant #include guards to your headers to optimize compile time for your clients.

DECLARING CONSTANTS

  • Often you want to define a number of public constants for your API. This is a great technique for avoiding the proliferation of hardcoded values throughout your client’s code, such as maximum values or default strings. For example, you might declare several constants in the global scope of your header in this way.
                const int MAX_NAME_LENGTH = 128;
                const float LOG_2E = log2(2.71828183f);
                const std::string LOG_FILENAME = "filename.log";
  • The issue to be aware of here is that only very simple constants for built-in types will be inlined by your C++ compiler. By default, any variable that you define in this way will cause your compiler to store space for the variable in every module that includes your header. In the aforementioned case, this will likely happen for both the float and the string constant. If you declare many constants and your API headers are included in many .cpp files, then this can cause bloat in the client’s .o object files and the final binary. The solution is to declare the constants as extern.
                extern const int MAX_NAME_LENGTH;
                extern const float LOG_2E;
                extern const std::string LOG_FILENAME;
  • Then define the value of each constant in the accompanying .cpp file. In this way, the space for the variables is only allocated once. This also has the additional benefit of hiding actual constant values from the header file: they are implementation details after all. 
  • A better way to do this is if you can declare the constants within a class. Then you can declare them as static const (so they will not count toward the per-object memory size).
                // myapi.h
                class MyAPI
                {
                public:
                                static const int MAX_NAME_LENGTH;
                                static const int MAX_RECORDS;
                                static const std::string LOG_FILENAME;
                };
  • You can then define the value for these constants in the associated .cpp file
                // myapi.cpp
                const int MyAPI::MAX_NAME_LENGTH = 128;
                const int MyAPI::MAX_RECORDS = 65525;
                const std::string MyAPI::LOG_FILENAME = "filename.log";
  • Another option to avoid the bloat issue in certain cases is to use enums as an alternative to variables or you could also use getter methods to return the constant values, such as
                // myapi.h
                class MyAPI
                {
                public:
                                static int GetMaxNameLength();
                                static int GetMaxRecords();
                                static std::string GetLogFilename();
                };
  • Declare global scope constants with extern or declare constants in classes as static const. Then define the value of the constant in the .cpp file. This reduces the size of object files for modules that include your headers. Even better, hide these constants behind a function call.

INITIALIZATION LISTS

  • C++ provides constructor initialization lists to let you easily initialize all of the member variables in your class. Using this feature can afford a slight performance increase over simply initializing each member variable in the body of the constructor.
  • 1st example
            // avatar.h
           class Avatar
           {
           public:
                      Avatar(const std::string &first, const std::string &last)
                      {
                                 mFirstName = first;
                                 mLastName = last;
                      }

           private
                      std::string mFirstName;
                      std::string mLastName;
           };
  • 2nd example - Use the following method, it will hide implementation details as well 
           // avatar.h
           class Avatar
           {
           public:
                      Avatar(const std::string &first, const std::string &last);

           private:
                      std::string mFirstName;
                      std::string mLastName;
           };

           // avatar.cpp 
           Avatar::Avatar(const std::string &first, const std::string &last) :
                                            mFirstName(first),
                                            mLastName(last)
           {           
           }



  • Because member variables are constructed before the body of the constructor is called, 
  • In the first example, the default std::string constructor will be called to initialize the mName member variable, and then inside the constructor, the assignment operator is called However,
  • In the second example, only the assignment operator is invoked.
  • Using an initialization list avoids the cost of calling the default constructor for each member variable that you include in the list.
  • Use constructor initialization lists to avoid the cost of a constructor call for each data member, but declare these in the .cpp file to hide implementation details.
  • Here are a few things to be aware of when using initialization lists:
    • The order of variables in the initialization list must match the order of the variables specified in the class.
    • Cannot specify arrays in an initialization list. However, you can specify a std::vector, which may be a better choice of data structure anyway.
    • If you are declaring a derived class, the default constructor for any base classes will be called implicitly. You can use the initialization list to call a non-default constructor instead. If specified, a call to a base class constructor must appear before any member variables.
    • If you have declared any of your member variables as references or as const, then you must initialize them via the initialization list (to avoid the default constructor defining their initial, and only, value).

MEMORY OPTIMIZATION

  • One key technique is to reduce the size of your objects: the smaller your objects are, the more of them can potentially fit into a cache. There are several ways that you can reduce the size of your objects.

  • Cluster member variables by type. Modern computers access memory a single “word” at a time. Your C++ compiler will therefore align certain data members so that their memory addresses fall on word boundaries. A number of unused padding bytes may be added to a structure in order to make this happen. By clustering all member variables of the same type next to each other, you can minimize the amount of memory loss to these padding bytes

  • Use bit fields. A bit field is a decorator for a member variable that specifies how many bits the variable should occupy, for example, int tinyInt:4. This is particularly useful for packing several bools into a single byte or for squeezing two or more numbers into the space of a single int. The downside is that there is normally a performance penalty for using bit field sizes that are not a multiple of 8, but if memory is your biggest concern then this may be an acceptable cost.


  • Use unions. A union is a structure where data members share the same memory space. This can be used to allow multiple values that are never used at the same time to share the same area of memory, thus saving memory. The size of a union is the size of the largest type in the union.
  • Don’t add virtual methods until you need them. I recommended this as a way to keep an API minimally complete, but there are also performance reasons to do this. Once you add one virtual method to a class, that class needs to have a vtable. Only one copy of the vtable needs to be allocated per class type, but a pointer to the vtable is stored in every instance of your object. This adds the size of one pointer to your overall object size (normally 4 bytes for a 32-bit application or 8 bytes for a 64-bit application).
  • Use explicit size-based types. The size of various types can differ by platform, compiler, and whether you are building a 32-bit or a 64-bit application. If you want to specify the exact size of a member variable, then you should use a type that specifically enforces this rather than assuming that types such as bool, short, or int will be a specific size. Unfortunately, the way to declare a fixed-size variable varies for different platforms. For example, on UNIX-based systems, the stdint.h header file provides types such as int8_t, uint32_t, and int64_t to specify an 8-bit integer, 32-bit unsigned integer, and a 64-bit integer, respectively.

  • Cluster the member variables based on their type and sort them based on the size of each type (bools, chars, then ints), then the size of the structure can be reduced to 32 bytes, a 33% reduction.
            class Fireworks_A
            {
                        bool mIsActive;
                        int mOriginX;
                        int mOriginY;
                        bool mVaryColor;
                        char mRed;
                        int mRedVariance;
                        char mGreen;
                        int mGreenVariance;
                        char mBlue;
                        int mBlueVariance;
                        bool mRepeatCycle;
                        int mTotalParticles;
                        bool mFadeParticles;
            };

            class Fireworks_B
            {
                        bool mIsActive;
                        bool mVaryColor;
                        bool mRepeatCycle;
                        bool mFadeParticles;
                        char mRed;
                        char mGreen;
                        char mBlue;
                        int mRedVariance;
                        int mGreenVariance;
                        int mBlueVariance;
                        int mTotalParticles;
                        int mOriginX;
                        int mOriginY;
            };


  • Cluster member variables by their type to optimize object size.
  • You can still squeeze a few more bytes out of the structure, however. By using bit fields you can make each of the bool flags occupy a single bit instead of an entire byte. Doing this lets you get the structure size down to 28 bytes, a 42% reduction.

            class Fireworks_C
            {
                        bool mIsActive:1;
                        bool mVaryColor:1;
                        bool mRepeatCycle:1;
                        bool mFadeParticles:1;
                        char mRed;
                        char mGreen;
                        char mBlue;
                        int mRedVariance;
                        int mGreenVariance;
                        int mBlueVariance;
                        int mTotalParticles;
                        int mOriginX;
                        int mOriginY;
            };
  • Use size-specific types, such as int32_t or uint16_t, to specify the maximum number of required bits for a variable.
            class Fireworks_D
            {
                        bool mIsActive:1;
                        bool mVaryColor:1;
                        bool mRepeatCycle:1;
                        bool mFadeParticles:1;
                        char mRed;
                        char mGreen;
                        char mBlue;
                        char mRedVariance;
                        char mGreenVariance;
                        char mBlueVariance;
                        int mTotalParticles;
                        short mOriginX;
                        short mOriginY;
            };
  • This version of our structure occupies only 16 bytes: a reduction of 66% from the original unoptimized size of 48 bytes.

COPY ON WRITE

  • One of the best ways to save memory is to not allocate any until you really need to. This is the essential goal of copy-on-write techniques. 
  • These work by allowing all clients to share a single resource until one of them needs to modify the resource. Only at that point is a copy made. Hence the name: copy on write. 
  • The advantage is that if the resource is never modified then it can be shared for all clients. This is related to the Flyweight design pattern, which describes objects that share as much memory as possible to minimize memory consumption
  • Use copy-on-write semantics to reduce the memory cost for many copies of your objects.


Array References

  • The primary purpose of this method is to perform. In essence, it is a way to collapse a series of connected nodes of a graph data structure into a sequential array data structure. This provides a data structure that can be very efficiently iterated over but also locates elements adjacent to each other in memory. 
  • The result is a data structure that can take better advantage of CPU caching strategies, as opposed to a tree structure where individual nodes in the tree may be fragmented across the process’s address space.
  • This technique is particularly efficient if the client keeps the same array around to service multiple calls to getAllPaths(). Also, any initial performance overhead to fill the array can be compensated for if the array is kept around to support multiple iterations over its elements.
  • Adopt an iterator model for traversing simple linear data structures. If you have a linked list or tree data structure, then consider using array references if iteration performance is critical.

ITERATING OVER ELEMENTS

  • Iterating over a collection of objects is an extremely common task for client code so it is worth spending some time looking at alternative strategies that offer different strengths and weaknesses. That way you can choose the best solution for your particular API requirements.
  • The STL approach to this problem is to use iterators. These are objects that can traverse over some or  all elements in a container class .
  • An iterator points to a single element in a container, with various operators available, such as operator* to return the current element, operator-> to access the members of the container element directly, and operatorþþ to step forward to the next element. 
  • This design intentionally mimics the interface of plain pointer manipulation in C/C++. Clients can then use the begin() and end() methods on each container class to return iterators that bound all elements in the container or they can use various STL algorithms that return iterators within the set of all elements, such as std::find(), std::lower_bound(), and std::upper_bound(). The following code segment provides a simple example of using an STL iterator to sum all the values in a std::vector:
            float sum = 0.0f;
            std::vector<float>::const_iterator it;
            for (it = values.begin(); it != values.end(); ++it)
            {
                        sum+=*it;
            }

  • In terms of your own API designs, here are some reasons why you may want to adopt an iterator model to allow your clients to iterate over data.

  1. Iterators are a well-known pattern that most engineers are already familiarwith.As such, using an iterator model in your own APIs will minimize the learning curve for users
  2. The iterator abstraction can be applied to simple sequential data structures, such as arrays or lists, as well as more complicated data structures, such as sets and maps, which are often implemented as self-balancing binary search trees such as red-black trees
  3. Iterators can be implemented quite efficiently, even as simply as a pointer in some cases.
  4. Iterators can be used to traverse massive data sets that may not even fit entirely into memory. For example, the iterator could be implemented to page in blocks of data from disk as needed and free previously processed blocks.
  5. Clients can create multiple iterators to traverse the same data and use these iterators simultaneously. In the case where clients wish to insert or delete elements while traversing the container, there are established patterns for doing this while maintaining the integrity of the iterators

Random Access

  • An iterator allows clients to traverse linearly through each element in a container. However, you may have cases where you wish to support random access to any element, such as accessing a specific element in an array or vector container. STL container classes that support random accesses provide this in a couple of ways.
  1. The [] operator. This is meant to simulate the array indexing syntax of C/C++. Normally this operator is implemented without any bounds checking so that it can be made very efficient.
  2. The at() method. This method is required to check if the supplied index is out of range and throw an exception in this case. As a result, this approach can be slower than the [] operator.

         float sum = 0.0f;
         const size_t len = values.size();
         for (size_t it = 0; it < len; ++it)
         {
                  sum += values[it];
         }

  • In terms of performance, these two methods are essentially equivalent.

References 

  • API Design for C++ Book by Martin Reddy

Wednesday, July 27, 2022

C++ API Design Best Practices - C++ Libraries Introduction


Libraries 

  • A library lets you package the compiled code and data that implement your API so that your clients can embed these into their own applications. Libraries are the instruments of modularization. 
  • This covers the different types of libraries that you can use and how you can create them on Windows. It also covers physical aspects of API design, namely exposing the public symbols of your API in the symbol export table of its library file.

Static Libraries

  • The static library contains object code that is linked with an end-user application and then becomes part of that executable
  • A static library is sometimes called an archive because it is essentially just a package of compiled object files.
  • These libraries normally have a file extension of .a on UNIX and Mac OS X machines or .lib on Windows
  •  A static library is only needed to link an application. It is not needed to run that application because the library code is essentially embedded inside the application. As a result, your clients can distribute their applications without any additional run-time dependencies
  • If your clients wish to link your library into multiple executables, each one will embed a copy of your code. If your library is 10 MB in size and your client wishes to link this into five separate programs, then you could be adding up to 50 MB to the total size of their product. Note that only the object files in the static library that are actually used are copied to the application. So in reality the total size of each application could be less than this worst case.
  • Linking a static library into an application causes the library code to be embedded in the resulting executable.


  • Your clients can distribute their applications without any concerns that it will find an incompatible library version on the end-user’s machine or a completely different library with the same name from another vendor
  • However, if your clients want to be able to hot patch their application, that is, they want to update the version of your library used by their application, they must replace the entire executable to achieve this. If this is done as an Internet-based update, the end user may have to download a much larger update and hence wait longer for the update to complete

Dynamic Libraries

  • Dynamic libraries are files linked against at compile time to resolve undefined references and then distributed with the end-user application so that the application can load the library code at run time 
  • This normally requires use of a dynamic linker on the end user’s machine to determine and load all dynamic library dependencies at run time, perform the necessary symbol relocations, and then pass control to the application
  • Dynamic libraries are sometimes called shared libraries because they can be shared by multiple programs. On UNIX machines they can be called Dynamic Shared Objects, and on Windows, they are referred to as Dynamic Link Libraries. They have a .so file extension on UNIX platforms, .dll on Windows, and .dylib on Mac OS X, for example, libjpeg.so or jpeg.dll.
  • Your clients must distribute your dynamic library with their application (as well as any dynamic libraries that your library depends on) so that it can be discovered when the application is run.
  • A dynamic library is used to link an application and is then distributed with that application so that the library can be loaded at run time.

  • Your clients’ applications will not run if the dynamic library cannot be found, for example, if the library is deleted or moved to a directory that is not in the dynamic library search path. Furthermore, the application may not run if the dynamic library is upgraded to a newer version or overwritten with an older version
  • Using dynamic libraries can often be more efficient than static libraries in terms of disk space if more than one application needs to use the library. This is because the library code is stored in a single shared file and not duplicated inside each executable. Note that this is not a hard and fast rule, however. As noted earlier, the executable only needs to include the object code from the static library that is actually used. So if each application uses only a small fraction of the total static library, the disk space efficiency can still rival that of a single complete dynamic library.
  • Dynamic libraries may also be more efficient in terms of memory. Most modern operating systems will attempt to only load the dynamic library code into memory once and share it across all applications that depend upon it. This may also lead to better cache utilization. By comparison, every application that is linked against a static library will load duplicate copies of the library code into memory.
  • If your clients wish to hot patch their application with a new (backwards compatible) version of your shared library, they can simply drop in the replacement library file and all of their applications will use this new library without having to recompile or relink.
  • You should prefer to distribute your library as a dynamic library to give your users greater flexibility. If your library is sufficiently small and stable, you may also decide to provide a static library version.

Dynamic Libraries as Plugins

  • Dynamic libraries are normally linked against an application and then distributed with that application so that the operating system can load the library when the application is launched. However, it is also possible for an application to load a dynamic library on demand without the application having been compiled and linked against that library.
  • This can be used to create plugin interfaces, where the application can load additional code at run time that extends the basic capabilities of the program. For example, most Web browsers support plugins to handle specific content types,
  • In terms of API development, this gives you the capability to create extensible APIs that allow your clients to drop in new functionality that your API will then load and execute

Importing and Exporting Functions

  • Mark its declaration with the following keyword if function to be callable from a DLL on Windows,

                __declspec(dllexport) void MyFunction();
                class __declspec(dllexport) MyClass;

  • If you want to use an exported DLL function

                __declspec(dllimport)


            // On Windows, compile with /D “_EXPORTING” to build the DLL
            #ifdef _WIN32
                #ifdef _EXPORTING
                    #define DECLSPEC __declspec(dllexport)
                #else
                    #define DECLSPEC __declspec(dllimport)
                #endif
            #else
                #define DECLSPEC
            #endif

  • You can then declare all of the symbols you want to export from your DLL as follows:
            DECLSPEC void MyFunction();
            class DECLSPEC MyClass;
  • As an alternative to modifying your source code with these __declspec declarations, you can create a module definition .def file to specify the symbols to export. A minimal DEF file contains a LIBRARY statement to specify the name of the DLL the file is associated with, and an EXPORTS statement followed by a list of symbol names to export.

            / / MyLIB.def
            LIBRARY “MyLIB”
            EXPORTS
                    MyFunction1
                    MyFunction2

The DLL Entry Point

  • DLLs can provide an optional entry point function to initialize data structures when a thread or process loads the DLL or to clean up memory when the DLL is unloaded
  • This is managed by a function called DllMain() that you define and export within the DLL
  • If the entry point function returns FALSE, this is assumed to be a fatal error and the application will fail to start.

            BOOL APIENTRY DllMain( HANDLE dllHandle,
                                                          DWORD reason,
                                                          LPVOID lpReserved)
            {
                            switch (reason)
                            {
                            case DLL_PROCESS_ATTACHED:
                                        // A process is loading the DLL
                                        break;
                            case DLL_PROCESS_DETACH:
                                        // A process unloads the DLL
                                        break;
                            case DLL_THREAD_ATTACHED:
                                        // A process is creating a new thread
                                         break;
                            case DLL_THREAD_DETACH:
                                        // A thread exits normally
                                        break;
                             }
                    return TRUE;
            }

Finding Dynamic Libraries at Run Time

  • When you run an executable that depends on a dynamic library, the system will search for this library in a number of standard locations, normally /lib and /usr/lib. If the .so file cannot be found in any of these locations the executable will fail to launch
  • Recall that the ldd command can be used to tell you if the system cannot find any dependent dynamic library. This is obviously a concern for creating executable programs that depend on your API.
  • Three main options are available to your clients to ensure that any executable they build using your API can find your library at run time.
    1. The client of your API ensures that your library is installed in one of the standard library directories on the end user’s machine, for example, /usr/lib. This will require the end user to perform an installation process and to have root privileges in order to copy files into a system directory.
    2. The LD_LIBRARY_PATH environment variable can be set to augment the default library search path with a colon-separated list of directories. Your clients could therefore distribute a shell script to  run their application where that script sets the LD_LIBRARY_PATH variable to an appropriate directory where your dynamic library can be found.
    3. Your clients can use the rpath (run path) linker option to burn the preferred path to search for dynamic libraries into their executable For example, the following compile line will produce an executable that will cause the system to search in /usr/local/lib for any dynamic libraries 

Shared Library Entry Points

  • It’s possible to define functions that will be called automatically when your shared library is loaded or unloaded. This can be used to perform library initialization and cleanup operations without requiring your users to call explicit functions to perform this.
  • One way to do this is using static constructors and destructors. This will work for any compiler and any platform, although you should remember that the order of initialization of static constructors is not defined across translation unit boundaries, that is, you should never depend on static variable in other .cpp files being initialized. Bearing this caveat in mind, you could create a shared library entry point in one of your .cpp files as follows:
                class APIInitMgr
                {
                public:
                    APIInitMgr()
                    {
                        std::cout << “APIInitMgr Initialized.” << std::endl;
                    }
                    ~APIInitMgr()
                    {
                        std::cout << “APIInitMgr Destroyed.” << std::endl;
                    }
                };

                static APIInitMgr sInitMgr;

 Loading Plugins on Windows

  • On the Windows platform, the LoadLibrary() or LoadLibraryEx() functions can be used to load a dynamic library into a process, with the GetProcAddress() function being used to obtain the address of an exported symbol in the DLL. 
  • Note that you do not need an import library .lib file in order to load a dynamic library in this way. To demonstrate this, consider the following simple plugin interface used to create a plugin.dll library.

                #ifndef PLUGIN_H

                #define PLUGIN_H

                #include <string>

                extern “C”

                __declspec(dllexport) void DoSomething(const std::string &name);

                #endif

  • Then the following code snippet illustrates how to load this DLL on demand and call the DoSomething() method from that library.
            // open the dynamic library
            HINSTANCE handle = LoadLibrary(“plugin.dll”);

            if (! handle)
            {
                        std::cout << “Cannot load plugin!” << std::endl;
                        exit(1);
            }

            // get the DoSomething() function from the plugin
            FARPROC fptr = GetProcAddress(handle, “DoSomething”);

            if (fptr == (FARPROC)NULL)
            {
                        std::cout << “Cannot find function in plugin: ” << error;
                        std::cout << std::endl;
                        FreeLibrary (handle);
                        exit(1);
            }

            // call the DoSomething() function
            (*fptr)(“Hello There!”);

            // close the shared library
            FreeLibrary (handle);

References 

  • API Design for C++ Book by Martin Reddy

Tuesday, July 26, 2022

C++ API Design Best Practices - EXTENDING VIA INHERITANCE


EXTENDING VIA INHERITANCE

An object-oriented mechanism for extending a class is inheritance.

This can be used to let your users define new classes that build upon and modify the functionality of existing classes in your API.

Adding Functionality

  • This is simply extending an existing class where only new methods are added to the base class.
  • An important point to reiterate here is that this can only be done safely if the base class was designed tobe inherited from. The primary indicator for this is whether the class has a virtual destructor.   
  • This is not an issue because when subclass of the new methods are stateless, that is, they do not allocate any memory that must be freed by the base class destructor.
  • However, this does highlight the issue that if you expect your users to inherit from any of your classes, you should declare the destructor for those classes to be virtual. 
  • Declaring a virtual destructor for a class is a signal to your users that you have designed it to be inherited from.

Modifying Functionality

  • C++ allows you to define functions in a derived class that override existing functions in a base class if they have been marked as virtual in the base class.

Prohibiting Subclassing

  • When you want to prohibit users from inheriting derived classes from the classes you provide to them. In Java, you can declare a class to be final to prevent others from inheriting from it, but C++ does not have a similar concept.
  • If you declare a class with a non-virtual destructor, this should be a signal to a good programmer that they should think twice about inheriting from the class. However, if you want a physical mechanism to prevent users from subclassing one of your classes, the easiest way to do this is to make all of its constructors private. Any attempt to derive from this class will produce a compile error because the derived class’s constructor cannot call your class’s constructor.

                class NonBase
                {
                public:
                                static NonBase* Create() { return new NonBase (); }

                private:
                                NonBase();

                };

                class Derived : public NonBase {};

                Derived d; // compile error!

  • The downside of this approach is that instances of NonBase cannot be created on the stack. Your clients must instead always allocate instances of NonBase using the NonBase::Create() static function. If this is undesirable, there is another solution. You can instead rely on virtual inheritance to ensure that no concrete class can inherit from NonBase

                class NonBase;
                class NonBaseFinal
                {
                private:
                                NonBaseFinal() {}

                                friend class NonBase;

                };

                class NonBase :virtual public NonBaseFinal
                {
                public:

                                void methodA();
                };

                class Derived : public NonBase {};
                Derived d; // compile error!


References 

  • API Design for C++ Book by Martin Reddy

Monday, July 25, 2022

C++ API Design Best Practices - C++ EXTENDING VIA PLUGINS

                                                                        Image source

Plugin Model Overview

Many examples of commercial software packages allow their core functionality to be extended

through the use of C/C++ plugins. 

For example, 

  • The Apache Web server supports C-based “modules,”
  • Adobe Photoshop supports a range of plugin types to manipulate images, and Web browsers such as Firefly, Chrome, 
  • Opera supports the Netscape Plugin API (NPAPI) for the creation of browser plugins such as the Adobe Flash or PDF Reader plugins.
  • The Qt toolkit can also be extended via the QPluginLoader class. (A server-based plugin API such as Apache’s module interface is sometimes referred to as a Server API or SAPI.)

The benefits of adopting a plugin model in your API are as follows.

  • Greater versatility. Your API can be used to solve a greater range of problems, without requiring you to implement solutions for all of those problems.
  • Community catalyst. By giving your users the ability to solve their own problems within the framework of your API, you can spark a community of user-contributed additions to your base design.
  • Smaller updates. Functionality that exists as a plugin can be updated easily independently of the application by simply dropping in a new version of the plugin. This can often be a much smaller update than distributing a new version of the entire application. 
  • Future-proofing. Your API may reach a level of stability where you feel that no further updates are necessary. However, further evolution of the functionality of your API can continue through the development of plugins, allowing the API to maintain its usefulness and relevance for a greater period of time. For example, the NPAPI has changed little in recent years, but it is still a popular method to write plugins for many Web browsers.
  • Isolating risk. Plugins can be beneficial for in-house development too by letting engineers change functionality without destabilizing the core of your system.

Plugin System Design  



The Plugin Manager lives in the Core API. It discovers and loads plugins that have been built against the Plugin API.

  • The Plugin API: This is the API that your users must compile and link against in order to create a plugin. I differentiate this from your Core API, which is the larger code base into which you are adding the plugin system.
  • The Plugin Manager: This is an object (often a singleton) in the Core API code that manages the life cycle of all plugins, that is, loading, registration, and unloading. This object can also be called the Plugin Registry.

C versus C++. 

The C++ specification does not define a specific ABI. Therefore, different compilers, and even different versions of the same compiler, can produce code that is binary incompatible. The implication for a plugin system is that plugins developed by clients using a compiler with a different ABI may not be loadable. In contrast, the ABI for plain C code is well-defined and will work across platforms and compilers.

Implementing Plugins in C++

  • Supporting C++ plugins can be difficult due to cross-platform and cross-compiler ABI problems. However, because this is a book about C++ API design, let’s take a few more moments to present some solutions that let you use C++ plugins more robustly.
  • One can use a binding technology for your plugins, for example, an IPC solution such as COM on Windows,  creating script bindings for your API and letting users write extensions using a cross-platform scripting language such as Python or Ruby (as If you absolutely need to use C++ plugins for maximum performance or you feel that creating a COM or script binding is too heavyweight for your needs, there are still ways that you can use C++ more safely in plugins.

The following list offers several best practices

  • Use abstract base classes. Implementing virtual methods of an abstract base class can insulate a plugin from ABI problems because a virtual method call is usually represented as an index into a class’s vtable.
  • Use C linkage for free functions. All global functions in your Plugin API should use C linkage to avoid C++ ABI issues, that is, they should be declared with extern "C". Similarly, function callbacks that a plugin passes to the Core API should also use C linkage for maximum portability
  • Avoid STL and exceptions. Different implementations of STL classes such as std::string and std::vector may not be ABI compatible. It is therefore best to avoid these containers in any function calls between the Core API and Plugin API. Similarly, because the ABI for exceptions tends to be unstable across compilers, these should be avoided in your Plugin API.
  • Don’t mix allocators. It’s possible for plugins to be linked against a different memory allocator than your API. For example, on Windows it’s common for debug builds to use a different allocator than release builds. The implication for the design of our plugin system is that either the plugin must allocate and free all of its objects or the plugin should pass control to the Core API to create and destroy all objects. However, your Core API should never free objects that were allocated by a plugin, and vice versa

The Plugin API

  • The Plugin API is the interface that you provide to your users to create plugins. the header file will contain functionality that allows plugins to communicate with the Core API.
  •  When the Core API loads a plugin, it needs to know which functions to call or symbols to access in order to let the plugin do its work. This means that you should define specifically named entry points in the plugin that your users must provide. 
  • There are several different ways that you can do this. For example, when writing a GIMP plugin, you must define a variable called PLUG_IN_INFO that lists the various callbacks defined in the plugin.
  • The two most basic callbacks that a plugin should provide are an initialization function and a cleanup function. As noted earlier, these functions should be declared with C linkage to avoid name mangling differences between compilers.
  • In a cross-platform plugin, system development, you will also have to deal with correctly using __declspec(dllexport) and __declspec(dllimport) decorators on Windows.

                // pluginapi.h

                #include "defines.h"

                #include "renderer.h"

                #define CORE_FUNC extern "C" CORE_API

                #define PLUGIN_FUNC extern "C" PLUGIN_API

                #define PLUGIN_INIT() PLUGIN_FUNC int PluginInit()

                #define PLUGIN_FREE() PLUGIN_FUNC int PluginFree()

                typedef IRenderer *(*RendererInitFunc)();

                typedef void (*RendererFreeFunc)(IRenderer *);

                CORE_FUNC void RegisterRenderer(const char *type,

                                                                 RendererInitFunc init_cb,

                                                                RendererFreeFunc free_cb);

  • Macros to define the initialization and cleanup functions for a plugin: PLUGIN_INIT() and PLUGIN_FREE(),
  • PLUGIN_FUNC() macro let plugins export functions for the Core API to call, as well as the CORE_FUNC() macro that exports Core API functions for plugins to call.
  • Function, RegisterRenderer(), which allows plugins to register new IRenderer classes with the Core API. Note that a plugin must provide both an init function and a free function for their new IRenderer classes to ensure that allocations and frees happen within the plugin
  • CORE_API and PLUGIN_API define. These let us specify the correct DLL export/import decorators under Windows. CORE_API is used to decorate functions that are part of the Core API, and PLUGIN_API is used for functions that will be defined in plugins. The definition of these macros is contained in the definitions.
            // defines.h
            #ifdef _WIN32
                #ifdef BUILDING_CORE
                        #define CORE_API __declspec(dllexport)
                        #define PLUGIN_API __declspec(dllimport)
                #else
                        #define CORE_API __declspec(dllimport)
                        #define PLUGIN_API __declspec(dllexport)
                #endif
            #else
                #define CORE_API
                #define PLUGIN_API
            #endif

            //  plugin.cpp
            #include "pluginapi.h"
            #include <iostream>

            class OpenGLRenderer : public IRenderer
            {
            public:
                        ~OpenGLRenderer() {}
                        bool LoadScene(const char *filename) { return true; }
                        void SetViewportSize(int w, int h) {}
                        void SetCameraPosition(double x, double y, double z) {}
                        void SetLookAt(double x, double y, double z) {}
                        void Render() { std::cout << "OpenGL Render" << std::endl; }
            };

            PLUGIN_FUNC IRenderer *CreateRenderer()
            {
                        return new OpenGLRenderer();
            }
            PLUGIN_FUNC void DestroyRenderer(IRenderer *r)
            {
                        delete r;
            }
            PLUGIN_INIT()
            {
                        RegisterRenderer("opengl", CreateRenderer, DestroyRenderer);
                        return 0;
            }

  • PLUGIN_INIT() function, which will get run whenever the plugin is loaded. This registers our OpenGLRenderer factory function, CreateRenderer(), and the associated destruction function, DestroyRenderer(). 
  • These are both defined using PLUGIN_FUNC to ensure that they are exported correctly with C linkage.
  • The RegisterRenderer() function essentially just calls the RendererFactory::RegisterRenderer()

Reasons for an explicit registration function to the Plugin API rather than letting plugins register themselves directly with the RendererFactory. 

  • One reason is simply to give us a layer of abstraction so that you could change RendererFactory in the future without breaking existing plugins. 
  • Another reason is to avoid plugins calling methods that use STL strings: note that RegisterRenderer uses a const char * to specify the renderer name.

The Plugin Manager

  • Now that you have a Plugin API and you can build plugins against this API, you need to be able to load and register those plugins into the Core API. This is the role of the Plugin Manager. Specifically, the Plugin Manager needs to handle the following tasks.

Load metadata for all plugins
  • These metadata can either be stored in a separate file (such as an XML file) or be embedded within the plugins themselves. In the latter case, the Plugin Manager will need to load all available plugins to collate metadata for all plugins. These metadata let you present the user with a list of available plugins for them to choose between.

Load a dynamic library into memory 
  • provide access to the symbols in that library, and unload the library if necessary. This involves using dlopen(), dlclose(), and dlsym() on UNIX platforms (including Mac OS X) and LoadLibrary(), FreeLibrary(), and GetProcAddress() on Windows. 

Pugin’s initialization and cleanup 
  • Call the plugin’s initialization routine when the plugin is loaded, and call the cleanup routine when the plugin is unloaded. These functions are defined by PLUGIN_INIT() and PLUGIN_FREE() within the plugin.
  • Because the Plugin Manager provides a single point of access to all of the plugins in the system, it is often implemented as a singleton. In terms of design, the Plugin Manager can be thought of as a collection of Plugin Instances, where each Plugin Instance represents a single plugin and offers functionality to load and unload that plugin. Here is an example implementation for a Plugin Manager:

            // pluginmanager.cpp
            #include "defines.h"
            #include <string>
            #include <vector>
            class CORE_API PluginInstance
            {
            public:
                        explicit PluginInstance(const std::string &name);
                        ~PluginInstance();
                        bool Load();
                         bool Unload();
                        bool IsLoaded();
                        std::string GetFileName();
                        std::string GetDisplayName();
            private:
                        PluginInstance(const PluginInstance &);
                        const PluginInstance &operator =const PluginInstance &);
                        class Impl;
                        Impl *mImpl;
            };

            class CORE_API PluginManager
            {
            public:
                        static PluginManager &GetInstance();
                        bool LoadAll();
                        bool Load(const std::string &name);
                        bool UnloadAll();
                        bool Unload(const std::string &name);
                        std::vector<PluginInstance *> GetAllPlugins();
            private:
                        PluginManager();
                        ~PluginManager();
                        PluginManager(const PluginManager &);
                        const PluginManager &operator =(const PluginManager &);
                        std::vector<PluginInstance *> mPlugins;
            };

Metadata 

  • The above design decouples the ability to access metadata for all plugins from the need to load those plugins. That is, if metadata such as the plugin’s display name is stored in an external file, you can call PluginManager::GetAllPlugins() without loading the actual plugins.
  • However, if metadata is stored in the plugins, then GetAllPlugins() can simply call LoadAll() first. The following example presents a sample external metadata file based on an XML syntax:

                <?xml version="1.0" encoding=’UTF-8’?>
                                <plugins>
                                                <plugin filename="oglplugin">
                                                <name>OpenGL Renderer</name>
                                </plugin>
                                 <plugin filename="dxplugin">
                                                <name>DirectX Renderer</name>
                                </plugin>
                                <plugin filename="mesaplugin">
                                                <name>Mesa Renderer</name>
                                </plugin>
                </plugins>

  • Irrespective of the approach to storing plugin metadata within an external file or embedded within each plugin, the following code outputs the display name for all available plugins:
                std::vector<PluginInstance *> plugins = PluginManager::GetInstance().GetAllPlugins();
                std::vector<PluginInstance *>::iterator it;
               
               for (it = plugins.begin(); it != plugins.end();++it)
                {
                                PluginInstance *pi = *it;
                                std::cout << "Plugin: " << pi->GetDisplayName() << std::endl;
                }

References 

  • API Design for C++ Book by Martin Reddy

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...