Все потоки
Поиск
Написать публикацию
Обновить
90.5

Системное программирование *

Обеспечение работы прикладного ПО

Сначала показывать
Порог рейтинга

MCP архитектура как развитие ручного подхода в LLM

Когда вы открываете ChatGPT и вставляете туда кучу текста — что реально происходит?
Всё складывается в один длинный «бутерброд»: данные, инструкции, системный промпт, даже куски схемы в Markdown. Никакого порядка. Это как если бы у вас в кодовой базе был один файл main.py, где и роуты, и бизнес-логика, и SQL-запросы.

Я хочу описать идею MCP кратко, поскольку в самой доке она не описана. А может быть даже и не закладывалась туда. Но очень похоже, что такая архитектура хорошо работает исходя из более фундаментальных принципов, чем просто разделение

Как это выглядит у ChatGPT

На схеме выше видно:

  • Есть Line Edit — пользователь копипастит сырые данные.

  • Есть Плагин — иногда он что-то подмешивает.

  • Всё это сливается в один большой Склеенный промпт, который уходит в LLM.

Мешанина как она есть

Как это делает MCP?

MCP приходит и говорит: «ребята, давайте хоть модули разнесём».

  • System Prompt — отдельная часть, где живёт логика «как правильно жить» для модели.

  • Instruction Layer — патчи и локальные корректировки.

  • Schema Registry — отдельный каталог, который описывает структуру данных (таблицы, поля, форматы).

  • Data Adapter — слой, который достаёт данные у провайдера строго по схеме.

  • Всё это связывает MCP хост, который собирает финальный запрос к LLM, который зачастую представляет собой Lang Chain

Итог: модель получает запрос не как «мусорный мешок», а как структурированный pipeline.

Почему это важно

  • Прозрачность. Можно отследить, какая часть отвечает за что.

  • Контроль. Можно менять системный промпт без страха поломать данные.

  • Расширяемость. Хочешь новый источник данных? Добавь адаптер, а не переписывай всё.

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

Простая метафора

  • ChatGPT — это когда у вас «final_final_v3.docx» и все правят его параллельно.

  • MCP — это когда у вас git с ветками, пайплайнами и CI с CQRS архитектурой (не шутка), читай выше

Теги:
+1
Комментарии0

Зачем работать напрямую с клиентом, когда можно просто выкинуть запрос на MCP и дать нейросети подумать? 😎

Расскажу кейс Vivo Chat. Проверка статуса заказов (замените на вашу сущность). Давайте по порядку

Клиент идёт к хосту, чтобы получить доступ к MCP

Итак, наш клиент — это тот, кто первым инициирует запрос. Всё начинается с того, что клиент заходит в свою систему, которая подключена к MCP-серверу через хост. Хост выполняет функцию посредника, направляя запросы и получая ответы от разных компонентов системы.

Клиент: «Привет, хост, мне нужно проверить заказ, и я хочу понять, что из инструментария MCP мне пригодится. Покажи мне, пожалуйста, список доступных инструментов и подсказок, чтобы я мог выбрать что-то нужное для этого запроса.»
Хост (перехватывает запрос и направляет его к серверу MCP): «Окей, сейчас все передам.»

Хост запрашивает у MCP список инструментов (Tools) и подсказок (Prompts)

Хост теперь идёт к MCP-серверу. Этот сервер знает всё, что связано с доступом к данным и обработкой запросов. В MCP сервере хранятся все инструменты (tools) и подсказки (prompts) для выполнения нужных действий.

Хост: «MCP, подкинь мне список доступных tools и prompts для запроса клиента.»
MCP-сервер: «Вот, держи, вот все инструменты и подсказки, которые у нас есть. Для каждого метода я также подготовил параметры, которые можно подставить.»

LLM, любимая нейросеть, выбирает метод из списка

Теперь, когда хост получил список инструментов и подсказок, он передает всё это в LLM (нейросеть), которая и будет решать, какой метод нужно использовать для конкретного запроса.

Хост: «LLM, тебе пришёл запрос от клиента. Вот список инструментов и промптов. Тебе нужно выбрать подходящий метод для выполнения запроса и подготовить все нужные параметры для этого метода.»
LLM: «Хорошо, я выбираю метод X из списка инструментов, и вот какие параметры мне нужны для этого метода. Я знаю, что нужно сделать, и использую соответствующие промпты, чтобы точно понять, что клиент хочет.»

LLM передает параметры и вызывает метод на MCP

Теперь, когда LLM выбрала нужный метод и подготовила параметры, она отправляет всё это хосту, который, в свою очередь, передает запрос обратно в MCP-сервер для выполнения.

LLM: «Вот всё, что мне нужно: метод X и параметры для выполнения. Передавай это на MCP.»
Хост: «Принято, иду к MCP.»

Хост направляется к MCP-серверу и передает запрос на выполнение метода.

MCP выполняет метод и возвращает результат

MCP-сервер теперь, получив все необходимые данные, выполняет метод и обрабатывает запрос. Всё, что нужно, уже у него под рукой: инструменты, параметры, контекст. Вся обработка происходит внутри MCP, и сервер возвращает результат хосту.

MCP-сервер: «Я выполнил метод X с этими параметрами. Вот результат: (ответ).»

Ответ от LLM клиенту

Теперь, когда MCP выполнил метод, хост получает ответ и передает его обратно в LLM, которая уже анализирует результат, добавляет необходимые детали (например, форматирует или уточняет ответ) и отдает всё клиенту.

Хост: «Вот ответ от MCP через LLM: (ответ). Всё готово!»
LLM: «Отлично, теперь я передаю результат обратно клиенту.»
Клиент: «Вау, всё так быстро! Спасибо, LLM!»

Итог

На мой взгляд в бизнес-приложениях — это самый частый кейс, а всё остальное, связанное с обновлениями статусами заказов, удалениями данных будет упираться в безопасность и комплаенс

Теги:
+3
Комментарии1

Я уже писал, из чего состоит протокол, как выглядят сервер и клиент, и зачем вообще всё это проксирование. Теперь — шаг вперёд: как вся эта архитектура живёт в реальных приложениях.

Если смотреть на схемы выше, то картина получается парадоксальная. Уровень связности высокий: запросы к LLM проксируются через хост, а MCP-сервера сидят как отдельные острова, каждый тянет свои данные — базы, файлы, API. На первый взгляд хочется спросить: «зачем такие сложности?». Ответ простой — тарификация и контроль. Если бы сервер напрямую ходил к LLM, владелец MCP оказывался бы заложником чужой экономики. А так запрос идёт через клиента/хост, и именно пользователь контролирует, когда и какой токен уходит в модель.

Важно понимать: MCP-сервер всегда на стороне провайдера данных. Это он готовит промпты, пишет функции, агрегирует источники. Ваша задача — не изобретать велосипеды, а собрать этих провайдеров, подключить через MCP и дальше использовать уже в своих оркестраторах (хоть LangChain, хоть самописные пайплайны).

Почему MCP имеет значение? Всё зависит от того, где вы стоите:
– Разработчику MCP сокращает время интеграции и даёт готовый слой абстракций.
– ИИ-приложениям MCP открывает экосистему источников и тулзов.
– Пользователю MCP гарантирует, что его данные не уходят вслепую, а действия согласованы.

В сухом остатке: MCP — это не про «ещё один протокол», а про баланс удобства и контроля. Пример банальный, но показательный: пользователь хочет слетать в Барселону. Серверы MCP подтянули календарь, историю поездок, поиск рейсов и отелей. Агент сложил всё в кучу и забронировал отпуск за минуты. Задача, которая вручную заняла бы часы.

В следующей части разберём, какие паттерны интеграции MCP-серверов с LLM-оркестраторами реально просто работают

Ссылки, как обычно, в моём канале

——————
Менеджер? Давай сюда!
Ищи работу здесь
Технологии и архитектура

Теги:
0
Комментарии0

Go + Windows = deadlock. Свет в конце тоннеля.

В прошлой статье я рассказывал о редком, но весьма опасном баге: поток под Windows зависал в вызове CancelIoEx, хотя документация Microsoft утверждает обратное. Суть проблемы — в пересечении синхронного и асинхронного ввода-вывода, где ядро Windows блокирует доставку APC, и поток остаётся навсегда «висящим».

История получила развитие не сама по себе: мы целенаправленно поднимали эту тему через support-кейс в Microsoft. В результате удалось подключить и Escalation Team, и разработчиков Go, ответственных за Windows-порт.

Финальный вывод: стандартная библиотека Go действительно использует неправильный API для отмены синхронных операций. Вместо CancelSynchronousIo, рекомендованного самой Microsoft, в коде до сих пор вызывается CancelIoEx.

👀 Сам проблемный вызов:
https://github.com/golang/go/blob/77f911e31c243a8302c086d64dbef340b0c999b8/src/internal/poll/fd_windows.go#L461

Хорошая новость: у команды уже есть рабочий proof-of-concept фикса:
https://go-review.googlesource.com/c/go/+/691395

Менее радостная часть: из-за сложности изменений и их влияния на рантайм правка запланирована только в Go 1.26 (февраль 2026). Бэкпорт в предыдущие версии практически исключён.

Что это значит для разработчиков

  • Если ваш сервис на Go под Windows внезапно «зависает» в CancelIoEx — это следствие бага в стандартной библиотеке, а не ваша ошибка.

  • До релиза Go 1.26 остаются обходные варианты:

    • не вызывать CancelIoEx для синхронных дескрипторов,

    • использовать CancelSynchronousIo, если есть возможность управлять потоками,

    • минимизировать использование пайпов в критичных местах.

Итог

Редкий flaky-тест Go (TestPipeIOCloseRace) оказался симптомом реальной и серьёзной проблемы. Благодаря эскалации через Microsoft Support и совместному разбору мы получили подтверждение, понятное объяснение и официальный фикс в планах.

⚡️ Если ваш Go-код на Windows зависает в CancelIoEx, теперь вы знаете: проблема признана и исправление уже в пути.

Теги:
+9
Комментарии0

Почему я больше никогда не буду использовать сервис bitly?

В общем я обычно достаточно аккуратен со всякого рода скамом, редко попадаю в такие ситуации, обычно хорошая чуйка на такие истории, но в этот раз не обошло меня стороной. 

Long story short: помогают тут ребятам с одним оффлайн проектом, нужно было быстро сделать QR код для флайеров, google выдает миллион сервисов для генерации, выбрал первый попавшийся “Create Your Free QR Codes”.  Удачно все сделал, заказали флаеры, не смотря на то, что по пути на каждому этапе было Free через 2 недели (когда уже все напечатано) прилетает вежливое “Oh no! Your Dynamic QR Codes will expire in 3 days”. И конечно же он перестанет работать если не заплатить ($20/month). 

Масштаб “потерь” - $40, но я готов заплатить $200 чтобы объяснить они строян бизнес на скаме и для кармы это вредно. 

