Приветствую, читатели!
Решил сделать цикл статей по написанию на С++, различных небольших программ. Под новые и стрые ОС. Мне кажется мы стали забывать как раньше программировали:) Для себя определил несколько важных критериев.
-
Код должен быть простым и понятным.
-
Код должен быть переносим, как минимум Windows и Linux, поддерживать 32-ух битные и 64-ех битные процессоры.
-
Не полагаться на стандартную библиотеку на всех платформах. Пишем свой минимальный вариант.
-
Быть совместимым с С++/C библиотеками, так как будем их использовать в будущем.
-
Программы и библиотеки которые я буду разрабатывать должны, делать что то осмысленное.
-
Минимум ассемблера, все в рамках С++.
По мере разработки я буду портировать код под разные в том числе и старые ОС, будем использовать старые компиляторы. Подходы в реализации графики, к примеру рисовать в буфер, без всех этих ваших ускорителей. Но важно, код на С++ должен оставаться простым и понимаемым и использовать возможности С++, шаблоны, ООП, 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 и шаблонов есть. Второй момент, весь код кроме системных функций одинаков для всех платформ, упрощается тестирование и отладка.
В итоге, мне интересно копаться во всех этих артефактах древности.
Автор: JordanCpp