Search code examples
c++windowsvisual-c++atlwtl

How to shrink the size of a WTL application?


WTL applications are quite small already. However, using VS 2005 a statically linked application with WTL 9.10 weighs in at 136 kB (139,264 Bytes) for the Win32 configuration.

Looking at the executable I noticed a static import of oleaut32.dll. A cursory look with dumpbin shows one import via ordinal.

OLEAUT32.dll
            4181C0 Import Address Table
            41C9B8 Import Name Table
                 0 time date stamp
                 0 Index of first forwarder reference

                  Ordinal   277

Inspecting oleaut32.dll one finds that the export is named VarUI4FromStr.

Digging a little with IDA, I found that VarUI4FromStr was used by ATL::CRegParser::AddValue. Following the dependents from there, showed two calls in ATL::CRegParser::RegisterSubkeys.

Cross-referencing the ATL code of my Visual Studio installation with the findings, I found that the culprit was ATL::CAtlComModule. It does a lot of TypeLib registration stuff that is simply not needed for my use case.

However, the linker seems to leave all of that in, because it cannot reasonably decide to throw it out.

How can I get rid of this seemingly superfluous import?


Solution

  • Alas, since WTL::CAppModule derives directly from ATL::CComModule, including the atlbase.h header while having _ATL_NO_COMMODULE defined leads to an error:

    Error   1   fatal error C1189: #error :  WTL requires that _ATL_NO_COMMODULE is not defined $(ProjectDir)\wtl\Include\atlapp.h  33  
    

    The actual culprit that ultimately pulls in ATL::CComModule is, however, ATL::CAtlComModule. So our goal is to get rid of both of them.

    We'll try to trick atlbase.h into excluding all the TypeLib registration code by defining _ATL_NO_COMMODULE anyway, but undefining it right after we're done including it. This way atlapp.h and other WTL headers won't "notice".

    #define _ATL_NO_COMMODULE
    #include <atlbase.h>
    #undef _ATL_NO_COMMODULE
    #include <atlapp.h>
    

    Obviously that gets us into some trouble:

    1>atlapp.h(1515) : error C2039: 'CComModule' : is not a member of 'ATL'
    1>atlapp.h(1515) : error C2504: 'CComModule' : base class undefined
    1>atlapp.h(1524) : error C2653: 'CComModule' : is not a class or namespace name
    1>atlapp.h(1543) : error C2653: 'CComModule' : is not a class or namespace name
    1>atlapp.h(1625) : error C3861: 'GetModuleInstance': identifier not found
    1>atlapp.h(1784) : error C2653: 'CComModule' : is not a class or namespace name
    1>atlapp.h(1806) : error C2065: 'm_nLockCnt' : undeclared identifier
    

    However, it looks like we should be able to fix this handful of errors quickly.

    First of all let's check where ATL::CComModule comes from: it's declared and defined in atlbase.h. We can surely declare our own class and squeeze it in between #include <atlbase.h> and #include <atlapp.h> (see above).

    Let's start with the basics, based on what can be found in atlbase.h:

    namespace ATL
    {
        class CComModule : public CAtlModuleT<CComModule>
        {
        public:
            CComModule() {}
        };
    };
    

    Now we get a different set of errors:

    1>atlapp.h(1524) : error C2039: 'Init' : is not a member of 'ATL::CComModule'
    1>        stdafx.h(31) : see declaration of 'ATL::CComModule'
    1>atlapp.h(1625) : error C3861: 'GetModuleInstance': identifier not found
    

    Excellent, so we're in fact only missing two more class functions: GetModuleInstance and Init. Let's pull those in as well. For good measure we'll also add GetResourceInstance:

    namespace ATL
    {
        class CComModule : public CAtlModuleT<CComModule>
        {
        public:
            CComModule() {}
            HINSTANCE GetModuleInstance() throw() { return _AtlBaseModule.m_hInst; }
            HINSTANCE GetResourceInstance() throw() { return _AtlBaseModule.m_hInstResource; }
            HRESULT Init(_ATL_OBJMAP_ENTRY*, HINSTANCE, const GUID*) throw() { return S_OK; }
        };
    };
    

    A short test proves that this is working fine. And the oleaut32.dll import is gone along with three more imports from ole32.dll. Great success!

    And as a result we have saved 12 kB on the executable size.

    Do you know other cool tricks to shrink WTL application size?

    IMPORTANT

    Note that depending on the features of ATL::CComModule which your code uses, you may have to fall back to using the original. The only other alternative is to extend the above mock version of ATL::CComModule to fix any compilation errors you may run into.

    CAUTIONARY NOTE

    I have not tested this extensively yet and I'll report back (by editing this answer) if I run into any trouble. However, from looking at it in both the ATL and WTL code as well as in the IDA database prior to my changes, I think this is a safe change.

    Bonus trick

    You may also use the 6001.18002 standalone WDK (for Vista SP1), which supports to target Windows 2000 (SP4) and newer. It includes ATL 7.0 and is therefore suited to build ATL+WTL applications.

    To do that you just need to add the following two lines to your sources file:

    USE_STATIC_ATL=1
    ATL_VER=70
    

    Thanks to the WDK, which uses msvcrt.dll, a system DLL, as its default (multithreaded dynamically linked) C runtime, you can then shrink the executable size further by linking dynamically to the C runtime. And in this case, since that particular C runtime is a system DLL (starting with Windows 2000), you can rest assured that it will work.

    All of that taken into account you can build standard Windows applications (GUI or console) and DLLs that are really small.

    Here is an example sources file which you can use as a template:

    # Name of the program
    TARGETNAME=progname
    # Prefix used for the intermediate/output paths (e.g. objfre_w2k_x86)
    TARGETPATH=obj
    # A program, not a driver or DLL or static lib
    TARGETTYPE=PROGRAM
    # windows == GUI, console == Console, native == no subsystem ...
    UMTYPE=windows
    # Use Unicode ("wide char") instead of ANSI
    UNICODE=1
    # By default the WDK build treats warnings as errors - this turns it off
    BUILD_ALLOW_ALL_WARNINGS=1
    # Link dynamically to msvcrt.dll
    USE_MSVCRT=1
    # Don't link against any of the ATL DLLs
    USE_STATIC_ATL=1
    # In the 7600.16385.1 WDK you can use 70 as well, which translates to 71
    ATL_VER=70
    USE_NATIVE_EH=1
     # RTTI is required for some ATL/WTL features
    USE_RTTI=1
    
    # GUI programs require these entry points
    !IF defined(UNICODE) && $(UNICODE)
    UMENTRY=wwinmain
    C_DEFINES=$(C_DEFINES) /DUNICODE /D_UNICODE
    !ELSE
    UMENTRY=winmain
    C_DEFINES=$(C_DEFINES) /DMBCS /D_MBCS
    !ENDIF
    
    # Include folders
    INCLUDES=$(DDK_INC_PATH);$(CRT_INC_PATH);$(SDK_INC_PATH);wtl\Include
    
    # Libraries to link to, not just import libraries
    TARGETLIBS=\
                $(SDK_LIB_PATH)\kernel32.lib \
                $(SDK_LIB_PATH)\user32.lib \
                $(SDK_LIB_PATH)\Comctl32.lib \
    
    # Give source files (also resources and .mc) in the current or parent directory
    SOURCES=...
    

    The 7600.16385.1 standalone WDK also works for this. The WDKs starting from the Windows 8 WDK now require a Visual C++ into which they can be integrated. This also leads to the binaries requiring the respective C runtime version instead of the msvcrt.dll from prior WDK versions.