Конечно же я не один такой, и большинство “сжимают зубы” и платят.  Например на редите 230 комментариев. 

Как вы поступаете в такой сиутации?  Я как-то с детства считаю что нельзя потокать и платить. 

Мои действия:

✅ никогда в жизни ни на одном из моих проектов не будет использоваться сокращалка Bitly

✅ написал далобу в FTC

✅ написал отзыв на Google Maps (там таких много)

☑️ найду и напишу отзывы на других сервисах

✅ попрошу вас помочь мне и присоединиться

P.S. Ресеч показал, что это не просто какие-то горе стартаперы, а проект компании Bitly (рейтинг 1.6 на Trustpilot).

Если вам нечем заняться и вам не сложно уделить 2-5 минут своего времени. 

Промпт для генерации отзыва:

Write a concise, neutral review of qr-code-generator.com (Bitly) in plain, easy-to-read language, 80-120 words. Context: It appears in Google for queries like "free qr code generator." During setup, the process is presented as free; any expiration notice, if present, seems absent or buried in terms. About 14 days later, the QR may be deactivated behind a paywall, leaving printed materials unusable unless payment is made to reactivate. The system does not allow converting that now paid QR to a free static code or redirecting it without payment. Many users report similar experiences online, including numerous Reddit comments. Keep the tone calm, factual, and non-accusatory. Do not include personal info or insults. Write in your own words.

Места куда стоит запостить отзыв:

- Google Maps (Bitly Office Google Maps)

- Trust Pilot

- G2

- Capterra


P.P.S. Есть кто-то из СМИ хочет изучить эту историю и написать подробнее - я буду рад ответить на вопросы и предоставить материалы. 

P.P.P.S. Особенно иронично выглядит позиция “Head of Happiness” в подписи письма

#justice 

Теги:
Всего голосов 6: ↑1 и ↓5-4
Комментарии12

«Клей» для GPIO в QEMU

В прошлой статье мы пришли к выводу, что QMP — это лучше, чем ничего. Но хочется большего — библиотеку или программу (желательно, уже готовую), которая умеет читать/писать и узнавать об изменении состояния через poll() / pselect() / select() / epoll() / read(). 

В таком случае для каждой модели GPIO нужен «клей», похожий на тот, что используется с chardev — мы включаем его прямо в модифицированный QEMU. Очевидное название такого «клея» — gpiodev. Вот его основные функции, которые сейчас почти полностью соответствуют GPIO UAPI в Linux:

  • сообщать количество линий, конфигурацию, название и потребителя каждой линии,

  • читать и задавать состояние линии,

  • отслеживать изменения состояния и конфигурации линии (вход/выход, запрос/освобождение).

«Клей» состоит из двух групп, первая — это индивидуальные для каждого модуля GPIO функции, которые gpiodev использует, чтобы запросить специфическую информацию:

  • LineInfoHandler() — информация о линии: имя, флаги и потребитель,

  • LineGetValueHandler() — состояние линии: условный 0 или 1,

  • LineSetValueHandler() — задать состояние линии: 0 или 1.

По аналогии с GPIO UAPI напрашиваются также функции LineGetMultiValueHandler() и LineSetMultiValueHandler() для запроса и выставления линий, но я решил ограничиться минимальным набором.

Можно ли организовать прозрачное взаимодействие с устройствами внутри QEMU — использовать те же библиотеки и инструменты, как и для реальных устройств? Читайте во второй части трилогии о долгом пути до GPIO в QEMU.

Теги:
Всего голосов 2: ↑2 и ↓0+2
Комментарии0

Как облачные сервисы продвигают продукты на хакатонах

Хакатон — способ поднять узнаваемость бренда и расширить воронку найма, все хорошо это знают.

При этом немногие учитывают, что хакатон — еще инструмент продвижения свой продукта среди разработчиков.

Облачные сервисы успешно этим пользуются. Ниже несколько кейсов с выводами.

Все топовые облака: Amazon, Google Cloud, Azure, Cloud.ru, VK Cloud, Yandex Cloud провели хакатоны в последние 3 года.

Самый масшабный был с призовыми в $150 000.

К участию приглашались стартапы для решения социально значимых задач. Единственное требование — использовать сервис в своем решении.

В РФ сильнее всего отметились Selectel и Cloud.ru, тогда под брендом SberCloud.

На SberCloud Advanced Hacking собралось 70 команд из малого и среднего бизнеса. Они внедряли облачные технологии в свои продукты — самые оригинальные решения получили призы.

Всего мы проанализировали 10 хакатонов от облаков. Делимся выводами.

  1. Хакатоны облачных сервисов выделяются необычно взрослой аудиторией: как правило, ЦА — средний и малый бизнес. Это потенциальные клиенты.

  2. Задачи ставятся максимально широко. Ограничения бывают
    А) по тематике, например, «сделайте что-то социально значимое»,
    Б) по технологии, например, собираются решения в области AI, Web3, Blockchain, IoT.
    Это идеально для хакатона.

  3. Максимум по участникам в РФ — около 260 человек. Есть простор для яркого крупного ивента.

Вывод: облачные сервисы и вообще все, кто производит продукт для разработчиков, одним махом закрывают сразу 3 задачи с помощью хакатона:

  • продвинуть продукт

  • поднять узнаваемость бренда

  • расширить воронку найма

Если ваша компания относится к таким — донесите ценность до бизнеса, это поможет «продать» идею хакатона внутри и оставить всех довольными.

