В этой статье я расскажу вам о реальных преимуществах горячей перезагрузки при создании игры с использованием raylib и покажу, как это легко сделать самому.

Во-первых, что такое райлиб?

Raylib - это то, чего я (правда) всегда хотел. Когда я обнаружил это в прошлом году, для меня это был взрыв.

Я всегда хотел легко создавать игры (или прототипы), просто кодируя.
Без навороченного редактора, в котором можно часами сидеть.
Без дополнительных «потрясающих функций, вам наверняка понадобятся часы, чтобы научиться ими пользоваться, просматривая смертельно длинные обучающие видеоролики», как в традиционных игровых движках (Unity, Unreal Engine, Lumberyard…). Чтобы быть ясным, я использовал и Unity, и Unreal, и они великолепны. Вы можете делать с ними действительно хорошие вещи быстро, и у них обоих отличный дизайн кода.
Но вы знаете, я программист, и что мне действительно нравится, так это писать код без необходимости часами искать в документации, как использовать некоторые компоненты. Мне также нравится иметь полную свободу в отношении архитектуры проектов, над которыми я работаю, в свободное время (если я хочу поэкспериментировать с полной архитектурой, ориентированной на данные и / или функционально ориентированной, я могу сделать это без каких-либо ограничений). И именно потому, что я думаю, что это мое свободное время, первое, что имеет значение, - это получать от этого удовольствие, не так ли? Иначе это не имеет смысла (по крайней мере, для меня). И с таким настроением raylib был как раз тем фреймворком, который мне был нужен!

Итак, raylib - это старый игровой фреймворк на C (C99), который работает с opengl3.3 (или opengl ES). В нем есть все необходимое для создания простых (или не очень простых на самом деле ...) игр, включая абстракции для работы со звуками, изображениями, сетками, шейдерами ...

Самое приятное то, что он написан на C, и вы знаете, что C отлично подходит для создания привязок! Итак, существует множество привязок для множества других языков программирования, таких как Go, Lua, Rust… На самом деле, довольно легко создать свою собственную привязку, потому что raylib зависит от очень немногих других внешних библиотек. На самом деле, есть только GLFW для обработки создания окна.

Raylib включает в себя emscripter и заголовки android + GLES, что означает, что вы можете создавать свою игру для Интернета или для Android, если хотите. На самом деле существует множество целей, для которых вы можете создать, включая Windows (конечно, вы даже можете создать свою игру как приложение W10), Linux, OSX, FreeBSD и даже Raspberry!

И лучшая часть raylib - это то, что API действительно крошечный. Нет функций, которые вам не нужны. Все хорошо названо. Вам просто нужна шпаргалка, и все готово! Не нужно искать документацию часами! На самом деле вы можете потратить большую часть своего времени на написание игры, вместо того, чтобы искать, как использовать некоторые компоненты. Сказал вам, это взрыв!

По всем этим причинам, я думаю, теперь вы понимаете, почему я люблю райлиб! Это крошечный, на старом простом языке C, с открытым исходным кодом, с очень разрешающей лицензией. Это дает действительно прочную основу для начинающих (и продвинутых программистов) для создания игр или любого другого графического приложения, которое вы хотите создать.

А теперь давайте перейдем к основной теме: горячая перезагрузка!

Во-первых, некоторые очевидные вопросы, которые вы можете задать, если разрабатываете свою игру или какое-либо другое программное обеспечение: что такое горячая перезагрузка и почему я должен когда-либо добавлять эту функцию в свою программу?

Итак, горячая перезагрузка состоит из перезагрузки некоторых частей (или даже огромных частей) вашего кода во время выполнения. По умолчанию raylib написан на C, и вы знаете, что C (как и любой другой системный язык) компилируется в исполняемый файл. Итак, традиционно, если вы хотите внести даже быстрые изменения в свою программу, вам всегда нужно выполнять следующие шаги:

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

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

Как вы думаете, почему разработчики полюбили Интернет с помощью HTML / CSS, JS или даже PHP? Ответ довольно прост: вы вносите изменения, вы перезагружаете страницу и видите свои изменения в реальном времени!

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

По этой причине появилось множество интерпретируемых языков, таких как Lua, Python, Ruby… Сейчас самый популярный язык программирования на самом деле также является интерпретируемым языком: JS (с огромным влиянием NodeJS в мире программирования).

В индустрии разработки игр Lua был классическим выбором до нескольких лет (и до сих пор используется в некоторых студиях). Но многие решили создать свой собственный «язык визуальных сценариев», что сейчас кажется нормой. Как, например, система Blueprint в Unreal Engine 4 (каждая огромная игровая студия в мире имеет свою собственную систему Blueprint внутри своего игрового движка, от Guerilla Games с Decima Engine до Massive и SnowDrop Engine…). Эти визуальные языки сценариев дают возможность непрограммистам быстро вносить некоторые изменения в код, не обращаясь к программистам, и видеть их изменения в реальном времени. Быстрее создавать прототипы - потрясающая возможность!

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

