Files
progcode/editorial/agent-rewrites/028.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 28,
"slug": "editorial-2027-03-field-d-lessons",
"title": "D и C ABI: как остановить ошибку на границе пакета",
"excerpt": "Если D и C по-разному понимают размер, layout или код возврата, ошибка проявляется далеко от FFI-вызова. Разбираем физический контракт пакета и проверяем его до передачи в C.",
"contentHtml": "<p>Сбой на границе D и C часто выглядит случайным. На одной архитектуре функция возвращает неверный идентификатор. На другой процесс падает при чтении поля. В тесте с коротким пакетом всё проходит, а реальный пакет ломает декодер. Цена ошибки высока: данные уже могли попасть в доменную логику, а причина остаётся в нескольких байтах, которые две стороны интерпретируют по-разному.</p><p>Тезис простой: FFI-вызов нельзя считать началом проверки. Сначала нужно подтвердить физический контракт пакета. Он включает размер, offsets, alignment, порядок байтов, набор обязательных полей, calling convention, ownership и код возврата. Только после этого пакет можно передавать в C. Имя структуры и успешная компиляция этого не доказывают.</p><h2>Что именно ломается</h2><p>ABI описывает представление типов и вызовов на машинной границе. В D и C совпадение названий полей не гарантирует совпадение layout. Между двумя полями может появиться padding. Указатель занимает разный размер на разных target-платформах. Директива packing меняет offsets. Сборка с другим compiler flag создаёт другой контракт, даже если исходный header не изменился.</p><p>Порядок байтов нужно проверять отдельно. Структура из памяти не является wire-форматом. Число <code>0x01020304</code> в little-endian и big-endian занимает те же четыре байта, но читается с разным значением. Если код копирует входной буфер в структуру без явного декодирования, ошибка будет похожа на неверный размер или повреждённый id.</p><p>Есть и семантическая часть. Поле <code>payload</code> может быть указателем, длиной или смещением внутри буфера. Ноль может означать пустой пакет, null или успешный результат. C-функция может частично заполнить output и вернуть ошибку. Поэтому проверка размера без проверки ownership и кода возврата создаёт ложное чувство безопасности.</p><figure><img src=\"/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg\" alt=\"Схема проверки D и C ABI: пакет проходит проверку размера, полей и порядка байтов до FFI-вызова.\" loading=\"lazy\" /><figcaption>Проверка отделяет физический контракт от вызова. Несовпадение возвращает пакет на границу и останавливает опасную операцию.</figcaption></figure><div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Поле имеет неверное значение только на одной платформе</td><td>Разный padding, alignment или размер указателя</td><td>Сравнить sizeof, offsets и alignment D/C на каждом target</td><td>Зафиксировать layout, выровнять типы или сериализовать поля явно</td></tr><tr><td>Число меняется после чтения буфера</td><td>Перепутан порядок байтов</td><td>Прогнать golden bytes с <code>0x01020304</code></td><td>Декодировать wire-формат явно, не копировать структуру целиком</td></tr><tr><td>Редкий crash после успешного вызова</td><td>Неверная длина или истёкшее владение памятью</td><td>Проверить pointer, length, lifetime и правило освобождения</td><td>Сузить wrapper, скопировать данные или вернуть ошибку до C</td></tr><tr><td>Ошибочный пакет выглядит успешным</td><td>Output читается до проверки кода возврата</td><td>Проверить порядок обработки return code и output</td><td>Сначала переводить ошибку, потом интерпретировать output</td></tr><tr><td>Тесты проходят, production-пакет не читается</td><td>Тест использует другой header, target или версию протокола</td><td>Сохранить hex-пакет, compiler flags и версию ABI</td><td>Добавить контрактный тест для реального target matrix</td></tr></tbody></table></div><h2>Минимальный безопасный порядок</h2><p>Начните с байтов, а не с вызова. Сохраните один пакет, который воспроизводит проблему, и его ожидаемую расшифровку. Укажите длину, архитектуру, endianness и версию контракта. Без этих данных «неверное поле» остаётся описанием симптома.</p><p>Затем составьте layout table. Для каждого поля запишите тип C, тип D, offset, размер, alignment, смысл, допустимый диапазон и владельца памяти. Если поле является указателем, рядом должна стоять длина и правило освобождения. Если их нельзя указать, wrapper не готов.</p><p>Только после этого сравните C header и D-объявление. Сверьте calling convention и compiler flags. Отдельно проверьте, не добавляет ли C-код packing или условную компиляцию. Сборка одного target не подтверждает остальные.</p><h2>Учебный фрагмент wrapper</h2><p>Ниже приведён учебный фрагмент. Он показывает порядок проверки пустого буфера и передачи длины. Имена функции, код ошибки и типы условны. Фрагмент не доказывает корректность конкретной библиотеки и не является production-результатом.</p><pre><code>extern(C) int decode_packet(\\n const(ubyte)* data,\\n size_t length,\\n uint* version,\\n);\\n\\n// Учебный пример: контракт библиотеки нужно подтвердить по её header.\\nint decode(scope const(ubyte)[] data, out uint version) @trusted\\n{\\n if (data.length == 0)\\n return -1;\\n\\n return decode_packet(data.ptr, data.length, &amp;version);\\n}</code></pre><p>В примере wrapper не передаёт null вместе с ненулевой длиной. Но это только одна проверка. Реальный контракт может требовать null для пустого буфера, завершающий ноль, выравнивание адреса или отдельный allocator. Поэтому правило нельзя переносить на библиотеку без чтения её header и документации.</p><p>Атрибут <code>@trusted</code> не означает, что C-вызов проверен компилятором. Он означает, что автор wrapper берёт на себя доказательство инвариантов. Держите такую функцию короткой. Не смешивайте в ней разбор формата, бизнес-правила и освобождение памяти. Чем шире trusted-зона, тем труднее проверить её границу.</p><h2>Проверка кода возврата</h2><p>Обрабатывайте результат внешней функции в фиксированном порядке: сначала код возврата, затем размер и версию output, затем семантику полей. Не используйте частично заполненную структуру после ошибки. Если C API допускает частичный output, это должно быть явно записано в контракте и покрыто отдельным тестом.</p><p>Для числовых полей нужны golden bytes. Возьмите известное значение, закодируйте его в требуемом wire-формате и сравните результат на D-стороне. Такой тест показывает, где ошибка: в байтах, offsets или выборе типа. Для указателей добавьте нулевую длину, длину ровно до границы и длину на один байт больше.</p><h2>Действия по порядку</h2><ol><li>Зафиксировать C header, D-объявление, compiler flags, target architecture, packing directives и calling convention.</li><li>Составить таблицу layout: размер, offset, alignment, тип, значение, длина и владелец каждого поля.</li><li>Сохранить golden bytes для корректного пакета, неверного endianness, обрезанной длины и неизвестной версии.</li><li>Вынести FFI в маленький wrapper и поставить проверки pointer, length, lifetime и кода возврата до передачи данных доменному коду.</li><li>Проверить валидный и отрицательный пути на каждой поддерживаемой архитектуре, включая границы 0, capacity и capacity+1.</li><li>Сохранить в отчёте hex-пакет, размер, target, версию ABI и точную ошибку. Это связывает симптом с физическим контрактом.</li></ol><h2>Когда проверка должна остановить вызов</h2><p>Остановите вызов, если размер не совпал, обязательное поле отсутствует, версия неизвестна, pointer не согласован с length или правило ownership не имеет ответа. Не пытайтесь «продолжить с тем, что удалось прочитать». На FFI-границе частичный успех часто превращается в повреждённое состояние выше по стеку.</p><p>Если layout зависит от платформы, есть два пути. Можно описать отдельные контракты и тестировать каждый target. Можно отказаться от передачи структуры и использовать явную сериализацию полей в буфер. Второй путь иногда медленнее, но уменьшает зависимость от padding и размера указателя. Выбирайте его, когда переносимость важнее нулевой копии.</p><h2>Ограничения</h2><p>Учебный wrapper не моделирует все правила D, C и конкретной библиотеки. Он не проверяет compiler lowering, alignment адреса, aliasing, null termination, thread safety, освобождение памяти и совместимость версий. Таблица layout не заменяет сборку маленького C helper и тест на целевом ABI. Документация языка объясняет общие правила, но не подтверждает vendor header.</p><p>Не называйте проверку успешной только потому, что код компилируется и один тест возвращает ожидаемое поле. Готовность требует повторяемого отрицательного пути. Ошибочный размер должен остановить вызов. Ошибочный порядок байтов должен быть виден в golden test. Ошибка C не должна превращаться в валидный доменный объект.</p><h2>Критерий готовности</h2><p>Граница готова, если для каждого target есть зафиксированные размер и layout, есть golden bytes и тесты на отрицательные случаи, wrapper проверяет pointer, length, lifetime и код возврата, а неизвестная версия или несовпадение контракта останавливает вызов. Проверка должна оставлять диагностический пакет: hex, target, версию ABI и причину отказа. Тогда следующая ошибка возвращается к конкретному байту, а не к предположению о языке.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://dlang.org/spec/abi.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Application Binary Interface</a> — правила представления типов, layout и ABI. Источник не знает compiler flags, packing directives и архитектуру конкретного проекта.</li><li><a href=\"https://dlang.org/spec/interfaceToC.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Interfacing to C</a> — правила extern(C) и взаимодействия D с C. Источник не подтверждает корректность неизвестного прототипа, ownership или кода возврата.</li><li><a href=\"https://dlang.org/spec/memory-safe-d.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Language Specification: Memory Safety</a> — границы memory safety и ручных указателей. Источник не проверяет семантику полей и контракт внешней библиотеки.</li></ul>"
}