Теги:
Рейтинг0
Комментарии0

Что ты сделал для хип-хопа IT-инфраструктуры в свои годы? Писал UEFI и BMC для высоконагруженного оборудования 

В YADRO есть распределенная команда, которая разрабатывает и сопровождает собственные программные реализации UEFI (BIOS) и BMC. Для самого разного оборудования — от серверов до телеком- и клиентского оборудования. 

Что делают BIOS и BMC в продуктах
Что делают BIOS и BMC в продуктах

Познакомиться с командой → 

Какие задачи выполняют в команде BIOS/BMC: 

  • Реализуют программную поддержку новых аппаратных продуктов компании, определяют протоколы и методы взаимодействия между программными и аппаратными компонентами продуктов YADRO.

  • Проводят верификацию микрокода и выполняют проверку прошивок микросхем всех продуктов компании. Выстраивают стратегию тестирования.

  • Исследуют новые программные и аппаратные технологии для применения в продуктах. Рефакторят код для повышения производительности.

  • Придумывают методы безопасного обновления прошивок BIOS и BMC, чтобы обеспечить минимальный или нулевой даунтайм.

  • Добавляют новые фичи и меняют существующие — от WebUI до политик управления аппаратными компонентами.

Команде нужно больше инженеров — разработчиков на С/C++, тестировщиков, автоматизаторов и техлидов. Знакомься с вакансиями на сайте и вовлекайся в трушные инженерные задачи на современном технологическом стеке. 

Теги:
Всего голосов 8: ↑8 и ↓0+9
Комментарии0

Последовательность Фибоначчи может конвертировать мили в километры с небольшой погрешностью

5 миль ≈ 8 км (5 и 8 - числа Фибоначчи). Реальность: 5 миль = 8.04672 км.

Почему?
1 миля = 1.609344 километра (точное значение).
Золотое сечение (φ) ≈ 1.618034

Погрешность возникает потому что отношение Fₙ₊₁ / Fₙ стремится к φ ≈ 1.618034, а точное соотношение миля/км = 1.609344.

Относительная погрешность: (1.618034 - 1.609344) / 1.609344 * 100% ≈ 0.54%.

Решил по фану реализовать конвертор милей в километры на C. Ссылка тут.

Advanced distance converter: miles to kilometers
Usage: ./bin/fib_miles2km [OPTIONS] [distance]

Options:
  -h, --help                     Show help information
  -f, --fib=ARG                  Convert miles to km using basic Fibonacci
  -b, --basic=ARG                Convert miles to km using standard formula
  -i, --fib-interp=ARG           Convert using Fibonacci interpolation
  -c, --fib-cache=ARG            Convert using cached Fibonacci
  -g, --fib-golden=ARG           Convert using golden ratio

Не знаю зачем, но прикольно :)

Теги:
Всего голосов 13: ↑12 и ↓1+18
Комментарии1

Как создать простейшую модель GPIO для QEMU

Предлагаю два варианта, которые я условно решил назвать MMIO и PCI. Последний — тоже MMIO, но в QEMU они добавляются разными путями. Начнем с сердца любой MMIO-модели — апертуры.

Апертура и адресное пространство

Как я упоминал в одной из своих статей, любое MMIO-устройство — это MemoryRegion с заданными шириной доступа и размером. Для того, чтобы он был виден CPU или другому устройству, такому как DMA, его нужно разместить в соответствующем адресном пространстве — например, пространстве, назначенном для cpu0:

      0x0                                    0xffffffffffffffff
      |------|------|------|------|------|------|------|------|
0:    [                    address-space: cpu-memory-0        ]
0:    [                    address-space: memory              ]
                    0x102000           0x1023ff
0:                  [             gpio        ]

В любое время можно посмотреть существующие адресные пространства и регионы памяти в мониторе QEMU:

(qemu) info mtree
[...]
address-space: cpu-memory-0
address-space: memory
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000102000-00000000001023ff (prio 0, i/o): gpio
[...]

Тогда в модели устройства нам нужно всего лишь создать такой регион и назначить ему соответствующие функции записи и чтения:

static const MemoryRegionOps mmio_mmio_ops = {
    .read = mmio_gpio_register_read_memory,
    .write = mmio_gpio_register_write_memory,
    .endianness = DEVICE_NATIVE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};
 
[...]
memory_region_init_io(iomem, obj, &mmio_mmio_ops, s,
                      "gpio", APERTURE_SIZE);
[...]

Фактически это означает, что все семейство инструкций Load/Store будет вызывать mmio_gpio_register_read_memory()/mmio_gpio_register_write_memory() при совпадении адреса чтения/записи с адресом региона в адресном пространстве.

static uint64_t mmio_gpio_register_read_memory(void *opaque, hwaddr addr, unsigned size);
static void mmio_gpio_register_write_memory(void *opaque, hwaddr addr, uint64_t value, unsigned size);

Передаваемые аргументы и возвращаемое значения интуитивно понятны. Отмечу, что hwaddr addr — это адрес относительно начала нашего региона, а не абсолютный адрес.

Нам остается лишь создать устройство и добавить его регион в файле машины:

gpio = qdev_new(TYPE_MMIO_GPIO);
sysbus_mmio_map(SYS_BUS_DEVICE(gpio), 0, ADDRESS);

Почти десять лет назад Никита Шубин, ведущий инженер по разработке СнК в YADRO, сделал возможность чтения и записи GPIO для QEMU. Читайте первую часть трилогии о долгом пути до GPIO в QEMU.

Теги:
Всего голосов 1: ↑1 и ↓0+1
Комментарии0

На днях в офисе я столкнулась с коллегами из направления системного программирования в «Криптоните». Они пишут на С++ — а ошибки на этом языке у нас ещё не было!

Поэтому я попросила их придумать код с ошибкой специально для Хабра — и вот что получилось!

Итак, есть ли в этом коде проблема кроме narrowing conversion? Ждём ваши варианты в комментариях.

#include <cstdint>
#include <vector>

struct Type
{
    Type(uint16_t, uint32_t = {}) 
    {}
};

int main()
{
    std::vector<Type> vector;
    std::uint32_t object_id{};

    // Есть предупреждение о narrowing conversion
    vector.insert(vector.begin(), {0, object_id}); 

    // Нет narrowing conversion
    vector.push_back({0, object_id}); 

    // Нет narrowing conversion
    vector.insert(vector.begin(), Type{0, object_id}); 
}

АККУРАТНО, ДАЛЬШЕ СПОЙЛЕР!

Проблема в том, что в строке

 vector.insert(vector.begin(), {0, object_id});

в вектор вставляется 2 элемента типа Type, а не один, как ожидает программист.

Причина в том, что метод insert у вектора имеет перегрузку (номер 5), которая вторым параметром принимает std::initializer_list. А компилятор, видя фигурные скобки в коде, в первую очередь пытается создать объект такого типа.

И тут ему это удается, потому что у конструктора Type второй параметр имеет значение по умолчанию, следовательно, Type умеет создаваться, если в конструктор передали только один аргумент. В итоге компилятор успешно создает std::initializer_list с двумя элементами типа Type.

Так как создание std::initializer_list выполняется с использованием uniform initialization, то компилятор следит за корректностью преобразований. В примере объект типа std::uint32_t передается в конструктор Type, который принимает первым параметром std::uint16_t. То есть, возникает риск потери точности (32 бита не могут поместиться в 16 бит).

Об этом компилятор и предупреждает. Вот упрощенный пример, где видим то же самое предупреждение.

Может возникнуть вопрос, почему создание Type от 0 не вызывает предупреждение, ведь 0 - это int, а он, скорее всего 32 бита и тоже не помещается в 16 бит. Но тут литерал. Компилятор видит, что 0 вмещается в 16 бит и не предупреждает. Но если поместить int в переменную, то также возникает предупреждение.

Теги:
Всего голосов 3: ↑3 и ↓0+3
Комментарии0

Устройство компилятора (кратко) на LLVM
Компилятор - инструмент конвертации исходного кода, написанного на высокоуровневом языке программирования в машинный код, который может исполнять компьютер.

Компилятор делится на 3 этапа:

  • FRONTEND - анализирует текст исходного кода и преобразует его в IR.

  • MIDDLE - анализирует и оптимизирует этот сгенерированный код IR.

  • BACKEND - преобразует IR в машинный код.

Сам компилятор, и собственно язык программирования состоит из нескольких частей.

Lexer - лексер

Лексер сканирует и превращает сырой текст в токены. То есть сам исходный код разбиваеты на набор токенов (такие как литералы, идентификаторы, ключевые слова, операторы, разделители)
Лексер читает исходный код символ за символом и идентифицирует последовательности символов, соответствующие определённым правилам языка.

Парсинг

Парсинг немного сложнее чем лексический анализ. Существует множество паресров и парсеров-генераторов.

Парсеры в компиляторах обычно принимают входные данные в форме токенов и строят определенное дерево - AST или дерево парсинга.

Обычно компиляторы строятся из множества маленьких компонентов, которые берут входные данные, меняют их или преобразуют их в различные выходные данные. Это одна из причин, по которым функциональные языки хорошо подходят для создания компиляторов. Другие причины — прекрасное сопоставление с эталоном и довольно обширные стандартные библиотеки. Прикольный факт: первая реализация компилятора Rust была на Ocaml.

Советую держать эти компоненты как можно более простыми и автономными — модульность сильно облегчит процесс. По-моему, то же можно сказать и о многих других аспектах разработки ПО.

AST

AST - абстрактное синтаксическое дерево. Это структурированное представление исходного кода программы в виде дерева, где каждый узел дерева представляет собой синтаксическую конструк языка программирования. Это дерево предоставляет абстракцию, которая позволяет анализировать и манипулировать программным кодом на высоком уровне.

IR

Эта часть занимается созданием [[1.2 IR]]. Через примитивы LLVM мы можем сгенерировать промежуточное представление. Каждому типу в AST дается метод, называемый codegen, который всегда возвращает объект значение LLVM, используемый для представления одного регистра присваивания (single assignment register), который является переменной для компилятора, которая может быть назначена только один раз. Интересно, что в этих примитивах IR то, что в отличии от ассемблера, они не зависят от какой-либо конкретной архитектуры машины, и это значительно упрощает работу для разработчиков языков, которым больше не нужно сопоставлять вывод в набор инструкций процессора. Теперь, когда фронтенд может генерировать IR, инструмент LLVM Optimizer используется для анализа и оптимизации сгенерированного кода. Он выполняет несколько проходов по IR и выполняет такие действия как устранение мертвого кода и скалярная замена агрегатов, и, наконец, это приводит нас к бекенду, где мы пишем модуль, который принимает IR в качестве входных данных, который выдает объектный код, который может работать на любой архитектуре.

Теги:
Всего голосов 6: ↑4 и ↓2+3
Комментарии0

LLVM IR: что это такое?

Главной особенностью LLVM является промежуточное представление кода (англ. Intermediate Representation, IR), форма, которую использует LLVM для представления кода в компиляторе. LLVM IR был разработан для выполнения функций промежуточного анализа и преобразований внутри оптимизатора компилятора. Ее создание имело целью решение множества специализированных задач, включая поддержку легковесных оптимизаций среды выполнения, кроссфункциональные и межпроцедурные оптимизации, полный анализ программы и агрессивные реструктурирующие преобразования. Промежуточное представление кода определено как язык первого порядка с четкой семантикой.

IR (Intermediate Representation) в контексте LLVM — это промежуточное представление кода. Это низкоуровневое, независимое от платформы и типобезопасное представление программного кода, которое используется в качестве промежуточного языка между интерфейсной частью и серверной частью компилятора.

define i32 @add1(i32 %a, i32 %b) {
entry:
  %tmp1 = add i32 %a, %b
  ret i32 %tmp1
}

define i32 @add2(i32 %a, i32 %b) {
entry:
  %tmp1 = icmp eq i32 %a, 0
  br i1 %tmp1, label %done, label %recurse

recurse:
  %tmp2 = sub i32 %a, 1
  %tmp3 = add i32 %b, 1
  %tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)
  ret i32 %tmp4

done:
  ret i32 %b
}

Этот код LLVM IR соответствует следующему коду на языке C, обеспечивающему возможность сложения целых чисел двумя разными способами:

unsigned add1(unsigned a, unsigned b) {
  return a+b;

}
// возможно не самый лучший способ сложения двух чисел
unsigned add2(unsigned a, unsigned b) {
  if (a == 0) return b;
  return add2(a-1, b+1);
}

Как видно из этого примера, LLVM IR — низкоуровневый RISC-подобный набор виртуальных инструкций. Как и настоящий набор инструкций RISC, он поддерживает линейные последовательности простых инструкций (сложение, вычитание, сравнение и ветвление). Эти инструкции имеют трехадресную форму. Это значит, что они берут некоторое количество входных данных и вычисляют результат в другом регистре. LLVM IR поддерживает метки и в целом выглядит как необычная форма языка ассемблера.

Строго говоря, промежуточное представление LLVM является четко определенным и единственным интерфейсом оптимизатора. Это означает, что всё, что необходимо знать, чтобы писать фронтенды для LLVM, это: что такое LLVM IR, как он работает и какие инварианты ему необходимы. Так как LLVM IR имеет текстовую форму, то имеет смысл создавать фронтенд, который выводит LLVM IR в виде текста, а затем отправляет его на оптимизатор и необходимый генератор кода при помощи каналов Unix.

Теги:
Всего голосов 5: ↑3 и ↓2+3
Комментарии0

Ближайшие события

Рэймонд Чен — ветеран компьютерной индустрии, который работает в Microsoft c 1992 года. Рэймонд участвовал в разработке OS/2, Windows 95, DirectX и оболочки Windows, а последние десятилетия отвечает за сохранение обратной совместимости системы. В своём блоге Old New Thing Чен регулярно делится забавными историями из разработки софта, но также показывает действительно полезные примеры.

На этот раз Чен показал, почему история буфера обмена не отражает быстрые изменения содержимого буфера. Рэймонд приводит следующий фрагмент кода от клиента. Этот код был написан для некой утилиты, вставляющей в историю буфера обмена объекты. В некотором роде историю прошлых изменений превращали в будущее — целью было предугадать, какие элементы пользователь хотел бы видеть в истории буфера обмена.

// В целях наглядности вся проверка ошибок опущена
#include <windows.h>

void SetClipboardText(HWND hwnd, PCWSTR text)
{
    OpenClipboard(hwnd);
    EmptyClipboard();
    auto size = sizeof(wchar_t) * (1 + wcslen(text));
    auto clipData = GlobalAlloc(GMEM_MOVEABLE, size);
    auto buffer = (LPWSTR)GlobalLock(clipData);
    strcpy_s(buffer, size, text);
    GlobalUnlock(clipData);
    SetClipboardData(CF_UNICODETEXT, clipData);
    CloseClipboard();
}

// Чтобы они были под рукой, разместим эти строки в истории буфера обмена
static constexpr PCWSTR messages[] = {
    L"314159", // номер бага, который мы хотим исправить
    L"e83c5163316f89bfbde7d9ab23ca2e25604af290", // коммит, к которому привязываем ошибку
    L"Widget polarity was set incorrectly.", // комментарий, который нужно добавить
};

int wmain([[maybe_unused]] int argc,
          [[maybe_unused]] wchar_t* argv[])
{
    auto tempWindow = CreateWindowExW(0, L"static", nullptr, WS_POPUPWINDOW,
            0, 0, 0, 0, nullptr, nullptr, nullptr, nullptr);

    for (auto message : messages)
    {
        SetClipboardText(tempWindow, message);
    }
    DestroyWindow(tempWindow);
    return 0;
}

Код записывает в буфер обмена последовательно три строковые переменные. Однако при запуске утилиты в истории буфера обмена оказывалась лишь одна — последняя. Куда делись две остальные?

Дело в том, что служба истории буфера обмена работает асинхронно через механизм Clipboard Format Listener, существующий с эпохи Windows Vista. В этом механизме через функцию Add­Clipboard­Format­Listener приложение добавляет себя в качестве листенера. После этого никаких дополнительных опросов буфера обмена проводить не нужно — система сама оповестит приложение, если буфер изменился.