Ответ для меня довольно прост: во-первых, (лично), даже если это звучит невероятно интересно, у меня нет времени создавать язык сценариев. У меня недостаточно свободного времени, и все, что я хочу, - это создавать базовые игры на raylib, а не на языках. Затем язык сценариев по своей сути, как я уже сказал, «интерпретируемый». Это означает, что во время выполнения программе необходимо «на лету» просмотреть код сценария и «преобразовать» его в инструкции. У этого есть цена.

Но вы можете ответить нам raylib, и если все, что мы хотим, - это создать простую игру, эта стоимость времени выполнения не имеет большого значения. И ты прав. Но я думаю, что язык сценариев порождает другую проблему, более проблематичную. Используя один из них, для определенной части вашей игры (например, IA) у вас будет два компонента программного обеспечения, которые нужно поддерживать: основной код вашей программы (в C / C ++ в нашем случае) и код сценария.

И если вы в какой-то момент испортите свои привязки к языку сценариев, вам придется исправить ужасные ошибки.

С другой стороны, что мы можем сделать, чтобы работать максимально продуктивно и быстро выполнять итерацию, - это реализовать горячую перезагрузку с помощью файлов dll (в Windows, файлов в Linux). И вполне вероятно, что raylib написан на C (например, а не на C ++), потому что это намного проще сделать со старым простым языком C, где у вас нет классов, только набор структур и функций.

Пришло время показать пример кода!

Итак, как это сделать? Во-первых, давайте создадим действительно простую программу, используя raylib и raygui (который является аддоном raylib для быстрого создания элементов графического интерфейса).

Вот наш файл с именем main.c, который содержит функцию main:

#include "raylib.h"
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"
int main(void){
    InitWindow(1280, 720, "Test"); // 1280x720 window named "Test"
    while(!WindowShouldClose()){
        BeginDrawing();
            ClearBackground(RED);
        EndDrawing();
    }
    CloseWindow();
    return 0;
}

Мы можем скомпилировать его с помощью этой команды в Windows с помощью mingw, предполагая, что raylib.h и raygui.h находятся в папке include и что raylib + glfw3 доступны как статические библиотеки внутри папки libs:

gcc main.c -I./include -L./libs -lglfw3 -lraylib -lopengl32 -lgdi32 -o main.exe

Наконец, у нас есть исполняемый файл main.exe, который просто создает окно и заполняет его красным цветом, который мы можем запустить с помощью этой команды:

./main.exe

Действительно просто.

А теперь давайте добавим графический интерфейс. Давайте добавим кнопку с помощью raygui в это скучное красное окно:

#include "raylib.h"
#define RAYGUI_IMPLEMENTATION
#include "raygui.h"
int main(void){
    InitWindow(1280, 720, "Test"); // 1280x720 window named "Test"
    while(!WindowShouldClose()){
        BeginDrawing();
            ClearBackground(RED);
            if(GuiButton((Rectangle){ 500, 200, 250, 60 }, "TEST BUTTON")) {
                puts("Button pressed\n");
            }
        EndDrawing();
    }
    CloseWindow();
    return 0;
}

Опять же, это действительно просто: мы просто добавляем кнопку с текстом ТЕСТОВАЯ КНОПКА с помощью функции GuiButton из raygui. Эта кнопка будет расположена в точке x = 500, y = 200, ее длина будет 250 пикселей, а ширина - 60 пикселей.
Если кнопка нажата, на консоли отобразится:
Кнопка нажата

Чтобы быть чище, я вырезал часть определений из кода raygui.h (который по умолчанию содержал объявления и определения, это файл только для заголовков) и скомпилировал его в статическую библиотеку, которую я скопировано / вставлено в папку libs.

Итак, теперь мы можем скомпилировать код с помощью той же предыдущей команды gcc, просто подумайте о добавлении инструкции -lraygui в качестве параметра перед -lglfw3. А затем, как и раньше, выполните программу для тестирования нашего кода.

gcc main.c -I./include -L./libs -lraygui -lglfw3 -lraylib -lopengl32 -lgdi32 -o main.exe

Итак, вы согласны с тем, что у нас есть простая программа, которая содержит только окно и кнопку. Но если мы хотим изменить все, что связано с этим графическим интерфейсом, нам нужно каждый раз перекомпилировать его и перезапускать. Да уж, скучно ...

Пришло время погрузиться в самое интересное с горячей перезарядкой!

Во-первых, давайте разделим наш код, создав здесь некоторую абстракцию, с новым файлом, который в нашем случае просто называется core.c (который будет внутри папки src, а для ясности, мы перемещаем main.c в эту папку тоже), связанный с его заголовочным файлом core.h (который находится внутри папки include) .

