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

Решил сделать цикл статей по написанию на С++, различных небольших программ. Под новые и старые ОС. Мне кажется мы стали забывать как раньше программировали:) Для себя определил несколько важных критериев.

  1. Код должен быть простым и понятным.

  2. Код должен быть переносим, как минимум Windows и Linux, поддерживать 32-ух битные и 64-ех битные процессоры.

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

  4. Быть совместимым с С++/C библиотеками, так как будем их использовать в будущем.

  5. Программы и библиотеки которые я буду разрабатывать должны, делать что то осмысленное.

  6. Минимум ассемблера, все в рамках С++.

По мере разработки я буду портировать код под разные в том числе и старые ОС, будем использовать старые компиляторы. Подходы в реализации графики, к примеру рисовать в буфер, без всех этих ваших ускорителей. Но важно, код на С++ должен оставаться простым и понимаемым и использовать возможности С++, шаблоны, ООП, STL, libc. Собираться на современных компиляторах и ОС.

Ещё одна безумная идея, это сделать поддержку 16 битных процессоров. Компилятор Open Watcom умеет создавать такие бинарники для Windows 3.1 и MS-DOS. И поддерживает, что то похожее на С++ 11. Вполне хватит. Кстати пока у нас не так много кода, портирование не должно занять много времени. Обязательно об этом упомяну в следующих статьях. Я не буду запускать на старых ос только консольные приложения. Основной упор сделаю на графику и разберу, как оно все работало на таких скромных характеристиках древних ПК.

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

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

Разрабатывать под Windows я буду в MSVC 2022, под Linux компилятором GCC 13.0

Весь код находится в репозитории RetroFan

Отучаем программу от стандартной библиотеки.

Я использую cmake для всех платформ. В случае msvc для сборки добавил флаг /NODEFAULTLIB

    set(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB")

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

extern "C" int main();
extern "C" int mainCRTStartup();
extern "C" void _RTC_InitBase();
extern "C" void _RTC_Shutdown();
extern "C" void _RTC_CheckEsp();
extern "C" void __CxxFrameHandler3();
extern "C" void __CxxFrameHandler4();
extern "C" void _RTC_CheckStackVars();

После чего пробуем собрать и пустая программа весит 2кб. Она запускается и закрывается.

Для linux опции компилятора выглядят так:

    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -nostdlib -nostartfiles -nodefaultlibs -nostdinc -s -O2 -fno-exceptions -fno-rtti -fno-builtin")

Я пока не одолел до конца Linux версию, пока поставил заглушки. К следующей статье, разберусь как дергать системные вызовы в Linux и напишу минимальный менеджер памяти используя sbrk. Пока я не смог распарсить нагугленное:)

Выводим на консоль данные.

Так как у нас ничего нет, будем обращаться к системным вызовам Windows. Что бы не тянуть весь Windows.h, просто добавляем нужные нам макросы, функции и константы. Такой файл выглядит так. В первую очередь реализуем изи вариант printf. Который умеет просто выводить char'ы. Со временем обязательно стянем код printf из musl реализуем полное форматирование, а пока для простоты восприятия оставим.

Для каждой ос будем реализовывать простую функцию вывода на консоль:

int PortableWrite(const char* data, size_t count)

А уже её будет дергать наш самописный libc.

#if defined(_WIN32)
    #include "../Windows/Portable.h"
#elif defined(__unix__)
    #include "../UNIX/Portable.h"
#elif defined(__MSDOS__)
    #include "../DOS/Portable.h"
#endif

int printf(const char* text)
{
	return PortableWrite(text, strlen(text));
}

Если вдруг вам показалось, что вы увидели упоминание DOS, вам не показалось:) Ну, что теперь в 2025 году под MS-DOS не разрабатывать? Пока там заглушки.

Писать на С с классами хорошо, но все же С++ предоставляет кучу возможностей. Шаблоны, ООП, namespace'ы. Будем всем этим пользоваться по полной, правда сначала нужно написать.

Пишем malloc, free и new, delete.

В Windows динамической памятью управляют функции HeapX. И они идеально ложатся на malloc и free.

void* PortableAllocate(size_t bytes)
{
	return HeapAlloc(_heap, 0, (SIZE_T)bytes);
}

