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

Сидел я значит за свои столом, читал Хабр, листал хаб старое железо и ничего не предвещало беды. В последнее время появилось много обзоров на старые ноутбуки, что даже захотелось найти и прикупить. Смотрю, где то дос стоит, windows 95.

Умилялся скриншотами, вчитывался в характеристики девайсов, pentium 1 c 100 mhz и 16 мб озу. Смешно конечно по нашим меркам. Естественно в голову приходили мысли, железо конечно абсурдно по производительности, но если применить современные фичи С++ то вполне можно и под них писать да хоть игры. Правда нет таких современных компиляторов поддерживающих такую дремучую древность.

Ещё раз окинул взглядом репозиторий по SimpleOS где получилось перевести хост версию на модули С++ 20. И так они мне понравились, что когда я загрузил сою библиотеку LDL, даже пропало желание программировать, так как проект написан на С++ 98 с серьезными ограничениями, нет namespace, свои минимальные шаблоны и т.д Вздохнул, и сказал про себя было бы здорово если бы получилось переписать к примеру тот же LDL на С++ 23 и сохранить совместимость со старым железом.

И тут сработал триггер в голове, подождите, что то подобное я где то видел. Начал искать гуглить и нашел.

Если в кратце

Михал понял, что объектные файлы, созданные AOT-компилятором CoreRT в 2020 году, можно собрать компоновщиком из Visual C++ 2.0 образца 1994 года. В результате получается машинный код, скомпонованный с интерфейсами Win32s, работающими в 16-разрядной Windows 3.11. Магия. Респект Михалу.

А, что так можно было?

И тут Бендер всё понял :-)

У меня был план и я его придерживался.

Дано, большое желание программировать под ретро железо используя современные фичи С++ 23. Windows 11 и Visual Studio Community 2026 (на днях обновился)

Что бы план сработал нужно.

  1. Собирать код компилятором msvc 2026 c указанием что бы компилировал только в 32 битный x86, ещё отключить исключения и стандартную библиотеку, этим компилятор будет порождать только чистые и совместимые obj файлы.

    Спасибо Microsoft что за больше чем 25 лет не стали кардинально менять формат obj. Ну хоть, что то они не сломали:)

  2. Берем линкер от Visual C++ 6.0 и скармливаем эти obj файлы.

  3. Получаем exe совместимый с Windows 95.

Теперь практика.

Создаем main.cpp

import std;
import Win32;

int main()
{

    std::string title   = "Hello Habr!";
    std::string message = "Crazy programming!";

    MessageBoxA(GetActiveWindow(), title.c_str(), message.c_str(), 0x00000000L);

    ExitProcess(0);

	return 0;
}

Просто выводим самым простым способом, текст.

Создаем модуль Win32.ixx

export module Win32;

extern "C" 
{
    __declspec(dllimport) void* __stdcall GetProcessHeap();
    __declspec(dllimport) void* __stdcall HeapAlloc(void* hHeap, unsigned int dwFlags, unsigned int dwBytes);
    __declspec(dllimport) int   __stdcall HeapFree(void* hHeap, unsigned int dwFlags, void* lpMem);
    __declspec(dllimport) void* __stdcall GetActiveWindow();
    __declspec(dllimport) int   __stdcall MessageBoxA(void* hWnd, const char* lpText, const char* lpCaption, unsigned int uType);
    __declspec(dllimport) void  __stdcall ExitProcess(unsigned int);
}

Экспортируем нужные нам функции.

Так как нет STL и libc то нужно для совместимости кода написать свой минимальный вариант.

    void* malloc(size_t size)
    {
        return HeapAlloc(GetProcessHeap(), 0, size);
    }

    void free(void* ptr)
    {
        if (ptr)
        {
            HeapFree(GetProcessHeap(), 0, ptr);
        }
    }

Базовые вещи, поддержку динамической памяти.