// inside core.h
#pragma once
void core_create_window(
    const unsigned short in_width,
    const unsigned short in_height,
    const char* in_title
);
void core_execute_loop();
bool core_window_should_close();
void core_close_window();
// inside core.c
#include "core.h"
#include "raylib.h" 
#define RAYLIB_IMPLEMENTATION 
#include "raygui.h"
void core_create_window(
    const unsigned short in_width,
    const unsigned short in_height,
    const char* in_title
){
    InitWindow(in_width, in_height, in_title);
}
void core_execute_loop(){
    BeginDrawing();
        ClearBackground(RED);
        if(GuiButton((Rectangle){ 500, 200, 250, 60 }, "TEST BUTTON")) {
            puts("Button pressed\n");
        }
    EndDrawing();
}
bool core_window_should_close(){
    return WindowShouldClose();
}
void core_close_window(){
    CloseWindow();
}

Это означает, что наш файл main.c теперь просто включает core.h, и файлы заголовков raylib не нужны. И он просто вызывает функции из core.c (наш уровень абстракции).

// inside main.c
#include "core.h"
int main(void){
    core_init_window(1280, 720, "Test"); // 1280x720 window named "Test"
    while(!core_window_should_close()){
        core_execute_loop();
    }
    core_close_window();
    return 0;
}

Итак, если мы скомпилируем нашу программу с помощью следующих команд, она должна будет делать то же самое, что и раньше:

// Compiling the core.c file into an object file
gcc -c ./src/core.c -I./include -o core.o
// Compiling the full project to get the executable file
gcc ./src/main.c core.o -I./include -L./libs -lraygui -lglfw3 -lraylib -lopengl32 -lgdi32 -o main.exe

В результате получается окончательный исполняемый файл main.exe, который выполняет то же самое, что и раньше.

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

Горячая перезагрузка в Windows означает файлы DLL. Итак, в нашем примере здесь нам нужно преобразовать наш файл core.o в файл dll и получить его функции по адресу в нашем файле main.c. чтобы иметь возможность позвонить им. По сути, это все, что нам нужно.

Но сначала нам нужно добавить несколько определений в наш заголовочный файл core.h, чтобы компилятор собирал core.c как общую библиотеку (dll) при передаче конкретный параметр в качестве параметра на этапе компиляции (в нашем случае это будет BUILD_LIBTYPE_SHARED).

// inside core.h
#pragma once
#if defined(_WIN32) && defined(BUILD_LIBTYPE_SHARED)
    #define CORE __declspec(dllexport) // We are building core as a Win32 shared library (.dll)
#elif defined(_WIN32) && defined(USE_CORE_LIB_SHARED)
    #define CORE __declspec(dllimport) // We are using core as a Win32 shared library (.dll)
#else
    #define CORE // We are building or using core as a static library
#endif
void core_create_window(
    const unsigned short in_width,
    const unsigned short in_height,
    const char* in_title
);
void core_execute_loop();
bool core_window_should_close();
void core_close_window();

Теперь, когда эти определения добавлены, мы можем скомпилировать файл code.c как динамическую библиотеку (dll).

Для этого с помощью gcc нам просто нужно добавить одну команду: gcc -shared file.o -o file.dll после создания файла core.o в соберите его в файл dll:

// Compiling the core.c file into an object file
// The option BUILD_LIBTYPE_SHARED indicates that we
// are building the core.c file to be used as a dll
gcc -c -DBUILD_LIBTYPE_SHARED core.c -I./include -o core.o
// Assembling the core.o object file into a dll file
gcc -shared core.o -o core.dll

Наконец, у нас есть независимый фрагмент кода на C, который можно использовать в нашей основной программе (внутри main.c).
Но как мы можем это сделать?

Первое, что нужно сделать, это включить файл libloaderapi.h в файл main.c, но для максимальной чистоты мы будем включать его, только если мы используют core.c в качестве файла dll. Если мы традиционно используем наш файл core.c просто как объектный файл, мы не хотим включать этот файл libloaderapi.h в наш проект. Помните, вы всегда хотите включать как можно меньше зависимостей (это действительно важно для скорости компиляции).

Вы, вероятно, спросите, почему мы используем именно этот файл заголовка, а не просто классический windows.h. На самом деле причин две:

  • Во-первых, очевидный: включив windows.h, вы фактически включаете в свой проект много кода. Много кода, который вы, вероятно, никогда не будете использовать. Итак, я повторяю себя, но это важно, помните: когда вы можете импортировать в свой проект как можно меньше кода, делайте это! Это действительно экономит время при компиляции всего проекта.
  • С другой стороны, raylib определяет некоторые базовые структуры для своих нужд, и вы можете столкнуться с ужасными проблемами при импорте windows.h из-за того, что некоторые структуры определены в windows.h с тем же именем, что и у raylib. Например, raylib определяет структуру с именем Rectangle, которая определяется как другая структура внутри windows.h (для gdi purpuses). Поэтому, за исключением случаев, когда вы хотите изменить raylib и самостоятельно переименовать некоторые структуры и функции, не используйте windows.h.