void PortableFree(void* ptr)
{
	HeapFree(_heap, 0, ptr);
}

А уже libc дергает Portable функции.

void* malloc(size_t bytes)
{
	return PortableAllocate(bytes);
}

void free(void* ptr)
{
	PortableFree(ptr);
}

Перегружаем глобальные new и delete.

void* operator new(size_t size)
{
	return malloc(size);
}
void operator delete(void* ptr)
{
	free(ptr);
}

void* operator new[](size_t size)
{
	return malloc(size);
}

void operator delete[](void* ptr)
{
	free(ptr);
}

Пишем свои строки.

Цель не написать за раз весь функционал std::string, он довольно велик. Пока сделаем изи строки с минимальным интерфейсом. Особо сложного там нет и для написания строк я подсматривал в реализацию STL в gcc. Но там настолько всратое наименование, из-за этого пришлось потратить довольно много времени, что бы всё это понять.

Я не стал копировать код напрямую из STL, потому, писал код для понимания. Сами строки.

Пока строки занимают всего 290 строк. Вполне понятного и читаемого кода.

Итоговый вариант программы выглядит так:

#include <stdio.h>
#include <string>

int main()
{
	std::string str1 = "I am ";
	std::string str2 = "litle program!";
	std::string str3 = str1 + str2;

	printf(str3.c_str());

	return 0;
}

Бинарник занимает 3,5 кб для 32 бит и 4,0 кб для 64 бит. Не так уж и плохо. Можно в следующих статьях, еще поиграться с размером. Но в принципе, и такой размер меня устраивает.

В следующих статьях, будем уже работать с графикой. Так же код останется кроссплатформенным, графика будет работать на Windows и Linux.

Ещё большой плюс в том, что я пишу всё с нуля это независимость от компиляторов, в том числе и старых. К примеру в некоторых версиях может не быть поддержки STL, но поддержка namespace и шаблонов есть. Второй момент, весь код кроме системных функций одинаков для всех платформ, упрощается тестирование и отладка.

В итоге, мне интересно копаться во всех этих артефактах древности.

Обновление:

Добавил namespace WinAPI для функций windows.

Добавил расширение hpp для файла с реализацией строк std::string. Что бы гитхаб мог подсвечивать синтаксис С++.

