Приветствую, Хабравчане!
Краткое содержание прошлой серии.
В далекой, далекой галактики...
В прошлой статье, ссылаясь на успехи и общую идею Михала. Получилось используя компилятор MSVC 2026 и пару команд,заставить компилятор генерировать 32 битный код без этих ваших н��вомодных SSE и AVX, код чист как слеза младенца:) После чего эти порожденные obj файлы удалось скормить старичку линкеру от Visual C++ 6.0 В итоге получилось использовать встроенные фичи С++ 23 использовать для написания кода под Windows 95. Круто же?
Теперь я предлагаю, вжарить:)
Наш план, которого мы будем придерживаться.
Общая цель, что бы код С++ 23 который пишется под Windows 95, собирался бесшовно на новых компиляторах. Не требовалось писать именно конкретный ретро код. Пишем код в MSVC 2026, с подсветкой синтаксиса, рюшечками и бубенчиками. Используем отладку и т.д И в итоге просто собираем одной кнопкой через cmake. Цель всего этого непотребства не игнорировать современную разработку, а наоборот использовать ее и быть впереди, используем std, новые фичи, шаблоны, модули. Как я шутил в чатике по С++. Я единственный С# разработчик использующий модули С++ 20:)

Как говорится, поехали.
Первым делом, что бы наш код не отличался от обычного кода, нужно будет написать минимальную часть стандартной библиотеки.Не имеет смысл добавлять все API и весь функционал. Я не настолько упорот и понимаю, что жизнь всего одна. Но добавим основные контейнеры и удобные функции вроде: vector, string, move, unique_ptr, expected и другие фичи по маленьку.
Так же данный проект позволит мне продолжить написание цикла статей по разработке движка Arcanum на С++ 23 и оставить совместимость с Windows 95,98, Me.
Возможно многие зададут вопрос. Ну извините за откровенность, а нахрена все это, мягко выражаясь.
Отвечу.
Интерес, фан, а так же мне хочется показать, что С++ 23 это не огромный монстр, а лишь удачно спроектированный язык с учетом обратной совместимости. Что его стандартная библиотека, это не что то огромное, в java, python и других языках она намного больше в разы. Да и вообще показать, что не нужно С++ бояться, внешне это грубый инструмент, но познакомившись с ним, внутри он прост и прямолинеен. И позволяет многое.
И лично мне хочется написать движок для Arcanum используя С++ 23 обеспечить работу на древнем железе. Все говорят и пишут, о терафлопсах, гигабайтах, 2D графика не очень то поменялась с начала 90-ых. И если она работала тогда и не тормозила, значит используя современные инструменты, это можно сделать.
Предлагаю погрузиться в чарующий мир, шаблонов, разработки совместимых костыле и неописуемого восторга! Не все же json в json перекладывать, да докер в докер, в докер:) Со всем уважением к этой деятельности.
Для начала я предлагаю, написать минимально необходимую часть STL. Важная особенность, так как ни все компиляторы поддерживают import std; то я реализую std в старом варианте c заголовками. Но весь остальной код будет на модулях. Это даст совместимость с компиляторами на разных версиях в Linux из коробки и не потребуется его обновлять. А так же в будущем я планирую доведя стандартную библиотеку до более или менее охватывающего функционала, портировать разный софт бесшовно на Windows 95, для забавы ради.
Начнем.
Для начала создадим системные функции, для работы с памятью.
static HANDLE GetHeap() noexcept
{
static HANDLE h = GetProcessHeap();
return h;
}
static HANDLE GetOutput() noexcept
{
static HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
return h;
}
void* malloc(size_t size) noexcept
{
return HeapAlloc(GetHeap(), 0, size);
}
void free(void* ptr) noexcept
{
if (ptr)
{
HeapFree(GetHeap(), 0, ptr);
}
}Для работы с памятью полностью полагаюсь на ее механизмы, это не очень эффективно но работает. В будущем добавил в бэк лог, создать все же аллокатор чуть умнее. Как вариант можно взять и адаптировать из библиотеки musl.
extern "C" int main();
extern "C" void EntryPoint()
{
int result = main();
ExitProcess(result);
}EntryPoint это наша стартовая точка входа в программу. При запуске вызываем функцию и уже грузим привычный main. Для первой версии я не стал пока добавлять обработку аргументов.
Теперь можно создать stdlib.h
#pragma once
#include <cstddef>
void* malloc(size_t size) noexcept;
void free(void* ptr) noexcept;Уже начинают появляться всякие С++ модерн фичи, но как модерн уже лет 15 в стандарте:)
#include <stdlib.h>
#include <memory>
[[nodiscard]]
void* operator new(size_t bytes)
{
return malloc(bytes);
}
void operator delete(void* ptr)
{
return free(ptr);
}
[[nodiscard]]
void* operator new[](size_t bytes)
{
return ::operator new(bytes);
}
void operator delete[](void* ptr)
{
::operator delete(ptr);
}
[[nodiscard]]
void* operator new(size_t bytes, void* ptr)
{
return ptr;
}
[[nodiscard]]
void* operator new[](size_t bytes, void* ptr)
{
return ptr;
}
void __cdecl operator delete(void* ptr, size_t size) noexcept
{
::operator delete(ptr);
}
void __cdecl operator delete[](void* ptr, size_t size) noexcept
{
::operator delete[](ptr);
}Теперь можно использовать new и placement new.
Теперь до реализации контейнеров нужно реализовать std::move, если вы думаете что это какая то магическая штука, это нет так. Просто парочка шаблонов.
#pragma once
namespace std
{
template <typename T>
struct remove_reference
{
using type = T;
};
template <typename T>
struct remove_reference<T&>
{
using type = T;
};
template <typename T>
struct remove_reference<T&&>
{
using type = T;
};
template <typename T>
using remove_reference_t = typename remove_reference<T>::type;
template <typename T>
[[nodiscard]] constexpr remove_reference_t<T>&& move(T&& t) noexcept
{
return static_cast<remove_reference_t<T>&&>(t);
}
template <typename T>
[[nodiscard]] constexpr T&& forward(remove_reference_t<T>& t) noexcept
{
return static_cast<T&&>(t);
}
template <typename T>
[[nodiscard]] constexpr T&& forward(remove_reference_t<T>&& t) noexcept
{
return static_cast<T&&>(t);
}
}Сразу реализуем move и forward, по сути это просто удобные шаблоны завязанные на возможности стандарта. Мы же все понимаем, что он совсем не перемещает?:)
Что бы было совсем красиво, создадим std::allocator, в С++ 23 многое уже удалили не требуется писать rebind, bind удалил весь мусор. А ещё говорят, С++ становится сложнее.
#pragma once
#include <cstddef>
#include <memory>
namespace std
{
template <typename T>
class allocator
{
public:
using value_type = T;
using size_type = size_t;
constexpr allocator() noexcept = default;
template <typename U>
constexpr allocator(const allocator<U>&) noexcept
{
}
[[nodiscard]]
constexpr T* allocate(size_t n)
{
if (n == 0)
{
return nullptr;
}
return static_cast<T*>(::operator new(n * sizeof(T)));
}
constexpr void deallocate(T* p, size_t n) noexcept
{
if (p)
{
::operator delete(p);
}
}
};
template <typename T, typename U>
constexpr bool operator==(const allocator<T>&, const allocator<U>&) noexcept
{
return true;
}
template <typename T, typename U>
constexpr bool operator!=(const allocator<T>&, const allocator<U>&) noexcept
{
return false;
}
}Красота же, можно даже не реализовывать != компилятор сам на основе == его создаст. Для лучшей оптимизации добавляем constexpr и noexcept. Так как исключения отключены. Можем смело везде накидывать noexcept.
Важное уточнение, я не стремлюсь к супер эффективности здесь и сейчас. Моя реализация довольна дубовая и скорее всего с ошибками. К прмиеру в std::string нет оптимизации с короткими строками. Это все сделано, что бы уложиться по времени в несколько вечеров. Я обязательно буду править баги и оптимизировать контейнеры и алгоритмы, но все сделать сразу не получается. У меня есть работа, семья, потому максимальный лайт код режим.
В С++ 23 появился удобный класс для обработки ошибок, мы просто оборачиваем в него любой класс и можем проверить.
template<typename T, typename E>
class expected
{
private:
union
{
T _value;
E _error;
};
bool _hasValue;
public:
expected(const T& val) :
_value(val),
_hasValue(true)
{
}
expected(T&& val) :
_value(static_cast<T&&>(val)),
_hasValue(true)
{
}Это часть реализации, ничего сложного это простой union который может в себе содержать или то что мы в него передали, или ошибку.
Перейдем к умным указателям, решил начать с unique_ptr.
template<typename T, typename Deleter = default_delete<T>>
class unique_ptr
{
private:
T* _ptr = nullptr;
Deleter _deleter;
public:
constexpr unique_ptr() noexcept :
_ptr(nullptr)
{
}
constexpr unique_ptr(nullptr_t) noexcept :
_ptr(nullptr)
{
}По сути это просто обертка, которая умеет вызывать деструктор или кастомный деструктор.
Для создания указателя используем вариативные шаблоны.
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args)
{
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}Здесь нам и пригодился std::forward.
Как видите никакой магии нет. Да код не простой но логичный. Используем шаблоны в шаблонах:) И надеемся, что оно не только скомпилируется, но и будет работать.
Я не стал приводить реализацию string и vector. Особо интересного там нет. Они были написаны наспех. К примеру я пока не добавил emplace_back. Но в среднем по больнице, общий функционал реализован.
Так же создал простой ostream для вывода строк. Что бы писать в консоль
#include <ostream.hpp>
#include <system.hpp>
using namespace std;
ostream& ostream::operator<< (const string & str)
{
write(str.c_str(), str.size());
return *this;
}
ostream& ostream::operator<<(const char* str)
{
write(str, strlen(str));
return *this;
}
ostream& ostream::operator<<(char c)
{
write(&c, 1);
return *this;
}
ostream& ostream::operator<<(ostream& (*pf)(ostream&))
{
return pf(*this);
}
ostream& std::endl(ostream& os)
{
os << '\n';
return os;
}Функции write без буфера сразу пишет в консоль, просто и сердито. Обязательно нужно будет пофиксить. Так как сейчас это выглядит очень не оптимально, хотя вряд ли я буд выводить мегабайты текста.
В итоге возможно собрать такой код не только для современных систем но и для Windows 95.
#include <vector>
#include <string>
#include <iostream>
int main()
{
std::vector<std::string> vec;
vec.push_back("1");
vec.push_back("2");
for (auto i : vec)
{
std::cout << i << std::endl;
}
std::string message = "Crazy programming!";
std::cout << message << std::endl;
return 0;
}Всё работает.
И таким способом буду продолжать. Для движка Arcanum, требуется ещё добавить контейнеры unordered_map, set. shared_ptr, std::chrono, std::filesystem. Что бы код оставался высокоуровневым.
Далее для движка нужна работа с графикой, звуком и событиями ос. Естественно берем SDL 1.2 он стабилен и поддерживает Windows 95 из коробки, для шрифтом будем использовать SDL_ttf.
Но тянуть SDL 1.2 в новые версии ос это проблема. потому будем решать самым простым способом. Абстракцией. На основе SDL 1.2 создадим универсальный слой для работы с рендером, выводом графики и проигрыванием извука. То же самое сделаем и для SDL3. Так как API у них одинаковое можем, на уровне сборки подменять модули. В итоге мы пишем на современном С++ 23 используем новые фичи, универсальное API и код движка один для все систем работает на последней i9, под windows, linux так и под Windows 95.
Начинаем использовать модули.
Так как линковаться мы не можем с lib. Будем грузить динамически функции из dll.
module;
export module SDL.Loader;
import LDL.WinAPI;
import SDL.Init;
import SDL.Error;
import SDL.Events;
import SDL.Surface;
export
{
class SDL_Loader
{
public:
static void Init()
{
_load = LoadLibraryA("SDL.dll");
if (_load)
{
Bind(SDL_Init, "SDL_Init");
Bind(SDL_Quit, "SDL_Quit");
Bind(SDL_SetVideoMode, "SDL_SetVideoMode");
Bind(SDL_PollEvent, "SDL_PollEvent");
Bind(SDL_GetError, "SDL_GetError");
Bind(SDL_WM_SetCaption, "SDL_WM_SetCaption");
Bind(SDL_Flip, "SDL_Flip");
}
}
~SDL_Loader()
{
if (_load)
{
FreeLibrary(_load);
}
}
private:
template<typename T>
static void Bind(T& funcPtr, const char* name)
{
funcPtr = reinterpret_cast<T>(GetProcAddress(_load, name));
}
inline static HMODULE _load = nullptr;
};
}Пока функций мало, но для маленького прототипа достаточно. Ни одного макроса не использовано в модулях. Описания всех структур, функций и констант, завернуто в модули.
module;
export module SDL;
export import SDL.Color;
export import SDL.Events;
export import SDL.Init;
export import SDL.Key;
export import SDL.Mod;
export import SDL.Palette;
export import SDL.PixelFormat;
export import SDL.Rect;
export import SDL.Surface;
export import SDL.Types;
export import SDL.Loader;
export import SDL.Error;Это главный модуль экспортирующий все. Я специально не добавлял namespace, что бы не ломать совместимость с оригинальным кодом примеров.