Вернемся к теме.

Другая проблема, с которой мы сталкиваемся, которая позволяет нам выбирать между двумя вариантами, заключается в том, связываемся ли мы с raylib (статической библиотекой) внутри нашей dll или внутри исполняемого файла.

Оба решения могут быть приняты, но они предлагают очень разные решения для решения части функций загрузки.
В одном случае нам нужно получить состояние программы из dll в файл main.c, чтобы иметь возможность восстанавливать его всякий раз, когда мы перекомпилируем dll.
В другом случае нам нужно передать dll указатели на функции raylib, чтобы иметь возможность использовать их из нашего файла core.c.

Я выбрал второй метод, потому что его легче выполнить (по крайней мере, я думаю). Если бы raylib был инициализирован в куче, а не в стеке, мне, вероятно, пришлось бы выбрать первый. Потому что, когда вы инициализируете переменные в стеке, всякий раз, когда вы перекомпилируете программу / dll, все эти переменные сбрасываются до значений по умолчанию. Но если он инициализируется в куче, переменные не меняются при перекомпиляции dll. Они уже находятся в памяти, нам просто нужно выполнить шаг инициализации, который уже был выполнен, и все готово!

Итак, имейте в виду, что я выбрал второй вариант, который состоит из связывания raylib в нашей основной программе и создания моста для передачи указателей функций raylib в нашу dll.
Итак, давайте продолжим пример кода!

Первое, что нужно сделать, это подготовить функцию в нашем core.c, доступную только тогда, когда мы создаем этот файл как dll, которая будет выполнять мост между функциями raylib из нашего main.c в dll.

Итак, чтобы иметь возможность использовать эти функции (из raylib и raygui), нам также необходимо подготовить некоторые переменные-указатели функций, чтобы сохранить их в файле dll, чтобы иметь возможность использовать их позже. Помните, что мы не связываем raylib и raygui как статические библиотеки в нашем файле dll. Нам нужны функции brige.

Я создал функцию под названием core_load_raylib_functions для достижения этой цели внутри core.c, и потому что это функция, которая будет использоваться только при компиляции core.c как dll, объявлять ее внутри файла заголовка core.h нет необходимости.

Наконец, я удалил включения raylib и raygui внутри core.c, чтобы поместить их в основную программу, а не в dll (то есть внутри main.c).

// inside core.c
#include "core.h"
// Only for Hot Reload
#if defined(_WIN32) && defined(BUILD_LIBTYPE_SHARED)
    /* Below are the functions pointers we need to make the bridge
    between the main.c (which contains raylib and raygui)
    and this core.c which will be converted to a dll.*/
    /* To be short, it's to be able to call raylib and raygui
    functions even if we are not linking them as static libraries
    into this dll. */
    // Raylib functions
    void (*raylib_init_window)(int width, int height, const char* title);
    void (*raylib_close_window)();
    bool (*raylib_window_should_close)();
    void (*raylib_begin_drawing)();
    void (*raylib_end_drawing)();
    void (*raylib_clear_background)(Color color);
    // Raygui functions
    bool (*raygui_gui_button)(Rectangle bounds, const char *text);
    void (*raygui_gui_set_style)(int control, int property, int value);
    // This function will be one of the first one to be load 
    // from this dll in main.c. It's role is actually to initialize
    // our raylib and raygui functions pointers, to create the bridge.
    CORE void core_load_raylib_functions(
        void (*const in_init_window)(int width, int height, const char* title),
        void (*const in_close_window)(),
        bool (*const in_window_should_close)(),
        void (*const in_begin_drawing)(),
        void (*const in_end_drawing)(),
        void (*const in_clear_background)(Color color),
        bool (*const in_gui_button)(Rectangle bounds, const char *text),
        void (*const in_gui_set_style)(int control, int property, int value)
    ){
        // Raylib functions
        raylib_init_window = in_init_window;
        raylib_close_window = in_close_window;
        raylib_window_should_close = in_window_should_close;
        raylib_begin_drawing = in_begin_drawing;
        raylib_end_drawing = in_end_drawing;
        raylib_clear_background = in_clear_background;
        // Raygui functions
        raygui_gui_button = in_gui_button;
        raygui_gui_set_style = in_gui_set_style;
    }
#else
    // I redefine the raylib and raygui functions
    // I use here, because whenever we are using the dll
    // or not, I want to be able to have one single code,
    // without #if/#else in the functions below.
    
    // Raylib functions
    #define raylib_init_window InitWindow
    #define raylib_close_window CloseWindow
    #define raylib_window_should_close WindowShouldClose
    #define raylib_begin_drawing BeginDrawing
    #define raylib_end_drawing EndDrawing
    #define raylib_clear_background ClearBackground
    // Raygui functions
    #define raygui_gui_button GuiButton
    #define raygui_gui_set_style GuiSetStyle
