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

No comments:

Post a Comment

LeetCode C++ Cheat Sheet June

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