-
Notifications
You must be signed in to change notification settings - Fork 176
Simple Library Tutorial
This is a tutorial that will show you how to build an executable that consumes a library. Since both use cases are common, the library will have a static (.lib) and a shared (.dll) library configuration.
In an empty folder, create a folder called executable and put a new file called main.cpp. Copy the following code inside it.
// main.cpp
#include <iostream>
#include <library/function.hpp>
using namespace std;
int main(int, char**)
{
cout << "ack(3, 2) is " << Ack(3, 2) << endl;
}
Now, create a library folder and create the following 5 files.
// apiexport.hpp
#if !defined(_LIBRARY_APIEXPORT_HPP)
#define _LIBRARY_APIEXPORT_HPP
// dllexport boilerplate
#if defined(LIBRARY_DLL)
# if defined(_MSC_VER)
# if defined(LIBRARY_COMPILE)
# define LIBRARY_API __declspec(dllexport)
# else
# define LIBRARY_API __declspec(dllimport)
# endif
# elif defined(__GNUC__) || defined(__clang__)
# if defined(LIBRARY_COMPILE)
# define LIBRARY_API __attribute__ ((visibility ("default")))
# endif
# endif
#endif
#if !defined(LIBRARY_API)
# define LIBRARY_API
#endif
#endif // _LIBRARY_APIEXPORT_HPP
// function.cpp
#include "precomp.hpp"
#include "function.hpp"
long Ack(long m, long n)
{
if (!m) return n + 1;
if (!n) return Ack(m - 1, 1);
return Ack(m - 1, Ack(m, n - 1));
}
// function.hpp
#if !defined(_LIBRARY_FUNCTION_HPP)
#define _LIBRARY_FUNCTION_HPP
#include <library/apiexport.hpp>
LIBRARY_API long Ack(long m, long n);
#endif // _LIBRARY_FUNCTION_HPP
// precomp.cpp
#include "precomp.hpp"
// precomp.hpp
#if !defined(_FUNCTION_PRECOMP_HPP)
#define _FUNCTION_PRECOMP_HPP
#include "apiexport.hpp"
#endif // _FUNCTION_PRECOMP_HPP
The C++ library expects LIBRARY_COMPILE
to be defined when building the
library but not when including its headers. In addition, whenever the library
is being built or consumed as a DLL, the LIBRARY_DLL
symbol must also be
defined. This is not the prettiest or most efficient way to do a library in
C++, but this forces us to configure this from Sharpmake.
Since we have 2 projects (a library and an executable) it is good practice to create a base class to set the common properties.
using System.IO;
using Sharpmake;
// Both the library and the executable can share these base settings, so create
// a base class for both projects.
abstract class BaseSimpleLibraryProject : Project
{
public BaseSimpleLibraryProject()
{
// Declares the target for which we build the project. This time we add
// the additional OutputType fragment, which is a prebuilt fragment
// that help us specify the kind of library output that we want.
AddTargets(new Target(
Platform.win32 | Platform.win64,
DevEnv.vs2015,
Optimization.Debug | Optimization.Release,
OutputType.Dll | OutputType.Lib));
}
[Configure]
public virtual void ConfigureAll(Project.Configuration conf, Target target)
{
// This is the name of the configuration. By default, it is set to
// [target.Optimization] (so Debug or Release), but both the debug and
// release configurations have both a shared and a static version so
// that would not create unique configuration names.
conf.Name = @"[target.Optimization] [target.OutputType]";
// Gives a unique path for the project because Visual Studio does not
// like shared intermediate directories.
conf.ProjectPath = Path.Combine("[project.SharpmakeCsPath]/generated/[project.Name]");
}
}
Note that we are not putting [Generate]
on this base class because it does
not generate a project, it is just a common base class for common properties.
The target has an additional fragment: OutputType
. This fragment does not
have any semantics in Sharpmake, but it is there for library projects to use
to specify whether they generate a shared library (OutputType.Dll
) or a
static library (OutputType.Lib
.) This is exactly what we need here.
Create the project that will represent the library in the code base.
// The library project.
[Generate]
class LibrarySimpleLibraryProject : BaseSimpleLibraryProject
{
public LibrarySimpleLibraryProject()
{
Name = "SimpleLibrary.Library";
SourceRootPath = @"[project.SharpmakeCsPath]/library";
}
public override void ConfigureAll(Project.Configuration conf, Target target)
{
base.ConfigureAll(conf, target);
// Setup the precompiled headers for the project. Just assigning a
// value to those fields is enough for Sharpmake to understand that
// the project has precompiled headers.
conf.PrecompHeader = "precomp.hpp";
conf.PrecompSource = "precomp.cpp";
// Sets the include path of the library. Those will be shared with any
// project that adds this one as a dependency. (The executable here.)
conf.IncludePaths.Add(@"[project.SourceRootPath]/..");
// The library wants LIBRARY_COMPILE defined when it compiles the
// library, so that it knows whether it must use dllexport or
// dllimport.
conf.Defines.Add("LIBRARY_COMPILE");
if (target.OutputType == OutputType.Dll)
{
// We want this to output a shared library. (DLL)
conf.Output = Configuration.OutputType.Dll;
// This library project expects LIBRARY_DLL symbol to be defined
// when used as a DLL. While we could define it in the executable,
// it is better to put it as an exported define. That way, any
// projects with a dependency on this one will have LIBRARY_DLL
// automatically defined by Sharpmake.
conf.ExportDefines.Add("LIBRARY_DLL");
// Exported defines are not necessarily defines as well, so we need
// to add LIBRARY_DLL as an ordinary define too.
conf.Defines.Add("LIBRARY_DLL");
}
else if (target.OutputType == OutputType.Lib)
{
// We want this to output a static library. (LIB)
conf.Output = Configuration.OutputType.Lib;
}
}
}
A lot of things are going on in the ConfigureAll
method, but it is quite
straightforward. The important parts are IncludePaths
, ExportDefines
and
Defines
.
IncludePaths
is a list of the include paths needed to compile the library,
but also to consume the library. For this reason, the include paths
specified here will be added not only to the library, but also to any other
project that depends on it. If you want include paths that are private to the
library and not visible to projects that consume the library, you must use
IncludePrivatePaths
instead.
ExportDefines
is a list of defined symbols that are exported in projects that
consume the libary as a dependency, while Defines
is a list of defined
symbols that are used when building the library itself. Here, LIBRARY_DLL
must always be defined when using the library as a DLL, so we add it to both.
LIBRARY_COMPILE
must only be defined while building the library though (not
when consuming it) so we add it to Define
but not to ExportDefines
.
We also need a project for the executable.
// The executable that consumes the library.
[Generate]
class ExecutableSimpleLibraryProject : BaseSimpleLibraryProject
{
public ExecutableSimpleLibraryProject()
{
Name = "SimpleLibrary.Executable";
SourceRootPath = @"[project.SharpmakeCsPath]/executable";
}
public override void ConfigureAll(Project.Configuration conf, Target target)
{
base.ConfigureAll(conf, target);
// This line tells Sharpmake that this project has a dependency on the
// library project. This will cause all exported include paths and
// exported defines to be automatically added to this project.
conf.AddPrivateDependency<LibrarySimpleLibraryProject>(target);
}
}
Of note here is conf.AddPrivateDependency<LibrarySimpleLibraryProject>(target)
.
This is the line that tells Sharpmake that the executable project has a
dependency to the library.
// The solution. It contains both the executable and the library.
[Generate]
public class SimpleLibrarySolution : Solution
{
public SimpleLibrarySolution()
{
Name = "SimpleLibrary";
AddTargets(new Target(
Platform.win32 | Platform.win64,
DevEnv.vs2015,
Optimization.Debug | Optimization.Release,
OutputType.Dll | OutputType.Lib));
}
[Configure]
public void ConfigureAll(Solution.Configuration conf, Target target)
{
conf.Name = @"[target.Optimization]_[target.OutputType]";
conf.SolutionPath = @"[solution.SharpmakeCsPath]\generated";
// Adds the projects to the solution. Note that because the executable
// has a dependency to the library, Sharpmake can automatically figures
// out that it should add the library to the solution too, so the
// second line is not actually needed.
conf.AddProject<ExecutableSimpleLibraryProject>(target);
conf.AddProject<LibrarySimpleLibraryProject>(target);
}
}
public static class Main
{
[Sharpmake.Main]
public static void SharpmakeMain(Sharpmake.Arguments arguments)
{
// Tells Sharpmake to generate the solution described by
// SimpleLibrarySolution.
arguments.Generate<SimpleLibrarySolution>();
}
}
Save all your files and run Sharpmake. This should create a generated folder that contains a solution to build with Visual Studio.