#endif
void core_create_window(
    const unsigned short in_width,
    const unsigned short in_height,
    const char* in_title
){
    raylib_init_window(in_width, in_height, in_title);
}
void core_execute_loop(){
    raylib_begin_drawing();
        raylib_clear_background(RED);
        if(raygui_gui_button((Rectangle){ 500, 200, 250, 60 }, "TEST BUTTON")) {
            puts("Button pressed\n");
        }
    raylib_end_drawing();
}
bool core_window_should_close(){
    return raylib_window_should_close();
}
void core_close_window(){
    raylib_close_window();
}
// inside main.c
#include "core.h"
// Whenever we will pass this option to the compiler, we will be able to use our core.c code as a dll
#if defined(CORE_USE_LIBTYPE_SHARED) 
    #include <libloaderapi.h>
    #include "raylib.h"
    #include "raygui.h"
#endif
int main(){
    // This code is necessary only if we are using hot reload
    #if defined(CORE_USE_LIBTYPE_SHARED)
        HINSTANCE dll_handle = NULL; // Declare a pointer to the dll resource
        // We need to initialize some function pointers, because, 
        // as mentioned before, by using a dll, we will load every functions
        // we need to use from raylib, to our dll, (think of it as a bridge), 
        // because our dll is not aware of raylib. 
        // We are not linking its static library in our dll file.
        void (*core_load_raylib_functions_func)(
            void (*const in_init_window)(int width, int height, const char* title),
            void (*const in_close_window)(),
            void (*const in_begin_drawing)(),
            void (*const in_end_drawing)(),
            void (*const in_clear_background)(Color color),
            int (*const in_get_key_pressed)(),
            bool (*const in_is_key_down)(int key),
            bool (*const in_gui_button)(Rectangle bounds, const char *text),
        );
        // Functions pointers to be able to store and call functions from 
        // our dll file (the initialization of the window, the exec loop function...)
        void (*core_init_window_func)();
        void (*core_execute_loop_func)();
        void (*core_exit_func)();
        // Load our core.dll file as a dynamic library.
        // Regarding to the official windows documentation, the second parameter of 'LoadLibraryExA'
        // is always NULL (just here for future need), and the last one corresponds to a flag
        // which determine the action to be taken when loading the module. In our case, we use the 
        // default action (so the flag is 0)
        dll_handle = LoadLibraryExA("./core.dll", NULL, 0);
        if(dll_handle != NULL){
            // Load the 'core_load_raylib_functions' function from the dll into the
            // 'core_load_raylib_functions_func' function pointer
            core_load_raylib_functions_func = (void*) GetProcAddress(dll_handle, "core_load_raylib_functions");
            if (NULL == core_load_raylib_functions_func){
                printf("Can't call core_load_raylib_functions dll function");
                exit(1);
            }else{
                // If the function is correclty loaded, then we call it, 
                // and we pass the functions from raylib and raygui
                // we need.
                // It's the bridge between our main.c and our dll, to 
                // retrieve the raylib and raygui functions.
                core_load_raylib_functions_func(
                    &InitWindow,
                    &CloseWindow,
                    &BeginDrawing,
                    &EndDrawing,
                    &ClearBackground,
                    &GuiButton,
                    &GuiSetStyle
                );
            }
            core_init_window_func = (void*) GetProcAddress(dll_handle, "core_init_window");
            if (NULL == core_init_window_func){
                printf("Can't call core_init_window dll function");
                exit(1);
            }else{
                // If the 'core_init_window' function is correclty
                // loaded from the dll, we call it right now to
                // initialize the window.
                core_init_window_func();
            }
            core_execute_loop_func = (void*) GetProcAddress(dll_handle, "core_execute_loop");
            if (NULL == core_execute_loop_func){
                printf("Can't call core_execute_loop dll function");
                exit(1);
            }
            core_exit_func = (void*) GetProcAddress(dll_handle, "core_exit");
            if (NULL == core_exit_func){
                printf("Can't call core_execute_loop dll function");
                exit(1);
            }
        }else{
            // A problem occured when trying to load the dll file.
            // The most common mistake is a wrong given path to
            // the dll file.
            printf("Can't load the dll file.\n");
            exit(1);
        }
    #else
        // If we are not using the dll configuration, 
        // then we just need to call the 'core_init_window' function
        // from core.c
        core_init_window();
    #endif
    // Main loop
    while(1){
        #if defined(CORE_USE_LIBTYPE_SHARED)
            // We call the function loaded from the dll
            core_execute_loop_func();
        #else
            // If we are not using the dll configuration, 
            // we direclty call the function from core.c 
            core_exec_loop();
        #endif
    }
    #if defined(CORE_USE_LIBTYPE_SHARED)
        core_exit_func();
        // Important line here, we need to free the dll
        FreeLibrary(dll_handle);
    #else
        // If we are not using the dll configuration,
        // simply call the 'core_exit' function from core.c
        core_exit();
    #endif
    return 0;
}

