Пишем легаси с нуля на С++, не вызывая подозрение у санитаров. 01 — Маленькая программа

от автора

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

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

  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 и шаблонов есть. Второй момент, весь код кроме системных функций одинаков для всех платформ, упрощается тестирование и отладка.

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


ссылка на оригинал статьи https://habr.com/ru/articles/872834/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *