Skip to content
Flex Ferrum edited this page May 10, 2018 · 11 revisions

Getting started

In order to use Jinja2Cpp in your project you have to:

  • Clone the Jinja2Cpp repository
  • Build it according with the instructions
  • Link with your project.

Usage of Jinja2Cpp in the code is pretty simple:

  1. Declare the jinja2::Template object:
jinja2::Template tpl;
  1. Populate it with template:
tpl.Load("{{'Hello World' }}!!!");
  1. Render the template:
std::cout << tpl.RenderAsString(jinja2::ValuesMap{}) << std::endl;

and get:

Hello World!!!

That's all!

define the template for code generation:

The render procedure is stateless, so you can perform several renderings simultaneously in different threads. Even if you pass parameters:

    ValuesMap params = {
        {"intValue", 3},
        {"doubleValue", 12.123f},
        {"stringValue", "rain"},
        {"boolFalseValue", false},
        {"boolTrueValue", true},
    };

    std::string result = tpl.RenderAsString(params);
    std::cout << result << std::endl;

Parameters could have the following types:

  • std::string/std::wstring
  • integer (int64_t)
  • double
  • boolean (bool)
  • Tuples (also known as arrays)
  • Dictionaries (also known as maps)

Tuples and dictionaries can be mapped to the C++ types. So you can smoothly reflect your structures and collections into the template engine:

namespace jinja2
{
template<>
struct TypeReflection<reflection::EnumInfo> : TypeReflected<reflection::EnumInfo>
{
    static auto& GetAccessors()
    {
        static std::unordered_map<std::string, FieldAccessor> accessors = {
            {"name", [](const reflection::EnumInfo& obj) {return Reflect(obj.name);}},
            {"scopeSpecifier", [](const reflection::EnumInfo& obj) {return Reflect(obj.scopeSpecifier);}},
            {"namespaceQualifier", [](const reflection::EnumInfo& obj) { return obj.namespaceQualifier;}},
            {"isScoped", [](const reflection::EnumInfo& obj) {return obj.isScoped;}},
            {"items", [](const reflection::EnumInfo& obj) {return Reflect(obj.items);}},
        };

        return accessors;
    }
};

// ...
    jinja2::ValuesMap params = {
        {"enum", jinja2::Reflect(enumInfo)},
    };

In this cases method 'jinja2::reflect' reflects regular C++ type into jinja2 template param. If type is a user-defined class or structure then handwritten mapper 'TypeReflection<>' should be provided.

More complex example

Let's say you have the following enum:

enum Animals
{
    Dog,
    Cat,
    Monkey,
    Elephant
};

And you want to automatically produce string to enum and enum to string convertor. Like this:

inline const char* AnimalsToString(Animals e)
{
    switch (e)
    {
    case Dog:
        return "Dog";
    case Cat:
        return "Cat";
    case Monkey:
        return "Monkey";
    case Dog:
        return "Elephant";
    }
    return "Unknown Item";
}

Of course, you can write this producer in the way like this:

// Enum item to string conversion writer
void Enum2StringGenerator::WriteEnumToStringConversion(CppSourceStream &hdrOs, const reflection::EnumInfoPtr &enumDescr)
{
    auto scopedParams = MakeScopedParams(hdrOs, enumDescr);

    out::BracedStreamScope fnScope("inline const char* $enumName$ToString($enumScopedName$ e)", "\n");
    hdrOs << out::new_line(1) << fnScope;
    {
        out::BracedStreamScope switchScope("switch (e)", "\n");
        hdrOs << out::new_line(1) << switchScope;
        out::OutParams innerParams;
        for (auto& i : enumDescr->items)
        {
            innerParams["itemName"] = i.itemName;
            hdrOs << out::with_params(innerParams)
                  << out::new_line(-1) << "case $prefix$$itemName$:"
                  << out::new_line(1) << "return \"$itemName$\";";
        }
    }
    hdrOs << out::new_line(1) << "return \"Unknown Item\";";
}

Too complicated for writing 'from scratch'. Actually, there is a better and simpler way.

The simplest case

Firstly, you have to define enum description structure:

// Enum declaration description
struct EnumDescriptor
{
    // Enumeration name
    std::string enumName;
    // Namespace scope prefix
    std::string nsScope;
    // Collection of enum items
    std::vector<std::string> enumItems;
};

This structure holds the enum name, enum namespace scope prefix, and list of enum items (we need just names). Then, you can create populate instances of this descriptor automatically using clang front-end (ex. here: clang-based enum2string converter generator ). For our sample we create the instance manually:

EnumDescriptor descr;
descr.enumName = "Animals";
descr.nsScope = "";
descr.enumItems = {"Dog", "Cat", "Monkey", "Elephant"};

Secondly, you can define the jinja2 template (in the C++ manner):

std::string enum2StringConvertor = R"(inline const char* {{enumName}}ToString({{enumName}} e)
{
    switch (e)
    {
{% for item in items %}
    case {{item}}:
        return "{{item}}";
{% endfor %}
    }
    return "Unknown Item";
})";

And finally, you can render this template with Jinja2Cpp library:

jinja2::ValuesMap params {
    {"enumName", descr.enumName},
    {"nsScope", descr.nsScope},
    {"items", {descr.enumItems[0], descr.enumItems[1], descr.enumItems[2], descr.enumItems[3]}},
};

jinja2::Template tpl;
tpl.Load(enum2StringConvertor);
std::cout << tpl.RenderAsString(params);

And you will get on the console the conversion function mentioned above.

Reflection

Actually, with Jinja2Cpp you don't need to transfer data from your internal structures to the Jinja2 values map. Library can do it for you. You just need to define reflection rules. Something like this:

namespace jinja2
{
template<>
struct TypeReflection<EnumDescriptor> : TypeReflected<EnumDescriptor>
{
    static auto& GetAccessors()
    {
        static std::unordered_map<std::string, FieldAccessor> accessors = {
            {"name", [](const EnumDescriptor& obj) {return obj.name;}},
            {"nsScope", [](const EnumDescriptor& obj) { return obj.nsScope;}},
            {"items", [](const EnumDescriptor& obj) {return Reflect(obj.items);}},
        };

        return accessors;
    }
};

And this case you need to correspondingly change template itself and it's invocation:

std::string enum2StringConvertor = R"(inline const char* {{enum.enumName}}ToString({{enum.enumName}} e)
{
    switch (e)
    {
{% for item in enum.items %}
    case {{item}}:
        return "{{item}}";
{% endfor %}
    }
    return "Unknown Item";
})";

// ...
    jinja2::ValuesMap params = {
        {"enum", jinja2::Reflect(descr)},
    };
// ...

Every specified field will be reflected into Jinja2Cpp internal data structures and can be accessed from the template without additional efforts. Quite simply!

'set' statement

But what if enum Animals will be in the namespace?

namespace world
{
enum Animals
{
    Dog,
    Cat,
    Monkey,
    Elephant
};
}

In this case you need to prefix both enum name and it's items with namespace prefix in the generated code. Like this:

std::string enum2StringConvertor = R"(inline const char* {{enum.enumName}}ToString({{enum.nsScope}}::{{enum.enumName}} e)
{
    switch (e)
    {
{% for item in enum.items %}
    case {{enum.nsScope}}::{{item}}:
        return "{{item}}";
{% endfor %}
    }
    return "Unknown Item";
})";

This template will produce 'world::' prefix for our new enum (and enum itmes). And '::' for the previous one. But you may want to get rid of unnecessary global scope prefix. And you can do it this way:

{% set prefix = enum.nsScope + '::' if enum.nsScope else '' %}
std::string enum2StringConvertor = R"(inline const char* {{enum.enumName}}ToString({{prefix}}::{{enum.enumName}} e)
{
    switch (e)
    {
{% for item in enum.items %}
    case {{prefix}}::{{item}}:
        return "{{item}}";
{% endfor %}
    }
    return "Unknown Item";
})";

This template uses two significant jinja2 template features:

  1. 'set' statement. You can declare new variables in your template. And you can access them by the name.
  2. if-expressions. It works like a ternary '?:' operator in C/C++. In C++ the code from the sample could be written this way:
std::string prefix = !descr.nsScope.empty() ? descr.nsScope + "::" : "";

I.e. left part of this expression (before 'if') is a true-branch of the statement. Right part (after 'else') - false-branch, which could be omitted. As a condition you can use any expression, convertible to bool.

Current Jinja2 support

Currently, Jinja2Cpp supports the limited number of Jinja2 features. By the way, Jinja2Cpp is planned to be full jinja2 specification-conformant. The current support is limited to:

  • expressions. You can use almost every style of expressions: simple, filtered, conditional, and so on.
  • limited number of filters (join, sort)
  • limited number of testers (defined, startsWith)
  • limited number of functions (range, loop.cycle)
  • 'if' statement (with 'elif' and 'else' branches)
  • 'for' statement (with 'else' branch support)
  • 'set' statement

Supported compilers

Compilation of Jinja2Cpp tested on the following compilers (with C++14 enabled feature):

  • Linux gcc 5.0
  • Linux gcc 6.0
  • Linux gcc 7.0
  • Linux clang 5.0
  • Microsoft Visual Studio 2015 x86
  • Microsoft Visual Studio 2017 x86

Build and install

Jinja2Cpp has got only one external dependency: boost library (at least version 1.55). Because of types from boost are used inside library, you should compile both your projects and Jinja2Cpp library with similar compiler settings. Otherwise ABI could be broken.

In order to compile Jinja2Cpp you need:

  1. Install CMake build system (at least version 3.0)
  2. Clone jinja2cpp repository and update submodules:
> git clone https://github.com/flexferrum/Jinja2Cpp.git
> git submodule -q update --init
  1. Create build directory:
> cd Jinja2Cpp
> mkdir build
  1. Run CMake and build the library:
> cd build
> cmake .. -DCMAKE_INSTALL_PREFIX=<path to install folder>
> cmake --build . --target all

"Path to install folder" here is a path to the folder where you want to install Jinja2Cpp lib.

  1. Install library:
> cmake --build . --target install
  1. Also you can run the tests:
> ctest -C Release

Additional CMake build flags

You can define (via -D command line CMake option) the following build flags:

  • WITH_TESTS (default TRUE) - build or not Jinja2Cpp tests.
  • MSVC_RUNTIME_TYPE (default /MD) - MSVC runtime type to link with (if you use Microsoft Visual Studio compiler).
  • LIBRARY_TYPE Could be STATIC (default for Windows platform) or SHARED (default for Linux). Specify the type of Jinja2Cpp library to build.

Link with you projects

Jinja2Cpp is shipped with cmake finder script. So you can:

  1. Include Jinja2Cpp cmake scripts to the project:
list (APPEND CMAKE_MODULE_PATH ${JINJA2CPP_INSTALL_DIR}/cmake)
  1. Use regular 'find' script:
find_package(Jinja2Cpp)
  1. Add found paths to the project settings:
#...
include_directories(
    #...
    ${JINJA2CPP_INCLUDE_DIR}
    )

target_link_libraries(YourTarget
    #...
    ${JINJA2CPP_LIBRARY}
    )
#...

or just link with Jinja2Cpp target:

#...
target_link_libraries(YourTarget
    #...
    Jinja2Cpp
    )
#...
Clone this wiki locally