Wrapper DLL

From Crypto++ Wiki
Jump to navigation Jump to search

The topic of Wrapper DLLs comes up often on the mailing list. Usually it is in the context of a Windows DLL because folks try to use the FIPS DLL as a general purpose DLL. On occasion it comes up in the context of Linux, like on Android or iOS.

Trying to use the Visual Studio DLL is almost always a bad idea. The Visual Studio DLL is a FIPS DLL, it is hard to use and it is missing all non-FIPS classes. Also see the Visual Studio and FIPS DLL wiki pages, which provide a lot of the gory details.

Instead, directly use the static library. The static library will avoid the problems with the FIPS DLL, and make a program faster and more secure. DLL Hell affects more than just Windows systems. Also see Breaking the Links: Exploiting the Linker and Dynamic Linking. And things only got worse with Versioned Symbols.

If your heart is set on creating a Windows DLL with a selection of classes you provide, then see Converting static link library to dynamic dll on Stack Overflow. You can use the static library and a module-definition file (DEF file) to do it.

An additional problem on Linux and Unix is, the makefile can produce a shared object that exports nearly every symbol the library has. The shared object with symbols is over 41 MB on x86_64, and you won't use most of the code. It is also slow to load because the export table is so large. Once you investigate things you probably don't want to use the library's shared object.

Wrapper DLLs and shared objects can use symbol visibility to help control some of the problems with DLLs and shared objects. It is easier to control visibility and improve load times using a wrapper because you know exactly what must be exported. In fact GCC reports one C++ library went from a 6 minute load time to an 8 second load time when symbol visibility was used. Also see Visibility on the GCC wiki.

Wrapper DLLs and shared objects have one tricky area due to C++ exceptions. As a general rule of thumb, you should not let exceptions cross a module boundary. The DLL or shared object should always catch Crypto++ exceptions and return a success/failure code. If an exception is intended to cross a module boundary then Runtime Type Information (RTTI) must be available so the caller can catch the exception.

This page will show you how to create a wrapper DLL or shared object that is considerably smaller in size that you can use in you Android, iOS, Linux and Windows projects. If you use a C-interface then you can use them in managed languages like Java and .Net, too.

Related wiki articles are Nmake (Command Line), MSBuild (Command Line), Visual Studio and FIPS DLL.

Runtime Linking

Runtime linking is Windows specific, and Linux and Unix users can skip this section. By default the Visual Studio project uses static runtime linking. That usually causes problems in projects that use the C++ runtime in a dynamic configuration, like ATL, MFC and Qt. The problems include duplicate library symbols during linking and memory errors at runtime.

Before you begin to wrap the static library in a DLL you should probably change runtime linking from static to dynamic in the Visual Studio project files. You can find the instructions at Visual Studio | Runtime Linking.

Once you change the runtime linker settings you should clean and rebuild the library. After changing the setting you should not have problems with duplicate symbols when using libraries like ATL, MFC and Qt.

External API

The first thing you should do is design the basic interface for your wrapper DLL or shared object. For demonstration purposes this article creates a shared object to generate and verify SHA256 digests.

The wrapper DLL needs two functions to accomplish its goals. It will use a C interface to make calling its functions easy. It will also use visibility controls to avoid symbol pollution and make link-loading faster.

  • sha256_hash_message
  • sha256_verify_digest

Both functions need to take pointers to a message buffer and a digest buffer. sha256_hash_message reads the message and writes the digest. sha256_verify_digest reads both the message and digest.

Both functions will follow most C API conventions and return 0 for success and non-0 for failure.

The final API should look something like the following.

int sha256_hash_message(uint8_t* digest, size_t dsize,
                        const uint8_t* message, size_t msize)

int sha256_verify_digest(const uint8_t* digest, size_t dsize,
                         const uint8_t* message, size_t msize)

Wrapper DLL

The wrapper DLL is shown below. There is not much to it because the wrapper defers to the library for the heavy lifting. When compiling the wrapper on Windows you should define BUILDING_DLL so dllexport is exposed.

#include "cryptlib.h"
#include "sha.h"
#include <stdint.h>

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_DLL
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllexport))
    #else
      #define DLL_PUBLIC __declspec(dllexport)
    #endif
  #else
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllimport))
    #else
      #define DLL_PUBLIC __declspec(dllimport)
    #endif
  #endif
  #define DLL_LOCAL
#else
  #if __GNUC__ >= 4
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))
    #define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define DLL_PUBLIC
    #define DLL_LOCAL
  #endif
#endif

extern "C" DLL_PUBLIC
int sha256_hash_message(uint8_t* digest, size_t dsize,
                        const uint8_t* message, size_t msize)
{
    using CryptoPP::Exception;
    using CryptoPP::SHA256;

    try
    {
        SHA256().CalculateTruncatedDigest(digest, dsize, message, msize);
        return 0;  // success
    }
    catch(const Exception&)
    {
        return 1;  // failure
    }
}

extern "C" DLL_PUBLIC
int sha256_verify_digest(const uint8_t* digest, size_t dsize,
                         const uint8_t* message, size_t msize)
{
    using CryptoPP::Exception;
    using CryptoPP::SHA256;

    try
    {
        bool verified = SHA256().VerifyTruncatedDigest(digest, dsize, message, msize);
        return verified ? 0 : 1;
    }
    catch(const Exception&)
    {
        return 1;  // failure
    }
}

Symbol Visibility

There are some sharp edges when working with GCC symbol visibility and LD linker behavior. Namely, you have to do more than just compile with -fvisibility=hidden and -fvisibility-inlines-hidden. Unfortunately the GCC wiki article on Visibility does not discuss it.

The first thing you need to know is, when you define DLL_PUBLIC __attribute__ ((visibility ("default"))) and then use DLL_PUBLIC it only applies to your source code. In the example above that means it only applies to sha256_hash_message and sha256_verify_digest.