Исправил утечку памяти в методеstd::string.reserve. Спасибо пользователю https://t.me/ProCxx/675906

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


  1. Getequ
    10.01.2025 14:25

    Почему вы решили что это легаси если вы пишите новое приложение?


    1. JordanCpp Автор
      10.01.2025 14:25

      Это больше игра слов. Вроде как приложение новое, а технологии старые. На старте все устарело.


      1. dponyatov
        10.01.2025 14:25

        не legacy а retrodev (retro development) скорее


  1. Jijiki
    10.01.2025 14:25

    интересно, спасибо. осилить окна на линуксе не такая простая задача ) хотя реально вроде), а зачем писать на С++? извините строки вроде char*(1,.......32) нужного размера.

    может лучше void vector - но по соглашению что тип 1 на вектор (извините увидел void *ptr)

    преимущества С в том. что если задались вопросов реализаций многих например STL, то С идеально подходит там ничего нету


    1. JordanCpp Автор
      10.01.2025 14:25

      C++ удобен. Те же шаблоны и ООП. Код на С конкатенации двух строк выглядит, скажем так особенно...


      1. Jijiki
        10.01.2025 14:25

        понял вас, я дошел до более менее вменяемого кода который мне понравился на С++, научился освоил его и разочаровался. код монструозный, хотя всё по идиоме верно, как нужно


  1. Serpentine
    10.01.2025 14:25

    int printf(const char* text)
    {
    	return PortableWrite(text, strlen(text));
    }

    В стандартной библиотеке printf() объявляется как:

    int printf(char *fmt, ...)

    т.е. ожидается, что она имеет список аргументов переменной длины (и пользователи их туда напихают при первой же возможности). Ну и сама функция выводит форматированную строку, что само по себе нетривиальная задача и в том же K&R ее минимальная реализация демонстрируется через подключение stdarg.h

    Чтобы не парится, я бы ее заменил, например, на puts() и в ней или в самом PortableWrite() уже как-нибудь реализовал добавление '\n'

    Небольшое отступление. В древней книге "Системное программирование в среде Windows" Джонсона Харта есть приложение "Сопоставление функций Windows, UNIX и библиотеки C" и в табличке напротив виндовых HeapReAlloc() и HeapFree() там, где должны быть прописаны аналоги UNIX жирным текстом написано "Используйте библиотеку С".


    1. Jijiki
      10.01.2025 14:25

      поидее если стоит задача вывода средствами потока ввода/вывода, то надо проверить какие библиотеки есть проверить конфигурацию, создать потоки, и воспользоваться реализацией метода которая доступна из реализованного кода на платформе либо по сисколу либо по доступной обёртке сискола с созданными потоками

      https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringa

      https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-outputdebugstringw

      https://www.go4expert.com/articles/outputdebugstring-variable-arguments-t871/

      это пример


      1. Serpentine
        10.01.2025 14:25

        https://www.go4expert.com/articles/outputdebugstring-variable-arguments-t871/

        это пример

        По ссылке к OutputDebugString() для добавления функционала из printf() примотали изолентой стандартный sprinf() и макросы из stdarg.h (см. va_list, va_start и va_end), о чем я говорил.

        Честно имитировать стандартную printf() - это обрабатывать добрую пару десятков (если не больше) спецификаторов (%d, %x, %g, %p, etc.) и кучу их модификаторов (точность, флаги, ширина поля и пр.).


        1. JordanCpp Автор
          10.01.2025 14:25

          Честно имитировать стандартную printf() - это обрабатывать добрую пару десятков (если не больше) спецификаторов (%d, %x, %g, %p, etc.) и кучу их модификаторов (точность, флаги, ширина поля и пр.).

          Для начала добавить несколько основных вариантов.


        1. Jijiki
          10.01.2025 14:25

          человек добавил такие фичи но вы можете просто строки пулять, другой способ это IPC, это я тоже немного смотрел, там есть обертки у винды свои тоесть апишки, а на линуксе это неблокируемые буферы ввода/вывода на сколько помню, вобщем и там и там стандарты вызовов к нужным сисколам (тоесть грубо говоря если пишем консоль и нужны свои буферы - то поидее нужны доступы на чтение запись тоесть чтобы в вашей консоле работал cat и read login и прочее что и делает по доступу отображения шрифта консолью - консолью)


          1. Serpentine
            10.01.2025 14:25

            В своих комментариях я говорил про форматированные строки и функции со списком аргументов переменной длины, а не о выводе обычных null-terminated строк.


      1. JordanCpp Автор
        10.01.2025 14:25

        Спасибо за ссылки, посмотрю.


        1. Jijiki
          10.01.2025 14:25

          OutputD...String классная штукенция в своё время смотрел туториал не по вашей тематике, там скорее пре вводная в игры, обзорщик в начале концентрировался на тематике логирования, там смотрел тестил в итоге пришел к тому что проще просто открывать буфер и пулять в лог прям строку при отладке например игры очень удобно, например такой лог кидает строку в те буферы где выводится системная инфа, что загрузилось и прочее, ну сопсно общий лог как я понял


    1. JordanCpp Автор
      10.01.2025 14:25

      нетривиальная задача и в том же K&R ее минимальная реализация демонстрируется через подключение stdarg.h

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

      Чтобы не парится, я бы ее заменил, например, на puts() и в ней или в самом PortableWrite() уже как-нибудь реализовал добавление '\n'

      Спасибо за идею. Сразу и не подумал.

      там, где должны быть прописаны аналоги UNIX жирным текстом написано "Используйте библиотеку С".

      Там нет сноски, вроде но если очень хочется, то делайте так?:)


      1. Serpentine
        10.01.2025 14:25

        Там нет сноски, вроде но если очень хочется, то делайте так?:)

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

        Если честно, при задаче написать код, подходящий под DOS, Win32/64, UNIX, мне первым делом приходит в голову использовать как раз ANSI C (чтобы libc предоставлялась самими платформами) и затем делать обертки над специфичными API этих осей, ну и #define'ом обыграть прочие прелести из минувших времен типа far pointer'a.

        Ибо и без написания даже обрезанной кроссплатформенной libc интересных задач море.

        Как пример, на Хабре была статья о портировании примеров из книги Андре Ламота про DOS игры под современный win32.


        1. JordanCpp Автор
          10.01.2025 14:25

          Вы все правильно пишите. И у меня уже есть похожий проект в репе. Где я пишу все на С++ и полагаюсь на поставляемые libc. И как одна из опций сборки это будет работать. Но мне хотелось бы обеспечить поддержку именно своей версии минимальной libc и STL. Уже успешно доковырял linux версию. Осталось сделать malloc и free, на основе mmap и unmap. Системные вызовы linux уже вызываются. Можете посмотреть в репе. Делаю по примерам в интернете.

          Да и я много кода скопирую и для себя осмыслю. И в статье все шаги опишу. Мне интересно как оно все под капотом работает. Интересно же.

          Только С++, на С программировать желания нет.


          1. emusic
            10.01.2025 14:25

            хотелось бы обеспечить поддержку именно своей версии минимальной libc и STL

            С libc особых проблем нет, кроме поддержки локализованных строк. А вот в STL многое завязано на исключения, собственную поддержку которых делать нет смысла - она получится примерно такой же громоздкой, как и родная. Разве что подменить развесистые плюсовые исключения на минималистичные вроде SEH, с передачей кодов ошибок вместо объектов.


        1. JordanCpp Автор
          10.01.2025 14:25

          Вот пример. Работает на windows 32/64, linux 32/64, ms dos 16/32.

          https://github.com/JordanCpp/LDLHOL


        1. JordanCpp Автор
          10.01.2025 14:25

          Ещё один аргумент за С++, это нормальная ООП абстракция типизированная на шаблонах. А не как в glibc.


  1. JordanCpp Автор
    10.01.2025 14:25

    Всем спасибо за советы и предложения, коллективный разум это сила!


  1. voldemar_d
    10.01.2025 14:25

    Предлагаете вообще не использовать STL?

    О чем пункт про осмысленность программы? Разве это само собой не разумеется?

    Ассемблер зачем вообще, можете пояснить? И как это сочетается с кросс-платформенностью?


    1. JordanCpp Автор
      10.01.2025 14:25

      Предлагаете вообще не использовать STL?

      Нет, без STL будет слишком много ручной работы.

      О чем пункт про осмысленность программы? Разве это само собой не разумеется?

      Я решил добавить данный пункт. Что это не будет, что то бессмысленное. Типа только привет мир. Более сложное.

      Ассемблер зачем вообще, можете пояснить? И как это сочетается с кросс-платформенностью?

      Ассемблер при необходимости. Скорее всего он будет в сочетании с вызовами bios в ms dos. Прерывания вызывать.

      Только кроссплатформенность, иначе смысл теряется. То, что работает под ms-dos должно работать и под другими ОС. В этом смысл.

      Иметь единое API для старых и новых ОС. В том числе и для графики.


      1. voldemar_d
        10.01.2025 14:25

        Я что-то теряюсь в догадках: как можно написать код для графики, который будет одинаково работать и под MS DOS, и под другими ОС?


        1. JordanCpp Автор
          10.01.2025 14:25

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


          1. voldemar_d
            10.01.2025 14:25

            И под графическими оконными ОС?


            1. JordanCpp Автор
              10.01.2025 14:25

              Да. При загрузке палитровое изображение конвертируется в rgb.

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


              1. voldemar_d
                10.01.2025 14:25

                Что Вы называете "библиотекой для графики"? Код для загрузки файлов с графикой?

                Или для отображения графики на экране?


                1. JordanCpp Автор
                  10.01.2025 14:25

                  Вывод на экран графики. Примитивы, картинки. Опционально загрузчик разных форматов.


        1. JordanCpp Автор
          10.01.2025 14:25

  1. odisseylm
    10.01.2025 14:25

    printf без константной строки (формата), круто! Это же круто! (мало нам дыр, добавим ещё)

    И ваша статья уже устарела... Или так и планировалось?

    Иметь единое API для различных ОС (включая графику), включая MSDOS.
    Возникают огромные сомнения, что вы понимаете, что делаете (объем работ), если вы планируете писать что-то серьёзное.

    Не понимаю, почему статья не заминумована...


    1. JordanCpp Автор
      10.01.2025 14:25

      printf без константной строки (формата), круто! Это же круто! (мало нам дыр, добавим ещё)

      И ваша статья уже устарела... Или так и планировалось?

      Это же только начало. Я же упомянул в статье, что сделаем нормальный printf.


    1. JordanCpp Автор
      10.01.2025 14:25

      Не понимаю, почему статья не заминумована...

      Возможно плюсующим нравится?


    1. Jijiki
      10.01.2025 14:25

      задумка интересная, я тут поверил в себя взялся делать кросс-вызов окна и остановился на вводе/выводе (на X11 вылетает в нем есть широкий функционал но надо уметь работать с вводом выводом, на XCB вообще отлично работает, но но но тоже ввод/вывод - вообщем без ввода вывода обе работают, просто ввод/вывод отнимет много времени чтобы с ним разобраться) - пара вылетов вернула меня на библиотеку, но если получается и есть понимание почему нет ) (вообще конечно в винде просто ультимативные апишки по работе с окном и асинхронным вводом выводом просто уйму времени на разбирание ассинхронного ввода вывода пропускаешь, потомучто надо чтобы мышка с клавиатурой работали и кнопки повторялись на клаве, на иксах какойто барьер называется инпут лаг )


  1. JordanCpp Автор
    10.01.2025 14:25

    Мне понравилась эта статья. Приложение X11 на ассемблере.

    https://habr.com/ru/articles/840590/

    И стало интересно, а возможно ли добиться примерно такого же размера. Но на С++ и STL. Конечно настолько малого бинарника не получится, но хотя бы приблизиться получится.

    Это было долго, но мы справились!

    Мы написали (очень простую) программу с графическим интерфейсом на чистом ассемблере, без каких-либо зависимостей и уложившись в 600 строк кода.

    • Как далеко мы можем зайти в оптимизации бинарника?

    • C отладочной информацией: 10744 байт (10 Кб)

    • Без отладочной информации (stripped): 8592 байт (8 Кб)

    • С оптимизациями stripped and OMAGIC (--omagic это ключ линковщика, из рукводства: Set the text and data sections to be readable and writable. Also, do not page-align the data segment): 1776 байт (1 Kб)

    Вообщем вот такая программка с интерфейсом размером в 1 Кб.


    1. Jijiki
      10.01.2025 14:25

      поидее тогда библиотеки надо динамические, и компиляция соответствующая я например компилирую в Ofast - окно, загрузка моделек, скайбокс, волна, мышка клава, 261 кб


      1. JordanCpp Автор
        10.01.2025 14:25

        Я сейчас на windows простенькое окно вывожу. Без обработки ввода. 4,5 КБ для 32 бит и 5,5 КБ для 64 бит.

        Потом это все я оберну в абстракции единого API для linux и windows.


  1. Marsel323
    10.01.2025 14:25

    Эксперимент интересный, ждём реализацию std::cout и std::cin


  1. JordanCpp Автор
    10.01.2025 14:25

    Почти допилил linux версию, осталось реализовать malloc и free. Начал пилить графику.


  1. emusic
    10.01.2025 14:25

    После чего пробуем собрать и пустая программа весит 2кб

    Если бы Вы просто добавили объявления функций, как показано в примере, программа не собралась бы. :) Вы явно добавили определения-заглушки, имеет смысл отразить это в примере.

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


    1. JordanCpp Автор
      10.01.2025 14:25

      Это не требуется. Код будет собираться и быть совместимым и со стандартным STL и libc идущий в поставке компилятора. Но, что бы добиться минимального бинарника на всех платформах, дополнительно пишу свои совместимые прослойки.


  1. orefkov
    10.01.2025 14:25

    В своё время Джоэл Спольски в статье "Верблюды и песочницы" писал - "80% пользователей используют 20% функционала программы. Но не думайте, что реализовав 20% функционала, вы удовлетворите 80% пользователей. Потому что эти 20% функционала у каждого разные".


  1. slonopotamus
    10.01.2025 14:25

    TL;DR: автор пишет самодельный libc+stl, но при этом старательно маскирует это под невнятные фразы про легаси.


    1. JordanCpp Автор
      10.01.2025 14:25

      В следующих статьях я буду использовать для сборки под старые системы visual C++ 6.0, а может ещё древнее версию. Под старые версии linux, gcc 3. Куда уж легаснее?