Introduction
In the last part I showed how to start adding static libraries and setup to share information between the different portions of the CMake build system. This part will cover the last of the build targets, shared libraries which are a little more difficult than the static libraries. The difficulty is not on the CMake side but instead due to Windows being a bit of a pain in this area and requiring some additional work. Once the shared libraries are complete though, the final portion of the environment will be added, the unit test library.Unit tests are a great benefit to nearly any project. A set of proper tests can help in many ways, the most obvious being to catch bugs early. Unfortunately a number of the test libraries end up requiring a lot of boilerplate code and become a pain to write. As such, when looking for a testing library the number one goal was to find something as minimal and non-intrusive as possible. At the same time, a mocking system is also useful to have for larger and more complicated testing purposes. The Google Mock library googlemock supplies both the mocking and unit test libraries and it is very simple to use.
Download CMakeProject.zip 4.45K 8 downloads
and uncompress it somewhere if you don't have it from Part 2 of the series.
Parts 1 2 3
Shared Libraries
The final target type which we have not implemented as of yet is the shared library. At the basic level it is no different than using a static library and in fact, on Linux and OsX it is identical except it is a shared library. Unfortunately Windows is significantly more complicated due to the requirements of importing and exporting functions. We'll start by ignoring the problems on Windows for now and getting the basics functioning on Linux and OsX.The first thing to do is add a new set of directories for the shared library:
Create the following directories under CMakeProject/libraries:
World Include Source
Now edit the CMakeProject/libraries/CMakeList.txt as follows:
ADD_SUBDIRECTORY( libraries/Hello ) ADD_SUBDIRECTORY( libraries/World )
Finally add the new files:
CMakeProject/libraries/World/CMakeLists.txt:
SET( INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/Include ) INCLUDE_DIRECTORIES( ${INCLUDE_DIRS} ) SET( INCLUDE_FILES Include/World.hpp ) SET( SOURCE_FILES Source/World.cpp ) ADD_LIBRARY( World SHARED ${INCLUDE_FILES} ${SOURCE_FILES} ) # Export the include directory. SET( WORLD_INCLUDE_DIRS ${INCLUDE_DIRS} PARENT_SCOPE )
CMakeProject/libraries/World/Include/World.hpp:
#pragma once const char* const WorldString();
CMakeProject/libraries/World/Source/World.cpp:
#include <World.hpp> const char* const WorldString() { return "World!"; }
Regenerate the build and you now have a shared library. If you are using Linux or OsX, your library is complete and can be used by simply adding it to a target the same way the static library was added. Let's make a couple minor modifications in order to use this library and link against it.
CMakeProject/libraries/Hello/Source/Hello.cpp:
#include <Hello.hpp> const char* const HelloString() { return "Hello"; }
CMakeProject/tests/Hello2/main.cpp:
/* Hello World program */ #include <iostream> #include <functional> #include <Hello.hpp> #include <World.hpp> int main( int argc, char** argv ) { std::cout << HelloString() << " " << WorldString(); return 0; }
CMakeProject/tests/CMakeLists.txt:
CMAKE_MINIMUM_REQUIRED( VERSION 2.6.4 ) INCLUDE_DIRECTORIES( ${HELLO_INCLUDE_DIRS} ${WORLD_INCLUDE_DIRS} ) PROJECT( HelloWorld ) ADD_EXECUTABLE( Hello2 main.cpp ) TARGET_LINK_LIBRARIES( Hello2 Hello World )
With the given changes, Linux and OsX can build and run the modified 'Hello2' project and the string for "Hello" comes from a static library function and the "World!" comes from a shared library function. Unfortunately Windows is not so lucky and will fail to link.
CMakeProjectWindowsBroken.zip 5.8K 8 downloads
The Windows DLL
As with the OsX specific fix, using shared libraries with Windows is something of a black art. I don't intend to explain all the details, that would take another article. I will simply explain the basics of the fix and make the code work on all three platforms again.The basic problem with using shared libraries on Windows requires a little description. First off they are called dynamic link libraries (aka DLL) on windows. Second they are split into two pieces, the actual DLL portion which is the shared library where the code lives and a link library which is like a static library. Confusingly the link library usually has the file extention 'lib' just like an actual static library so don't get them confused. At this time, we are not telling the compiler to generate this intermediate file.
Generating the intermediate file is simple enough but you have to deal with it in a cross platform nature or you break Linux and OsX builds. Exporting the library symbol and making the link library is as simple as changing the function declaration to be the following:
__declspec( dllexport ) const char* const WorldString();
If you go ahead and make this change, the project compiles on Windows, but it will not run because it can't find the DLL. This problem is an annoyance with how VC tends to layout the projects when divided up in the manner suggested in this series of articles. Fixing this digs into the darker recesses of CMake properties which, for the moment we will avoid by cheating. For the time being, just copy the dll manually:
Copy from CMakeProject/build/vc11/libraries/World/Debug/World.dll to CMakeProject/build/vc11/tests/Hello2/Debug/World.dll
At this point Windows is functioning again, though with obvious cheats and actually a glaring error, though the error doesn't cause a problem at the moment. What's the error? Well, to be proper you are supposed to use '__declspec( dllexport )' only in the code building the DLL and then '__declspec( dllimport )' in code which uses the library. While it works as it is, it is best to follow the rules as closely as possible so as not to get strange behaviors/errors at a later time.
So, we'll extend the code to correct the issue of import versus export:
#ifdef BUILDING_WORLD # define WORLD_EXPORT __declspec( dllexport ) #else # define WORLD_EXPORT __declspec( dllimport ) #endif WORLD_EXPORT const char* const WorldString();
Rebuild and now you get a warning: "warning C4273: 'WorldString' : inconsistent dll linkage"
The reason is, we have not defined 'BUILDING_WORLD'. Since we want to define this only for the 'World' library, we edit the CMakeProject/libraries/World/CMakeLists.txt as follows:
SET( INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/Include ) INCLUDE_DIRECTORIES( ${INCLUDE_DIRS} ) SET( INCLUDE_FILES Include/World.hpp ) SET( SOURCE_FILES Source/World.cpp ) ADD_DEFINITIONS( -DBUILDING_WORLD ) ADD_LIBRARY( World SHARED ${INCLUDE_FILES} ${SOURCE_FILES} ) # Export the include directory. SET( WORLD_INCLUDE_DIRS ${INCLUDE_DIRS} PARENT_SCOPE )
Notice the new CMake command: 'ADD_DEFINITIONS'. As the name states, we are adding a definition to the macro preprocessor named 'BUILDING_WORLD' to the 'Hello2' target.
Windows is happy, we are following the rules and other than having to manually copy the DLL for the moment, it functions. There is one last issue, the macro will be applied to all platforms, something we don't want. So back in CMakeProject/libraries/World/Include/World.hpp, make the following change:
#ifdef _WINDOWS # ifdef BUILDING_WORLD # define WORLD_EXPORT __declspec( dllexport ) # else # define WORLD_EXPORT __declspec( dllimport ) # endif #else # define WORLD_EXPORT #endif WORLD_EXPORT const char* const WorldString();
CMake inserts per OS specific definitions based on the platform being targetted. In this case, as with using Visual Studio without CMake, '_WINDOWS' is defined by default. So, we can rely on this and now the code is safely cross platform and ready for commit.
Changing Output Directories
In order to finish correcting the Windows build we have to use a bit of CMake voodoo which is not often documented particularly well. Changing output directories. What we want is to output the executables and the libraries (shared and static) to a single output directory while leaving intermediate files wherever CMake/VC wants to store them. This is not required for OsX and Linux as the output generators there follow a more simplified directory structure. We also want to do this only for Visual Studio and not change the default behavior of nmake, CodeBlocks and other generators.Modify the CMakeProject/CMakeLists.txt to be the following:
CMAKE_MINIMUM_REQUIRED( VERSION 2.6.4 ) IF( APPLE ) SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -stdlib=libc++" ) SET( CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++" ) SET( CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libc++" ) SET( CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -stdlib=libc++" ) ENDIF( APPLE ) PROJECT( CMakeProject ) IF( MSVC ) SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/Binaries ) SET( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/Binaries ) SET( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/Binaries ) ENDIF( MSVC ) INCLUDE( libraries/CMakeLists.txt ) ADD_SUBDIRECTORY( tests )
Note that the detection for Visual Studio is performed after the 'PROJECT' command. There is a reason for this. CMake does not initialize many variables until after the 'PROJECT' command is issued. Among the variables not initialized is of course 'MSVC'. The reason for the delayed definition is that CMake reads the locally catched data at the point where it see's the 'PROJECT' command. So, while knowing the platform being run on and being able to modify the compile and linker flags can be done before the 'PROJECT' command, the specifics such as the generator in use are not known until the cache data is processed.
The next item to note is: why three directories? CMake divides the directories into three pieces to match the three target types, runtime for the executables, archive for static libraries and library for shared/dynamic libraries. While you could just set the executable and shared library paths, I include the static libraries in case something external to this build system wanted to link against the libraries. It is not required but makes things easier in advanced usages.
Finally, what is the 'PROJECT_BINARY_DIR' variable being used? Before we define any of the targets, this variable simply points to the directory CMake was launched from as it's out of source build location. In my case on Windows, I'm using '<root>/CMakeProject/build/Vc11' and as such that is what 'PROJECT_BINARY_DIR' is initialized with. There are many other variables, some which will also point to this location and could have been used. I just like this one because it is related to what I'm trying to accomplish, which is put all the final compiled binaries in one place.
Regenerate the project, rebuild all and you will now have a single directory with all the binary outputs within it. You can run the examples using Visual Studio and it will no longer complain that it can not find the DLL to load.
CMakeProjectFullFix.zip 6.08K 6 downloads
Unit Testing
With a title that states "Test Driven Development" in it, you would probably think that adding unit tests would have been done a bit earlier. The funny thing is that you have had a unit testing framework the entire time, it is built right into CMake itself called CTest. The downside is that I don't much like the way the built in functionality works and incorporating an external library is a good learning experience. So, we will start using googlemock in a little bit.During the course of writing this series I initially thought that I would stick to an 'all lower case' naming convention. In the process of writing, I mostly added code quickly and as such fell back on my habitual way of doing things without paying much attention. Other than to say I'm going to use camel case for the names, we'll skip the reasons and simply start with a fresh empty environment based on all the work we have done and also adding the final target divisions, so download the following ( CMakeEnvironment.zip 69.93K 7 downloads
) and we'll start adding the unit test library.
The New Environment
There is not much changed in the new environment other than applying the mentioned capitalizations and adding a couple new subdirectories along with the removal of 'tests'. The directories are intended as follows:- Build: A placeholder for where you can use CMake to store its cache files and such. I tend to use subdirectories such as: Vc11, Xcode, Makefiles, etc.
- External: The location we will be storing external libraries such as googlemock.
- Libraries: Static and shared libraries used by the projects.
- Applications: Your game, potentially another game and anything which is not a tool but uses the libraries.
- Tools: Obviously things which are used in the creation of the game. Exporters, converters, big level builders, etc.
Go Get GoogleMock
Download the gmock-1.6.0.zip file from googlemock and decompress it into the 'CMakeEnvironment/External' directory. You should end up with a new folder: 'CMakeEnvironment/External/gmock-1.6.0' which contains the library source. If you go looking around in the folder you will notice that it uses autoconf and m4 scripting to build on Linux/OsX and supplies a solution and project file for Visual Studio. This is normally fine for simple things but I want it integrated with the CMake build environment more tightly such that if I change global compiler flags it will rebuild along with the other sections of the project.As you will see, all the little steps we've gone through in setting up the environment with CMake in a clean manner will start to pay off as we integrate the library. Thankfully, as with many other Google libraries, the source contains a 'fused-src' directory which means we can add two headers and a source file to our project and almost be done with it. I say almost, since I want to make sure we integrate the library within the environment as cleanly as possible and make no modifications to the unzipped directory. Start by adding the following (and creating the new directories):
CMakeEnvironment/External/CMake/GMock/CMakeLists.txt:
# Use a variable to point to the root of googlemock within # the External directory. This makes it easier to update # to new releases as desired. SET( GOOGLEMOCK_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../gmock-1.6.0" ) # Make the file lists. SET( INCLUDE_FILES ${GOOGLEMOCK_ROOT}/fused-src/gtest/gtest.h ${GOOGLEMOCK_ROOT}/fused-src/gmock/gmock.h ) SET( SOURCE_FILES ${GOOGLEMOCK_ROOT}/fused-src/gmock-gtest-all.cc ) # Setup some build definitions for VC2012. IF( MSVC ) ADD_DEFINITIONS( -D_VARIADIC_MAX=10 ) ENDIF( MSVC ) # Add as a static library target. ADD_LIBRARY( GMock STATIC ${INCLUDE_FILES} ${SOURCE_FILES} )
This will allow GMock and GTest to compile on Windows and Linux. Unfortunately nothing ever seems to work without problems. The library refuses to compile on OsX due to Clang being detected as GCC and making a bad assumption about the availability of tr1 files. After arguing with it and even hacking the source I decided that for the time being, the easiest and most time effective method was to cheat. The benefit of cheating is under rated when you need to get things done and don't want to hack on external code. So, what exactly is this cheat? We'll add a directory for tr1 and a file for tuple, the file will simply alias the to C++11 tuple in order to meet the requirements. (I won't detail the changes, check out the various CMakeLists.txt if you are curious.)
Additionally, we have a link problem with the Linux environments to fix. If you tried to link against the GMock library and build you will get link errors, given the output it becomes pretty apparent that we've missed the threading libraries. In order to fix this, we get to learn a new command in CMake: FIND_PACKAGE. Anytime you need a set of specific includes and/or libraries to link against, there is usually a module which CMake supplies (or the community supplies) for the item. In this case we need to find the 'Threads' package. Once we do this, we add a new target link library to our binaries: '${CMAKE_THREAD_LIBS_INIT}'. This will take care of the linking on Linux and also because it is a variable, it will be empty on other platforms which don't need explicit linkage to an additional library.
As with static libraries in general, we know it is easy to link against the library but a bit more of a challenge to get the include paths setup. Additionally, the variadic work around for VC needs to be pushed out as a global item for all projects, otherwise they will fail to compile. This is all stuff we've dealt with before so it becomes easy. In the root CMake file move the 'ADD_DEFINITIONS' command to be just under the output path changes we made and also insert the FIND_PACKAGE command for all builds:
CMakeEnvironment/CMakeLists.txt:
# ############################################### # Make VC happy and group all the binary outputs, # also make sure GMock headers will compile in # all targets. IF( MSVC ) SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/Binaries ) SET( CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/Binaries ) SET( CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/Binaries ) ADD_DEFINITIONS( -D_VARIADIC_MAX=10 ) ENDIF( MSVC ) # ############################################### # Find the threading package for this machine. FIND_PACKAGE( Threads )
Remove the definition line from the GMock/CMakeLists.txt and add the following to the bottom of the file:
SET( GMOCK_INCLUDE_DIRS ${GOOGLEMOCK_ROOT}/fused-src PARENT_SCOPE )
Controlling Tests
The last thing to be done is to control if unit tests are compiled and built. When you start working this is not a big deal of course as you will likely only have a couple libraries and tests. After a while though, you may end up with more little "test" targets than actual libraries and applications. So, it is a good idea to go ahead and add the ability to disable tests from day one. Add the following item:CMakeEnvironment/CMakeLists.txt:
# ############################################### # Allow unit tests to be disabled via command # line or the CMake GUI. OPTION( BUILD_UNIT_TESTS "Build unit tests." ON )
Add this just under the 'PROJECT' command and it will show up in the CMake GUI as something you can turn on and off.
Putting The Results To Use
Two and a half articles later, we are finally ready to put our work to good use. I'm going to start by writing a 3D vector class since it is a common item and easily unit tested. The first thing to do is of course get the completed environment: CMakeEnvironmentComplete.zip 1.93MB 7 downloads. This file includes the googlemock library so it's quite a bit larger than the prior items.
The Math Library
I'm going to keep this very simple for the purposes of this article. I will not be implementing the actual class, just enough to show a unit test in action. Future articles will likely build off this work but this is going to close out the CMake work for the time being so we want to keep things simple. Let's start by adding the directories for the new 'Math' library:CMakeEnvironment Libraries Math Include Math Source Tests
Ok, so the first question is likely to be why did I duplicate the directory 'Math' under the 'Include' directory? This is simply a polution prevention item. Let's say you have a custom 'Collections' library and within that there is a 'Vector.hpp' file and of course the math library could have a 'Vector.hpp' file. If you include without the prefix of 'Collections' or 'Math', which file are you including? With the way we will setup the libraries, this problem is solved by forcing the users of the libraries to qualify the includes as follows:
#include <Math/Vector.hpp> #include <Collections/Vector.hpp>
This is another one of the personal preference items but I prefer avoiding problems from day one and not simply assuming they won't happen. If you don't like this, once we get done feel free to remove the nested directory. But be warned that in future articles you'll see some good reasons to use this pattern which can greatly simplify normally difficult processes.
Let's fill in the hierarchy with files now, add the following:
CMakeEnvironment/Libraries/Math/CMakeLists.txt:
SET( INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/Include ) INCLUDE_DIRECTORIES( ${INCLUDE_DIRS} ) SET( INCLUDE_FILES Include/Math/Math.hpp Include/Math/Vector3.hpp ) SET( SOURCE_FILES Source/Vector3.cpp ) ADD_DEFINITIONS( -DBUILDING_WORLD ) ADD_LIBRARY( Math STATIC ${INCLUDE_FILES} ${SOURCE_FILES} ) # Export the include directory. SET( MATH_INCLUDE_DIRS ${INCLUDE_DIRS} PARENT_SCOPE )
Add empty files for:
Include/Math/Math.hpp
Include/Math/Vector3.hpp
Source/Vector3.cpp
Tests/Main.cpp
Regenerate the environment and you should have your new Math library.
The First Unit Test
The first thing we want to do is add a unit test holder before we add any code to the library. What we need is a simple executable which can include the files from the library. Since this is part of the math library, we can simply add the test within 'CMakeEnvironment/Libraries/Math/CMakeLists.txt'. Additionally, we want the test to honor the flag we setup such that the test is not included when tests are disabled. Add the following to the end of the Math libraries CMake file:# Make a unit test holder for the Math library. IF( BUILD_UNIT_TESTS ) # Add the gmock include directories. INCLUDE_DIRECTORIES( ${GMOCK_INCLUDE_DIRS} ) ADD_EXECUTABLE( _TestMath Tests/Main.cpp ) TARGET_LINK_LIBRARIES( _TestMath Math GMock ${CMAKE_THREAD_LIBS_INIT} ) ADD_TEST( NAME _TestMath COMMAND _TestMath ) ENDIF( BUILD_UNIT_TESTS )
Regenerate and you get a new target '_TestMath'. In the CMake GUI if you set 'BUILD_UNIT_TESTS' to 'FALSE' and regenerate, the new target goes away as we desired. But, there is actually one more thing we want to do to make this even better in the future. In the root CMake file with the 'OPTION' command defining the 'BUILD_UNIT_TESTS' variable, add the following bit right afterward:
# ############################################### # Enable the CMake built in CTest system if unit # tests are enabled. IF( BUILD_UNIT_TESTS ) ENABLE_TESTING() ENDIF( BUILD_UNIT_TESTS )
Make sure to set 'BUILD_UNIT_TESTS' back to 'TRUE' and regenerate. A new target shows up in the build: "RUN_TESTS". This is a nice little target when you have many more tests then we will have here. Basically if you attempt to build this target is runs all the executables you add with 'ADD_TEST'. It is also great for automated build/continuous integration environments since running the target is easy compared to finding all the individual tests.
Now, lets add the actual unit test code:
CMakeEnvironment/Libraries/Math/Tests/Main.cpp
#include <gmock/gmock.h> int main( int argc, char** argv ) { ::testing::InitGoogleTest( &argc, argv ); return RUN_ALL_TESTS(); }
This is a do nothing bit of code right now until we add some code to the Math library and of course add some tests. So, let's fill in the vector class real quick:
CMakeEnvironment/Libraries/Math/Include/Math/Vector3.hpp:
#pragma once #include <Math/Math.hpp> namespace Math { class Vector3f { public: Vector3f() {} Vector3f( float x, float y, float z ); float X() const {return mX;} float Y() const {return mY;} float Z() const {return mZ;} private: float mX; float mY; float mZ; }; }
CMakeEnvironment/Libraries/Math/Source/Vector3.cpp:
#include <Math/Vector3.hpp> using namespace Math; Vector3f::Vector3f( float x, float y, float z ) : mX( x ) , mY( y ) , mZ( z ) { }
Rebuild and the library should build, the test case should build and even run with the following output:
[==========] Running 0 tests from 0 test cases. [==========] 0 tests from 0 test cases ran. (1 ms total) [ PASSED ] 0 tests.
So it is time to add our first tests. Add the following files:
CMakeEnvironment/Libraries/Math/Tests/TestAll.hpp:
#include <Math/Vector3.hpp> #include "TestConstruction.hpp"
CMakeEnvironment/Libraries/Math/Tests/TestConstruction.hpp:
TEST( Math, Vector3f ) { Math::Vector3f test0( 0.0f, 0.0f, 0.0f ); EXPECT_EQ( 0.0f, test0.X() ); EXPECT_EQ( 0.0f, test0.Y() ); EXPECT_EQ( 0.0f, test0.Z() ); Math::Vector3f test1x( 1.0f, 0.0f, 0.0f ); EXPECT_EQ( 1.0f, test0.X() ); EXPECT_EQ( 0.0f, test0.Y() ); EXPECT_EQ( 0.0f, test0.Z() ); Math::Vector3f test1y( 0.0f, 1.0f, 0.0f ); EXPECT_EQ( 0.0f, test0.X() ); EXPECT_EQ( 1.0f, test0.Y() ); EXPECT_EQ( 0.0f, test0.Z() ); Math::Vector3f test1z( 0.0f, 0.0f, 1.0f ); EXPECT_EQ( 0.0f, test0.X() ); EXPECT_EQ( 0.0f, test0.Y() ); EXPECT_EQ( 1.0f, test0.Z() ); }
Modify CMakeEnvironment/Libraries/Math/CMakeLists.txt by adding the test headers:
ADD_EXECUTABLE( _TestMath Tests/Main.cpp Tests/TestAll.hpp Tests/TestConstruction.hpp )
And include the 'TestAll.hpp' from the file CMakeEnvironment/Libraries/Math/Tests/Main.cpp:
#include <gmock/gmock.h> #include "TestAll.hpp" int main( int argc, char** argv ) { ::testing::InitGoogleTest( &argc, argv ); return RUN_ALL_TESTS(); }
Regenerate, build and run. The test should output the following:
[==========] Running 1 test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from Math [ RUN ] Math.Vector3f [ OK ] Math.Vector3f (0 ms) [----------] 1 test from Math (1 ms total) [----------] Global test environment tear-down [==========] 1 test from 1 test case ran. (5 ms total) [ PASSED ] 1 test.
Congratulations, you have a unit test for your new Vector3 class.
The completed environment and example unit test can be downloaded here CMakeEnvironmentWithTest.zip 1.93MB 10 downloads
. Please provide feedback on any build problems you might have and I'll attempt to get things fixed.