Напишем базовый пример, создадим окно и будем по esc закрывать.
import SDL;
int main()
{
SDL_Loader::Init();
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
return -1;
}
auto screen = SDL_SetVideoMode(800, 600, 24, SDL_SWSURFACE);
if (!screen)
{
return -1;
}
bool running = true;
while (running)
{
SDL_Event event = {};
if (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
{
running = false;
}
}
SDL_Flip(screen);
}
SDL_Quit();
return 0;
}Предлагаю начать писать универсальный слой для движка. Использовать свою реализованную стандартную библиотеку. Подмена идет на уровне сборки.
module;
#include <expected>
#include <memory>
#include <string>
export module Graphics;
import SDL;
import Events;
export namespace Graphics
{
class Canvas
{
public:
Canvas(SDL_Surface* screen) :
_running(true),
_screen(screen)
{
}
~Canvas()
{
SDL_Quit();
}
bool GetEvent(Events::Event& dest)
{
if (_running)
{
SDL_Event event = {};
if (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
{
dest.Type = Events::Event::IsQuit;
}
}
}
return _running;
}
void StopEvent()
{
_running = false;
}
void Update()
{
SDL_Flip(_screen);
}
private:
bool _running;
SDL_Surface* _screen;
};
std::expected<std::unique_ptr<Canvas>, const char*> CanvasNew(int width, int height, const std::string& title)
{
SDL_Loader::Init();
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
return std::unexpected(SDL_GetError());
}
auto screen = SDL_SetVideoMode(width, height, 24, SDL_HWSURFACE);
if (!screen)
{
SDL_Quit();
return std::unexpected(SDL_GetError());
}
SDL_WM_SetCaption(title.c_str(), nullptr);
return std::make_unique<Canvas>(screen);
}
}Стильно, модно, молодежно!
import Framework;
using namespace Events;
using namespace Graphics;
int main()
{
auto canvasResource = CanvasNew(800, 600, "Canvas SDL");
if (!canvasResource)
{
return -1;
}
auto& canvas = *canvasResource;
Events::Event event;
while (canvas->GetEvent(event))
{
if (event.Type == Events::Event::IsQuit)
{
canvas->StopEvent();
}
canvas->Update();
}
return 0;
}Умные указатели сами вызовут деструкторы. Не нужно заботиться о закрытии окна или обработке ошибки, мы лишь ее проверяем. Очень удобный механизм.
Пример запуска на Windows 95.