The second thing you need to know is, you are linking against libcryptopp.a. The members of the static archive were not compiled with symbol visibility and -fvisibility=hidden and -fvisibility-inlines-hidden did not affect the object files in the archive. Everything in the archive is still public.

The third thing you need to know is, you need to use LD's -Wl,--exclude-libs,ALL to avoid re-exporting the static library's symbols. In this case the LD option is needed to avoid making all of libcryptopp.a symbols public in your shared object.

The three rules above mean the minimum command line to use for your wrapper shared object is:

$ g++ -fPIC -fvisibility=hidden -fvisibility-inlines-hidden -c sha256_wrapper.cxx
$ g++ -shared -o sha256_wrapper.so sha256_wrapper.o ./libcryptopp.a -Wl,--exclude-libs,ALL

The -Wl,--exclude-libs,ALL is the important one on Linux with a GNU linker. Without the option the linker will re-export symbols from libcryptopp.a that were not stripped or removed.

If you are working on Solaris then see the documentation for -xldscope. If you are working on OS X then see this question and answer. Other compilers and linkers on other platforms will need to be researched.

DLL Sizes

Earlier we said the library's DLL or shared object exports nearly every symbol the library has, and it makes for a bloated shared object. Below we compare the numbers on Linux using the code for the shared object shown earlier. The measurements were taken on Fedora 28 x86_64.

First, we build the Crypto++ and enable dead code stripping and garbage collection on unused symbols. Notice the use of -ffunction-sections, -fdata-sections and -Wl,--gc-sections. If you are working on OS X then you would use -ffunction-sections, -fdata-sections and -Wl,-dead_code.

$ make shared lean -j 4
g++ -DNDEBUG -g2 -O3 -fPIC -pthread -pipe -ffunction-sections -fdata-sections -c cryptlib.cpp
g++ -DNDEBUG -g2 -O3 -fPIC -pthread -pipe -ffunction-sections -fdata-sections -c cpu.cpp
...

g++ -shared -o libcryptopp.so.7.1.0 -Wl,-soname,libcryptopp.so.7 -DNDEBUG -g2 -O3 -fPIC -pthread
-pipe -ffunction-sections -fdata-sections -Wl,--gc-sections cryptlib.o cpu.o integer.o ...

Then inspect the size of libcrpytopp.so. The size of the library is large because debug symbols are present. Stripping the library will reduce the size by orders of magnitude, but it will still be 6 or 8 MB in size.

$ ls -Al libcryptopp.so*
lrwxrwxrwx. 1 cryptopp cryptopp       20 Aug 26 13:45 libcryptopp.so -> libcryptopp.so.7.1.0
lrwxrwxrwx. 1 cryptopp cryptopp       20 Aug 26 13:45 libcryptopp.so.7 -> libcryptopp.so.7.1.0
-rwxrwxr-x. 1 cryptopp cryptopp 41561968 Aug 26 13:45 libcryptopp.so.7.1.0

And finally strip libcrpytopp.so.

$ objcopy --strip-debug libcryptopp.so
$ ls -Al libcryptopp.so*
lrwxrwxrwx. 1 cryptopp cryptopp      20 Aug 26 14:20 libcryptopp.so -> libcryptopp.so.7.1.0
lrwxrwxrwx. 1 cryptopp cryptopp      20 Aug 26 14:20 libcryptopp.so.7 -> libcryptopp.so.7.1.0
-rwxrwxr-x. 1 cryptopp cryptopp 6167384 Aug 27 10:48 libcryptopp.so.7.1.0

Now the wrapper shared object.

$ g++ -DNDEBUG -g2 -O3 -fPIC -ffunction-sections -fdata-sections -c sha256_wrapper.cxx
$ g++ -o sha256_wrapper.so -DNDEBUG -g2 -O3 -shared -fPIC -ffunction-sections -fdata-sections
-fvisibility=hidden -fvisibility-inlines-hidden sha256_wrapper.o ./libcryptopp.a
-Wl,--gc-sections -Wl,--exclude-libs,ALL

After the wrapper shared object is built you can check the export table:

$ nm -CD sha256_wrapper.so | grep ' T '
00000000000184e8 T _fini
00000000000069d0 T _init
00000000000095b0 T sha256_hash_message
0000000000009710 T sha256_verify_digest

The unstripped size of the wrapper shared object.

$ ls -Al sha256_wrapper.so
-rwxrwxr-x. 1 cryptopp cryptopp 13300848 Aug 27 10:49 sha256_wrapper.so

And the stripped size of the wrapper shared object.

$ objcopy --strip-debug sha256_wrapper.so
$ ls -Al sha256_wrapper.so
-rwxrwxr-x. 1 cryptopp cryptopp 182576 Aug 27 10:50 sha256_wrapper.so

So there is a significant savings (6.2 MB vs 182 KB) when using the wrapper shared object instead of the library's shared object.

Takeaways

A lot of information was covered in the previous sections. Here are the takeaways.

  • provide a wrapper DLL or shared object for your app
  • provide a flat C-interface to callers
  • don't let exceptions cross module boundaries
  • use DLL_PUBLIC macro to export symbols
  • use the Crypto++ static archive
  • compile with -fvisibility=hidden -fvisibility-inlines-hidden for symbol visibility
  • compile with dllexport on Windows for symbol visibility
  • compile with -ffunction-sections -fdata-sections for symbol stripping
  • compile with /Gy on Windows for symbol stripping
  • link with -Wl,--exclude-libs,ALL for symbol visibility
  • link with -Wl,--gc-sections or -Wl,-dead_strip for symbol stripping
  • link with /OPT:REF on Windows for symbol stripping

References

Here is a small list of references.