Как стать автором
Обновить

Комментарии 18

1. А вы делали замеры скорости работы вашей реализации и оригинальной?
2. А есть ли возможность сделать биндинг вашей библиотеки в Python и использовать за место оригинальной Jinja2?
1. Ещё нет. Но к концу публикации (должна быть ещё минимум одна часть) сделаю. Я знаю, что эта информация интересна. Да и самому интересно.
2. Честно говоря, пока особого смысла в таком бэкпорте не вижу. У Python уже есть Jinja2. Если у вас есть какие-то аргументы к такому биндингу — внимательно выслушаю.
В Python есть пакеты которые написаны на чистом Python, а есть пакеты совпадающие по API, но в них код переписан на на C или C++. Это делают чтобы получить выигрыш в производительности. К примеру, пакеты profile и cProfile, decimal и cdecimal, StringIO и cStringIO и т.д. Было бы здорово, если бы была в наличии сJinja2…
Я догадываюсь, что существуют такого рода пакеты. Но конкретно с Jinja2 два момента:
1. Оригинальная (питоновская) jinja, как библиотека — несколько больше, чем просто шаблонизатор. Там довольно богатое API, хотя и всё крутится вокруг рендеринга шаблонов.
2. В любом случае говорить об этом можно не раньше, чем померю перформанс и реализую все возможности языка шаблонов.
Буду с нетерпением ждать новой статьи с замерами производительности. Эта статья и код понравились, спасибо!
Да. Теперь уже и самому интересно стало. :)
По-быстрому накидал пару кейсов. Для C++:
TEST(PerfTests, PlainText)
{
    std::string source = "Hello World from Parser!";

    Template tpl;
    ASSERT_TRUE(tpl.Load(source));

    jinja2::ValuesMap params;

    std::cout << tpl.RenderAsString(params) << std::endl;
    std::string result;
    for (int n = 0; n < Iterations * 100; ++ n)
        result = tpl.RenderAsString(params);

    std::cout << result << std::endl;
}

TEST(PerfTests, SimpleSubstituteText)
{
    std::string source = "{{ message }} from Parser!";

    Template tpl;
    ASSERT_TRUE(tpl.Load(source));

    jinja2::ValuesMap params = {{"message", "Hello World!"}};

    std::cout << tpl.RenderAsString(params) << std::endl;
    std::string result;
    for (int n = 0; n < Iterations * 100; ++ n)
        result = tpl.RenderAsString(params);

    std::cout << result << std::endl;
}

Для питона:
import unittest
from jinja2 import Template

class PerfTestCase(unittest.TestCase):

    def _test_plait_text_render(self):
        tpl = Template('Hello World from Parser!')

        result = tpl.render()
        print (result)
        for n in range(0, 10000 * 100):
            tpl.render()

        print (result)

    def test_simple_substitute_text(self):
        tpl = Template('{{ message }} from Parser!')

        result = tpl.render(message='Hello World!')
        print (result)
        for n in range(0, 10000 * 100):
            tpl.render(message='Hello World!')

        print (result)


Цифры вышли такие. Для C++:
[----------] 2 tests from PerfTests
[ RUN ] PerfTests.PlainText
Hello World from Parser!
Hello World from Parser!
[ OK ] PerfTests.PlainText (2084 ms)
[ RUN ] PerfTests.SimpleSubstituteText
Hello World! from Parser!
Hello World! from Parser!
[ OK ] PerfTests.SimpleSubstituteText (2581 ms)
[----------] 2 tests from PerfTests (4665 ms total)

Для питона:
d:\projects\work\Personal\Jinja2Cpp\python_jinja2>python -m unittest perf_test.PerfTestCase._test_plait_text_render
Hello World from Parser!
Hello World from Parser!
.
----------------------------------------------------------------------
Ran 1 test in 5.474s

OK

d:\projects\work\Personal\Jinja2Cpp\python_jinja2>python -m unittest perf_test.PerfTestCase.test_simple_substitute_text
Hello World! from Parser!
Hello World! from Parser!
.
----------------------------------------------------------------------
Ran 1 test in 6.543s

OK


