software-development
December 14, 2023

Портирование на другую ОС

Немного раскрою эту сложную тему на конкретном и реальном примере. В этот раз рассказ будет про современный С++ и сетевой фреймворк Pistache, а портировать будем с Linux на FreeBSD.

Вы разве не знали что бывает Swagger для самопального фреймворка на C++ ? Читайте мой бложег - тут веселее чем в зоопарке! Фоновая картинка вот тут.

О чем это все

Допустим, вы написали на свою голову некий проект на C++. Решив использовать не портируемые на раз-два C# или Java а старый добрый злобный нативный C++ с сегфолтами и утечками памяти.

Классический путь портирования Windows -> Linux опишу как нибудь в другой раз, там тоже много всего интересного и замечательного.

А сегодня будет про более редкий вариант портирования: из Linux в BSD.

Портируемый проект

Случайно наткнулся на вот такую радость юного плюсолюба:

Pistache is a modern and elegant HTTP and REST framework for C++. It is entirely written in pure-C++17* and provides a clear and pleasant API.

Да да, никакого залежалого говна мамонта из 90х, это действительно современный веб-фреймворк, на современном C++ (как бы дико это не звучало).

Вот что внутри:

  • A multi-threaded HTTP server to build your APIs
  • An asynchronous HTTP client to request APIs
  • An HTTP router to dispatch requests to C++ functions
  • A REST description DSL to easily define your APIs
  • Type-safe headers and MIME types implementation

Красивый, элегантный и готовый к использованию, вот так выглядит минимальный HTTP-сервер на нем:

#include "pistache/endpoint.h"
using namespace Pistache;

class HelloHandler : public Http::Handler
{
public:
    HTTP_PROTOTYPE(HelloHandler)
    void onRequest(const Http::Request& /*request*/, Http::ResponseWriter response) override
    {
        response.send(Pistache::Http::Code::Ok, "Hello World\n");
    }
};
int main()
{
    Pistache::Address addr(Pistache::Ipv4::any(), Pistache::Port(9080));
    auto opts = Pistache::Http::Endpoint::options()
                    .threads(1);
    Http::Endpoint server(addr);
    server.init(opts);
    server.setHandler(Http::make_handler<HelloHandler>());
    server.serve();
}

А вот так выглядит код вебсервиса с REST и интеграция со Swagger, правда красиво?

К сожалению с этим проектом есть одна мааааленькая проблемка:

I understand your frustration. Currently, Pistache does not compile on windows as it's using Unix and Linux-specific primitives like epoll(). While it would be pretty easy to create an abstraction for sockets, the story is not the same for epoll(). Pistache uses a Reactor design-pattern for I/O control based on epoll(). The goal of this pattern is to get notified when an event becomes ready on a particular file descriptor (we can call it the ready-ness model).

Вообщем этот замечательный фреймворк не портируемый на данный момент, хотя работы ведутся. Но при этом он достаточно компактный чтобы применив свой обширный опыт попробовать портировать самому и при этом не утонуть в правках.

Ниже, по шагам я опишу весь процесс портирования и расскажу о типичных проблемах.

Шаг первый: сборка

Забираем исходный код проекта:

git clone https://github.com/pistacheio/pistache.git

Сборка реализована с помощью meson и ninja, который надо будет установить:

pkg install meson ninja

Запускаем первый шаг сборки, согласно инструкции с небольшим отличием (см. ниже):

meson setup build                                 \
    --buildtype=release                             \
    -DPISTACHE_USE_SSL=true                         \
    -DPISTACHE_BUILD_EXAMPLES=true                  \
    -DPISTACHE_BUILD_TESTS=true                     \
    -DPISTACHE_BUILD_DOCS=false                     \
    -DPISTACHE_USE_CONTENT_ENCODING_BROTLI=true     \
    -DPISTACHE_USE_CONTENT_ENCODING_DEFLATE=true    \
    --prefix="$PWD/prefix"    

