C++ Dynamic Library Support

ISO/IEC JTC1 SC22 WG21 N2407 = 07-0267 - 2007-09-10

Lawrence Crowl, crowl@google.com, Lawrence@Crowl.org

  • Practice
  • Proposal
  • Changes
  • Introduction

    The construction and use of dynamic libraries has become a significant requirement on modern software development. Unfortunately, their interaction with C++ varies between implementations and is often underspecified on any given implementation.

    The problem with dynamic libraries in C++ is that the benefits they provide introduce another layer of visibility. This additional layer of visibility is intended to provide for additional isolation, but is in direct contradiction to the one-definition rule.

    See the following papers for more complete discussion of the issues. The latter paper has an extensive set of references.

    In practice, programmers are able to work around the contradition and produce well-formed and reliable programs. Changing the standard to recognize and guide existing practice will markedly improve program construction. Unfortunately, a coherent change to the standard may well require changes to some of the C++ ABIs, and hence should be done as part of the standard rather than as a Technical Report.

    Benefits

    The primary feature of dynamic libraries is the means to defer the binding of a library interface to an implementation of that interface until program execution. This defered binding provides a number of benefits to a program.

    The second feature of dynamic libraries is isolation. Isolation means that accidents of implementation are not exposed to the users of the library. That is, the set of bindable symbols provided by the library is exactly the set of symbols in its interface; none of the implementation-specific symbols are bindable.

    The third feature of dynamic libraries is resolution. Resolution means that the system can resolve multiple definitions of a symbol. There are two general strategies for resolution, dependence and interposition. More colloquially, these are "the Windows way" and "the Unix way", respectively.

    The fourth feature of dynamic libraries is conditional loading. Conditional loading means that the name of a dynamic library can be computed at run-time and then brought into the load set. This feature is also known as "plug-in".

    The fifth feature of dynamic libraries is removal. Removal means that a dynamic library can be taken out of the load set, or alternatively, that the load set is not monotonically increasing.

    Terminology

    We adopt the terminology of Matt Austern, N1400 Toward standardization of dynamic libraries:

    executable
    A program that the user runs.
    dynamic library
    A library that is bound ot the executable at run time.
    load unit
    Either an executable or a dynamic library.
    load set
    The executable together with all dynamic libraries laoded in the execution of the program.
    direct dependences
    The load units available to the static linker to satisfy symbols undefined by the load unit.

    In addition, we introduce additional terminology that is necessary to clarify the constraints of dynamic libraries.

    symbol
    A named function, type, or variable. (Typedefs are not symbols.)
    visibility
    The visibility of a symbol is whether or not it is isolated.
    exclusive object definition
    An object definition that may appear in only one translation unit. Regular functions have these definitions. Regular initialized variables have these definitions.
    replicable object definition
    An object definition that may appear in multiple translation units, provided the definitions are the same. Inline functions and template functions have these definitions. Uninitialized variables sometimes have these definitions, which are also known as tentative definitions.
    exclusive class definition
    A class definition in which at least one of its function or static data members has a exclusive definition. An exclusive class definition is effective if and only if the compiler emits class meta data along with exactly one exclusive member definition.
    replicable class definition
    A class definition that does not meet the criteria of an exclusive class definition.

    Practice

    This section describes some existing practice. It is not a complete description; Benjamin Kosnik, N1976 Dynamic Shared Objects: Survey and Issues provides more details.

    Isolation

    There are several approaches to the syntax for specifying or retracting isolation for a symbol.

    Microsoft
    Symbols are isolated by default. The declaration specifier __declspec(dllexport) specifies that a symbol definition is not isolated. The declaration specifier __declspec(dllimport) specifies that a symbol declaration is satisfied by an non-isolated symbol.
    GNU on Unix
    Symbols are not isolated by default. A declaration attribute specifies that a symbol is isolated, e.g. __attribute__((visibility("hidden"))).
    Sun
    Default symbol isolation is defined by a command-line option, with the default of the option being that symbols are not isolated by default. For a given symbol, the visibility is specified with a storage class, e.g. __global or __hidden.
    Pete Becker, N1428 / N1496 Draft Proposal for Dynamic Libraries in C++
    The syntax is only notional, not a formal proposal. Symbols are isolated by default. For a given symbol, the visibility is specified with a storage class, e.g. shared.
    Lawrence Crowl, N2117 Minimal Dynamic Library Support
    The syntax is hinted as a storage class.

    In addition to specifying (non-)isolation for a single symbol, it is convenient to have a syntax for specifying (non-)isolation for a region of code, particularly in header files. There are fewer examples of such syntax.

    GNU on Unix
    A pragma can push and pop default visibility.
    #pragma GCC visibility push(hidden)
    #pragma GCC visibility pop
    Pete Becker, N1428 / N1496 Draft Proposal for Dynamic Libraries in C++
    The syntax is only notional, but the shared storage class can be placed before a brace-enclosed region, much like extern "C".

    Resolution

    There are two primary approaches to resolution of multiple symbol definitions.

    Windows
    A reference is bound to the definition of a symbol in a statically dependent library. Thus a library may have not have a function replaced by the application. A consequence is that a library may not offer replicable functions without substantial work. This work is necessary to meet the application-replaceable semantics of the global allocation operators.
    Unix
    The first definition of a symbol in the ordered load set is chosen for all references. That is, the first definition interposes on other definitions. A consequence is that a library may have any function replaced by the application.

    As always, there are complications. Modern Unix systems provide for "protected" resolution, in which a reference to a protected symbol defined within the same load unit will bind to that definition irrespective of any prior definitions in the ordered load set.

    Furthermore, some Unix systems, e.g. Sun and GNU/Linux, provide the ability to resolve a symbol to a dependent library in preference to normal interposition resolution.

    Proposal

    We propose C++ dynamic library support that exploits existing operating system facilities for dynamic libraries. Furthermore, we structure that support so that complexity rises with benefits. The Committee can choose the features that it needs. Finally, we specifically avoid trying to solve the whole problem, concentrating instead on those portions of the problem that affect large amounts of code. If an aspect of the program generally only affects a few lines of code, we leave it to programmers to write platform-specific code.

    Late Binding

    The first feature of dynamic library support is late binding. Late binding is entirely consistent with the current standard, and no change is necessary for this feature.

    Isolation

    The second feature of dynamic library support is isolation. To enable isolation, the standard must recognize the load unit as an intermediate layer of visibility between a translation unit and the program.

    One load units are present, the standard must provide a mechanism that specifies whether a symbol is isolated to a load unit or visible to all load units.

    The primary mechanism for isolation is and should remain namespaces. Namespaces provide the best foundation for preventing symbol clashes. However, namespaces are insufficient for two reasons. First, they are transparent to functions with C linkage. Second, they are not robust to an adversarial use of implementation details. As a consequence, an additional mechanism is necessary.

    Given a mechanism for isolation, the standard must admit multiple definitions for the same symbol, provided that those definitions are isolated from each other.

    For the isolation syntax, we propose to avoid introducing a new keyword and extend the public, protected, and private labels to load unit visibility for namespace-scoped symbols. Symbols with public or protected labels are not isolated. (The distinction between public and protected appears later.) Symbols with a private label are isolated to a load unit and are distinct from any non-isolated symbol or any isolated symbol declared in another load unit. Specifically, functions and variables have distict addresses while types have distinct typeids.

    For class definitions, any meta-data must be isolated as well. Achieving distinct typeids for isolated types is most likely to require an implementation to change the ABI of the language.

    The member function and static member variable symbols associated with a class have the visibility of their containing class. That is, within class definitions, the labels have their existing access-specifier meaning. Furthermore, class member definitions outside of a class definition ignore the prevailing visibility, and instead use the visibility of the class definition.

    A label within a declarative region extends to the next label or to the end of the region, whichever comes first. Any label in effect immediately before a declarative region will be in effect immediately after that region. There are two kinds of declarative regions, namespace and language linkage. Programmers can limit the scope of such labels at global scope, or within a namespace region, by enclosing them in language linkage (extern "C++" { }) regions. For example:

    
    extern "C++" {
    private:
        int my_helper( int a ) { return a+1; }
    public:
        int give_me_more( int a ) { return my_helper( a+1 ); }
    }
    

    To assist in migration of existing code, the visibility in effect at the beginning of a translation unit is implementation-defined. Within headers, programmers should place all labels within a declarative region so as to preserve the implementation default.

    We considered using the proposed annotation facility, Jens Maurer, Michael Wong, N2379 Towards support for attributes in C++, but decided against using it because the isolation specification does not meet the "ignorable" criteria for attributes. That is, removing the isolation indication would produce ill-formed programs.

    Resolution

    The third feature of dynamic library support is resolution of symbol references to multiple definitions. This topic is somewhat complicated, and we approach it in two ways.

    Single Definition

    The simplest proposal is to simply define multiple definitions of non-isolated symbols as an error.

    Because existing dynamic linker technology has only one category of definition, any replicable definition appears as though there were multiple exclusive definitions. Therefore, the simplest standard would simply prohibit non-isolated replicable definitions. A consequence is that the standard library would need careful thought as to which parts were applicable to a shared dynamic library and which parts were applicable to a replicated static library.

    A more usable standard would support non-isolated replicable definitions provided that the definitions are identical. Doing so is not conceptially difficult; the primary problem is choosing a unique address/typeid. The dynamic linker can simply choose one of the definition artifacts. The existing Unix interposition resolution approach meets these semantics exactly. The existing Windows dependence resolution approach poses a problem, normally yielding different addresses within different load units. Potential solutions to this is to require each library obtain addresses from a shared table or to simply live with different addresses for what are conceptually the same function. Programmers rarely rely on inline functions having identical addresses.

    Multiple Definitions

    When multiple definitions are available for exclusive definitions, the implementation must resolve references to definitions. Unfortunately, neither the Unix approach nor the Windows approach appears to fully solve the problem. The Unix interposition approach leaves programs vunerable to inconsistent definitions when functions are inlined and interposed. The Windows dependence approach prevents interposition, as required for the global allocation operators. To resolve this issue, we propose to "do both".

    Syntactically, we refine the label syntax introduced above for isolation. Semantically, we leave much implementation-defined because detailed specification of compile and link commands is beyond the scope of the standard.

    For example, and by way of illustration, the standard library would have the following declarations.

    
    namespace std {
        typedef void (*new_handler)();
    protected:
        new_handler set_new_handler( new_handler ) throw();
    }
    extern "C++" {
    public:
        void * operator new( std::size_t ) thorw( std::badalloc );
    }
    

    The primary problem with different replicable definitions is that current linker technology is unable to determine that two definitions are replicants of each other. Furthermore, replicants are often involved in inlining, and a non-inline call with different semantics from an inline expansion is bound to cause inconsistency and potentially failure. Therefore, we propose to prohibit public replicable definitions.

    Conditional Loading

    The fourth feature of dynamic library support is conditional loading. In terms of isolation and resolution, conditional loading introduces no new issues. The two new issues are initialization and destruction order for static-duration variables and finding a root symbol for the library.

    We believe that the order of initialization and destruction as defined in Lawrence Crowl, N2382 Dynamic Initialization and Destruction with Concurrency provides for sufficiently late execution of initializers to admit conditional loading.

    Finding the root symbol on a library generally involves converting a string containing some form of the symbol name into an address. As this code has low static frequency, we choose to not standardize it. Programmers will need to specialize their code for each supported platform.

    Removal

    The fifth feature of dynamic library support is library removal. This feature is also known as closing a dynamic library. The implications on order of destruction of static-duration and thread-duration variables could be severe. So, rather than try to define a precise meaning, we intend to provide advice to programmers on how to avoid the problems. In particular,

    As code to remove a dynamic library also has low static frequency, so we chose to not standardize it. Programmers will need to specialize their code for each supported platform.

    Changes

    The changes to text of the standard will go here. The extent of those changes depends on which features the committee chooses to support.

    The intent is to cover core language changes only, leaving standard library changes to a separate paper.