При получении уведомления служба истории буфера обновляет собственно историю буфера обмена. Но из-за асинхронности событие может происходить с задержкой. Как объясняет Чен, из-за асинхронной природы обновлений при получении WM_CLIPBOARD­UPDATE от Clipboard Format Listener буфер может успеть обновиться ещё раз.

Как считает Рэймонд, это даже не баг, а фича. Так получается избегать приложений, которые быстро спамили бы в буфер обмена множество изменений. Если даже пользователь не успевает воспользоваться содержимым буфера, то сохранять это для истории смысла нет, указывает Чен.

В другом посте из своего блога Рэймонд объяснил механизмы утилит-просмотрщиков буфера обмена с синхронными обновлениями буфера. Здесь периодически выполняется опрос GetClipboardSequenceNumber. У данного подхода тоже есть проблемы: редкий опрос угрожает привести к пропуску изменения буфера, но слишком частые запросы создадут лишнюю нагрузку на систему.

Рэймонд обещает в следующий раз показать, как исправить код выше.

Теги:
Всего голосов 5: ↑4 и ↓1+6
Комментарии0

Читаемость Си-кода: грустный ликбез, чтобы жить стало веселее

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

И всему виной (ожидаемо) выражения препроцессора и, как следствие, макросы. Да, вы правильно подумали. Именно те части кода, в которых вызываются такие легенды как:

Последние два очень легко спутать, если читать код невнимательно. Вместо них иногда рекомендуют использовать

#if defined // вместо #ifdef
#if !defined // вместо #ifndef

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

Чего далеко ходить - все open source проекты пестрят именно сокращенными вариантами этих выражений и с этим уже ничего не поделаешь.

А вот первое выражение из первой тройки игроков иногда вообще открывает врата ада, когда используется многострочный макрос с бэкслэш-символами (\) на концах строк.

Конечно же выражения препроцессора - это очень гибкий и полезный инструмент, позволяющий делать всё:

  • условную компиляцию;

  • выравнивание структур;

  • предотвращать повторные включения файла;

  • работать со строками;

  • грамотно оборачивать повторяющийся код и т.д. и т.п.

Но бывают случаи, когда упрощение кода с точки зрения алгоритма делает его слабо читаемым для того самого бедного программиста, пытающегося прочесть этот шедевр машинописного текста:

struct {
  const char *name;
  const char *value;
#define _SPECIAL(x) { .name = #x, .value = b->x, }
} specials[] = {
  { .name = "object", .value = b->object_string, },
  _SPECIAL(host),
  _SPECIAL(endpoint),
#undef _SPECIAL
};

*ну все, можно начинать грустить*

Чтобы разобраться, давайте очистим код от макросов, но сохраним суть:

struct {
  const char *name;
  const char *value;
} specials[] = {
  { .name = "object", .value = b->object_string, },
  { .name = "host", .value = host, },
  { .name = "endpoint", .value = endpoint, },
};

Что понятно из очищенного варианта:

  • инициализируется массив specials[];

  • типом данных этого массива является структура с полями *name и *value;

  • поля элементов массива задаются вручную.

Но как можно этот процесс немного автоматизировать и не прописывать вручную одинаковые строки?

Правильно, с помощью макроса:

#define _SPECIAL(x) { .name = #x, .value = b->x, }

который определяется после объявления полей структуры.

Как обрабатывается аргумент x:

  • имя аргумента преобразуется в строку с помощью макроса "#x" и присваивается полю *name;

  • полю *value присваивается значение поля структуры b с названием аргумента x (тут нужно убедиться, что поле с именем x действительно существует в структуре b).

То есть чтобы при заполнении массива не писать каждый раз одинаковые строчки:

{ .name = "host", .value = host, },
{ .name = "endpoint", .value = endpoint, }

можно вызвать выражение _SPECIALS:

_SPECIAL(host),
_SPECIAL(endpoint),

Ну и в самом конце вызываем удаление созданного макроса, чтобы оно не было использовано в коде в дальнейшем:

#undef _SPECIAL

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

А какие выражения и макросы в Си видели вы?

Теги:
Всего голосов 7: ↑7 и ↓0+10
Комментарии2

Что такое быть Unix-программистом? Быть наполовину сисадмином (и вот почему)

Как вы поняли, этот пост на Хабре начался со смелого заявления. Конкретно в данном случае я не хотел бы раскладывать по полкам абсолютно все навыки, которые нужны Unix-программисту для успешной работы. Их можно получить простым запросом в поисковике или к любому чат-боту типа ChatGPT, DeepSeek и т.д. (на ваш вкус и цвет)

Я же отметил именно те, приобрести которые стоило мне многих набитых шишек. Они связаны с работой в проектах, состоящих из утилит, содержащих по несколько тысяч строк кода каждая.

Итак, навыки, правила, они же житейские мудрости, они же грабли, на которые наступал.

1. Сначала править конфиги и только потом - код. Это база

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

Сначала проверь конфигурацию и все её возможные варианты! Если все эти варианты исчерпаны, то только тогда, в последнюю очередь смотри в код!

Я очень долго привыкал к этой мысли, тратя тонны времени на чтение кода, так и не найдя там ошибки. А после приходил коллега-сисадмин, который пошарил конфигурацию, почитал документацию, поиграл с настройками и всё решил.

2. Владеть инструментами командной строки

Логично, но не очевидно на первый взгляд. В отличие от обычного программиста (в сферическом вакууме), когда ты можешь ограничиться пошаговой отладкой или логами, Unix-программист должен уметь работать с командной строкой. Хоть и не обязательно знать все команды и их опции "на зубок", но нужно понимать, для каких случаев какие утилиты полезны.

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