И сразу же получаем сапогом по лицу первую ошибку:

Эта ошибка — результат очень простой проверки в скрипте сборки (файл meson.build в корне проекта):

if host_machine.system() != 'linux'
	error('Pistache currenly only supports Linux. See https://github.com/pistacheio/pistache/issues/6#issuecomment-242398225 for more information')
endif

Проверка нужна для того чтобы сразу красиво отсечь все попытки сборки на неподдерживаемой ОС, не вводя пользователя в заблуждение адскими трассировками ошибок, которые мы сейчас увидим.

Отключаем проверку закомментировав весь блок:

И повторно запускаем первый шаг сборки:

Как видите первый шаг выполняется и появляется папка build.

У вас могут в этом месте появиться ошибки связанные с ненайденными библиотеками, но они достаточно очевидны и исправляются простой установкой необходимой библиотеки из пакета.

Все редкие библиотеки, которые использует данный фреймворк выкачиваются при сборке из github (что будет видно по логу сборки):

Запускаем вторую стадию:

meson compile -C build

Получаем новую ошибку, в этот раз настоящую:

Вот тут уже придется править код. Открываем файл src/common/http.cc в любимом редакторе и смотрим место с ошибкой:

Проблема тут в том что флага MSG_MORE в реализации sendto для BSD нет, вот за что этот флаг отвечает в линуксе (выдежка из документации):

MSG_MORE (since Linux 2.4.4)
The caller has more data to send. This flag is used with
TCP sockets to obtain the same effect as the TCP_CORK
socket option (see tcp(7)), with the difference that this
flag can be set on a per-call basis.

Since Linux 2.6, this flag is also supported for UDP
sockets, and informs the kernel to package all of the data
sent in calls with this flag set into a single datagram
which is transmitted only when a call is performed that
does not specify this flag. (See also the UDP_CORK socket
option described in udp(7).)

Чтобы обойти данную проблему — обратимся к Марксу старому стабильному проекту, где все хорошо с портируемостью. В качестве такого проекта я взял Python, конкретнее его системный модуль, отвечающий за работу с сокетами.

Вот интересное нам место:

#ifdef  MSG_MORE
    ADD_INT_MACRO(m, MSG_MORE);
#endif

Т.е. это явно означает что сей флаг — опциональный, поэтому его отключение ничего глобально не сломает.

Убираем флаг, чтобы вызов стал вот таким:

И снова запускаем вторую стадию сборки:

meson compile -C build

Получаем новую ошибку, на первый взгляд фатальную:

Вот тут уже могут опуститься руки, поскольку в BSD нет прямого аналога механизма epoll:

The epoll API performs a similar task to poll(2): monitoring
multiple file descriptors to see if I/O is possible on any of
them. The epoll API can be used either as an edge-triggered or a
level-triggered interface and scales well to large numbers of
watched file descriptors.

The central concept of the epoll API is the epoll instance, an
in-kernel data structure which, from a user-space perspective,
can be considered as a container for two lists:

Это означает что на горизонте только что замаячила огромная такая жопа, в виде необходимости перепахивать сетевую логику фреймворка ради поддержки kqueue:

The kqueue() system call provides a generic method of notifying the
user when an event happens or a condition holds, based on the results
of small pieces of kernel code termed filters. A kevent is identified
by the (ident, filter) pair; there may only be one unique kevent per
kqueue.

Вообщем если бы я был чуть менее опытным, то где-то на таком месте все бы и закончилось — объем работы по перепиливанию логики из epoll в kqueue врядли бы влез в формат статьи.

Но выход силой все же есть, в виде специальной библиотеки epoll-shim. Вот что делает этот маленький мамин помощник:

This is a small library that implements epoll on top of kqueue. It has been successfully used to port libinput, libevdev, Wayland and more software to FreeBSD: https://www.freshports.org/devel/libepoll-shim/

А вот как раз и наш случай описан:

It may be useful for porting other software that uses epoll as well.

Так что берем эту радость в оборот, первым делом ставим пакет в систему:

pkg install libepoll-shim

Затем добавляем условие в скрипт сборки:

if host_machine.system() in ['freebsd', 'netbsd', 'openbsd', 'dragonfly']
    deps_libpistache += dependency('epoll-shim',required: true)
endif

Добавляем в районе стоки 53, ориентируясь на блок формирования списка зависимостей для самой библиотеки Pistache:

deps_libpistache = [
	dependency('threads'),
	date_dep
]

И запускаем сборку опять:

meson compile -C build

В этот раз ошибки будет две, одна простая, вторая сложная. Начнем разумеется со сложной:

Сложность тут в том, что в этот раз «магической библиотеки» эмулирующей линуксовый sendfile не нашлось. А значит придется погружаться на самое дно в дебри реальной разработки. Хотя и не очень глубоко.

Начнем с матчасти:

sendfile() copies data between one file descriptor and another.
Because this copying is done within the kernel, sendfile() is
more efficient than the combination of read(2) and write(2),
which would require transferring data to and from user space.

По-сути это такая специальная функция, созданная для быстрого копирования данных на уровне ядра Linux между файлом и сокетом.

У FreeBSD тоже есть свой sendfile, но с другим, несовместимым набором параметров:

// Linux версия
ssize_t sendfile(int out_fd, int in_fd, off_t *_Nullable offset,
                        size_t count);
// FreeBSD
int sendfile(int fd, int s, off_t offset, size_t nbytes,
	   struct sf_hdtr *hdtr, off_t *sbytes,	int flags);

Вот и что с этим прикажете делать?

Писать адаптер разумеется.

Закомментируем строку:

#include <sys/sendfile.h>

в src/common/transport.cc и перезапускаем сборку.

Получим при ошибке указание на конкретное место с вызовом sendfile:

Ввиду моей лени ради уменьшения размеров статьи, я просто вытащил вот такую функцию из одного из прошлых проектов:

ssize_t xsendfile(int out, int in, off_t offset, off_t off_bytes)
{    
    size_t bytes = off_bytes;
   	// проверка на размер структуры 
   	if (bytes > SSIZE_MAX) 
	{
		bytes = SSIZE_MAX;
	} 
    off_t nbytes = 0;
    if (-1 == sendfile(in, out, offset, bytes, NULL, &nbytes, 0)) {
     /* 	
    	    FreeBSD возвращает код ошибки EAGAIN после частичной записи,
    	    что является нормальным поведением при неблокирующем вводе-выводе 
     */
      if (errno == EAGAIN && nbytes > 0) 
      {
	   	return nbytes;
	  }
      return -1;
    }
    return nbytes;
}

Вот что такое SSIZE_MAX:

#define	SSIZE_MAX	__SSIZE_MAX	/* max value for an ssize_t */
#endif

Дальше я вставил эту функцию в самый конец файла src/common/utils.cc, после строчки завершения макроса:

#endif /* PISTACHE_USE_SSL */

Данный файл уже был включен в исправляемый в процессе transport.cc:

#include <pistache/utils.h>

Поэтому дополнительных телодвижений делать не надо. Также я добавил сигнатуру функции в конец заголовочного файла include/pistache/utils.h, тоже после строки завершения макроса:

ssize_t xsendfile(int out, int in, off_t offset, off_t off_bytes);

Дальше думаю очевидно, что надо заменить вызов sendfile на наш новый xsendfile в районе строки 386 файла src/common/transport.cc.

Вот это место:

#endif /* PISTACHE_USE_SSL */
            bytesWritten = xsendfile(fd, file, offset, len);
#ifdef PISTACHE_USE_SSL

После этого делаем повторный запуск сборки и видим что нашелся еще один файл с подключенным заголовочным файлом sys/sendfile.h:

Но тут все проще, поскольку функции из этого заголовочного файла на самом деле не использовались. Поэтому достаточно закомментировать эту строку:

//#include <sys/sendfile.h>

И проехать дальше.

А дальше у нас еще одна ошибка с флагом, которого нет в BSD:

Вот за что этот флаг отвечает:

To set or get a TCP socket option, call getsockopt(2) to read or setsockopt(2) to write the option with the socket family argument set to SOL_TCP. In addition, most SOL_IP socket options are valid on TCP sockets. For more information see ip(4).

Это не просто флаг, а тип флага, который указывает на то что сам флаг применяется ко всем TCP-сокетам или к сокетам с IP-адресацией. И в FreeBSD его такого нет, зато вместо есть вот такое:

When manipulating socket options the level at which the option resides
and the name of the option must be specified. To manipulate options at
the socket level, level is specified as SOL_SOCKET.

Вот его и будем использовать, заменив SOL_TCP на IPPROTO_TCP в двух местах файла src/server/listener.cc, строки 177:

TRY(::setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, &hint, sizeof(hint)));      

и 182:

TRY(::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)));

Перезапускаем сборку и получаем следующую ошибку — но уже где-то в тестах. Сие безусловно хорошо, поскольку означает что сборка самого фреймворка прошла успешно:

Возникает эта ошибка из-за того что в BSD функция wait() находится в отдельном заголовочном файле:

 #include <sys/wait.h>
 pid_t wait(int *status);

Вот что она делает, если кто вдруг не знает:

The wait() function suspends execution of its calling thread until
status information is available for a child process or a signal is re-
ceived. On return from a successful wait() call, the status area con-
tains information about the process that reported a status change as
defined below.

Сигнатура совпадает с версией в Linux, поэтому достаточно лишь подключить заголовочный файл в тесте tests/listener_test.cc:

Снова перезапускаем сборку, получаем новую ошибку, но уже в примерах (тесты успешно собрались — ееее!):

Проблема в том что скрипт сборки тестовых проектов забыли обновить при добавлении поддержки библиотеки brotli, берем вот это условие из meson.build в папке tests:

# If built with Brotli compression support enable decoder for unit testing...
if get_option('PISTACHE_USE_CONTENT_ENCODING_BROTLI')
    brotli_dep = dependency('libbrotlidec')
endif

и добавляем в скрипт examples/meson.build.

Конечный результат должен выглядеть вот так:

# SPDX-FileCopyrightText: 2021 Andrea Pappacoda
#
# SPDX-License-Identifier: Apache-2.0

pistache_example_files = [
	'custom_header',
	'hello_server',
	'http_client',
	'http_server_shutdown',
	'http_server',
	'rest_server',
	'rest_description'
]

threads_dep = dependency('threads')
brotli_dep = dependency('', required: false)
rapidjson_dep = dependency('RapidJSON')

# If built with Brotli compression support enable decoder for unit testing...
if get_option('PISTACHE_USE_CONTENT_ENCODING_BROTLI')
    brotli_dep = dependency('libbrotlidec')
endif

foreach example_name : pistache_example_files
	executable('run'+example_name, example_name+'.cc', dependencies: [pistache_dep, threads_dep,brotli_dep,rapidjson_dep])
endforeach

Обратите внимание на строку:

rapidjson_dep = dependency('RapidJSON')

Это еще одна потерянная зависимость в примерах, приводящая вот к такой ошибке:

После всех правок лучше всего удалить папку build и выполнить чистую полную сборку:

rm -rf build/
meson setup build                                 \
    --buildtype=release                             \
    -DPISTACHE_USE_SSL=true                         \
    -DPISTACHE_BUILD_EXAMPLES=true                  \
    -DPISTACHE_BUILD_TESTS=true                     \
    -DPISTACHE_BUILD_DOCS=false                     \
    -DPISTACHE_USE_CONTENT_ENCODING_BROTLI=true     \
    -DPISTACHE_USE_CONTENT_ENCODING_DEFLATE=true    \
    --prefix="$PWD/prefix"
meson compile -C build

После запуска должна пройти сборка, полностью и без ошибок:

В итоге должны собраться и тестовые проекты из каталога examples:

Запустим самый простой пример:

./build/examples/runhello_server

Вот так это выглядит:

Код:

/*
 * SPDX-FileCopyrightText: 2016 Mathieu Stefani
 *
 * SPDX-License-Identifier: Apache-2.0
 */
/*
   Mathieu Stefani, 13 février 2016
   Example of an hello world server
*/

#include "pistache/endpoint.h"
using namespace Pistache;
class HelloHandler : public Http::Handler
{
public:
    HTTP_PROTOTYPE(HelloHandler)

    void onRequest(const Http::Request& /*request*/, Http::ResponseWriter response) override
    {
        response.send(Pistache::Http::Code::Ok, "Hello World\n");
    }
};
int main()
{
    Pistache::Address addr(Pistache::Ipv4::any(), Pistache::Port(9080));
    auto opts = Pistache::Http::Endpoint::options()
                    .threads(1);
    Http::Endpoint server(addr);
    server.init(opts);
    server.setHandler(Http::make_handler<HelloHandler>());
    server.serve();
}

На этом все, поздравляю:

Вам (мне) удалось портировать аж целый веб-фреймворк на C++ с одной ОС на другую!

Можете по такому поводу накатить, благо повод достойный.

Но конечно же я на этом не остановился и сваял собственный тестовый проект на этом замечательном фреймворке.

Тестовый проект

Именно его в запущенном виде вы и наблюдаете на титульной картинке, это максимально простой вебсервис на C++, реализующий REST API бекэнд для простой гостевой книги. С вполне стандартными методами «добавления-просмотра-списка самих записей».

Еще для всех методов реализовано описание и возможность вызова через интерфейс Swagger5 — ключевая фича для проекта на C++!

Ну и для совсем полной красоты, вся эта радость была локализована на русский. На веб-фреймворке и C++, напоминаю, это там где sefaultы и падения в .core — вообщем там где все веселье и чад кутежа в разработке ПО.

Прежде чем продолжать рассказ о проекте, сделаю небольшое лирическое отступление.

Пару слов о Swagger

Это крайне важная и нужная штука, используемая в современной веб-разработке для описания API:

When described by an OpenAPI document, Swagger open-source tooling may be used to interact directly with the API through the Swagger UI. This project allows connections directly to live APIs through an interactive, HTML-based user interface. Requests can be made directly from the UI and the options explored by the user of the interface.[5]

Фактически это промышленный стандарт, активно применяемый при разработке бекэнда на Java/C#/Node.js

Исторически так получилось, что Swagger появился сначала в Java-мире, затем распространился на другие языки и managed-платформы, но вот для «тру-хардкор» проектов на Си или C++ его не применяли.

Хотя для C++ любое документирование всегда считалось уделом слабых и нежных, а «реальные пацаны» изучают параметры вызова функции сразу через отладчик ага.

Вообщем веб-фреймворк на C++ это само по себе явление, но еще и поддержка Swagger «из коробки» это прямо провокация.

Именно так и появилась данная статья — мне стало очень интересно. Ладно, идею думаю вы поняли, возвращаемся к проекту.

Начну немного с конца — с аннотированного исходного кода, а ниже опишу как его запускать. Итак господа и дамы — перед вами исходный код:

/*
Пример использования фрейморка Pistache с REST, Swagger5 и блудницами
*/
#include <algorithm>
#include <pistache/endpoint.h>
#include <pistache/http.h>
#include <pistache/router.h>
#include <pistache/serializer/rapidjson.h>
#include <pistache/http.h>
#include <pistache/http_headers.h>
#include <cstdio>
#include <string>
#include <vector>
// в FreeBSD это часть libc
#include <uuid.h>
using namespace Pistache;

namespace Generic
{   /*
       реализует самый простой ответ для проверки работы сервера
    */
    void handlePing(const Rest::Request&, Http::ResponseWriter response)
    {   // создание объекта Mime для типа данных text/plain 
        auto m = MIME(Text, Plain);
        // дополнительно указываем кодировку
        // в заголовке будет text/plain; charset=utf-8
        m.setParam("charset", "utf-8");    
        response.headers()
                        .add<Pistache::Http::Header::Server>("pistache/0.1")
                        .add<Pistache::Http::Header::ContentType>(m);
        // отправляем стандартный 200 OK с текстовыми данными в UTF-8                    
        response.send(Http::Code::Ok, "работает!");
    }
}
/*
  Класс для описания API и связки со Swagger
*/
class GbEndpoint
{
public:
    // конструктор тут наследуется от заранее заданного
    explicit GbEndpoint(Address addr)    
        : httpEndpoint(std::make_shared<Http::Endpoint>(addr))
        , desc("Тестовое API Swagger", "1.0")  
    { }
    /*
        начальная инициализация, thr это количество используемых тредов
    */
    void init(size_t thr = 2)
    {
        auto opts = Http::Endpoint::options()
                        .threads(static_cast<int>(thr));
        // инициализация предка                
        httpEndpoint->init(opts);
        // формирование метаданных Swagger
        createDescription();
    }
    /*
       запуск нашего REST-сервера
    */
    void start()    
    {
        // инициализация роутера
        router.initFromDescription(desc);
        // отдача метаданных Swagger
        Rest::Swagger swagger(desc);
        swagger
            // префикс в url по которому будет работать Swagger UI
            .uiPath("/doc") 
            // локальный путь до каталога со Swagger 5
            .uiDirectory("../swagger-ui-5.10.3/dist")
            // url по которому наш сервер будет отдавать метаданные для Swagger
            .apiPath("/gb-api.json")
            .serializer(&Rest::Serializer::rapidJson)
            .install(router);
        httpEndpoint->setHandler(router.handler());
        // собственно запуск
        httpEndpoint->serve();
    }
private:
        /*
          немного Сишного говнокода для генерации UUID без
          внешних библиотек
        */
        std::string generateId() {
            uuid_t uu;
			uint32_t status = uuid_s_ok;
			char *str = NULL;
			uuid_create(&uu, &status);
			if (status == uuid_s_ok)
			{
				uuid_to_string(&uu, &str, &status);
				if (status == uuid_s_ok)
				{				
				    std::cout << str << std::endl;
				    std::string s(str);
				    return s;                    
				}				
			}
       return "";
     }
       
    /*
      Создание описания функций для Swagger 
    */
    void createDescription()
    {   
          desc
            .info()
            // у вас будет очевидно Proprietary, это нужно ради
            // генерации ссылки на текст лицензии
            .license("Apache", "http://www.apache.org/licenses/LICENSE-2.0");
        // обработчик ошибки 500
        auto backendErrorResponse = desc.response(Http::Code::Internal_Server_Error, 
        "Что-то плохое случилось на бекэнде");
        // не стал заморачиваться с https, поскольку на практике все равно
        // бекэнд будет отдавать HTTP, а https будет на уровне прокси-сервера
        desc
            .schemes(Rest::Scheme::Http)
            // глобальный префикс API 
            .basePath("/v1")
            // указание использовать Mime тип JSON как для ввода так и для вывода 
            .produces(MIME(Application, Json))
            .consumes(MIME(Application, Json));
        desc
            // подключение тестового API для проверки сервера
            .route(desc.get("/ping"))
            .bind(&Generic::handlePing)
            .response(Http::Code::Ok, "Ответ на вызов ping")
            .hide();
        // формирование цепочки префиксов для API
        auto versionPath = desc.path("/v1");
        auto recordsPath = versionPath.path("/records");
        // описание API для получения всех записей
        recordsPath
            .route(desc.get("/all"))
            .bind(&GbEndpoint::retrieveAllRecords, this)
            .produces(MIME(Application, Json))
            .response(Http::Code::Ok, "Возвращает список записей")
            .response(backendErrorResponse);
        // .. для получения записи по id
        recordsPath
            .route(desc.get("/getRecord/:id"), "Получить запись")
            .bind(&GbEndpoint::doGetRecord, this)
            .produces(MIME(Application, Json))
            .parameter<Rest::Type::String>("id", "ID записи")
            .response(Http::Code::Ok, "Найденная запись")
            .response(Http::Code::Not_Found, "Запись не найдена")
            .response(backendErrorResponse);
        // .. для всратого варианта добавления или изменения записи
        recordsPath
            .route(desc.post("/addRecord/:title/:message"), "Создать или изменить запись")
            .bind(&GbEndpoint::doAddUpdateRecord, this)
            .produces(MIME(Application, Json))
            .consumes(MIME(Application, Json))
            .parameter<Rest::Type::String>("title", "Заголовок")
            .parameter<Rest::Type::String>("message", "Текст")
            .response(Http::Code::Ok, "Запись успешно обновлена")
            .response(Http::Code::Created, "Создана новая запись")
            .response(Http::Code::Not_Found, "Запись не найдена")
            .response(backendErrorResponse);    
    }   
    /*
        метод реализует отдачу всех записей нашей гостевой
    */ 
    void retrieveAllRecords(const Rest::Request&, Http::ResponseWriter response)
    {         
      rapidjson::StringBuffer sb;
                rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(sb);         
      writer.StartArray();
        for (std::vector<MyRecord>::const_iterator rItr = records.begin(); rItr != records.end(); ++rItr)
            rItr->Serialize(writer);
      writer.EndArray();                      
      response.send(Http::Code::Ok, sb.GetString());        
    }
    /*
      .. добавление и изменение записи
    */
    void doAddUpdateRecord(const Rest::Request& request, Http::ResponseWriter response)
    {
        std::string id = "";
        // если был найден параметр id - используем
        if (request.hasParam(":id"))
        {            
            id =request.param(":id").as<std::string>();       
        } else {            
            // если нет - создаем новый id
            id = generateId();         
        }
        std::string title = "";
        std::string message = "";  
        // получение поля 'title'      
        if (request.hasParam(":title"))
        {
            auto value = request.param(":title");
            title = value.as<std::string>();
        }        
        // получение поля 'message'
        if (request.hasParam(":message"))
        {
            auto value = request.param(":message");
            message = value.as<std::string>();
        }
        // поиск записи по id
        Guard guard(recordsLock);
        auto it = std::find_if(records.begin(), 
           records.end(), [&](const MyRecord& record) {
                    return record.id() == id;
           });          
        std::cout << "id: " << id.c_str()  << std::endl;     

        // если запись не была найдена - добавляем в наше мега-хранилище
        if (it == std::end(records))
        {
         // создаем объект из набора полей
         MyRecord record(id,title,message);
         // добавляем в 'хранилище'
         records.emplace_back(record);
         // отдаем назад в виде JSON           
         rapidjson::StringBuffer sb;
         rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(sb);
         record.Serialize(writer);            
         response.send(Http::Code::Created, sb.GetString());            
        }
        // если нашли - отдаем один лишь ID
        else
        {
            auto& record = *it;
            response.send(Http::Code::Ok, std::move(record.id()));
        }
    }
   /*
      получить запись по id
   */
    void doGetRecord(const Rest::Request& request, Http::ResponseWriter response)
    {   // получаем параметр запроса
        auto id = request.param(":id").as<std::string>();
        // ставим блокировку на операцию со списком
        Guard guard(recordsLock);
        // поиск в списке записи по id, полным перебором да
        auto it = std::find_if(records.begin(), records.end(), [&](const MyRecord& record) {
            return record.id() == id;
        });
        // если запись с таким id не найдена - выдаем сообщение об ошибке и код 404 
        if (it == std::end(records))
        {
            response.send(Http::Code::Not_Found, "Запись не найдена.");
        }
        else
        {   // если запись нашлась - сериализуем ее в JSON
            const auto& record = *it;
            
            rapidjson::StringBuffer sb;
            rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(sb);
            record.Serialize(writer);            
            response.send(Http::Code::Ok, sb.GetString());
        }
     }
    /* 
      Класс с записью аля гостевая
    */ 
    class MyRecord
    {
    public:  
        explicit MyRecord(std::string id, 
        std::string title, std::string message)
            : id_(std::move(id))
            , title_(std::move(title))
            , message_(std::move(message))
        { }        
        const std::string& id() const
        {
            return id_;
        }
        const std::string& title() const
        {
            return title_;
        }

