Introduction
Any larger project involves code from different sources. When reusing our own code or downloading something from GitHub, we have to fight with different layouts of source code, include files and libraries.
This tip wants to show a way to ease this pain. It is the result of many wounds acquired over many years of practice and many of those were self-inflicted. One more thing: this is not a universal panacea; it applies to C/C++ projects and works even better for those using Visual Studio.
We will consider a common situation where we have developed two libraries cool_A
and cool_B
that need to be reused in an application SuperApp
.
Library Code Layout
For each library, we have include files, source files and we produce a static link library. Here is some ASCII art showing the general code layout:
DevTreeRoot
|
+-- cool_A
| |
| +-- include
| | |
| | +-- cool_A
| | |
| | +-- hdr1.h
| | |
| | +-- hdr2.h
| +-- src
| | |
| | +-- file1.cpp
| | |
| | +-- file2.cpp
| +-- project file (cool_A.vcxproj) and other stuff
|
+-- cool_B
|
+-- include
| |
| +-- cool_B
| |
| +-- hdr1.h
| |
| +-- hdr4.h
+-- src
| |
| +-- file1.cpp
| |
| +-- file2.cpp
|
+-- project file (cool_B.vcxproj) and other stuff
So far, not much different from what you already know except for the following:
RULE 1 - Include files that need to be visible to users are placed in a subfolder of the include folder. The subfolder has the same name as the library.
If users of cool_A
have managed to have cool_A/include on the include path (we will see in a moment how), they can refer to hdr1.h file like this:
#include <cool_A/hdr1.h>
The advantage is that it avoids name clashes between different libraries. In our case, if a program uses both cool_A
and cool_B
, the corresponding include directives will be:
#include <cool_A/hdr1.h>
#include <cool_B/hdr1.h>
Note that I didn't say anything about the output library. I will get to that soon.
Using Symbolic Links for Fun and Profit
Windows users are not so accustomed to symbolic links as they showed up rather late in the Windows world. They can however bring significant benefits for managing multiple projects.
Following the structure shown before, our application that uses cool_A
and cool_B
will have an include
folder but in this folder, we will place symbolic links to cool_A
and cool_B
include folders. The folder structure will look something like this (angle brackets denote symbolic links):
DevTreeRoot
|
+-- SuperApp
| |
| +-- include
| | |
| | +-- <cool_A>
| | | |
| | | +-- hdr1.h
| | | |
| | | +-- hdr2.h
| | |
| | +-- <cool_B>
| | | |
| | | +-- hdr1.h
| | | |
| | | +-- hdr4.h
| | other header files
| |
| +-- src
| | |
| | +-- source files
| other files
...
To create those symbolic links, assuming your current directory is SuperApp\include, you have to issue the following commands:
mklink /d cool_A \DevTreeRoot\cool_A\include\cool_A
mklink /d cool_B \DevTreeRoot\cool_B\include\cool_B
With the magic of symbolic links, SuperApp
needs to have only its include
folder mentioned in the include path and all the other libraries used will automatically become available.
We can now use the same trick for static link libraries. By convention, we will put them in the lib
folder. This time, however, we are going to place the lib folder at the root of development tree and place symbolic links in each project that uses it. Without repeating the parts already shown of the files layout, here is the part related to lib folder (again, angle brackets denote symbolic links):
DevTreeRoot
|
+-- cool_A
| |
| ...
| +-- <lib>
| |
| all link libraries are here
+-- cool_B
| |
| ...
| +-- <lib>
| |
| all link libraries are here
+-- SuperApp
| |
| ...
| +-- <lib>
| |
| all link libraries are here
+-- lib
|
all link libraries are here
RULE 2: The static link libraries folder, lib exists at the root of development tree and it is made visible through symbolic links in each project that uses it.
If there are different flavors of link libraries (debug, release, 32-bit, 64-bit) they can be accommodated as subfolders of the lib folder.
Automation
Creating the symbolic links required by a project can be automated with a simple batch file. I use the name mklinks.bat in all my projects and here is how it would look for SuperApp
:
:MAKELINKS
if not exist lib\nul mklink /d lib %DEV_TREE_ROOT%\lib
pushd "%~dp0include"
if not exist cool_A\nul mklink /d cool_A %DEV_TREE_ROOT%\cool_A\include\cool_A
if not exist cool_B\nul mklink /d cool_B %DEV_TREE_ROOT%\cool_B\include\cool_B
popd
Integration with Visual Studio
This scheme can be easily applied to any C/C++ Visual Studio project. There are just a few configurations to apply to a project:
- Set include path to (or add to it) $(SolutionDir)include
- For any library, set the output path to $(SolutionDir)lib\$(PlatformTarget)\$(Configuration)\
- For any application (or DLL), set library path to $(SolutionDir)lib\$(PlatformTarget)\$(Configuration)
Final Notes
-
Some libraries can depend on other libraries. In our example, cool_B
might use the cool_A
library. In that case, it would just need a symbolic link to cool_B
include folder.
-
Two more recommendations for where to place various files in Visual Studio projects. They have shown to scale well even with complicated project hierarchies:
- Set the intermediate files directory to $(SolutionDir)o\$(ProjectName)\$(PlatformTarget)\$(Configuration)\
- For any application (or DLL) set the output path to $(SolutionDir)app\$(ProjectName)\$(PlatformTarget)\$(Configuration)\
Using the Sample Code
First of all, the code is neither "cool" nor "super". It is just some demo code intended to show the benefits of following these rules. Download it and follow these steps:
- run mklinks.bat scripts in all projects (
cool_A
, cool_B
, SupperApp
) - build the libraries
cool_A
and cool_B
- build the application
SuperApp
Conclusions
Using symbolic links and a few simple rules makes code reuse a lot easier. The different settings used in Visual Studio are the result of many years of refinement until I've got everything right. As an example, the recommendation to set the intermediate files directory to $(SolutionDir)o\$(ProjectName)\$(PlatformTarget)\$(Configuration)\ is based on a few goals:
- It is nice to have all object files in one place to be able to clean a project easily, hence the common o folder.
- You might have multiple projects in one solution and their intermediate files need to be kept separate, hence the $(ProjectName) part.
- For sure, each "flavor" of build (x86, x64, debug, release, etc.) needs to be separate, hence the $(PlatformTarget)\$(Configuration) part.
This system works very well for different Git repositories where you can just fetch the code from each repository and combine them using symbolic links.
History
- 7th October, 2021 - Initial version