Доброго времени суток, уважаемые пользователи Хабра.

Я не то что бы профессиональный разработчик на C++, в основном я занимаюсь геймдевом на UE5 (по крайней мере последнее время). Но последнее время достаточно часто я балуюсь разработкой десктоп приложений под Windows. Для красоты и простоты я задумывался об использовании именно react. Но из выбора что я увидел было 2 варианта:

  1. Tauri - Фреймворк под Rust с отрисовкой фронтенда сделанном на React и т.п.

  2. Electron -  фреймворк для разработки кроссплатформенных настольных приложений с использованием веб-технологий (скопировал описание с гугла)

Electron я отмел сразу, так как хотел писать бэкенд на чем то более удобном чем JS/TS для себя. Tauri мне очень понравился, но изучение Rust заняло немного времени. И смотря на это все я подумал, что можно реализовать какой-нибудь аналог Tauri используя C++.

Немного посидев поизучав информацию, я примерно выстроил себе план работы, из этого вылился первый MVP Shine. Фреймворк на данный момент поддерживает только Windows и тесно связан с vcpkg, так как находится на очень ранней стадии разработки, но его уже вполне себе можно потыкать и использовать.

Репозиторий проекта

А теперь к установке и использовании:

Для начала то, что нам нужно для запуска:

  • CMake

  • Node.JS

  • CLion (можете использовать что-то своё, мне комфортнее в нём)

Дальше мы открываем терминал и прописываем команду:
npm create shine-app@latest

У нас появится простая на данный момент настройка проекта, а именно указание его имени:

CLI создания приложения
CLI создания приложения

Дальше после того как мы введём имя приложение он создаст папку со всем, что нам нужно для реализации приложения

CLI Вывод
CLI Вывод

Структура проекта у нас выглядит следующим образом:

D:.
├───cmake # кастомные cmake функции для удобства сборки и запуска
├───frontend # Тут у нас находится фронтенд, сам UI приложения
│   ├───public
│   └───src
│       └───assets
├───generated # Тут у нас лежат ассеты собранные в бинарник, чтобы не таскать их за собой в релизе
├───scripts # Скрипты сборки для удобства
├───shine # Код библиотеки
│   ├───components
│   │   ├───include
│   │   │   └───shine
│   │   │       └───components
│   │   └───src
│   └───core
│       ├───include
│       │   └───shine
│       │       └───engine
│       └───src
│           └───engine
│               └───win32
└───src # Код нашего приложения (в данном случае main.cpp)

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

Дальше мы открываем проект в CLion (или там где удобно вам) и настраиваем профили CMake, У меня это выглядит так:

Мои настройки CMake
Мои настройки CMake

Дальше идем в View -> Tool Windows -> vcpkg

Открываем vcpkg
Открываем vcpkg

Далее выбираем наш существующий манифест, и нажимаем на карандашик сверху чтобы он работал с нашими CMake профилями:

UI с vcpkg окном
UI с vcpkg окном

Кликаем галочку Add vcpkg integration to existing CMake profiles

Галочка что нужно нажать
Галочка что нужно нажать

Далее кликаем ПКМ по корневому CMakeLists.txt и нажимаем Reload CMake Project.

Расположение Reload CMake Project
Расположение Reload CMake Project

Дальше мы можем спокойно запустить приложение и увидеть что оно работает:

UI Интерфейс template приложения
UI Интерфейс template приложения

Идею темплейта я решил взять просто с Tauri, простое окно где можно ввести имя и при нажатии Greet наше C++ ядро отформатирует сообщение и выведет его во фронтенде:

После нажатия Greet
После нажатия Greet

Со стороны C++ наш handler функции greet выглядит следующим образом:

SHINE_COMMAND(greet) {
    // У функции собранной с SHINE_COMMAND по дефолту есть аргументы
    // В развернутом виде функция выглядит примерно так:
    // nlohmann::json greet(const nlohmann::json& args)
    std::string name_str = args["name"];

    return std::format("Hello, {}", name_str);
}

и указание того, что мы можем вызвать эту функцию из фронтенда:

app.GetRouter().AddHandlers({SHINE_HANDLER(greet)});

После этого наш фронтенд знает что мы можем вызывать функцию greet с помощью метода invoke. Выглядит это следующим образом:

    async function handleGreet() {
        if (!name.trim()) return;

        setIsLoading(true);
        try {
            // Вызываем функцию и записываем ответ в setGreetMsg
            const res = await invoke('greet', { name });
            setGreetMsg(res.result || res);
        } catch (error) {
            setGreetMsg("Error: Could not connect to Shine Core");
        } finally {
            setIsLoading(false);
        }
    }

Дальше мы можем как угодно переделывать наш проект React и делать наше UI. При сборке в дебаге приложение просто слушает локалхост с нашим портом, в Shine есть поддержка HMR и при изменении кода, UI в приложении так же изменится. При релизе же, проект фронтенда собирается и после чего переводятся в байты, дальше мы используем эти байты внутри приложения не распаковывая их храним в памяти.

Помимо всего этого у нас есть конфиг файл: shine.conf.json, выглядит он так по стандарту:

{
  "window": {
    "title": "Shine Secure App",
    "width": 900,
    "height": 600
  },
  "capabilities": {
    "allowedCommands": [
      "fs_read_text_file",
      "window_drag"
    ]
  }
}

В нем пока что мы указываем title окна приложения, его размеры. Помимо этого есть ещё 2 параметра конфига:

  • frameless - Отключает рамки у окна

  • resizable - Разрешает / запрещает изменение размера окна

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

Учитывая что мы пишем на C++, и запихиваем ассеты фронтенда прямиком в бинарник, приложение из template имеет достаточно приятный вес - всего 845кб, при этом он не требует зависимостей для запуска и можно отправлять голый .exe пользователю и он запустится на его системе.

Вес релизного приложения
Вес релизного приложения


В планах у меня реализовать поддержку Linux и MacOS, а так же возможность получать состояние элементов фронтенда из плюсов и взаимодействия с ними. Например заполнять прогресс бар условный из C++ при какой-нибудь загрузке и т.п.

Это моя первая попытка сделать что-либо полезное на C++ для опенсорса, буду очень рад объективной критике, советам и контрибьютингу.