export
{
    void* operator new(size_t bytes)
    {
        return std::malloc(bytes);
    }

	void* operator new[](size_t bytes)
	{
		return std::malloc(bytes);
	}

	void* operator new(size_t bytes, void* ptr)
	{
		return ptr;
	}

	void* operator new[](size_t bytes, void* ptr)
	{
		return ptr;
	}
}

Перегружаем аллокаторы, они нам нужны что бы написать минимальные std::string

	typedef base_string<char> string;

Я не стал приводить внутренности строки, они не особо интересны, но в итоге все это экспортируется в пространство имен std;

Код main не меняется, это очень важно.

В чем суть?

Такой способ позволяет писать на С++ 23 под ретро железо с Windows 9x. Пишем в студии код, отлаживаем, тестируем, используем фичи, лямбды, constexpr, модули.

Но при сборке именно под старые ОС используем std реализацию, сейчас в ней не очень много кода, только базовые вещи, что бы запустилось и работало. Общий код остается единым не требуется его переписывать, нужно лишь ограничить проект фичами. Не использовать потоки, файловую систему, мьютексы и исключения. STL можно написать свою, добавить основные контейнеры vector, unordered_map, array.

Из коробки доступно string_view, span и т.д

Для поддержки сборки создал каталог make в нем батники для сборки и сам линкер от vc 6.0

Файл компиляции compiler.bat

call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars32.bat"

cl /O2 /std:c++20 /arch:IA32 /GS- /Zc:threadSafeInit- /GR- /EHa- /c ../Win32.ixx
cl /O2 /std:c++20 /arch:IA32 /GS- /Zc:threadSafeInit- /GR- /EHa- /c ../std.ixx
cl /O2 /std:c++20 /arch:IA32 /GS- /Zc:threadSafeInit- /GR- /EHa- /c ../main.cpp

pause

Файл линковки linker.bat

link /SUBSYSTEM:CONSOLE,4.0 /NODEFAULTLIB /ENTRY:EntryPoint /OUT:main.exe  ^
main.obj                                                                   ^
std.obj                                                                    ^
kernel32.lib                                                               ^
user32.lib

pause  

Не самый удобный способ, но позже добавлю вариант сборки в cmake, пока cmake собирает только под современные системы.

cmake_minimum_required(VERSION 3.28)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_CXX_SCAN_FOR_MODULES ON)

set(SOURCES 
    Win32.ixx)

add_executable(main "main.cpp")
target_sources(main PUBLIC FILE_SET CXX_MODULES FILES ${SOURCES})

Теперь все ради чего были все наши мытарства!

Оно живое и даже работает:)

Слышу первый же вопрос, зачем?

Отвечаю.

Мне по фану. Я хочу портировать свои проекты на С++ 23 и оставить совместимость со старыми системами. Только они могут показать, красоту кода через производительность. На моём основном ПК i5 11400 и 48 гб озу (купил до полного пи.. подорожания) На нём ничего не тормозит, ни мой проект LDL, не проект по воссозданию движка Arcanum по которому я пишу цикл статей. А именно на старом железе, где мало памяти у процессора смешные 50-150 mhz и вдруг на них работает графика, проигрывается звук, какой никакой геймплей, 2D графика, бывает даже 3D. И это работает приемлемо, твой код продуман, уже не получится не приходя в сознание писать код чик чик и в прод. Мне это нравится, а если ещё и доступен современный С++ 23 это прекрасно и расширяет вообще сферу ретро программирования, не нужно искать старые компиляторы, пиши на современных собирай и проверяй на Windows 9x.

Это лишь пока проверка идей, ещё предстоит разделить модуль std на контейнеры и реализации определенных фич типа expected. Заменить winapi на реализацию std cout в небольшом объеме. Главное, что бы код который пишут на современном ПК, мог без изменений собираться таким способом с подменой всего std, но с доступам к основным фичам С++ 23. Я думаю это клёво.

Теперь я могу продолжать свой цикл статей по разработке движка Arcanum на С++ 23 и не отказываться от поддержки Windows 98.

В ближайшее время обновлю, реализацию std добавлю поддержку linux. Основные контейнеры STL. Думаю, что в Linux с такими пируэтами даже проще.

Есть конечно вариант портировать последний gcc для сборки наитивно под Windows 98, как это сделал пользователь fsb4000, для gcc 11. Но пока я к такой древней и темной магии не готов:)

Обязательно напишу статью о более серьезных успехах, когда они будут в портировании LDL на С++ 23 код с работой на Linux, Windows. Сейчас же это описание возможно безумного, странного, но все же рабочего способа, запустить современный С++ код, на старых системах.

Отвечу на вопрос кому оно вообще нужно то?

Отвечаю. Нам.

К примеру в русскоязычной сфере это конечно Old-Games.ru

Это центр всех любителей и энтузиастов старых, игр, программ, ПК. И я рад, сказать, что тоже являюсь частью. Но мы не только играем в старые игры, но так же создаем новые проекты для старых систем. Новые переводы для старых игры, новые моды, новые улучшения. Все это делается ради фана. Потому, что нравится, у каждого есть своя работа и свои заботы. Но все жё находится время и на такое хобби. Здесь нет KPI или менеджеров, чем хочешь тем и занимайся.

Все же есть в старом железе, свое очарование!

Буду раз, предложениям, советам, критике и рад пообщаться в комментариях.

Ссылка на весь представленный код