Эмулируемое железо

Пока Microsoft заявляет, что ПК 3-5 летней давности устарели и не подходят для Windows 11. Мы развеиваем это наглый маркетинговый ход.
И вот небольшой пример:
#include <string>
#include <iostream>
int main()
{
std::string s1 = "Hello ";
s1 += "World!";
std::cout << s1 << std::endl;
return 0;
}
Репозиторий исходников: https://github.com/JordanCpp/ModularLDL
Если вам хочется всё же увидеть как удалось скрестить MSVC + линкер от Visual C++ 6.0
Строго 18+:)
Скрытый текст
cmake_minimum_required(VERSION 3.31)
project(SDL LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT Embedded)
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/INCREMENTAL:NO")
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/INCREMENTAL:NO")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/INCREMENTAL:NO")
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT Embedded)
set(CMAKE_MSVC_RUNTIME_LIBRARY_DEFAULT "")
if(MSVC)
foreach(flag_var
CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
string(REPLACE "/RTC1" "" ${flag_var} "${${flag_var}}")
string(REPLACE "/Od" "" ${flag_var} "${${flag_var}}")
string(REPLACE "/MDd" "" ${flag_var} "${${flag_var}}")
string(REPLACE "/MD" "" ${flag_var} "${${flag_var}}")
string(REPLACE "/MTd" "" ${flag_var} "${${flag_var}}")
string(REPLACE "/MT" "" ${flag_var} "${${flag_var}}")
endforeach()
endif()
set(CMAKE_GENERATOR_PLATFORM Win32)
set(CUSTOM_LINKER "${CMAKE_CURRENT_SOURCE_DIR}/make/link.exe")
set(BUILD_EXE
SDL1
GraphicsExample
ExampleVector
ExampleString
main)
set(CMAKE_CXX_LINK_EXECUTABLE "\"${CUSTOM_LINKER}\" <CMAKE_CXX_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> /OUT:<TARGET> <LINK_LIBRARIES>")
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/SDL/SDL.dll DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/)
foreach(BUILD_EXE ${BUILD_EXE})
add_executable(${BUILD_EXE} "${BUILD_EXE}.cpp")
include_directories("LDL_stdcpp/stdcpp")
include_directories("LDL_stdcpp/source")
target_sources(${BUILD_EXE} PUBLIC FILE_SET CXX_MODULES FILES
"LDL_WinAPI/LDL.WinAPI.ixx"
"LDL_WinAPI/LDL.WinAPI.Types.ixx"
"LDL_WinAPI/LDL.WinAPI.Kernel32.ixx"
"LDL_WinAPI/LDL.WinAPI.User32.ixx")
target_sources(${BUILD_EXE} PUBLIC FILE_SET CXX_MODULES FILES
"SDL/SDL.ixx"
"SDL/SDL.Types.ixx"
"SDL/SDL.Init.ixx"
"SDL/SDL.Rect.ixx"
"SDL/SDL.Color.ixx"
"SDL/SDL.Palette.ixx"
"SDL/SDL.PixelFormat.ixx"
"SDL/SDL.Surface.ixx"
"SDL/SDL.Events.ixx"
"SDL/SDL.Key.ixx"
"SDL/SDL.Mod.ixx"
"SDL/SDL.Loader.ixx"
"SDL/SDL.Error.ixx")
target_sources(${BUILD_EXE} PUBLIC
"LDL_stdcpp/source/windows/system.cpp"
"LDL_stdcpp/source/memory.cpp"
"LDL_stdcpp/source/string.cpp"
"LDL_stdcpp/source/ostream.cpp")
target_sources(${BUILD_EXE} PUBLIC FILE_SET CXX_MODULES FILES
"Framework/Framework.ixx"
"Framework/SDL1.Graphics.ixx"
"Framework/SDL1.Events.ixx")
target_compile_options(${BUILD_EXE} PRIVATE /O2 /arch:IA32 /GS- /Zc:threadSafeInit- /GR- /EHa- /kernel /d2FH4-)
set_target_properties(${BUILD_EXE} PROPERTIES
MSVC_INCREMENTAL_LINKING OFF
LINKER_LANGUAGE CXX)
target_link_options(${BUILD_EXE} PRIVATE
"/SUBSYSTEM:CONSOLE,4.0"
"/NODEFAULTLIB"
"/ENTRY:EntryPoint")
target_link_libraries(${BUILD_EXE} PRIVATE kernel32.lib user32.lib)
endforeach()Рад, буду вашим комментариям, советам и предложениям!
@antoshkka, когда вы уже переведете userver на модули? Если под Windows 95 взлетело, то точно пора! (шутка, с огромным уважением к проекту)
Начало положено, конечно в итоге это просто костыль, но рабочий. Может очень подойти для написания всяких ретро тулз или движков, не ограничивая себя в старых компиляторах и стандартах. Это уже не мало.
Спасибо за внимание!