Хорошо, теперь все готово, мы можем скомпилировать его с помощью файла dll.

Для этого нам понадобятся следующие команды:

// Compiling the core.c file into an object file 
// (the option USE_CORE_LIB_SHARED is passed to be able to have proper functions 
// which can be called from a dll file)
gcc -c core.c -I./include -DUSE_CORE_LIB_SHARED -o core.o
// Assembling the core.o object file into a dll file
gcc -shared core.o -o core.dll

Затем мы можем скомпилировать наш файл main.c и создать исполняемый файл:

// Compiling the full project to get the executable file
gcc main.c -I./include -L./libs -lraygui -lglfw3 -lraylib -lopengl32 -lgdi32 -o main.exe

Как вы, наверное, заметили, мы удалили файл core.o из этой команды, потому что мы его больше не используем (на самом деле мы загружаем и используем во время выполнения файл dll, который был собран из этого core.o файл).

А теперь, если мы запустим приложение:

./main.exe

Мы получили тот же результат, что и раньше. Итак, наша dll правильно загружена, и все работает нормально!

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

Думаю, вы, вероятно, сможете понять, что нам нужно сделать, чтобы сделать это сейчас.
Лично мне нравится иметь возможность нажимать клавишу, чтобы приостановить программу, внести некоторые изменения, перекомпилировать файл dll, и просто нажмите Enter в терминале, чтобы продолжить программу с моими изменениями, загруженными на лету.

Для этого нам нужно создать другую функцию моста между нашим core.c (dll) и main.c файлом (основная программа), чтобы знать, когда нажатие касания и сообщает основному циклу, что нам нужно приостановить программу, пока мы не внесем наши изменения.

// inside core.c
#include "core.h"
// Only for Hot Reload
#if defined(_WIN32) && defined(BUILD_LIBTYPE_SHARED)
    /* Below are the functions pointers we need to make the bridge
    between the main.c (which contains raylib and raygui)
    and this core.c which will be converted to a dll.*/
    /* To be short, it's to be able to call raylib and raygui
    functions even if we are not linking them as static libraries
    into this dll. */
    // Raylib functions
    void (*raylib_init_window)(int width, int height, const char* title);
    void (*raylib_close_window)();
    bool (*raylib_window_should_close)();
    void (*raylib_begin_drawing)();
    void (*raylib_end_drawing)();
    void (*raylib_clear_background)(Color color);
    // Raygui functions
    bool (*raygui_gui_button)(Rectangle bounds, const char *text);
    void (*raygui_gui_set_style)(int control, int property, int value);
    // This function will be one of the first one to be load 
    // from this dll in main.c. It's role is actually to initialize
    // our raylib and raygui functions pointers, to create the bridge.
    CORE void core_load_raylib_functions(
        void (*const in_init_window)(int width, int height, const char* title),
        void (*const in_close_window)(),
        bool (*const in_window_should_close)(),
        void (*const in_begin_drawing)(),
        void (*const in_end_drawing)(),
        void (*const in_clear_background)(Color color),
        bool (*const in_gui_button)(Rectangle bounds, const char *text),
        void (*const in_gui_set_style)(int control, int property, int value)
    ){
        // Raylib functions
        raylib_init_window = in_init_window;
        raylib_close_window = in_close_window;
        raylib_window_should_close = in_window_should_close;
        raylib_begin_drawing = in_begin_drawing;
        raylib_end_drawing = in_end_drawing;
        raylib_clear_background = in_clear_background;
        // Raygui functions
        raygui_gui_button = in_gui_button;
        raygui_gui_set_style = in_gui_set_style;
    }
    // This variable determines if we need to activate hot reload
    // function or not from the main loop, inside main.c
    // That could be a bool, of course, but I'm not including
    // stdbool.h. So, 0 if false, 1 if true.
    char core_active_hot_reload = 0;
    CORE void core_get_value_hot_reload(char* out_index){
        *out_index = core_active_hot_reload;
    }
#else
    // I redefine the raylib and raygui functions
    // I use here, because whenever we are using the dll
    // or not, I want to be able to have one single code,
    // without #if/#else in the functions below.
    
    // Raylib functions
    #define raylib_init_window InitWindow
    #define raylib_close_window CloseWindow
    #define raylib_window_should_close WindowShouldClose
    #define raylib_begin_drawing BeginDrawing
    #define raylib_end_drawing EndDrawing
    #define raylib_clear_background ClearBackground
    // Raygui functions
    #define raygui_gui_button GuiButton
    #define raygui_gui_set_style GuiSetStyle
