Решил я поиграться в ардуинки. Выбор пал на ESP32-C3 с 0,42-Дюймовым OLED-Модулем за какие-то там 200 руб. Пока оно едет из Китая думаю, дай потыкаюсь со средой разработки. Раньше дела с ней не имел.
Оказывается есть две версии под линухой. Одна на электроне v. 2.3.8 и старая 1.8.19 на Java. Но об ентом я ещё не знал, естественно выбрал поновее и получил засаду - вечно крутится на заставке. Думал наши разрабы АльтЛинухи накосячили с опакечиванием, перепробовал кучу сборок со стороны (не из репов Альта) - везде одно и тоже - вечно крутится заставка. Запустил иде из консоли и увидел туеву хучу ошибок с невозможностью чего-то там скачать. Направление гугления тут же изменилось и выяснилось, что www.arduino.cc лочит россиян из-за Cloudflare (эдакий сервис для защиты сайтов от хакерских атак). Что и и как произошло между Cloudflare и РФ вспоминать не стал и начал думать как пустить Ардуино-ИДЕ через тор-сеть. Придумать не удалось
Вот тогда я и узнал, что разница между старой и новой ИДЕ исключительно в интерфейсе, более того, настроек сети в новой версии я не нагуглил (возможно пальчиками где-то в конфигах и прописывается). А вот в старой версии пожалуйста Файл-->Настройки->Сеть:
При этом все библиотеки и "платы" начали загружаться. И да, версия из репозиториев Альта. Между прочим, ардуинки часто используются не только как хобби, но и в образовательных целях. Но политиканам насрать на это.
Всем доброго времени суток, с вами на связи Lis-ST. Это мой первый пост, не на данном сайте, а вообще, (не зря же я состоял в "Лиге лени"), но на общем фоне нейроистерии и всё большего распространения нейросетей кто-то должен хотя бы минимально объяснить, что же такое "Нейросеть", а так-же как правильно с ними работать и чего в принципе от них можно ожидать. В данном посте затронем только самые основы.
Немного истории
Здесь можно было бы надергать текста с вики, различных ресурсов, прогнать всё это через нейронку и вставить тысячи нудных строк на 2 часа чтения, но мне лень, поэтому пробежим вкратце по основным моментам.
Сама идея, создать что-то, что работало бы схожим образом с мозгом, как ни удивительно, появилась задолго до появления компьютеров. Первая математическая модель нейрона была создана ещё в 1943 году.
Первая же нейросеть появилась в 1950 году, примерно в это время начали появляться первые предшественники современных компьютеров. Умела она немного, могла только отличить круг от квадрата, или квадрат от круга, может ещё от треугольника, но ничего сложнее. И на этом всё развитие данной идеи практически остановилось по банальной причине - недостаток вычислительной мощности. Но, это не значит, что от идеи полностью отказались. Её постепенно развивали, предлагались новые модели, способы тренировки но очень существенный толчок в развитии нейронок я связываю с 2015 годом, именно в этот год была основана OpenAI и в 2016 году она положила начало нейроистерии, выпустив в публичный доступ, нет, не нейросеть, а платформу для разработки и обучения нейронок.
Основы
Тут определённо стоило бы начать с определения того, что такое нейрон, но чем больше будет определений, формул и подобного, тем хуже для восприятия будет текст, поэтому простыми словами - это то, что может принимать, обрабатывать, хранить и передавать информацию. Нейронная сеть - попытка повторить данные свойства нейрона.
Примитивное сравнение мозга и нейронки, не передающее суть.
На сегодняшний день придумано множество разных нейронок, разберём самую примитивную, как на картинке выше, она состоит из трёх слоёв: входной, скрытый и выходной.
Входной слой: на него в нейронку поступают какие либо данные - текст, числа, изображения, координаты и т.п.. Здесь
данные приводятся к удобному для нейронки виду (обычно это числа от 0 до
1), чтобы слишком большие значения не «свели сеть с ума» или не спалили ваш компьютер. У вас закономерно возникнет вопрос, как можно фото котика представить в виде обычного числа? Всё просто, это не одно число, а тысячи, на каждый пиксель выделяется по входному нейрону, который просто задаёт удерживает числовый значения. Так, для картинки 1024 на 1024 пикселя нам потребуется 1 048 576 входных нейронов.
Скрытый слой, на нем остановимся намного подробней, так как он и ответственен за всю магию нейронных сетей. Это по сути «сито», которое фильтрует данные, чтобы найти в них смысл. Нейрон скрытого слоя берет данные на входе, преобразованные в числовые значения и умножает их на «важность» (вес). Например, при
поиске кошки на фото — острые уши и хвост важны, а цвет фона — нет. Затем все результаты складывает в одно число, а полученная сумма сравнивается с порогом, который сформировался при обучении нейронки. Если число достаточно большое, нейрон
«просыпается» и передает сигнал дальше. Если нет — молчит.
Выходной слой — это финал, где нейросеть выдает готовый ответ. Он превращает сложные вычисления скрытых слоев в понятный формат. Если в скрытых слоях нейроны «ищут признаки», то в выходном слое они просто «голосуют» за итоговый результат.
Архитектура и типы нейросетей
В попытках получить наилучший результат, уменьшить требуемые вычислительные мощности для работы нейронок, постоянно придумывались новые варианты их архитектуры, способов тренировки, обработки данных. Так появились на свет нейросети с несколькими скрытыми слоями, каскады нейросетей, свёрточные нейронки и многие другие.
Здесь всё очень сложно, пока на этом не будем заострять внимания, но для общего понимания вот очень краткий список основных архитектур и более подробная картинка для наглядности:
ANN (Нейросети прямого распространения / Перцептроны)
CNN (Сверточные нейросети)
RNN (Рекуррентные нейросети)
LSTM / GRU (Сети с долгой краткосрочной памятью)
Transformers (Трансформеры — основа современных GPT)
GAN (Генеративно-состязательные сети)
Autoencoders (Автоэнкодеры)
GNN (Графовые нейросети)
... наглядности северного пушного зверька. Актуально на сентябрь 2025г.
Завершение
Пока мы коснулись только того, из чего состоит нейросеть, самые основы её работы, но без более подробного разбора мы не придём к пониманию того, почему же у меня от некоторых постов, связанных с нейросетями, весьма не слабо горит кресло. К данному вопросу мы перейдём завтра.
П.С.:
Как-то не так я себе это представлял, капля текста, две картинки, куда два часа исчезли то?
Короткий познавательный пост, основанный на моей больной фантазии. Все совпадения случайны.
Обратился как-то один человек на профильном форуме с вопросом, как запустить (не сказать чтобы старую) игру на Windows 10. Игра (хотя я бы назвал это технодемо) была разработана на легендарном XNA 4.0 (земля пухом) и планировалась к выпуску через Steam Greenlight (и ему земля пухом) еще в далеком 2012, после чего заброшена и убрана со всех магазинов.
Игра была куплена тем человеком еще в тот момент, когда она легально продавалась. А спустя годы она просто перестала подавать признаки жизни. $5 за полную версию уплочены, а поиграть нельзя. Обидно.
Так как типовые решения по типу установки XNA Redistributable не помогли, то был предложен единственно возможный выход из ситуации - отладка через dnSpy.
Внимание: обратная разработка хоть прямо и не запрещена законодательно, но разрешенные случаи весьма ограниченны. В других странах разрешен явный запрет любых манипуляций через лицензию, превращающий это занятие во вполне наказуемое. Не повторяйте дома.
Так как ПО было приобретено легально и оно не работает, то этот теоретический случай допустим в соответствии со статьей 1280 гражданского кодекса.
Демонстрировать буду на бесплатной демоверсии, скачать можно здесь.
dnSpy
Театр начинается с вешалки, а программа на C# с атрибутов сборки. Они нам сейчас не сильно интересны, поэтому пытаемся сразу запускать отладку. С неизвестными программами лучше использовать виртуальную машину, так как может быть встроена какая-нибудь пакость для кулхацкеров, но мне можно и так.
Ошибка. Открываем стек вызовов и ищем место, где произошел провальный запрос в сеть.
А вот и виновник торжества. По сообщению из исключения можно сделать вполне однозначный вывод, что проблемой является скупердяйство и непредусмотрительность автора, который решил перестать платить за домен и хостинг, а вместе с этим случайно (а может и намеренно) сделал запуск игры невозможным без интернета. Так как на сайте заявлено отсутствие DRM, то это явно недосмотр.
Что тут происходит?
Если почитать документацию на XNA, то этот метод предназначен для загрузки контента и вызывается автоматически при запуске игры. Первым делом выполняется инициализация некоторых вещей, после чего открывается файл настроек (options.xml) и начинает последовательно считываться. Как только считываются параметры аккаунта для таблицы рекордов, то происходит отправка логина+пароля на официальный сайт и попытка получить ID. Если сервер возвращает Failure, то открывается диалог входа, который можно пропустить. Если сервер возвращает число, то игра запускается. А вот если сервер недоступен, то вылетает исключение, которое никак не обрабатывается и приводит к молчаливому вылету.
Диалог входа официально можно пропустить.
На этом этапе есть 2 варианта действий:
Вручную отредактировать настройки и убрать пункт об аккаунте. Тогда игра будет всегда запускаться без входа.
Физически убрать запрос на несуществующий сайт.
Нормальные герои всегда идут в обход, так что переходим к написанию небольшого патча. (На самом деле, о первом варианте я додумался не сразу. Более того, полная версия зависает при убирании этого пункта из настроек).
Выделяем строчку с запросом и нажимает "Изменить инструкции IL". Открывается окно с заголовком "Изменение тела метода".
Это - язык MSIL. Он очень прост и сильно напоминает Forth, на самом деле, но новичка может отпугнуть.
Ключевым элементом является стек. Что такое стек я уже когда-то рассказывал. Это некоторое место, на вершину которого можно последовательно добавлять значения (числа, строки и прочие объекты), а так же снимать их в обратном порядке.
Не эти стеки, но суть та же. Можно добавлять и снимать сверху.
ldloc.2: Значение локальной переменной под номером 2 (с типом строки)
ldstr: Строку "&p=".
ldloc.3: Значение локальной переменной под номером 3 (тоже строка).
Потом мы вызываем String::Concat(), принимающий в себя 4 строки и соединяющий их вместе. При вызове метода будут сняты 4 верхние строки со стека и добавлена новая строка.
Вызов следующего метода (WebClient::DownloadString()) снимает строку, полученную от String::Concat(), и веб-клиент. В результате должна остаться только 1 строка, возвращенная WebClient::DownloadString().
Следующим этапом мы должны снять эту строку со стека и сохранить её в локальную переменную 1, но этого не произойдет, так как WebClient::DownloadString() всегда будет проваливаться и выбрасывать исключение. Исключение, если его не обработать сразу, будет раз за разом проваливаться глубже по стеку вызовов, пока не достигнет обработчика или дна. Так как никаких обработчиков по пути нет, то исключение достигает дна и приводит к вылету.
Чтобы избежать получения исключения, мы можем убрать всё, что предназначено для запроса в сеть. Примерно вот так:
8 инструкций превратились в 2: загрузка строки с числом (которое может быть любым) и сохранение в локальную переменную под номером 1.
Сохраняем и проверяем результат.
Было.
Стало.
Из декомпилированного кода полностью исчез кусок с запросом. Осталась лишь загрузка константной строки и сравнение с "Failure", которое всегда будет успешным.
Сохраняем модуль и пытаемся запускать опять.
Жопиздан!
С этой задачей успешно справился человек, который не смыслит в программировании вообще ничего. По его словам, он джва года ждал эту игру!
Но не всё так просто. Если попытаться пройти любой уровень, то вылетит то же самое исключение.
Для исправления точно так же выделяем строку и опять редактируем инструкции в теле метода. Но так как отправка статистики нонче ни на что не влияет, то просто удаляем выделенные инструкции без остатка.
А для надежности выделяем WebClient, нажимаем "Анализировать" и смотрим, где еще может ждать нас подлянка.
В Finish.Update() мы ошибку только что исправили, в Game1.LoadContent() исправили в самом начале, а Login.button1_Click(), Program.Main() и Title.Update() нас не интересуют, так как достаточно просто не делать того, что приведет к обращению в сеть (а конкретно: не открывать диалог входа, не указывать никакие аргументы при запуске и не пытаться авторизоваться через главное меню (вроде бы не реализовано)).
Числа Фибоначчи - это последовательность, где каждое очередное число равно сумме двух предыдущих. 1 = 0+1, 2 = 1+1, 3 = 2+1, 5 = 3+2, 8 = 5+3, ...
В программировании для вычисления этой и подобных последовательностей (факториалы, степени, если нет прямой возможности...) используются циклы.
Цикл представляет собой некоторые условия: начальные условия, условия завершения, изменение данных для достижения условий завершения. (Заметка для тех кто в теме, это касается любых циклов, а не только цикла for)
А вот то, что делается внутри цикла называется ТЕЛОМ цикла.
Большинству пользователей известно, почему возник Вомбат. На этом я не стану заострять внимание.
А хочу обратить внимание на сложность разработки подобных сайтов. Даже на уровне MVP (минимально жизнеспособный продукт).
Даже если есть готовые наработки, то в любом случае надо продумать: модель, то есть схему базы данных, дизайн веб-морды, общее взаимодействие, то есть бизнес-модель,...
Независимо от всего разрабатываю своего бобра (это скорее будет что-то вроде Хабра, а не очередной аналог Пикабу). И уже раз десять сносил БД из-за невозможности внести изменения (миграции). Ну, может я такой рукожоп и просто не знаю как правильно сделать. 🤷♂️
Нужно продумать внешний вид сайта. Тут сильно помогает такой инструмент как Figma. Да, дизайнеры Figm-ой занимаются. 🙃После пары настроек пользоваться одно удовольствие. Но мало задизайнить. Надо это ещё и в код перенести. А это уже вёрстка. Довольно нудное занятие.
В общем, чего хотел сказать, захотите сделать свой Пикабу с блекджеком и плюхами, то готовьтесь к тому, что заебётесь...
Всем доброго здравия, уважаемые вомбатовцы (я не знаю, как у вас тут принято обращаться друг к другу, надеюсь, никого не обидел).
Кто-то из вас знает, кто-то нет, но где-то месяц тому назад мы весьма немаленькой компанией были вынуждены покинуть насиженное место. Некоторых из нас, в частности меня, заклеймили злодеем вором сайтов, но я человек простой, мне во всей этой грязи возиться лень, я пару раз какашкой кинулся. да самоустранился. «Собака лает, караван идёт».
В итоге что получилось? Решили мы написать новый проект с нуля. Когда-то давно, ещё тогда, когда был большой исход с Пикабу (как раз когда и Вомбат появился на свет). Я решил написать свою площадку. В итоге довёл её за пару месяцев до состояния, что этим можно было пользоваться, но потом работа отъела ресурсы, а когда решил вернуться опять к проекту, уже вышли в открытый доступ варианты Того же Вомбата и Капибары. Свой я отложил в долгий ящик (искренне был уверен, что забросил навсегда).
Но тут вот как получилось.
Использовать код того проекта мне показалось не самой хорошей идеей, так как и технологии устарели и я чуть умнее стал, поэтому начал писать с нуля, но оглядываясь на опыт прошлой писанины.
В качестве основы был выбрал фреймворк Laravel. Почему? Да потому что я его знаю вдоль и поперёк и на нём подобные штуки поднимаются достаточно легко и быстро.
Большой вопрос по фронтэнду. Я вроде как фулстек, но JS фреймворки, аля VUE и React считаю излишеством, тем паче, что открытого АПИ у проекта всё равно нет и не планируется. Поэтому решил совместить удобное с быстрым. И тут мне под руки попалась такая интересная фигня (ну как попалась, чат ГПТ мне её посоветовал. Говорит: «Потыкай в неё палочкой, перспективная фигня»), как Livewire.
И вот, совместив стандартный ларавелевский шаблонизатор, вот эту вот вундервафлу и обычный ванильный JS, удалось собрать проект буквально за пару недель. Пришлось правда отложить все прочие свои поделки, но тут уж как получается 🤷♂️
Ещё две недели активный тестов небольшой, но очень умелой группой пользователей. И вот сегодня я открыл сайт для общего польования.
Согласно пункту правил 10, я, вроде как могу тут делиться ссылками на свои ресурсы, но с другой стороны, этот ресурс не полностью мой, там за ним целая толпа стоит. Поэтому не буду гневить ВомбатоМодераторов. Тем более, что цели переманить людей отсюда к нам у меня нет, я просто хвастаюсь ☺
Что могу сказать по итогу: задачка интересная. И работы предстоит по её доделыванию уйма, но и опыт это прям хороший. Особенно по работе с БД. Так что, готов ответить на вопросы, поделиться, если у кого-то есть что спросить. А если кто-то вдруг ещё и шарит в описанных мной технологиях, буду раз помощи)
Linux, а для меня основная ОС для дома (удевлены?), является продолжателем самых первых операционок семейства UNIX, фактически - родоначальника всех ОСей. И есть в ней такая замечательная "либа", некий аналог виндовой DLLки. Практически во всех линухах (и частенько в MAC-OS/Windows) не только библиотека есть, но и её обёртка, что бы можно было использовать в командной строке.
Так вот, разраба данной библиотеки хакнули, причём конкретно его самого. Это наивысшый уровень социального инжиниринга.
Т.к. его библиотека была распростанённой, и достаточно эффективной, то хакеры решили "хакнуть" большинство дистров на базе Linux одним махом.
Там была цельная спецоперация.
Сначала долбили разраба фич-реквестави (пожелалками) - продолжалось енто пару лет. Потом появляются коммитеры, которые енти фичи реализуют. Их количество росло, а чел (ну не буду я его ФИО писать, что блы поисковики его опять пердолили ссылаясь на ентот пост - не хочу), просто устал, выгорел. И дал права над исходниками левому чуваку, который был наиболее активен среди коммитеров.
Результат не заставил себя ждать - в качестве очередного патча, в библиотеку был внедрён бэкдур, позволяющий получить права root.
Так будет тост: "Не давайте прав кому либо выше Ваших! Программисты - вы боги-создатели своего кода".
Нуссс. Начну из далека. На заре вычислительной техники народ довольствовался механическими вычислительными машинками. Не буду на них останавливаться - это тема для длинющего поста, начиная с камешков на берегу и заканчивая машинкой "Энигма".
Потом, почти одновременно, появились аналоговые вычислительные машины - АВМ. Они имели крайне узкую направленность, и грубо говоря пытались электронными, а зачастую и физическими методами повторить некий физ. процесс и получить результат/расчёт того или иного физ. процесса. Головоломка уже не для программистов, а для физиков и математеков.
штЫрьками собираем схему для нужного вычислеия - соединения подключают различные блоки интеграторов, делителей, усилителей пр. Всё должно быть выполнено очень точно. Это пик развития аналоговой электроники
Про квантовые компы я промолчу тихи в тряпочку - сколько ни читал - муть мутная и мне непонятная. Стар я видать стал, а может стал и СуперСтар...
Говорить я буду о привычных компах - которые работают с единичками и ноликами (троичные погибли, хотя в СССР были нехилые наработки). Вроде всё норм, но комп, процессор, понимает как раз енти нули и единички. Ещё на этапе проектирования железяки народ понял енту проблему и начал объединять их в группы, кратные степени двойки. Так появился байт (2^3 = 8), а потом и слово (2^4 = 16). И тут же различные производители железа придумали первый геморрой для программистов - порядок следования байт в слове. Little-Endian и Big-Endial - эти слова знакомы каждому сетевику и привычно вызывают когнитивный диссонанс...
Но ладно, вернёмся к ассемблеру. На заре программирования под цифровые процессора народ вбивал программу тумблерами - битик за битиком. Производительность труда была на наименьшейшем уровне, а вероятность щёлкнуть не тот битик самой большой.
Нервы не выдержали и мисс Кэтлин Бут решила облегчить себе работу, создав первый в мире ассемблер для ARC2 (военный авиационный вычислитель).
Эту идею подхватили остальные. Что самое интересное - на языке Ассемблера можно добиваться максимальной производительности (самая популярная библиотека ffmpeg имеет множество ассемблерного кода под разные архитектуры) - выжать из процессора все соки. Такое же происходит в различных математических библиотеках.
Вроде всё с ассемблером хорошо, кроме одного - там команды процессору заменены на мнемокоды - краткие символьные представления команды процессора. Писать программу стало удобно, понятно. Появились кросс-ассемблеры (низкий уровень llvm)
Но оснавная засада осталась - трудоёмкость программирования и половина башки программиста занята архитектурой компа (да-да, там не только про проц надо помнить, но и про периферию).
спёрто с https://blog.skillfactory.ru/glossary/assembler/
А что итого? Я понимаю, что питонисты нифига не поймут - они даже не понимают, что каждая питон-либа это просто обёртка под высокоэффективным кодом на Си. Ассемблер жив и будет жив, те же "тысяча строк" ассемблера под линуксом - это и есть основа ядра ОС. Потом уже можно писать на Си, Си++, даже Расте (чёт ОС растоманов зачахла - видать пришло понимание, что язык высокого уровня не может использоваться наравне с Ассемблером или СИ).
Язык Ассемблера, как продился, так и будет жив. Есть яыки программирования близкие к АСМу - тот же Си, или Форт, но никто не даст программисту той мощи и контроля за компом как АСМ. Тот же Си в своих диалектах везде имеет возможность АСМ-вставок.
Что же до программирования на АСМе - мне он нравится, но... Всегда надо иметь баланс в голове. Поверьте strncpy() из Си будет более оптимизирована чем ваша реализаций на АСМе, а вот к-либо формула, вычисляющая интеграл, которого нет в clib - лучше уже на асме, особенно если оно вычисляется в цикле..
О проекте: Пишем один код - собираем на разные 8 бит МК!
https://vm5277.ru - это универсальное решение для embedded-разработки, которое позволяет сократить время создания прошивки для 8 бит микроконтроллеров в разы.
Как это работает?
Пишешь код на Java подобном языке (чистое ООП, без головной боли с указателями и не читабельным кодом)
Компилятор автоматически генерирует оптимизированный ассемблерный код под выбранную платформу
Код работает поверх легковесной RTOS, написанной на ассемблере для максимальной производительности
Ассемблер-сборщик финализирует проект в бинарный файл прошивки
Что входит в решение:
Высокоуровневый компилятор с Java подобным синтаксисом
Максимально оптимизированная RTOS, целиком на ассемблере для каждой платформы
Унифицированные драйвера - один API для UART, SPI, I2C, GPIO на всех МК
Стандартные библиотеки (Runtime): Базовые типы данных (Float, String), математические функции (Math), работа с памятью и другие абстракции, не зависящие от платформы.
Драйверы верхнего уровня (High-Level Drivers): Унифицированные API для работы с периферийными устройствами: датчиками (I2C/SPI), дисплеями, SD-картами, беспроводными модулями (ESP8266, Bluetooth).
Ключевые преимущества:
Скорость разработки: Создавайте функционал на высокоуровневом языке быстрее, чем на Си
Производительность: RTOS и драйверы оптимизированы до уровня чистого ассемблера, экономя каждый байт и такт
Переносимость: Переход с AVR на PIC или STM8 - дело нескольких правок, а не изучение и адаптация библиотек
Проект находится на ранней стадии, но я активно над ним работаю. Уже можно видеть, как высокоуровневый код на Java-подобном языке превращается в чистый и эффективный ассемблер! Это ещё не итоговый вариант, но прогресс уже есть.
Что уже работает в этом примере:
Наследование интерфейсов: Test implements Number
Динамическое выделение памяти: оператор new
Полиморфизм: вызов метода через переменную интерфейсного типа (Number.toByte())
Проверка типа во время выполнения: оператор is (аналог instanceof)
Полноценная работа с объектами: конструкторы, методы, поля.
Интеграция с RTOS: вызов системных функций (System.out).
Исходный код custom.j8b
Итоговый ассемблер код(стр. 1, черновой, могут быть ошибки)
Итоговый ассемблер код(стр. 2, черновой, могут быть ошибки)
Также приведу одну из функций RTOS(код сырой, может содержать ошибки)
1. Метаданные класса: Компилятор автоматически формирует структуру для поддержки RTTI (Run-Time Type Information), необходимую для instanceof.
2. Динамическое создание объекта в куче: Код конструктора new Byte(0x08) транслируется в вызов менеджера динамической памяти (os_dram_alloc) и инициализацию полей.
3. Проверка типа (is / instanceof):
Оператор if(b1 is Byte) компилируется в вызов процедуры j8bproc_instanceof_nr, которая проверяет метаданные объекта.
4. Полиморфный вызов метода:
Вызов b1.toByte() через интерфейс Number преобразуется в универсальный механизм поиска и диспетчеризации метода.
5. Интеграция с системными сервисами:
Вывод в "консоль" (System.out) — это вызов системного сервиса ОСРВ.
Что это значит?
Это доказывает, что подход vm5277 работоспособен. Мы можем писать на высокоуровневом ООП-языке, а под капотом получать код, который:
Эффективно использует память: объекты размещаются в куче, метаданные компактны.
Сохраняет производительность: ключевые операции (выделение памяти, проверка типов) вынесены в оптимизированные ассемблерные процедуры.
Является переносимым: этот же Java-код, после завершения работы над платформами, сможет работать на PIC и STM8 (ограничений почти нет).
Многие новички в программировании совершают эту ошибку — пытаются написать всё и сразу. Говорю им, что нужно разбивать на подзадачи и писать маленькими порциями — всё равно пишут всё сразу, а потом ищут ошибки сразу по всему коду. Вместо того, чтобы отлавливать их на маленьких кусочках.
Начинающим вообще каждые 2-3 строчки лучше запускать. 10 строк кода уже много, уже тяжеловато в них ориентироваться. Но вот пишут, делают ошибки, искать их и исправлять ещё не умеют, поэтому при исправлении часто только ещё сильнее ломают... Сколько мучений на ровном месте.
Одна из частых ошибок молодых программистов — экономия времени на чтении. Читают текст недостаточно внимательно, не понимают, что требуется, как именно должен быть оформлен результат — и создают вообще не ту программу, тратя кучу времени.
Читать нужно раз 5. Даже если там 2 строчки — 5 раз хотя бы. Тогда шансов так ошибиться гораздо меньше.
Всем, кто захочет покичиться своими знаниями лингвистики, рекомендуется ознакомится со словарями и комментариями к посту
Наверное невозможно найти человека, который бы никогда не встречал программ на Java. Сначала телефоны с J2ME, потом Android, который использует свои варианты реализации JVM и свой байткод. Даже многие сим-карты внутри используют свою оптимизированную разновидность Java.
Даже JavaScript обязан своим названием популярности Java, хотя и не имеет с ней прямого родства. (При этом некоторый промежуток времени существовали Java-апплеты)
3 миллиарда устройств уже 18 лет исполняют программы на Java
По официальной информации, 23 мая 1995 года, ровно 30 лет назад, Sun Microsystems выпустили первую бета-версию Java. Первая полноценная официальная версия JDK 1.0 выйдет только 23 января 1996.
Изначально язык должен был называться "Oak" (дуб, который рос рядом с офисом разработчиков), после чего был переименован "Green" (зеленый), а потом в честь кофе с острова Ява. А в качестве целей языка ставились:
Простота, объектно-ориентированность и знакомость.
Надежность и безопасность.
Независимость от архитектуры и переносимость.
Высокая производительность.
Интерпретируемость, многопоточность и поддержка динамического программирования.
Синтаксис был практически полностью позаимствован из C++, который уже был знаком многим программистам. Были выброшены процедурные артефакты C в виде "бродячих" глобальных переменных и функций, всё должно принадлежать классам. Также выбросили некоторые "неудачные" решения C++, к примеру дружественные классы, множественное наследование (его заменили интерфейсы), перегрузку операторов (зачем?!) и еще немного. Вместо ручного управления памятью было решено внедрить сборщик мусора, чтобы облегчить разработку и снизить количество ошибок и уязвимостей.
Главной особенностью языка стала концепция "напиши единожды, запускай везде" (хотя правильнее это будет назвать "напиши единожды, отлаживай везде" из-за различных реализаций и окружения), возможная благодаря тому, что код сначала компилируется в стандартизированный и независимый от процессора байткод для виртуальной машины (JVM), а в машинный код переводится только на машине потребителя, учитывая её архитектуру и особенности. Получается что-то среднее между интерпретируемым и компилируемым языком.
Вместо Swift там должен быть Rust
Язык получился... интересным.
То, что планировалось простым, в итоге превратилось в многословное нечто, а в сочетании со всякими архитектурными извращениями стало притчей во языцех о громоздкости корпоративного стиля. Потом появились совместимые с JVM альтернативы по типу Kotlin, ибо Java старательно игнорирует и не вводит ничего, что бы могло замазать многословность и неудобство. Android давно официально рекомендует использовать его вместо Java.
Добиться высокой скорости на интерпретируемом языке со сборкой мусора тоже проблемно. JIT замедляет запуск и жрет память, AOT поддерживается плохо и с ним невозможно реализовать некоторые вещи (кодогенерацию, к примеру), плюс остается сборщик мусора. Плюс некоторые не очень удачные решения и ограничения, тоже не способствующие высокой производительности (видно на контрасте с C#, в котором их исправили).
Тем не менее, Java на очень долгое время стала самым востребованным языком и отлично продолжает жить и сейчас, находясь на 4 месте популярности по версии TIOBE и дважды становясь языком года (в 2005 и 2015).
«Языки делятся на те, которые все ругают, и на которых никто не пишет» - Бьерн Страуструп
Хотел написать про работу системы управления памятью, но понял, что для полноценной статьи у меня не хватает практических знаний. Поэтому кратко
Куча
Баян
Помимо стека, о котором было рассказано в прошлой статье и который содержит локальные переменные, в "потребительской" программе в среднестатистической ОС еще бывают глобальные переменные и динамическая память. Глобальные переменные описываются внутри сегментов .bss и .data, резервируясь в памяти сразу после загрузки образа программы (по-правильному это называется процессом).
Динамическая память иначе называется кучей, так как представляет из себя кучу динамических данных, который могут появляться и исчезать во время работы.
Для работы с динамической памятью в C существует malloc(size_t), запрашивающий (выделяющий, аллоцирующий) у ОС кусок памяти указанного размера и возвращающий указатель на первый байт, и free(void *), принимающий этот указатель и возвращающий (освобождающий) кусок назад в ОС.
В C++ есть операторы new и delete, они устроены гораздо сложнее и я не буду их рассматривать, ибо не пишу на нем. Помимо запроса памяти у ОС они занимаются ее инициализацией.
Если по какой-то причине стандартной библиотеки языка в наличии не имеется, то в Windows за память отвечает HeapAlloc и устаревшие GlobalAlloc и LocalAlloc из времен Windows 3.11 (malloc к ним и обращается внутри. Еще есть парные HeapFree, GlobalFree и LocalFree). Они предоставляют больше контроля над результатом, но проприетарны для Windows. А полный список функций для работы с памятью в WinAPI есть здесь, их сотни на любой случай жизни. Можно даже внутри контекста чужого процесса выделить или освободить кусок памяти (обычно такое используется вирусами и прочей нечистью).
Утечки
А что будет, если постоянно выделять, но не освобождать? А еще лучше: выделять, но терять адреса выделенной памяти. ОС достаточно быстро подскажет правильный ответ.
В теории есть несколько возможных выходов, можно даже обработать подобный случай и, к примеру, экстренно освободить всё выделенное (только если не терять указатели) или попытаться спасти нужные данные из разваливающейся программы, но если никаких действий не предпринять, то ОС заставит процесс экстренно прекратить свою деятельность и существование.
Случай с потерей указателей на выделенную память называется утечкой памяти и ее последствия необратимы. Но как только ОС завершает выполнение процесса, то вся его память, даже утекшая, становится снова свободной.
Ленивые программисты и мусоросборка
Отслеживание правильности выделения и освобождения памяти является достаточно сложной и опасной задачей, особенно если требуется обеспечивать стабильность и безопасность (лучше даже не думать, что будет, если рукожопый программист забьет на MISRA и устроит утечку памяти в подсистеме тормозов в Тесле), и практически невыполнимой в некоторых парадигмах (особенно в функциональном программировании). Поэтому еще с древнейших времен (с 1959 года) существуют языки со сборкой мусора:
Особенности
Первое: в таких языках указателей или нет вообще, или они очень ограничены в пользовании (C#).
Второе: для работы с динамической памятью вместо указателей применяются ссылки. Ссылка по факту представляет из себя тот же указатель, только очень сильно ограниченный. Над ссылками запрещены все операции, кроме присваивания значения другой ссылки и разыменования, в то время как указатели имеют права и возможности обычных целых чисел.
Третье: Каждый выделенный кусок памяти всегда занят каким-то объектом.
Четвертое: такие языки всегда тянут за собой подсистему сборки мусора, которая самостоятельно учитывает каждый объект и выделенный под него кусок памяти, а еще подсчитывает количество активных ссылок на каждый подчиненный объект. Это называется подсчетом ссылок.
Пятое: как только процент использованной памяти переходит через порог, то запускается сборщик мусора, который сканирует все объекты и количество существующих на них ссылок. Если ссылок на объект нет (то есть он "утек"), то его память освобождается.
Минусы и проблемы
Дорого. При злоупотреблении сборщик мусора может запускаться слишком часто, а каждый его запуск вносит заметный лаг. Особенно критично это в играх.
Он не устраняет все возможные способы вызвать утечку памяти, к примеру просто выделяя бессмысленные данные, но сохраняя на них ссылки. Или если объект, который управляется сборщиком мусора будет содержать в себе то, что сборщиком мусора не управляется (открытые файлы или нативная память вне его подчинения).
А еще он изредка может удалять то, что удалять не нужно, поэтому появляются костыли уровня GC.KeepAlive() (а-ля самый маленький метод в C#).
Но несмотря не это, практически все современные языки используют сборку мусора, кроме C++ и Rust. C++ по большей части перешел на умные указатели, которые тоже считают количество активных ссылок, но делают это без сборщика мусора. А Rust использует свой уникальный Borrow Checker, который понимают только растофилы.
Не рекомендуется принимать данную статью близко к сердцу или как истину в последней инстанции. Скорее как научпоп-грелку для мозгов. Тут довольно много упрощений, упущений или просто странно написанных моментов. Если что-то объяснено совсем криво, то добро пожаловать в комментарии.
Все сказанное далее применимо везде, но детали описаны для защищенного режима x86 (только я опустил префиксEдля регистров) и языков семейства C (в основном C и C++, местами C#). Для понимания рекомендуется знать про указатели и базово представлять, что происходит в процессоре.
Приятного чтения.
Думаю все помнят, что такое функция в математике
Начнем со сложного
Глоссарий
Чувствую, что большинство не имеет ни малейшего представления об указателях
В части про x86 я упоминал о том, что процессор представляет из себя бешенный самоуправляемый калькулятор, которому указом может быть разве что хранилище кода, аппаратный сброс и немаскируемое прерывание. И что у него есть системная шина, состоящая из шины адреса, шины данных и шины управления, к которой подключена память, хранящая данные и код.
А память это огромная одномерная полоска, в которой каждая ячейка имеет свой порядковый номер от 0 до 2^(разрядность шины адреса)-1. И указатель представляет из себя самую обычную численную переменную, хранящую номер (адрес) любой ячейки.
Я не понимаю, почему для многих это настолько сложная тема, а существенное количество новичков забрасывают изучение C после встречи с ними. Они в своей сути максимально элементарны (до тех пор, пока не нужно исправлять уязвимости и ошибки, которые появились в результате злоупотребления или неполного понимания нюансов).
Также существуют ссылки, так или иначе представляющие из себя подвид указателей, но обычно с запретом на арифметику с ними (основная сложность и опасность указателей) и/или подсчетом количества активных ссылок для сборки мусора.
Все языки или имеют указатели (Pascal, C, C++, блоки unsafe в C# и Rust), или являются ссылочными (Java, C#, Python, JS, т.д. (почти все современные языки)). С++ тоже имеет ссылки, но как синтаксический сахар над указателями, призванный упростить их передачу в функции и избавиться от проблемы нулевых указателей.
Ассемблер - если вы даже примерно не знаете, что это такое, то первую часть статьи вероятно можно пролистать и перейти на обсуждение парадигм.
Регистр - именованная численная переменная внутри процессора. Может иметь особое предназначение, а может просто использоваться для хранения любых чисел и арифметики. Их немного.
Инструкция - одиночная команда, представляет из себя последовательность из нескольких байт со специальным значением. Любая программа представляет из себя последовательность инструкций внутри памяти. Указатель на инструкцию, которая будет выполнена следующей, содержится в регистре IP, который сам увеличивается после выполнения каждой инструкции. По-хорошему стоило нарисовать пошаговую наглядную анимацию с демонстрацией всех регистров и куском ассемблерного кода, но мне лень, а в гугле я ничего толкового не нашел.
Метка - константа, содержащая адрес чего-либо. К примеру процедуры или глобальной переменной. Примерно как метка для goto в высокоуровневых языках, но универсальнее. Применяются при написании на ассемблере, в процессоре как таковые не существуют и разрушаются до обычных чисел при ассемблировании и линковке.
Разыменование - операция над указателем, когда тот превращается в переменную с адресом, который был записан в указателе (своеобразный пульт Д/У для переменной).
Парадигма - "стиль" написания и постройки архитектуры программы, частично определяется языком. Технически на том же C можно писать в практически любой парадигме (через костыли можно писать в стиле ООП, через макросы реализовать метапрограммирование, а через нестандартные расширения вообще ядерный бред), но родная для него - процедурная. А Go, к примеру, хоть и имеет недоклассы и методы, но лишен практически всех благ ООП и не сильно далеко ушел от C.
Синтаксический сахар - необязательная возможность, которая сокращает количество кода, повышает его читаемость или удобство поддержки
Стек
Большинство процессоров Фон-Неймановской архитектуры в своей конструкции предлагают механизмы стека. Кто играл в покер должны вспомнить стеки фишек. То есть некие значения, сложенные друг на друга (обычно это переменные, в частности адреса в памяти). При этом основными операциями являются добавление фишки на вершину (инструкция PUSH) и ее снятие (инструкция POP). Еще можно косвенно читать и заменять (перезаписывать) фишки относительно вершины (на вершину указывает регистр SP, неявно обновляется через POP и PUSH) или основания (указывает регистр BP) вглубь.
Стеки можно переключать (но не в MOS6502), это нужно для многозадачности
Помимо хранения локальных переменных стек позволяет делать довольно интересную вещь: мы можем положить на стек все необходимые аргументы (x в математике, но их может быть несколько), адрес следующей инструкции (взяв из указателя инструкции IP, это будет адрес возврата), после чего совершить прыжок на какой-нибудь другой адрес (сохранение адреса и прыжок делаются инструкцией CALL).
А на этом адресе может быть функция. Сначала она кладет текущий BP на стек, после чего приравнивает основание к вершине (BP к SP), тем самым создав для себя "новый стек" сразу после предыдущего (это еще называется стековым кадром), в котором якобы лежит только значение старого основания стека (BP), чтобы можно было восстановить его перед возвратом.
Серое это стековый кадр от предыдущей функции
После чего функция может прочитать переданные ей аргументы относительно основания своего стека вниз (технически это будет выход за границы текущего стека), выполнить с этим какие-либо действия (записать в файл, вывести в консоль, просто перемножить) и сохранить результат (обычно результат сохраняется не на стек, а в регистр AX).
После чего функция восстанавливает старое значение BP, сняв его со стека, и выполняет команду RET, которая снимает со стека адрес возврата и совершает переход на него, тем самым переключившись на инструкцию сразу после CALL.
И эта система так или иначе перекочевала в большинство высокоуровневых языков начиная с FORTRAN. К примеру в C CALL превратился в круглые скобки, RET в return, а адреса и метки в имена (при этом без скобок они все еще являются указателями, то есть адресами).
Пример кода (к сожалению, штатного форматирования не предусмотрено):
#include <stdio.h> int pow2(int x) { return x * x; } int main() { int y = pow2(16); // Вызов функции. В y будет сохранено число 256 printf("%p", pow2); // Без скобок вместо вызова просто выведет адрес функции return 0; // Возврат нуля из главной функции означает отсутствие ошибок. }
Такой стиль программирования называется процедурным (выделение кода в блоки называется структурным). А вот называть процедурный язык функциональным совершенно неправильно, ибо функциональное программирование ≠ процедурное, они даже в разных категориях (императивное и декларативное).
А теперь скомпилируем этот код и разберем ассемблерный листинг
Важно понимать, что стек в большинстве архитектур традиционно растет от больших адресов к меньшим (и в x86). То есть для того, чтобы отодвинуть его вершину вверх, от регистра SP нужно отнимать значения, а вот для ужимания и съедания ненужных значений к указателю на вершину значения прибавляют. И для доступа к значениям относительно основания или вершины это тоже важно учитывать. Это может звучать запутанно, но через время привыкаешь и всё становится очевидным.
Надеюсь это возможно будет разобрать. Код скомпилирован MSVC v19.28 для x86 со стандартными настройками в Godbolt
Post scriptum
Это всё довольно упрощенно. Как минимум, в защищенном режиме используется больше 5 разных соглашений о вызове, которые отличаются деталями реализации. Это было описание для cdecl, обычно используемого в C. Еще часто используются соглашения pascal, fastcall, thiscall, winapi и другие. Fastcall, к примеру, избегает хранения аргументов на стеке, если их возможно передать через регистры, что улучшает производительность. А winapi отличается от cdecl тем, что функция сама очищает стек от аргументов для себя при возврате. А еще я упустил, к примеру, сохранение регистров, которые функция может перезаписать и испортить, а потому обязана предварительно сохранить и перед возвратом восстановить, передачу переменного количества аргументов (как в printf) и возврат значений шире 32 бит (которые не влезут в EAX).
Плюс сейчас мало кто компилирует ПО под защищенный 32-битный режим, а в длинном режиме (AMD64) используется пара других соглашений, основанных на fastcall и имеющих несколько отличий друг от друга.
Так процедура или функция? Или подпрограмма?
Процедурное программирование предлагает делить код на подпрограммы, которые принято называть функциями и процедурами (функция обычно является наиболее понятным, частым и обобщенным названием, поэтому я его использую).
Процедура от функции отличается только тем, что функция возвращает какое-то значение (как в математике), а вот процедура этого не делает. Не во всех языках явно есть процедуры (в Pascal есть, но не в C). В таком случае их заменяют функции, возвращающие ничего (void, Unit, undefined, None).
Хотя и тут есть свои особенности. К примеру функция, возвращающая void в C и Java является прямым аналогом процедур, как-либо использовать возвращенное значение из такой функции невозможно, ибо его нет физически. А вот Unit в Kotlin это синглтон (а-ля единственная и уникальная константа уникального типа), ссылку на который можно присвоить в переменную, но в этом особого смысла нет. Undefined в JS и None в Python тоже уникальные константы специальных типов.
Но не тут-то было
Вроде бы процедура никогда не может ничего вернуть...
Только она этого никогда не делает напрямую. При она этом может записать результат в глобальную переменную, а еще часто принимает в себя указатели или ссылки, по которым может записать результат. Это еще удобно тем, что можно "вернуть" несколько значений. Пример:
void procedure(int x1, int *x2, int *x3) { // Функция ничего не возвращает, то есть это процедура *x2 = x1 * x1; // Разыменовываем указатель и записываем по его адресу результат. *x3 = x1 * x1 * x1; // Разыменовываем другой указатель и записываем по его адресу результат. } int y1, y2; procedure(16, &y1, &y2); // В y1 оказался результат, аналогичный прошлому примеру. А в y2 куб числа.
PS: оператор звездочка при указании типа превращает его в тип-указатель, а при применении на переменную-указатель разыменовывает ее до изначальной переменной. Амперсанд превращает переменную в указатель на нее (иногда еще называется оператором получения адреса).
То есть мы вернули сразу 2 разных значения из процедуры, которая якобы ничего не возвращает. Чудеса. Подобные чудеса есть в том числе в Pascal с явным делением на процедуры и функции (плюс там это сделано немного удобнее). Хотя механизм тут отличается от того, который используется в возврате значения из функции и совпадает с механизмом передачи обычных аргументов, поэтому никакой магии.
Еще про связь с математикой
Главное отличие функций в программировании от функций в математике в том, что они могут делать что-то на стороне и не обязаны возвращать одинаковый результат при одинаковых аргументах.
К примеру функция получения случайного числа по определению не может существовать в математике, если она не принимает в себя предыдущее случайное число или зерно для его видоизменения. Или функция записи в файл, возвращающая 0 в случае успеха и другое число при провале. Ко всему прочему, такая функция имеет побочный нематематический эффект, то есть запись в файл, что тоже недопустимо традиционной математикой без высоких абстракций.
Поэтому придумали чистые функции. По сути это ограничитель, которые делают функцию полным отражением таковой в математике. Им запрещено возвращать разные значения при одинаковых аргументах (точнее запрещено всё, что может такое позволить сделать), запрещено обращаться к нечистым функциям, запрещено обращаться к тому, что не является аргументом или локальной переменной, запрещены вообще любые действия, которые могут сделать что-то на стороне (даже функция sin() в C не всегда является чистой, ибо может зависеть от состояния FPU).
Чистые функции через ключевое слово pure явно есть в D и FORTRAN (проверка на чистоту во время компиляции), а также являются основой функционального программирования.
Чистая процедура тоже имеет право на жизнь, используя механизм со ссылками (на счет указателей не уверен из-за возможности арифметики над ними).
Функциональное программирование
Это очень сложная категория, которую постоянно путают с процедурным программированием. А еще это де-факто противоположный стиль: декларативный. Традиционное императивное программирование детально описывает процесс получения результата, а декларативное сам результат, без деталей реализации (хотя разделение обычно довольно нечеткое). При этом второй типичен для языков разметки типа HTML и CSS. То есть, условно, как одна и та же операция могла бы выглядеть в императивном и декларативном стиле:
document.tags.A.color = "blue" /* Императивный (JSSS). Сделать ссылки синими */
a { color: blue } /* Декларативный (CSS). Ссылки должны быть синими */
Почувствуйте разницу.
И функциональное программирование я никогда не изучал и слишком мало о нем знаю. Так что готовьтесь к ошибкам и не воспринимайте всё за чистую монету.
Внутри чистого функционального программирования
Основано полностью на математике, все функции обязаны быть чистыми. Операция присваивания запрещена (разрешены константы), переменных в привычном виде нет. Прикольно? Очень!
Во многих процедурных языках функции и процедуры являются объектами второго класса (не путать с классами из ООП), что не позволяет их свободно присваивать в переменные, передавать как аргументы в другие функции или возвращать из них (только через указатели). Функциональные языки расценивают функцию как объект первого класса, то есть их можно, а часто нужно передавать в другие функции напрямую.
Это дает некоторые преимущества, особенно в плане безопасности и при работе с многопоточностью (по причине неизменяемости данных и отсутствия глобального состояния), но вся концепция имеет один фатальный недостаток: вы мало чего полезного можете сделать, ибо что ввод, что вывод являются математически нечистыми, а потому запрещены. Вот такое вот гениальное изобретение безумных математиков.
Функциональное и процедурное программирование. Холст, масло
Каждый чисто функциональный язык выкручивается из этого по-своему, к примеру через монады. Это позволяет им существовать вне шуток и даже использоваться на практике.
Это не все особенности функциональных языков, но одни из самых важных. Самый известный такой язык: Haskell. Функциональные F#, Lisp, ML и многие другие не являются 100% чистыми.
Смешанное функциональное программирование
Последнее время часто используются смешанные языки, к примеру вместе с процедурным или объектно-ориентированным программированием, что избавляет от ограничений математики, но дает гибкость в том, что функциями можно оперировать как с любыми другими типами данных, а еще дает много очень удобного сахара вроде замыканий и лямбд. Это C#, Python, JS, частично Java и C++ (в них нужны костыли в виде интерфейсов из одного метода или оберток над указателями).
Особенности
Функции как объект первого класса. К примеру в C# это реализовано через систему делегатов, которые представляют из себя тип-обертку для функций:
Action<string> printer = Console.WriteLine; // Action<string> - тип-делегат. Неявно создаем его объект и присваиваем туда функцию printer("Hello, World!"); // Вызываем функцию через делегат
Локальные функции: как обычные, только вложенные в другую функцию (объявленные внутри нее)
void Func1() { // Глобальная функция void Func2() { // Локальная функция Console.WriteLine("В локальной функции"); } Func2(); }
Лямбды: возможность объявить безымянную функцию посреди кода (часто удобнее локальных)
var pow2AsLambda = x => x * x; // => - оператор лямбда-выражения pow2AsLambda(5); // Вернет 25
Замыкания (можно использовать вместе с лямбдами и локальными функциями):
int someValue = 42; var pow2AsLambda = x => x * x + someValue; // someValue будет захвачено замыканием, хотя напрямую не передано pow2AsLambda(5); // Вернет 67
Особенность замыканий в том, что они могут захватить локальную переменную родительской функции внутрь себя, продлевая ей время жизни за пределы блока с кодом. После чего такую лямбду можно передать в другую функцию, которая просто так не имеет доступа к someValue (в обычных условиях someValue вообще уже будет уничтожен), а вот переданная лямбда сможет ее прочитать или записать всегда и откуда угодно. И самое интересное то, что значение этой переменной будет сохранятся между вызовами к замыканию. То есть она становится глобальной, но видимой только из функции-замыкания.
ООП
Посмотрим на индекс TIOBE по популярности языков. Фиолетовым я пометил чистые функциональные языки. Языки, имеющие возможности, присущие функциональным языкам (смешанные) - синим, процедурным - красным, а объектно-ориентированным (ООП) - зеленым. Языки, в которых нельзя объявить функцию, принадлежащую самой себе или модулю, процедурными считать не совсем корректно (Java).
Не претендую на 100% точность
Видите фиолетовые точки? И я не вижу. А вот синих 12. В то же время красных и зеленых по 16. При этом реально применяемый чисто процедурный язык всего 1: С. Остальные имеют какую-либо встроенную поддержку других парадигм.
И сейчас практически всё большое ПО пишут в ООП. Ибо позволяет хоть как-то сдерживать структурированность кода после перехода с десятков тысяч строк к сотням (и миллионам).
Внутри
ООП является развитием идеи процедурного программирования. Проблема была в том, что помимо кода в программе существуют еще и данные, которые было бы неплохо связать с соответствующим им кодом. И через некоторое время после появления процедур появились структуры (они же записи в Pascal, не путать со структурным программированием).
Структуры были удобным способом объединить несколько переменных в единое целое. Пример:
typedef struct user_struct { char *username; int userid; int reputation; } user_t; user_t someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.reputation += 10; // Обращаемся к полю
(Вместе с этим объявляем структуру новым типом через typedef, что не делается по умолчанию в C, в отличии от C++. Один из ярких примеров несовместимости C и C++):
После чего user_t превращается в новый тип, как int или char. Можно создать полноценную переменную типа user_t (или массив такого типа), а потом обратиться к какой-нибудь ее части (полю) через оператор точки (.). Можно сделать функцию, которая будет принимать или возвращать переменную типа user_t (к примеру someuser). Можно сделать указатель на user_t, тогда для обращение к полю используется оператор стрелки (->). Или можно встроить переменную типа одной структуры в другую, это тоже не запрещено. Внутри структурная переменная представляет из себя последовательно слепленные поля в одно целое.
Если поля разного размера, то часто добавляют пустоты между полей в целях выравнивания по размеру наибольшего поля и улучшения производительности, но это сложная тема
Возвращаемся к ООП
Суть ООП в том, что помимо полей структуры могут содержать процедуры и функции. Такая структура называется классом, переменная класса - объект, а процедура класса - метод.
Метод принципиально ничем не отличается от любой процедуры или функции, кроме того, что неявно принимает в себя специальный аргумент this (в Python это делается явно, в некоторых языках вместо this может быть self). Он содержит в себе ссылку/указатель на объект класса, от которого был вызван. Пример:
class User { public: char *username; int userid; int reputation;
void print() { printf("Пользователь \"%s\" (%i). Репутация: %i\n", this->username, this->userid, this->reputation); // Постоянно писать this-> не обязательно. Если локальной переменной с таким именем нет, то будет произведен доступ к полю } }; User someuser = { "IvanKr08", 17002, 253 }; // Инициализируем значениями someuser.print(); // Вызвали метод, print() неявно получил в себя указатель this, который указывает на someuser someuser.reputation += 10;// Обращаемся к полю someuser.print(); // Теперь вывод поменяется
Да, никто не мешает объявить обычную функцию, которая будет явно принимать в себя указатель на User и ничего не поменяется, и так повсеместно делают в C (тот же WinAPI на этом построен целиком и полностью), но ООП на самом деле крайне сложная, большая и холиварная тема, которая развивалась на протяжении 60 лет в разных направлениях и которую поднимать тут глупо. Только опишу еще несколько разновидностей методов:
Статичный метод - как обычный метод, только не получает в себя this и вызывается от имени класса, а не объекта (не someuser.method(), а User::method()). Зачем это нужно и в чем отличие от просто функции? Статичный метод можно спрятать за инкапсуляцией, плюс он привязан к классу, а не болтается в глобальном пространстве имен.
Конструктор - автоматически вызывается при создании нового объекта. Может иметь аргументы, тогда обязан явно вызываться как функция с именем класса, т.е. User("IvanKr08", 17002, 253)
Деструктор - есть в C++. В C# называется финализатором и имеет несколько важных отличий. Автоматически вызывается перед уничтожением объекта (к примеру выход из области видимости, явное удаление оператором delete или удаление сборщиком мусора в C#). Не может иметь аргументов.
Свойство - есть в Delphi и C#, куда перешел от первого. С точки зрения синтаксиса это поле, которое можно читать и записывать, только вместо прямого обращения в память вызываются соответствующие методы (set и get). Пример: ... int SomeProperty{ get =>42; set => Console.WriteLine("Set"); } ... test.SomeProperty = 10; // Значение никуда не сохранится, будет выведено "Set" в консоль Console.WriteLine(test.SomeProperty); // Всегда будет 42
Перегрузка оператора - специальный метод, который позволяет переопределить стандартные операции для типа. К примеру нельзя сложить два объекта User или User и число, но если перегрузить оператор +, то можно будет определить свою логику для этого (не обязательно, чтобы оно что-то реально складывало. Это может быть любое действие).
Индексатор - синоним перегрузки оператора квадратных скобок. Объект начинает вести себя как массив.
Функтор - перегрузка оператора круглых скобок (иначе называется перегрузкой оператора вызова функции). Позволяет "вызывать" объект так же, как и функцию. Звучит запутанно, но можно погуглить. Есть в C++. Также бывает функтор в функциональном программировании, но он не имеет ничего общего с функтором в C++.
Виртуальная функция (метод) - самый интересный и сложный тип, составляет основу полиморфизма. Вкратце объяснить его невозможно, но одним из ключевых принципов ООП является наследование, то есть один класс (наследник) копирует в себя все поля и методы другого класса (родитель), и может добавить новые. И суть в том, что указатель (ссылка) на объект класса-наследника может быть присвоен в переменную-указатель родительского типа. А самое интересное то, что любые обращения (к примеру вызовы методов), сделанные через родительский указатель, будут вести себя аналогично обращениям, сделанным напрямую через указатель с типом наследника. Это называется полиформизмом. Но это было бы не слишком полезным, если бы не возможность объявить метод в родительском классе виртуальным, а в наследнике полностью переопределить его код.
И то, что я забыл
Послесловие
Если вы осилили эти 3000 слов и даже смогли что-то понять, то найдите и распечатайте себе утешительную грамоту. А я устал и пойду искать смысл жизни...
UPD: исправлены косяки форматирования, очепятки, добавлено еще пару пунктов про разновидности методов и функциональное программирование.
Не совсем i8086. Источник: http://ic.onidev.fr/map/AMD_9586A.html
Думаю, что ни для кого не секрет, что подавляющее большинство современных ПК используют архитектуру, которой скоро исполнится 50 лет. Ее современный вариант заметно отличается от того, что было в 1978 году, но при этом сохраняет практически полную двоичную совместимость (современные ПК без особого труда запускают MS-DOS, проблемы начинаются при работе с периферией). Я попытался собрать наиболее ключевые особенности, этапы эволюции и поколения архитектуры.
Вступление
1978 год. Произошло несколько политических революций, сменилось трое римских пап, открыли первый спутник Плутона, многие еще не родились, а Intel выпустили 16-битного наследника i8080: i8086, который в последствии практически полностью вытеснил другие архитектуры из потребительских ПК и стал серьезным шагом к стандартизации.
Рынок ПК на тот момент и еще в ближайший десяток лет был слабо похож на современный. Было много относительно бюджетных машин на MOS6502 (Apple I, Apple II, разные Commodore) и Z80 (ZX Spectrum), к середине/концу 80х начали появляться машины на заметно более совершенном и 16/32-битном Motorola 68K (тоже очень интересная архитектура), но общее у них было ровно одно: абсолютная несовместимость ни с кем и никак. Нет, появлялись +- совместимые между собой серии по типу Amiga или Macintosh, но они были проприетарными, а в конечном итоге загнулись (Amiga перерождалась, но в итоге умерла. Macintosh выжил только благодаря удаче и iMac, в последствии перейдя на x86 на много лет).
Причина: IBM-PC.
Про i8086
Во-первых, крайне краткий и упрощенный экскурс в работу наиболее типового процессора Фон-Неймановской архитектуры.
По факту процессор представляет из себя бешенный калькулятор, который последовательно выполняет различные операции и преобразования над числами, попутно управляя самим собой. Для этого у него есть
Многое поменялось, но суть осталась
1. Шина и память. По сути для процессора это одно и то же. Их можно представить как длинную полоску из нумерованных ячеек, способных хранить число от 0 до 255 (представьте себе швейный сантиметр), что не всегда верно (физически шина и память устроены сложно и разделены на много устройств, но при связи по шине они обычно превращаются именно в одномерную ячеистую полоску). Для чего нужна память понимают, наверное, все.
Вместе с этим там же обычно сидят периферийные устройства, к которым можно обращаться так же, как и к памяти. Пункт диапазонов памяти в диспетчере устройств Windows по сути и отвечает за то, как устройства делят эту шину между собой и памятью.
Шина состоит из шины данных, адреса и управления. Ширина шины данных это одна из ключевых характеристик, которая определяет битность процессора, еще от нее сильно зависит скорость работы с устройствами и памятью. По ней передаются, как ни странно, данные. Шина адреса представляет из себя индекс в полоске ячеек, по которому требуется произвести действие. А шина управления используется, к примеру, для выбора между чтением по адресу и записью.
2. Регистры. Маленькие именованные кусочки памяти внутри процессора. Самое быстрое и легкодоступное, что у него есть (современная разница в скорости по сравнению с памятью примерно как между взять карандаш из ящика (10 секунд) и поехать за ним в магазин (пол часа-час)). Бывают двух ключевых типов: общего назначения и особого (специального) назначения.
Первые обычно имеют размер машинного слова и используются как хранилище операндов для операций. К примеру 2 регистра могут использоваться как слагаемые, после чего в первый будет помещена сумма (особенность многих архитектур в том, что они не могут явно задействовать больше 2 регистров. Частое исключение - FMA). Ко всему прочему, регистры обычно выступают посредником при чтении/записи памяти (опять таки, зависит от архитектуры, но в некоторых вообще запрещены операции напрямую с памятью без предварительной загрузки всего в регистры, другие разрешают только один аргумент для операций брать из памяти). Таких регистров относительно мало, обычно от 4 до 32.
Название вторых крайне общее, ибо все они имеют абсолютно разные предназначения. Чаще всего встречается регистр флагов, в котором каждый бит отвечает или за состояние процессора, или за результат логической операции (для этого обычно есть инструкция CMP, которая вычитает одно число из другого (отбрасывая результат) и заносит в регистр флагов статистику: первое число было больше, меньше, равно, т.д. Потом этим может воспользоваться инструкция условного перехода). Еще есть стековые регистры и регистры, уникальные для архитектуры, но этот экскурс и так слишком длинный.
Самый важный и присутствующий везде регистр: указатель инструкции. Указывает, в какой ячейке памяти находится выполняемая инструкция. Самостоятельно увеличивается после выполнения каждой инструкции, но может быть явно перезаписан инструкцией условного или безусловного перехода на адрес (if-else в языках высокого уровня).
Если вы не закрыли пост, не уснули и за вами не приехала дурка, то продолжаем.
А теперь конкретно про i8086 и x86
20 бит шина адреса, то есть мегабайт ОЗУ, 16 бит шины данных, Фон-Неймановская архитектура, CISC, аппаратные деление и умножение, 4 16-разрядных регистра общего назначения (AX, BX, CX, DX), 8-битные регистры общего назначения, физически совмещенные с 16-битными (AL, AH, BL, BH, т.д. Делят на 2 части 16-битные регистры), 2 индексных (SI, DI. Для строковых операций), 4 сегментных (сегмент кода CS, сегмент стека SS, сегмент данных DS, дополнительный сегмент ES), 16-битный регистр флагов (FLAGS), указатель инструкции (IP). Защиты памяти (MMU) нет, полноценных механизмов многозадачности тоже.
i8088 отличался тем, что имел 8-битную шину данных и технически его можно было назвать 8-битным процессором. Это его замедляло, но зато с ним можно было построить более дешевую систему на старой 8-битной обвязке.
Сегменты
Пропущенная мною часть описания процессора, так как она присуще именно x86 по причине 20-битной шине адреса. Указатель инструкций 16-битный, все операции с памятью тоже 16-битные. Естественно, что 16-битным адресом покрыть все 20 бит адресного пространства было бы как минимум проблемно, как максимум невозможно. Но надмозговые инженеры Intel выход нашли: теперь у нас есть сегменты, а все операции с памятью локальны по отношению к ним. Это создало жуткий геморрой, особенно в высокоуровневых языках (3 типа указателей: ближние, дальние и огромные), но зато облегчило портирование старого ПО с i8080, всё адресное пространство которого влезает в 1 сегмент.
По факту сегмент представляет из себя смещение для логического адреса по отношению к физическому адресу. Значение сегментного регистра умножается на 16 (сдвигается на 4 бита) и прибавляется к логическому адресу для вычисления физического адреса, который будет выдан на шине адреса. Это приводит к тому, что у одного физического адреса появляется 16 логических "синонимов".
Если вы ничего не поняли, то это нормально. Никто не понимает, а потом приходит прозрение (и ночные кошмары). Я не знаю, как это нормально объяснить. У меня есть график, но я не уверен, будет ли он читаем и понятен
Блоки это сегменты, по горизонтали физическое адресное пространство (1 мегабайт), внутри блоков логический 16-битный адрес (64 килобайта, которые можно адресовать внутри сегмента). Вертикаль показывает наложение логических адресов на физические (те самые 16 "синонимов"). При этом в i80286 возможно переполнение и получение доступа к памяти за пределом 1 мегабайта
CISC и RISC
Это легко. CISC предлагает увеличенный набор инструкций взамен на сложность архитектуры и процессора. Время выполнения и длина инструкций может быть совершенно непредсказуемой, иногда встречаются конструкции из высокоуровневых языков (к примеру строковые операции в x86. Подсчет длины строки (strlen()) можно реализовать де-факто одной инструкцией). Удобно для написания на ассемблере, часто не очень удобно для разработчиков компиляторов. Вместе со сложностью растет энергопотребление. Это x86, i8080 и M68K.
RISC же предлагает упрощенный набор инструкций взамен их максимальной оптимизации. Все инструкции должны умещаться в строго одинаковое количество байт. Вместе с этим часто запрещено брать операнды из памяти и увеличено количество регистров. Часто запрещено обращаться к памяти без выравнивания по словам. Иногда даже нет операций деления и умножения, их приходится реализовывать программно. Типовые представители: ARM, RISC-V. MOS6502 можно в некоторой степени назвать RISC, но у него только 1 регистр и один аргумент он всегда берет из памяти (тогда так можно было делать, память была примерно равной по скорости с процессором).
Есть другие варианты, такие как VLIW или шуточные MISC, URISC и ZISC. В дикой природе не встречаются, только если VLIW у "Эльбруса".
А теперь IBM
Сюда хоть кто-то дочитал?
Как гром среди ясного неба начался 1981 год, а IBM представили свой IBM-PC, использовавший i8088. И знаете, получилось хорошо. Проблема была одна: дорого (зачем выкидывать пару зарплату на какой-то электронный гроб?). Но их покупали для бизнеса, покупали просто небедные энтузиасты, причем в больших количествах. Ожидания оправдались в 9 раз.
16-килобайтная версия стоила $1,500 (не забывайте про инфляцию, это около $5000 сейчас). Apple II с 4 килобайтами на момент выхода в 1977 стоил $1,298. Но, конечно, к моменту выхода IBM-PC Apple II успел подешеветь и нарастить память, хотя отставание в производительности было колоссальным. Но простенькие машинки Commodore были многократно дешевле и до, и после.
Amiga вышла сильно позже (1985) и в начале тоже стоила неприятно, но потом подешевела и нашла своих покупателей благодаря отличному звуку и графике. Пока IBM предлагал исключительно пищалку и ядовитый EGA (а то и малиново-голубой CGA) вплоть до 87 года за много тысяч, Amiga уже в 1985 предлагала вот такое (а еще графическую многозадачную ОС), а в 1987 делала это же за $800 в базовой комплектации. Очевидно, что покупал обычный человек себе домой, не искушенный бизнесом и работой в Excel.
А теперь главная ошибка IBM, которая их одновременно и погубила, и сделала IBM-PC стандартом: они не стали закрывать архитектуру за патентами и сделали ее крайне расширяемой. Любой мог прийти и купить за небольшую сумму всю необходимую документацию вплоть до исходников BIOS, после чего начать продавать свои платы расширения или вообще компьютеры целиком, полностью совместимые с другими IBM-PC. Причем делать это стали уже через год и очень активно. Так активно, что IBM обос... профукались и в итоге к 2006 году продали свой компьютерный бизнес от греха подальше.
Но статья у нас о поколениях процессоров, так что мы летим назад в 1982 год...
i80286
Технически существовал i80186 и i80188, но они совершенно неинтересны. Не могу сказать, чтобы и i80286 был сильно интересным.
Первое существенное отличие второго поколения x86: шина адреса теперь 24 бита, то есть 16 мегабайт. А в процессоре появился новый режим: защищенный. При этом режим, в котором работал i8086, стал называться реальным. Все последующие процессоры поддерживают все режимы предыдущих, и при этом всегда запускаются в реальном, даже спустя 50 лет. Помимо этого добавили сотню новых инструкций, в основном для работы с защищенным режимом, и нарастили производительность.
Суть в том, что в реальном режиме был коммунизм: все жили равно и ни у кого не было привилегий ограничений. Но это было крайне опасно, неудобно и мешало созданию полноценных многозадачных ОС, так как любая программа могла залезть в другую и что-нибудь ей поломать, а то и влезть и сломать ОС. Даже не обязательно специально. И на перспективе ограничение на объем ОЗУ начинало переходить из космического в потенциальную проблему недалекого будущего.
Защищенный режим на то и защищенный, что работает в связке с MMU, который позволяет разграничивать регионы памяти под разные программы и привязывать логические адреса к разным физическим адресам, тем самым позволяя реализовать виртуальную память, файлы подкачки и прочее.
Но были у защищенного режима фатальные проблемы. Ключевая: переключаться из реального в защищенный режим было легко, а вот из защищенного в реальный... ну можно было аппаратно сбросить процессор (попросив нажать пользователя на кнопку Reset). Ни о какой одновременной работе защищенного ПО со старым для реального режима речи идти не могло. Потом придумали подвести хитрую схему для программного сброса и вручную сохранять то, что будет при этом сбросе утеряно, но проблему полностью это не решало как минимум из-за серьезных тормозов и отсутствия должных механизмов для "кастрирования" от вредных привычек того, что хочет работать в реальном режиме.
Ничего хорошего из этого выйти не могло, так как всё старое ПО затачивалось под реальный режим с MS-DOS и не могло работать в ОС, использующих защищенный режим. А ОС без программ никому не нужна.
По поводу технической части мне сказать особо нечего, ибо я не работал с этим процессором и практически ничего не знаю о нем. Знаю, что осталась сегментация, но она работала абсолютно иначе и гораздо адекватнее.
I80386
Прошло 3 года. На дворе 1985 год, Intel учли свои ошибки и разработали новый вариант защищенного режима. Это самый интересный и второй по важности режим, который повсеместно использовался вплоть до конца 2000х и продолжает неявно использоваться до сих пор.
Во-первых, теперь процессор стал полностью 32-битным. Поверх старых 16-битных регистров нарастили новые регистры с префиксом 'E'. То есть теперь есть EAX, EBX, EIP, ESP, EBP, EFLAGS и так далее. Но не сегментные регистры, они остались 16-битными. Шина данных и адреса тоже стали 32-битными (шину адреса любят обрезать под лимиты конкретной платформы, но технически ее возможно было сделать 32-битной без модификаций архитектуры. В последствии разработали расширение PAE, что позволило расширить ее свыше 32 линий и 4 гигабайт).
Во-вторых, теперь появился третий (четвертый) режим: виртуальный 8086.Он совмещал в себе особенности работы реального режима и защищенность защищенного, позволяя достаточно эффективно переключаться между ними, а еще реализовывать одновременную работу множества программ реального режима внутри одной ОС одновременно с защищенными. При этом подобный псевдореальный режим оставался достаточно безопасным, так как многие опасные наглости эмулировались и не допускались напрямую к железу, а память была изолирована.
Продолжение следует
К сожалению, мысль о написании подобного текста у меня возникла слишком поздно, а на часах 4 часа 5 часов утра и я уже физически не в состоянии продолжать писать этот пост. Если это будет интересно, то я напишу продолжение, в котором полноценно расскажу про защищенный режим, костыли реального режима и пропущенный промежуток до появления длинного режима (i80486, Pentium). И так страшно представить, сколько неточностей и откровенно грубых ошибок я тут понаписывал на автомате. Если вы их нашли - просьба указать в комментариях
Обещал не делить посты на куски и такой облом, извините
Всем доброго времени! Наткнулся я не так давно на вот такое изображение:
И решил порассуждать не тему того, а плоха ли такая ситуация и как вообще бывает, как идет разработка backend и frontend в вещах которыми я занимался, возможно кому то будет интересно, а кому то захочется подискутировать со мной)
Во первых о понятиях, что такое "Бекенд" и "Фронтенд", я работал с разными разработчиками и даже студиями и понял что иногда эти понятия отличаются у разных людей (особенно когда речь идет о том как HR их понимает), по этому поясню что я в них вкладываю:
Frontend - "передний конец", это весь визуал который видит человек пользуясь программным обеспечением, а так же скрипты которые исполняются на стороне (на устройстве) пользователя.
на примере сайта, это его верстка, а так же javascript который перелистывает слайды, раскрывает выплывающие окна и т.д.
Backend - "задний конец", это всё что исполняется "под капотом" программы, в случае сайта это на сервере, в случае какой то настольной программы, внутрипрограмные механизмы, например обращающиеся к каким то функциям операционной системы и производя какие то расчеты и действия
на примере сайта, это его серверная часть, механизмы которые к примеру работают с базой данных, получают из неё пост для страницы на которой находится пользователь, и к примеру комментарии для этого поста и т.д.
А теперь как раз о том, что разрабатывается первее и почему.
Возьмём вебсайт, вы например хотите что бы вам сделали интернет магазин, обычно этапы разработки выглядят так:
- Создание и утверждение дизайна
- Верстка дизайна
- Подключение к верстке т.н. "Движка" или cms или "системы управления контентом", или же что очень редко и индивидуально, написания этой самой системы с нуля в порядке индивидуальной разработки и внедрения её в вёрстку.
т.е. по сути тут выходит что разработка Бекенда идёт после Фронтенда - и получается что как будто то что показано в меме это естественный ход событий и ни каких вопросов это не может вызывать?
Стоит сказать что последовательность работ которую я описал, это обычно усреденная разработка, где люди не сильно запаиваются составлением подробных ТЗ. По ней выходит то что когда готов дизайн, и на основе него вёрстка, это всё передается программисту работающему над Бэекендом, и он сразу видит, что и где ему нужно будет подключать и в каком виде: "Здесь у нас поиск, с возможностью сортировки и выбору диапазона дат постов", "Тут мы выводим чат, который обновляется каждые пару секунд" и т.д. Программист сразу визуально видит какие данные в какие элементы интерфейса ему нужно выводить, от чего ему более понятен план работ, по тому что он "визуален".
Но предположим что сроки разработки сжаты, и требуется что бы разработка обоих частей проходила одновременно, в таком случае необходимо очень точное техническое задание, где будет подробно прописано то как будет работать бэекенд, какова будет структура базы данных, какие данные при каких обстоятельствах будут из неё извлекаться, каким образом обрабатываться, в каком виде перезаписываться обратно и в какой части условного ещё не созданного даже в виде дизайна интерфейса отображаться.
Это довольно фантастический план, так как обычно не бывает заказчиков которые чётко знают как должен выглядеть их сервис\сайт\по и даже не всегда представляют себе точный его функционал.
Это всё зачастую формируется как раз во время разработки и утверждения дизайна, и по этому программист который с самого старта одновременно с дизайном начал разрабатывать свой бекенд, чаще всего обречен по ходу появления дизайна и вёрстки вносить коррективы в свою работу, а иногда даже переписывать её часть.
Но есть например аспекты которые можно на мой взгляд проработать и без дизайна, например структуру базы данных и её таблиц, создать какие то общие функции, провести базовую настройку движка или фреймфорка с которым будет работа.
Но всё же когда дизайн утверждён и верстка окончена и принята заказчиком, это уже гарантирует стабильность разработки. (при условии конечно если заказчику не взбредёт в голову что то на ходу изменять, но такие вещи должны быть учтены в договоре и проходить за дополнительную оплату)
Примерно то же самое происходит и при разработке приложения для телефона.
Программисту всегда проще подключать бекенд к чему то готовому, чем прорабатывать так называемые API (простым языком объясняя это точки доступа через которые интерфейс получает данные из бекенда) почти в слепую или по скупому ТЗ (я просто ещё не видел по настоящему подробных ТЗ за исключением тех которые в своей педантично душной манере составлял).
По этому становится очевидно что это вполне себе нормальная ситуация, когда Фронтенд давно готов, на него можно посмотреть, потыкать, а Бекенд ещё только подключается, а то ещё и вовсе в стадии разработки.
Но есть один сценарий когда бекенд идёт первее фронтенда - это когда разрабатывается какое то техническое программное обеспечение у которого изначально вовсе нет интерфейса, например какой нибудь локальный сервер - пишется собственно сама програмина, а её настройка и в целом взаимодействие с ней происходит через терминал\консоль\командную строку.
В данном случае интерфейс появляется уже после того как ПО было разработано, зачастую таким программным обеспечением можно пользоваться и без интерфейса через всё тот же терминал, отправляя команды которые ты вводишь в него вручную (прям как хакер из кинофильма), а графический интерфейс создаётся уже потом(иногда очень сильно потом) просто для удобства пользователя и отображает все стандартные и часто используемые опции (для более тонких настроек за частую используется всё равно командная строка) и по сути графический интерфейс, отправляет те же самые команды что вы бы вводили в терминал, просто делает это по нажатию на кнопочки.
На этом всё, надеюсь кому то было интересно почитать эти возможно сумбурные рассуждения, сейчас попробую выловить все грамматические ошибки в этом опусе, перед тем как нажать кнопку "опубликовать пост"😅