Приветствую, Хабравчане!

Краткое содержание прошлой серии.

В далекой, далекой галактики...

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

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

Спасибо за внимание!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вам всё это непотребство?
36.36%Это самое безумное, что я видел4
27.27%Вот если бы под dos, а так каждый студент может3
27.27%Как доедешь, напиши.3
0%Книга лучше0
9.09%Не согласен с автором, в оценке раннего творчества Маяковского1
Проголосовали 11 пользователей. Воздержавшихся нет.