#endif
void core_create_window(
    const unsigned short in_width,
    const unsigned short in_height,
    const char* in_title
){
    raylib_init_window(in_width, in_height, in_title);
}
void core_execute_loop(){
    // Only for Hot Reload
    #if defined(_WIN32) && defined(BUILD_LIBTYPE_SHARED)
        unsigned int key_pressed = raylib_get_key_pressed();
        // In my case, if the key pressed is the 'R', I perform
        // hot reload
        if(key_pressed == 114){
            core_active_hot_reload = 1;
        }
    #endif
    raylib_begin_drawing();
        raylib_clear_background(RED);
        if(raygui_gui_button((Rectangle){ 500, 200, 250, 60 }, "TEST BUTTON")) {
            puts("Button pressed\n");
        }
    raylib_end_drawing();
}
bool core_window_should_close(){
    return raylib_window_should_close();
}
void core_close_window(){
    raylib_close_window();
}

Итак, теперь в каждом цикле основного цикла внутри main.c нам просто нужно вызвать этот core_get_value_hot_reload, и если значение возвращается 1 (= true), нам нужно написать код для освобождения библиотеки, дождаться нажатия клавиши ввода, перезагрузить dll и продолжить цикл!

Конечно, для этого нам нужно инициализировать новый указатель на функцию, чтобы загрузить это core_get_value_hot_reload в начале программы, иначе мы не сможем его вызвать. из основного цикла.

Фактически, нам не нужно перезагружать каждую функцию из core.c при перезагрузке dll. Например, нам не нужна наша функция core_init_window, потому что окно уже инициализировано. Нам просто нужны функции, которые будут использоваться во время основного цикла или в конце нашей программы (например, core_exit).

// inside main.c
#include "core.h"
// Whenever we will pass this option to the compiler, we will be able to use our core.c code as a dll
#if defined(CORE_USE_LIBTYPE_SHARED) 
    #include <libloaderapi.h>
    #include "raylib.h"
    #include "raygui.h"