Запускалось на одной машине. C++-версия — сборка MSVC 14.0, Release, x64. Питон — версии 3.5.1
Дальнейшие исследования показали, что питоновская версия выигрывает только на шаблоне такого характера: "{% for i in range(20)%} {{i}} {%endfor%}"
Но стоит его модифицировать так: "{% for i in range(20)%} {{i}}-{{loop.index}} {%endfor%}" — и всё. Снова проигрывает.
Раз Вы все равно используете boost, то можно попробовать использовать boost::regex вместо std::regex. Как сейчас дела у VS обстоят не знаю, но в VS 2013 (то же самое касалось и GCC, вот только версию не помню, наверно это было под Debian 8) у меня производительность boost::regex была в ~15 раз выше, чем у std::regex.

Буду иметь в виду. Спасибо.

Прошу прощения у комментатора tgregory, который поделился соображениями и опытом. При ответе промахнулся по ссылке и вместо «Подтвердить» нажал «Отклонить». Интерфейс, разумеется, подтверждения не спросил.
Восстанавливаю из почты:
Делал нечто подобное, правда скорее придерживался стиля Go(text/template) (с изменениями вызова функций и индексации элементов). Jinja вроде похож. Вот пара идей, которые вам могут показаться полезными:

1. В токенах имеет смысл указывать span, т.е. от и до, причём позицию удобно отслеживать сразу в виде {byte_pos, line, column} (обратите внимание, что для того чтобы правильно считать column понадобится рудиментарная поддержка юникод). Это пригодится при выводе сообщений об ошибках.
2. В грамматику языка ввести сырой текст как терминал.
3. Дать лексеру два состояния: парсинг выражений; парсинг сырого текста. Читать лексером посимвольно и дать ему понимание что состояние надо переключать в момент порождения токенов начала/конца выражений.
4. Избавиться от boost::variant и позволить Value быть функцией (с С++11 можно на шаблонах заворачивать произвольные функции и лямбды, давая возможность их проброса в движок в виде Value)
1. Я храню posFrom и posTo. То есть тот же спан. Отдельно веду учёт разрывов строк, чтобы по позиции начала можно было определить строку и колонку. Всё делается с привязкой с «ширине» символа.
2, 3. Сырой текст не парсится в принципе. Регулярка мне для того и нужна, чтобы отделить одно от другого. Парсится только то, что находится внутри скобок.
4. А чем boost::variant плох? На счёт позволить значению быть функцией — тут интересные аспекты возникают. Пробросить то несложно (reflection по этому принципу здесь и построен). Только авторы шаблонов захотят в эту функцию параметры передавать. А вот тут возникают интересности.
1. Похоже я этот момент в исходниках упустил.
2,3. Тут возможно сказывается моё предвзятое отношение к использованию регулярных выражений в задачах разбора, в Вашем случае оно оправдано.
4. boost::variant даёт доп. зависимость от буста, возможно это вкусовщина, но можно обойтись обычным union и позволить например Qt проектам не тянуть буст. Функцию можно иметь вида Value func(ValueList), т.е. в вашем языке выражений функции будут иметь только такой вид, а `C++` шаблоны будут делать лямбды обёртки приводящие обычные функции к такому виду, соответсвенно будет и возможность работать с функциями высших порядков.
Мне, в общем, тоже не очень нравится boost::variant на границе внешнего интерфейса. В том числе из соображений бинарной совместимости. Но адекватной замены ему нет, разве что собственный велосипед тащить или variant-light от Martin Moene ( github.com/martinmoene/variant-lite ). Возможно, в эту сторону посмотрю, т. к. в случае C++17 он «мягко» переключается на стандартный вариант. А вот из потрохов boost выпилить всё равно не выйдет — достаточно много завязок на него.
На счёт функций — такой стиль вызова будет несколько, гм, выбиваться из общей схемы, которая допускает и позиционные, и именованные параметры.
Ещё про вызовы. Внутри используется структура типа такой:
struct CallParams
{
    std::unordered_map<std::string, Value> kwParams;
    std::vector<Value> posParams;
};
>> До сих пор реализация этого движка была только для Python.

Ну… не только. Есть и для node.js: github.com/mozilla/nunjucks
Есть реализации разной степени готовности и для других популярных языков.
Согласен. В PHP, например, есть Twig, который не является портом Jinja, но по факту максимально похож.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.