Skip to content

Simple Library Tutorial

Lambert Clara edited this page Mar 10, 2021 · 3 revisions

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.

The Sample Code

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.

The Script

Common base class.

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.

The Library Project

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.

The Executable Project

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

// 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>();
    }
}

Run Sharpmake and Compile!

Save all your files and run Sharpmake. This should create a generated folder that contains a solution to build with Visual Studio.