#endif
int main(){
    // This code is necessary only if we are using hot reload
    #if defined(CORE_USE_LIBTYPE_SHARED)
        HINSTANCE dll_handle = NULL; // Declare a pointer to the dll resource
        // We need to initialize some function pointers, because, 
        // as mentioned before, by using a dll, we will load every functions
        // we need to use from raylib, to our dll, (think of it as a bridge), 
        // because our dll is not aware of raylib. 
        // We are not linking its static library in our dll file.
        void (*core_load_raylib_functions_func)(
            void (*const in_init_window)(int width, int height, const char* title),
            void (*const in_close_window)(),
            void (*const in_begin_drawing)(),
            void (*const in_end_drawing)(),
            void (*const in_clear_background)(Color color),
            int (*const in_get_key_pressed)(),
            bool (*const in_is_key_down)(int key),
            bool (*const in_gui_button)(Rectangle bounds, const char *text),
        );
        // Functions pointers to be able to store and call functions from 
        // our dll file (the initialization of the window, the exec loop function...)
        void (*core_init_window_func)();
        void (*core_execute_loop_func)();
        void (*core_get_hot_reload_func)(char* out_index);
        void (*core_exit_func)();
 
        // To be able to know if we need to perform hot reload
        // (0 = false, 1 = true)
        char activate_hot_reload = 0;
        // Load our core.dll file as a dynamic library.
        // Regarding to the official windows documentation, the second parameter of 'LoadLibraryExA'
        // is always NULL (just here for future need), and the last one corresponds to a flag
        // which determine the action to be taken when loading the module. In our case, we use the 
        // default action (so the flag is 0)
        dll_handle = LoadLibraryExA("./core.dll", NULL, 0);
        if(dll_handle != NULL){
            // Load the 'core_load_raylib_functions' function from the dll into the
            // 'core_load_raylib_functions_func' function pointer
            core_load_raylib_functions_func = (void*) GetProcAddress(dll_handle, "core_load_raylib_functions");
            if (NULL == core_load_raylib_functions_func){
                printf("Can't call core_load_raylib_functions dll function");
                exit(1);
            }else{
                // If the function is correclty loaded, then we call it, 
                // and we pass the functions from raylib and raygui
                // we need.
                // It's the bridge between our main.c and our dll, to 
                // retrieve the raylib and raygui functions.
                core_load_raylib_functions_func(
                    &InitWindow,
                    &CloseWindow,
                    &BeginDrawing,
                    &EndDrawing,
                    &ClearBackground,
                    &GuiButton,
                    &GuiSetStyle
                );
            }
            core_init_window_func = (void*) GetProcAddress(dll_handle, "core_init_window");
            if (NULL == core_init_window_func){
                printf("Can't call core_init_window dll function");
                exit(1);
            }else{
                // If the 'core_init_window' function is correclty
                // loaded from the dll, we call it right now to
                // initialize the window.
                core_init_window_func();
            }
            core_execute_loop_func = (void*) GetProcAddress(dll_handle, "core_execute_loop");
            if (NULL == core_execute_loop_func){
                printf("Can't call core_execute_loop dll function");
                exit(1);
            }
            core_get_hot_reload_func = (void*) GetProcAddress(dll_handle, "core_get_hot_reload");
            if(NULL == core_get_hot_reload_func){
                printf("Can't call core_get_hot_reload dll function");
                exit(1);
            }
            core_exit_func = (void*) GetProcAddress(dll_handle, "core_exit");
            if (NULL == core_exit_func){
                printf("Can't call core_execute_loop dll function");
                exit(1);
            }
        }else{
            // A problem occured when trying to load the dll file.
            // The most common mistake is a wrong given path to
            // the dll file.
            printf("Can't load the dll file.\n");
            exit(1);
        }
    #else
        // If we are not using the dll configuration, 
        // then we just need to call the 'core_init_window' function
        // from core.c
        core_init_window();
    #endif
    // Main loop
    while(1){
        #if defined(CORE_USE_LIBTYPE_SHARED)
            // We call the function loaded from the dll
            core_execute_loop_func();
            core_get_hot_reload_func(&activate_hot_reload);
            if(activate_hot_reload == 1){
                // First, we free the library
                FreeLibrary(dll_handle);
                // Then, we demand to the user to press a touch to continue 
                // (enter key, on the terminal which runs the main program)
                char c;
                puts("[INFO] Press enter after rebuilding the functions dll file\n");
                // While the user doesn't pres any key, the scanf "block" the program
                scanf("%c", &c);
                
                // If the user press enter, we need to reload the dll file, and the functions we need
                dll_handle = LoadLibraryExA("core.dll", NULL, 0);
                if (NULL != dll_handle){
                    core_load_raylib_functions_func = (void*) GetProcAddress(dll_handle, "core_load_raylib_functions");
                    if (NULL == core_load_raylib_functions_func){
                        printf("Can't call core_load_raylib_functions dll function");
                        exit(1);
                    }else{
                        core_load_raylib_functions_func(
                            &InitWindow,
                            &CloseWindow,
                            &BeginDrawing,
                            &EndDrawing,
                            &ClearBackground,
                            &GuiButton,
                            &GuiSetStyle
                        );
                    }
                    core_execute_loop_func = (void*) GetProcAddress(dll_handle, "core_execute_loop");
                    if (NULL == core_execute_loop_func){
                        printf("Can't call core_execute_loop dll function");
                        exit(1);
                    }
                    core_get_hot_reload_func = (void*) GetProcAddress(dll_handle, "core_get_hot_reload");
                    if(NULL == core_get_hot_reload_func){
                        printf("Can't call core_get_hot_reload dll function");
                        exit(1);
                    }
                    core_exit_func = (void*) GetProcAddress(dll_handle, "core_exit");
                    if (NULL == core_exit_func){
                        printf("Can't call core_execute_loop dll function");
                        exit(1);
                    }
                }else{
                    printf("Can't load the dll file.\n");
                    exit(1);
                }
            }
        #else
            // If we are not using the dll configuration, 
            // we direclty call the function from core.c 
            core_exec_loop();
        #endif
    }
    #if defined(CORE_USE_LIBTYPE_SHARED)
        core_exit_func();
        // Important line here, we need to free the dll
        FreeLibrary(dll_handle);
    #else
        // If we are not using the dll configuration,
        // simply call the 'core_exit' function from core.c
        core_exit();
    #endif
    return 0;
}

И вуаля!
Теперь мы можем перезагружать наш код из core.c на лету. Нам просто нужно нажать R внутри окна программы, внести наши изменения, перекомпилировать нашу dll с помощью команд, перечисленных ниже, затем нажать Enter в терминале, который запустил основную программу, и увидеть наши изменения в реальном времени. !

Прежде чем закончить эту длинную статью, я просто хотел написать это небольшое предупреждение: если вы хотите получить точно такое же состояние вашей программы после горячей перезагрузки, вам необходимо создать другую функцию (-ы) моста (-ов) между основными программу (main.c) и вашу dll (core.c в нашем случае), чтобы получить каждую отдельную переменную, экземпляры которой вы создали в стеке, в части dll.

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

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

Если хотите, в другой статье я мог бы показать вам, как стать еще более продуктивным, «автоматизируя» горячую перезагрузку с помощью вашего редактора кода (в моем случае: код Visual Studio). Не стесняйтесь сообщить мне в комментариях к этой статье, если это может вас заинтересовать!

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

Заключение

Надеюсь, эта статья была вам полезна, и вы сможете добавить горячую перезагрузку в свою программу / игру. Это действительно экономия времени при написании приложения!

Если у вас возникнут какие-либо проблемы, не стесняйтесь обращаться за помощью в комментариях ниже. Я буду более чем счастлив помочь вам!

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

Получайте удовольствие от игр, инструментов и всего, что вы задумали!