Комментарии (44)


  1. BorisU
    24.12.2025 12:21

    Тут примчались санитары и зафиксировали нас (с) :)


  1. JBFW
    24.12.2025 12:21

    А нет такого ощущения, что переизбыточное усложение C++, с добавлением всё новых и новых абстракций, приведет примерно к тому, что C++ перестанут пользоваться?

    Типа, "писать на C с плюсами (по книжке K&R 90-х) - это примитивно и устарело, а понять C++ 30 без 30-летнего опыта разработки на нём - нереально, ну его нафиг"


    1. JordanCpp Автор
      24.12.2025 12:21

      У меня нет 30-ти летнего опыта программирования. Но чуть знаю. Я бы посмотрел с другой стороны, к примеру java, python библиотек в стандарте намного больше но никто не говорит, что это языки монстры которые не возможно изучить. Но так же правда, что устраиваясь на мидл java или python нужно знать не только язык, но и его экосистему которая в разы больше С++.

      Нет у меня, нет ощущения, что он не нужен и вот вот от него в е откажутся.


    1. SilverTrouse
      24.12.2025 12:21

      Современные стандарты добавляют очень много приятных фичей, упрощающие жизнь. К примеру писать шаблоны до с++20 и после две разные вещи. Да даже до с++17 много вещей упрощающие визуал и понимание ( if constexpr, концепты, templete for в грядущем 26 стандарте)


      1. JBFW
        24.12.2025 12:21

        Проще было писать в 1999: никаких концептов, шаблонов и прочего: обычный С + некоторые удобные вольности (типа инкремента и переопределения функций) + классы.
        MFC например в таком стиле был написан примерно весь

        Увидеть сейчас это можно в библиотеках Ардуино, "тот самый С++".

        А вот потом пошло-поехало...


        1. JordanCpp Автор
          24.12.2025 12:21

          Ага помним.

          // helloapp.cpp : Minimal MFC Windows app.
          //
          // This is a part of the Microsoft Foundation Classes C++ library.
          // Copyright (C) 1992 Microsoft Corporation
          // All rights reserved.
          //
          // This source code is only intended as a supplement to the
          // Microsoft Foundation Classes Reference and Microsoft
          // WinHelp documentation provided with the library.
          // See these sources for detailed information regarding the
          // Microsoft Foundation Classes product.
          
          #include <afxwin.h>
          
          // Define a window class derived from CFrameWnd
          class CHelloWindow : public CFrameWnd
          {
          public:
          	CHelloWindow()
          		{ Create(NULL, "Hello World!", WS_OVERLAPPEDWINDOW, rectDefault); }
          };
          
          // Define an application class derived from CWinApp
          class CHelloApp : public CWinApp
          {
          public:
          	virtual BOOL InitInstance();
          };
          
          // Construct the CHelloApp's m_pMainWnd data member
          BOOL CHelloApp::InitInstance()
          {
          	m_pMainWnd = new CHelloWindow();
          	m_pMainWnd->ShowWindow(m_nCmdShow);
          	m_pMainWnd->UpdateWindow();
          	return TRUE;
          }
          
          CHelloApp HelloApp;  // HelloApp's constructor initializes and runs the app 


          1. JBFW
            24.12.2025 12:21

            Я про синтаксис )

            MFC - отдельная тема. Так-то можно ещё на чистом winapi писать, со всеми этими объявлениями класса окна и какими-то костылями из Паскаля


            1. Siemargl
              24.12.2025 12:21

              А Паскаль то тут при чем?

              MFC это тоненькая обвязка WinAPI + hwnd.


              1. JBFW
                24.12.2025 12:21

                А вы погуглите.

                int PASCAL MessageBox(HWND, LPCSTR,...)

                Это влияло на работу со стеком. И где-то там ещё были строки паскалевского типа, где первый байт длина, и строка не завершается х0, как в С. Потому что в самом начале Винду вообще планировали на Паскале делать.

                И MFC это совсем не тоненькая обвязка, а такой "классный монстр", построенный на наследовании всех классов от единого предка, CObject. CWnd в том числе.

                В глубинах которой как раз и шло обращение к winapi через hwnd.

                А вот hwnd - это всего лишь указатель на экземпляр класса окна в ОС, handle window, это как раз составная часть winapi. Все объекты в ней - windows, у каждого свой handle, и через него идёт передача events в окно.

                Где-то на дисках в каталогах .../1/old/old/win/doc.... до сих пор валяется документация, наверное...


                1. Siemargl
                  24.12.2025 12:21

                  Мне не надо гуглить, а на нем писал =)

                  От Паскаля там Pascal calling convention, в Win16 API. Паскаль-строк не припоминаю. Байку такую слышал про написание на паскале, но достоверность не знаю


                  1. Siemargl
                    24.12.2025 12:21

                    Upd. Кстати на Win11 прекрасно работает MS Visual C++ 1.1 32-bit с MFC 2.1 (всего 1.5 МБ исходников)


                    1. JBFW
                      24.12.2025 12:21

                      По нынешним временам - да, можно сказать "тонкая обвязка".
                      Но тогда...

                      Делал как-то программку типа "Нортона" (только графическую, с иконками каталогов и файлов) - на голом winapi она заняла что-то около 50 килобайт (всего !), на MFC подобное - от 300 и больше.

                      Когда хард - 240Мб весь - это существенно.

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

                      Но в целом, С++ тогда был простым - я об этом говорю.


                  1. JordanCpp Автор
                    24.12.2025 12:21

                    Точно помню информацию, что старые mac os как раз использовали pascal для разработки и довольно широко. После чего со временем перешли на objective C.


                1. JordanCpp Автор
                  24.12.2025 12:21

                  В чертогах памяти, вспоминается что использование PASCAL как пример

                  long FAR PASCAL WndProc ( HWND hwnd, UINT msg, UINT wParam, LONG lParam ) 

                  использовали, не потому, что первые версии были на pascal, а потому, что это ускоряло код. Откуда эта информация, не помню.


                  1. Siemargl
                    24.12.2025 12:21

                    Нет, все проще.

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

                    pascal и stdcall отличаются только порядком передачи параметров, очистка стека вызываемой ф-цией.


    1. RH215
      24.12.2025 12:21

      Скорее, наоборот, то, что было с 11 по 23 упростило разработку до какого-то вменяемого уровня. Сложность языка всё ещё запредельная, но писать стало приятнее.


    1. Jijiki
      24.12.2025 12:21

      тут надо затронуть 1 очень важную деталь, железный конь на фон Неймане который уже не тот когда был придуман С - и новое будущее, которое стучится к нам с Растом, и почему-то верификация, если на неё посмотреть архитектурно смотрится уже не так как тогда когда пытались доказать что Раст имба, тоесть мы буквально шагнули в будущее когда первые байты Раста тронули центральный процессор, так что 86-64 будем посмотреть, но эволюция двинулась вперед в мире процессоров однозначно


    1. windsurfer69
      24.12.2025 12:21

      Наоборот, начиная с С++11 наконец на С++ стало писать проще, благодаря новым функциям языка и новым шаблонам разработки. И проявился такой неожиданный эффект - код на новых стандартах просто работает на всех системах, где этот стандарт поддерживается - фактически - везде. С++ повезло, что у него есть Бьёрн с его энергией и авторитетом.


  1. blackstrip
    24.12.2025 12:21

    Скоро майкрософт закроет C++ вообще, откажется от его использования и забудет о нем)

    Microsoft к 2030 году планирует отказаться от языков программирования C и C++ и переписать кодовые базы на Rust с помощью «ИИ-инфраструктуры» 

    Об этом рассказал главный инженер компании в посте на LinkedIn.

    Модернизация крупных кодовых баз и переход на Rust — не официальное заявление компании, а планы ведущего инженера Microsoft Галена Ханта. Он выложил пост на LinkedIn, в котором рассказал, что ищет инженера-программиста, чтобы убрать «каждую строку C и C++ из Microsoft к 2030 году». Для решения этой «ранее немыслимой задачи» команда собирается «объединить ИИ и алгоритмы для переписывания крупнейших кодовых баз».


    1. JordanCpp Автор
      24.12.2025 12:21

      Читал новость, первый же комментарий: помянем:))


    1. RH215
      24.12.2025 12:21

      Это что-то уровня отказа от Кобола. Они, конечно, могут отказаться от новых проектов на С++, но переписать весь старый код до 2030 -- это абсурд.
      (Вообще, этот проект больше исследование, чем план)


    1. Ulrih
      24.12.2025 12:21

      у ИИ есть "наработки" в миллионы строк кода на Rust'е? или оно будет учиться на этом?


      1. Jijiki
        24.12.2025 12:21

        просто напишут в первом диалоге чтобы ждал файл по приходу файла транслировал этот код в равный Расту, недели 3(около 4 дней трансляции, далее около 1к вопросов на закрепление и правки нейрослопом) и далее допил командой около 3 лет что-то такое я думаю

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

        тоесть если это гпт 5.5 это именно так но тут момент готовы ли они кидать в сетку код

        ну в любом случае наверняка уже знают лучше как это сделать я только предположил

        а может не так, может просто будут брать куски и с асистентом перепиливать на Раст в обычном режиме

        оба варианта на 3 года уйдёт если там много связного кода


        1. JordanCpp Автор
          24.12.2025 12:21

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


          1. Jijiki
            24.12.2025 12:21

            может и так но мат доказательство Раста на фриз версии удобнее чем ловить ошибки на С с goto


          1. SHAD0W137
            24.12.2025 12:21

            Скриньте этот твит


          1. Okeu
            24.12.2025 12:21

            в 2030 году выйдет директор Microsoft и скажет,

            приглашаем присоединиться вас к программе Windows Insider =)


    1. rbdr
      24.12.2025 12:21

      Их последний вин11 уже прекрасный антипример. Есть сомнения, что раст какой-нибудь им поможет.


      1. JordanCpp Автор
        24.12.2025 12:21

        Допустим даже если поможет, а толку то. Безопасная, но тормозная, неудобная, сливающая тоннами информацию о пользователе, за его же деньги. Проблемы с обновлением, оптимизацией, купите ПК с tpm 2.0 и бла, бла, бла.


  1. Stariy2003
    24.12.2025 12:21

    "Ну хоть, что то они не сломали:)" - Крофь ис глас текот.


    1. JordanCpp Автор
      24.12.2025 12:21

      Ага, есть такое. Поправлю.


  1. Siemargl
    24.12.2025 12:21

    Без исключений печально, std будет неполной.

    Сторонний линкер с полной поддержкой COFF с MS совместимыми исключениями не вариант подобрать?


    1. JordanCpp Автор
      24.12.2025 12:21

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


  1. CrashLogger
    24.12.2025 12:21

    А что такого они сломали в Visual Studio, что экзешник не может запускаться под Win98 ? Стандарт PE файлов вроде не менялся, что там может быть несовместимо ?


    1. JordanCpp Автор
      24.12.2025 12:21

      Версия файла, но это не проблема. Проблема в новых API которые используются sв реализации стандартной библиотеки, исключения которые полагаются на новый windows api. Тоже самое с рантайм. Еще сам формат новые версии добавляют дополнительные секции, которые может обрабатывать только новый линкер, который знает только о новой винде.

      Я думаю, в linux проблем будет меньше в этом плане. Пока мне не до конца понятная вещь это именно исключения, есть понимание что у каждого компилятора опора на конкретное api и если его подменить, есть шанс что заведется.


      1. Siemargl
        24.12.2025 12:21

        Кстати, тоже вариант. Несовместимость обычно в startup коде. Может быть, простая его подмена и решит большинство вопросов.

        Проблема в новых API которые используются sв реализации стандартной библиотеки, исключения которые полагаются на новый windows api

        Это очень сомнительно.

        А вот рантайм в целом, наоборот, может тоже взять постарше.


        1. JordanCpp Автор
          24.12.2025 12:21

          Ещё поэкспериментирую с поддержкой исключений. Уверен, что решение есть.


  1. DrMefistO
    24.12.2025 12:21

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

    • иногда происходит добавление обращений к WinAPI, которые недоступны в старых ОС

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


    1. JordanCpp Автор
      24.12.2025 12:21

      Я это учел, использую winapi которые есть начиная с windows 95.

      Для того, что бы не было sse применяю ключи компиляции /arch:IA32

      Данные проблемы как раз легко обходятся.


  1. JordanCpp Автор
    24.12.2025 12:21

    Я ожидал проблем с манглингом имен, но их нет. Протестировал на дополнительных модулях, классы, доп using'и и компилятор применяет совместимые названия, линкер от vc 6.0 корректно все линкует.


  1. JordanCpp Автор
    24.12.2025 12:21

    Данный подход расширяет программирование под ретро системы. С++ 23 его фичи и использование современных компиляторов хорошо оптимизируют код. Это дополнительная производительность, за которую по сути я не плачу это из коробки. Очень приятно, мне в библиотеке LDL приходилось для софт рендера дублировать работу с разными форматами изображений, с помощью С++ я смогу это упростить и быть уверенным, что все заинлайниться так как буду использовать шаблоны, constexpr.


  1. begin_end
    24.12.2025 12:21

    О, аналогично играюсь, компилируя на современной ОС минимально возможные exe, работоспособные как сейчас, так и в Windows 95. Правда, я фанат Делфи (в данной форме применения - деградировавшего почти до Паскаля + Win32 api). Также использую специфичный подход, беря .lib файлы и старый майкрософтский линкер v.5.12.

    Сборка

    Хоть это действительно больше игрушка, но по этой технологии реально написал пару микроутилит, актуально применяющихся в практике локального администрирования по сей день.


    1. JordanCpp Автор
      24.12.2025 12:21

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

      Вторым шагом можно сделать модули для SDL 1.2 и SDL3 для новых и старых систем. В модулях загружать через GetProcAddress.

      Создать модуль с единым api который бы вызывал данные модули в зависимости от системы. Доделать STL.

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


  1. TitovVN1974
    24.12.2025 12:21

    А как использование всех этих возможностей влияет на производительность?

    Получается ли эффективный код?