        const std::string& message() const
        {
            return message_;
        }
        /* 
          сериализация в JSON
        */ 
        template <typename Writer> 
        void Serialize(Writer& writer) const {
        writer.StartObject();
        writer.String("id");
        writer.String(id_.c_str());
        writer.String("title");
        writer.String(title_.c_str());
        writer.String("message");
        writer.String(message_.c_str());    
        writer.EndObject();        
        }
    private:         
         // тестовые поля
        std::string id_;
        std::string title_;
        std::string message_;
    };

    using Lock  = std::mutex;
    using Guard = std::lock_guard<Lock>;
    Lock recordsLock;
    std::vector<MyRecord> records;
    std::shared_ptr<Http::Endpoint> httpEndpoint;
    Rest::Router router;
    Rest::Description desc; 
};
/*
 стартовая точка приложения
*/
int main(int argc, char* argv[])
{
   Port port(9080);
   int thr = 2;
   // обработка входящих аргументов
   if (argc >= 2)
   {
        // получение номера порта
        port = static_cast<uint16_t>(std::stol(argv[1]));
        // получение кол-ва используемых тредов
        if (argc == 3)
            thr = std::stoi(argv[2]);
   }

   // создаем объект адреса с указанием порта, слушать будем все интерфейсы
   Address addr(Ipv4::any(), port);
   
   std::cout << "Ядер = " << hardware_concurrency() << std::endl;
   std::cout << "Используется " << thr << " потоков" << std::endl;
   // создание объекта класса с нашим API
   GbEndpoint gb(addr);
   // вызов инициализации
   gb.init(thr);
   // запуск сервиса
   gb.start();
}

Теперь про то как все это запустить.

Я использовал самый простой вариант — включил свой код в качестве одного из примеров сразу в сборку, поэтому просто сохраняете код выше в файл test_swagger_server.cc в каталоге examples и правите meson.build в корне:

После чего запускаете сборку заново и пробуете запустить полученный бинарник.

Иии... оно упадет.

Потому что вам перед запуском еще необходимо скачать Swagger UI:

git clone https://github.com/swagger-api/swagger-ui/releases/tag/v5.10.3

и не забыть прописать путь к каталогу dist вот тут:

  swagger
            // префикс в url по которому будет работать Swagger UI
            .uiPath("/doc") 
            // локальный путь до каталога со Swagger 5
            .uiDirectory("../swagger-ui-5.10.3/dist")      

После чего запустить и наслаждаться.

Эпилог

Тема портирования ПО невероятно сложная и объемная, я описал лишь небольшую ее часть и показал решение самых «детских проблем» на небольшом и очень простом проекте.

Конечно же проблем куда больше чем тут описано, но в размеры статьи они бы просто не поместились.

Наша команда обладает определенным опытом в столь сложной теме портирования ПО, поэтому если у вас есть такая задача и нужна помощь в столь сложной теме — пишите, мы постараемся помочь.