Приветствую, Хабравчане!
Краткое содержание прошлой серии.
В далекой, далекой галактики...
В прошлой статье, ссылаясь на успехи и общую идею Михала. Получилось используя компилятор 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 взлетело, то точно пора! (шутка, с огромным уважением к проекту)
Начало положено, конечно в итоге это просто костыль, но рабочий. Может очень подойти для написания всяких ретро тулз или движков, не ограничивая себя в старых компиляторах и стандартах. Это уже не мало.
Спасибо за внимание!
Комментарии (5)

TimurZhoraev
20.01.2026 11:20Самое главное в C23 для эмбеддинга это типизированные enum-ы и битовые поля на их основе. Теперь наверняка можно нормально в отладчике просматривать текст а не цифры регистров.
Стильно, модно, молодежно!
Боюсь это уже несколько старомодно.
Слишком долго изучать то где есть синтаксис, когда проще использовать С-ассемблер
make simple application on C winapi for SDL 1.2 please
Add error and exception catch using C
#include <SDL.h> #include <windows.h> // Required for SEH and MessageBox #include <stdio.h> // Helper to log SDL errors and exit void CheckSDLError(const char* msg) { const char* error = SDL_GetError(); if (error && *error != '\0') { char fullMsg[256]; snprintf(fullMsg, sizeof(fullMsg), "%s: %s", msg, error); MessageBox(NULL, fullMsg, "SDL Error", MB_OK | MB_ICONERROR); SDL_Quit(); exit(1); } } int main(int argc, char* argv[]) { SDL_Surface* screen = NULL; int running = 1; // Layer 1: Standard SDL Error Handling if (SDL_Init(SDL_INIT_VIDEO) < 0) { CheckSDLError("SDL_Init Failed"); } screen = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE | SDL_DOUBLEBUF); if (!screen) { CheckSDLError("Could not set video mode"); } SDL_WM_SetCaption("Robust SDL 1.2 App (2026)", NULL); // Layer 2: Win32 Structured Exception Handling (SEH) // This catches crashes (Access Violations, etc.) during the main loop __try { SDL_Event event; while (running) { while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) running = 0; } // Simulate a potential crash scenario for testing // int* crash = NULL; *crash = 10; SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 50, 50, 50)); if (SDL_Flip(screen) == -1) { CheckSDLError("SDL_Flip Failed"); } SDL_Delay(16); } } __except(EXCEPTION_EXECUTE_HANDLER) { // This block runs if the program crashes inside the __try block MessageBox(NULL, "A critical system exception occurred (the app crashed).", "Fatal Exception", MB_OK | MB_ICONSTOP); } SDL_Quit(); return 0; }
JordanCpp Автор
20.01.2026 11:20Привет ембедерам! Но у вас же нет здесь ассемблера. Только си.
Я понимаю, почему так выглядит си код. Нет деструктора, есть похожее на исключения. Причем данный механизм именно windows, хотя возможно он скрыт за макросами, для переносимости.
kovserg
Это конечно здорово. Но проблемма не в компиляторе, а во внешних библиотеках. Они бодро вызывают методы и интерфейсы которых нет например в win95. Я хочу сказать что вам нужна база данных где есть все методы, структуры и константы с указанием когда они появились и как их эмулировать если их нет на целевой платформе. Потом можно спокойно генерировать модули, заголовочнве файлы, загрузчики и заглушки для методов. Например эмулировать несколько дисплеев если на платформе их еще не было или были добавлены специально из вредности. И потом можно будет использовать сторонние библиотеки, собрав их с этими переходниками.
JordanCpp Автор
Всякого можно придумать. К примеру динамически загружать функции из msvcrt.dll какой она там версии. Посоветовали, взять реализации STL из STL Port и MS STL. Точечно, буду копировать реализацию, очищая от макросов.
Главная причина, это желание писать движок Arcanum на С++ 23 и сохранить совместимость с Windows 95, 98, Me. Добавлю, то что нужно движку. На остальное пока нет, времени и сил.