Например:

- при невозможности пошаговой отладки использовать perf, чтобы проанализировать стек вызовов

- уметь пользоваться grep'ом для поиска и выделения нужной информации из конкретных файлов

- использовать sed для формирования файлов без лишней информации (например в логах убирать строки с наличием отметок времени)

3. Уметь работать с виртуальными машинами, докером, анализаторами трафика

Например, не плодить виртуалки, а делать снэпшоты (вы скажете: "Спасибо, капитан-очевидность!", но я видел на своей практике тех, кто, не зная про снэпшоты, плодил виртуалки). Держать мастер-копии виртуалок с предварительно настроенной конфигурацией для быстрого развертывания новых машин. Понимать, для каких целей проще использовать докер-контейнер и т.д.

4. Работать с огромным количеством открытых одновременно утилит

Как ни странно, я встречал разработчиков, которые задавали вопрос: "Зачем так много всего?"

Ответ прост: когда непонятно поведение программы, нужно принимать во внимание всё.

Резюме: с таким набором навыков в какой-то момент начинаешь себя чувствовать, как оператор из Матрицы.

А какие особенности в Unix-разработке (и не только) подметили вы?

Теги:
Всего голосов 4: ↑4 и ↓0+4
Комментарии4

Я тут опубликовал библиотеку для программирования в парадигме потоков данных (dataflow) на микроконтроллерах: https://github.com/Zubax/ramen

Она чрезвычайно проста (один заголовочный файл) и работает на любой платформе без портирования.

Простейший пример --- сумматор:

               ┌────────┐
       (float) │ Summer │ (float)
 in_a ◄────────┤        │◄──────── out_sum
               │        │
       (float) │        │
 in_b ◄────────┤        │
               └────────┘
struct Summer
{
    ramen::Puller<float> in_a;
    ramen::Puller<float> in_b;
    ramen::Pullable<float> out_sum = [this](float& out) { out = *in_a + *in_b; };
};

Больше примеров, а также объяснение зачем это нужно при наличии альтернатив, по ссылке. Всем спасибо.

Теги:
Всего голосов 5: ↑4 и ↓1+3
Комментарии0

Готовим прошивки для BIOS. Нужно всего лишь...

В YADRO есть команда, которая разрабатывает и отлаживает UEFI для самых разных продуктов компании. Подробнее о ней — в видеоролике → .

Первая особенность работы — в распределенной команде. Серверы, на которых инженеры разрабатывают прошивки, могут находиться в Москве, а разработчики — в других городах: от Владивостока до Калининграда и от Архангельска до Смоленска.

Системы, для которых разрабатывают прошивки в YADRO, делятся на два типа: с BMC и без него.

Больше о работе команды и, в целом, об истории BIOS/UEFI читайте в статье → 

А если тоже хотите стать «поваром» UEFI, присмотритесь к одной из вакансий:

→ Старший/ ведущий разработчик на C++ (Linux/OpenBMC)

→ Старший разработчик на С (BIOS/UEFI)

→ Тимлид разработки в OpenBMC

Теги:
Всего голосов 3: ↑3 и ↓0+5
Комментарии0

SystemVerilog отжил свое? На пятки наступает Scala/Chisel?

DARPA, управление перспективных исследовательских проектов Минобороны США, описывает Chisel как технологию, позволяющую маленьким командам создавать большие цифровые проекты. И я вполне могу с этим согласиться, но есть нюансы.

Chisel — это, по сути, библиотека Scala, а точнее, Domain Specific Language. Языку Scala уже больше 20 лет, он постоянно развивается, сочетает функциональное и императивное программирование. При написании кода на Scala вам доступны все библиотеки Java. 

Scala — это масштабируемый язык, который позволяет добавлять свои языковые конструкции. На основе Scala можно создать язык под свои задачи. Так 12 лет назад и поступили инженеры в Беркли: выкинули из Verilog 90%, оставив только нужное, и обернули все это в Scala. Получился Chisel. 

Chisel используют прежде всего для создания RTL-описаний. Также он позволяет проводить симуляцию несложных модулей. Это удобно для создания юнит-тестов и моделирования работы различных алгоритмов. В плане симуляции не стоит возлагать на Chisel такие же надежды, как на System C или что-то подобное. Симулировать вы сможете лишь очень маленькие схемки, а генерировать — хоть целые кластеры из тысяч процессоров, вообще все, что захотите.

На основе Chisel/Scala можно написать свой HLS-инструмент (High Level Synthesis), где одним росчерком пера вы будете создавать очень большие схемы, что с использованием одного Verilog невозможно.

В блоге YADRO Денис Муратов подробно сравнил Chisel/Scala с SystemVerilog в создании RTL-описаниях, раскрыл основные преимущества и недостатки альтернативы, а также ее дополнительные возможности — функциональное программирование и переиспользование модулей.

Теги:
Всего голосов 4: ↑4 и ↓0+5
Комментарии0

Новый релиз языка Zig 0.14.0

Github Release

Описания изменений версии

Я информирую о релизе постом только потому, что у меня лично сейчас нет свободного времени написать полноценную статью об всех изменениях. Изменений много.

Важное изменение, которое всё же я упомяну - появился свой backend для компилятора. То есть отказ от LLVM состоялся, но на данный момент только для Linux x86_64.

Теги:
Всего голосов 4: ↑3 и ↓1+4
Комментарии0
1

Вклад авторов