The Scenario: Make an ATL based class library that can be used from both an ATL based console application and an MFC/ATL based GUI application. The class library cannot contain any MFC links. The GUI application must use ATL. The console application may not use any MFC, or link to any library that does.
Ok, that sounds like a pretty easy order, but if your not used to playing with some linker settings, you'll get tripped up fast. You could always go the easy route by adding
/FORCE:MULTIPLE in the linker command line options, but then every time you compile you will get treated to tons of linker warnings. This is not the way to the shining path! Our goal is to make a well behaved library that will link up without any special options specified in the consuming applications, other than importing the library, and including the header file.
I'm going to call any application that links to a library a consumer. Consumers will #include the library header, and then link to the library file in thier linker stage. In the sample, ConsoleApp, and Application are both consumers.
Know Thy Enemy (Your Linker Error Nemises)
Fig 0: Setting the configuration for building.
If you compile the sample project with the configuration setting set to "Errors Galore" (Fig 0), you will be treated to loads of nasty looking error messages. LNK2005, LNK2019, LNK4098, LNK1169, LNK1120
Table of possible linker messages from "Errors Galore" configuration.
||<symbol> already defined in <library> (<object generating the error>)|
||unresolved external symbol "<symbol signature>" referenced in function <function signature>|
||defaultlib '<library name>' conflicts with use of other libs; use /NODEFAULTLIB:library|
||one or more multiply defined symbols found|
||<X> unresolved externals|
Taking out the trash
Let's start with everyone's least favorite, the often seen LNK4098. This error hangs out with LNK2005. You can fix both by setting your linker options for the library to match the most restricted application that will use your library. In this case, the MFC GUI application demands that it be linked with the Multi-Threaded DLL. If you are compiling in a debug setting, you must link with Multi-Threaded Debug DLL (See Fig1). Once you set this option for your library and recompile it, you will see LNK4098 and LNK2005 beat a hasty retreat. Because you are also going to use this library with an ATL console application, you must set Runtime Library to Multi-Threaded DLL for console application as well. NOTE: If you have other libraries that you are using that expose function signatures similar to the ones in your library, then you may still get LNK2005 errors. Find the offending library, and remove it from your project. If it's a default library, and you are sure you won't need it, you can add it to the Ignore Default Library list (See Fig2).
Fig 1: Setting the Runtime Library to match the Runtime Library used by the consuming application. If you were setting this option in Release configuration, then you would select Multi-threaded DLL (/MD).
Fig 2: Sometimes when there's no other option, you can just ignore the library that the compiler is complaining about. This is not the way to walk the shining path.
Now, you can try to recompile the sample in the " Less Errors " configuration, and see that we have successfully banished LNK4098 and LNK2005. You will immediately notice that we still haven't gotten rid of an LNK2019 and LNK1120 error. What is really curious about this is that the
ConsoleApp linked just fine to the library, and the call from
ConsoleApp to the library is the same as the call from Application to the library. What's happening here is under the hood in atlstr.h, the header that contains the ATL definition for
CString. The definition for
CString in VC++ 7 is radically different from the VC++ 6 one in that it is a template class, where the VC++ 6 implementation is just a class. The problem is that MFC's
CString default template is different than the one that ATL is using. What we need to do is to get both the library and the consumer application to use the same signature for the linker. Then, the linker can find the signature in the library at link time, and you won't get those nasty LNK2019 and LNK1120 errors.
MFC defines CString as:
typedef ATL::CStringT< TCHAR, StrTraitMFC< TCHAR > > CString;
While ATL defines
CString as (With
typedef CStringT< TCHAR, StrTraitATL< TCHAR > > CString;
This difference in the
StrTrait is where all the problems with LNK2019 and LNK1120 come from.
The solution I'm going to use came from this MSDN article, which shows a similar tactic, but unfortunately the method on MSDN makes the library link to MFC, and we don't want that for our ATL library. What I am going to do instead is to change the function in the library to use
CAtlString instead of
CString in the method calls. If you look at the definition for
CAtlString, you will notice that it is just a #define for
CStringT, with a specific template. Because
CAtlString explicitly states the signature for the parameter, this enables the linker to find the function in the library because it is looking for a function with parameters of type
CStringT< TCHAR, StrTraitATL< TCHAR > >.
Also, I am including atlstr.h in the library's header file. This way, when the header file is included in a consumer application,
CAtlString will be defined for the header file, and you won't get undefined symbols linker errors for
CAtlString. This also ensures that the consumer application doesn't need any funky configuration changes; just include and go! To see the application link up correctly, change the configuration to "Debug" or "Release" and compile it.
On with the code
Because there are 3 individual projects in the attached sample and they are mostly in default state, I am not going to cover them in any great detail. Let's hit the highlights, and see the source code for other miscellenous comments.
void HelloWorld(CAtlString message);
void HelloWorld(CString message);
Above, we see the source for the library include file. When included in a consumer application, this file will include atlstr.h, so that CAtlString is defined.
#pragma once#define WIN32_LEAN_AND_MEAN //Exclude rarely-used stuff from
#define CAtlString CString
Here is the stdafx.h for the Library. You should note at the bottom that I am defining
CAtlString to a
CString. This may seem counter productive, as in the HelloLib.h header, I just changed all the
CAtlString's! However, without this #define, the intellisense doesn't seem to want to display the quick pick function list that I love so much. Thus, by casting this in stdafx.h for the library (and only the library), intellesense is enabled. When the library compiles, the compiles automatically selects the proper
CStringT definition for ATL, as this is an ATL based project. I placed this #define in stdafx.h so that it would not be included in the library header file that will be used by a consumer. This way, the
CAtlString declarations are in full effect when the consumer includes the library.
#pragma comment(lib, "..\\Library\\Debug\\Library.lib")
#pragma comment(lib, "..\\Library\\Release\\Library.lib")
int _tmain(int argc, _TCHAR* argv)
printf("Calling into library.\n");
hw.HelloWorld("I was called from a console application.");
Above, the code for ConsoleApp.cpp, shows you how to select between a debug edition of an included library, and a release version of an included library. I found this to be a quite handy trick, as it instructs the linker to bring in a library without having to adjust the project settings at all! The Application demo also uses the same trick, so I won't bother to put it in here.
Points of Interest
I think that it's kind of an issue with ATL/MFC as they are technologies that can be mixed together, they should also be able to work around each other. The above solution is rather hacky, even tho it gets the job done simply. I would rather have ATL or MFC check to see if one or the other is included, and use the proper signature, but maybe that could be a goal for VC++ 8...
4/11/2003 - Initial creation / Shining path walk.
4/15/2003 - Not as smart as I thought I was / Path suddenly went dim... Had to change several things to keep from redefining CString in MFC applications when importing the library headers. Path is now back in view.