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

в 14:16, , рубрики: c++, кросплатформенность

Приветствую, читатели!

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

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

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

Автор: JordanCpp

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js