function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); } const p = (text) => '
' + text + '
'; const h2 = (text) => '' + escapeHtml(text) + '';
const ol = (items) => '| ' + cell + ' | ').join('') + '
|---|
| ' + cell + ' | ').join('') + '
in и postcondition через out. Это не универсальная валидация входа и не замена тестам. Контракт полезен, когда условие принадлежит самой функции: размер диапазона, допустимый индекс, инвариант результата. Для пользовательского файла всё равно нужна отдельная ошибка с безопасным сообщением и понятным форматом.'),
p('При выборе языка не обещайте, что contract автоматически ускорит программу или найдёт бизнес-ошибку. Он проверяет условие в точке выполнения и зависит от режима сборки. Важнее сначала назвать, кто владеет условием: parser, domain service или boundary с C. Тогда один и тот же контракт можно повторить в тесте и в обработчике ошибки, не пряча смысл в assertion.'),
h2('Действия по порядку'),
ol([
'Записать единицу нагрузки, бюджет задержки, размер данных, target-платформы и входные ограничения.',
'Найти измеряемый bottleneck и проверить, находится ли он внутри кода, который язык действительно изменит.',
'Сравнить D и текущий вариант по сборке, библиотекам, отладке, размеру бинарника и навыкам поддержки.',
'Для нативной границы описать C ABI, владение памятью и ошибку; unsafe-участок выделить отдельно.',
'Собрать минимальный прототип с одним workload и одинаковой методикой замера, затем зафиксировать результат и цену поддержки.',
]),
h2('Ограничения и следующий шаг'),
p('Фильтр требований не измеряет скорость и не говорит, что D лучше. Порог throughput в примере вымышленный и не переносится на другое железо. Контракты не устраняют логические ошибки, зависимость от внешней библиотеки или стоимость сборки. D также не делает код переносимым автоматически: platform ABI, linker и runtime остаются частью решения.'),
p('Следующий шаг — взять одну горячую функцию и сделать парный прототип на текущем языке и D с одинаковым входом и выходом. Замерьте cold start, steady-state, память и время разработчика на исправление намеренной ошибки. Решение «остаться» будет таким же полезным результатом, как переход, если оно опирается на эти поля.'),
], [
{ key: 'functions', use: 'Function contracts и атрибуты D используются для объяснения pre/post conditions и границы функции.', boundary: 'Спецификация не выбирает язык для продукта и не даёт benchmark конкретного workload.' },
{ key: 'memory', use: 'Категории @safe, @trusted и @system используются при оценке нативной границы.', boundary: 'Memory safety не гарантирует отсутствие логических, portability и performance ошибок.' },
{ key: 'abi', use: 'ABI-граница включена в матрицу выбора как часть доставки бинарника.', boundary: 'Спецификация ABI не описывает настройки конкретного компилятора и платформы.' },
]);
const mechanism = revision({
slug: 'editorial-2027-03-mechanism-d-lessons',
title: 'D: @safe и @trusted на границе C API',
categories: ['D', 'Безопасность памяти'],
cover: '/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg',
excerpt: 'Как провести маленький unsafe-участок через проверенный интерфейс и не считать атрибут @safe доказательством всей системы.',
readingMinutes: 16,
}, [
p('Проблема FFI-кода появляется там, где D вызывает C-функцию с указателем и отдельной длиной буфера. Если длина пришла из другого источника, вызов может прочитать за пределами памяти, даже когда внешний метод выглядит коротким. Цена ошибки — повреждение памяти, падение процесса или уязвимость, которую трудно воспроизвести по обычному input. Один атрибут на публичной функции не исправляет неверное условие границы.'),
p('В D для этой границы различаются @safe, @trusted и @system. @safe-код ограничивает операции, которые могут привести к memory corruption. @trusted разрешает узкий участок, но ответственность за его интерфейс остаётся у автора. @system не даёт компилятору такого обещания. Механизм работает, если unsafe-код короткий, его входы проверены, а наружу выходит безопасное представление данных.'),
h2('Сначала проверяем размер, потом вызываем C'),
p('C-функция часто получает pointer + length. Сам указатель не содержит длину, поэтому компилятор не может вывести, что заявленный диапазон действителен. В D безопасный wrapper должен принять массив или slice, проверить нужное условие и передать только диапазон, размер которого известен. Если C API требует null-terminated string, одного массива байт тоже недостаточно: нужна отдельная проверка завершающего байта.'),
p('Изолируйте правила владельца. Если C-функция сохраняет указатель после возврата, wrapper должен либо запретить такой вызов, либо передать копию с понятным временем жизни. Атрибут scope помогает выражать ограничения escape там, где включена соответствующая проверка, но он не заменяет договорённость с внешней библиотекой. Любая функция, которая сохраняет адрес, требует отдельного чтения API и теста.'),
figure('/assets/editorial/2027/d-lessons-2027-constraint-matrix.svg', 'Матрица границы D и C API: размер буфера, владелец, атрибут безопасности и допустимый результат проверки.', 'Схема связывает техническое ограничение с проверкой и стоп-условием. Зелёный путь начинается только после проверки длины и времени жизни.'),
table('Роли атрибутов на FFI-границе', ['Уровень', 'Что разрешает', 'Что обязан проверить инженер', 'Типичная ошибка'], [
['@safe', 'ограниченный набор операций', 'что вызовы и значения остаются безопасными', 'считать весь вызванный C безопасным'],
['@trusted', 'узкая ручная обёртка', 'инвариант указателя, длины и lifetime', 'поместить большой модуль в trusted'],
['@system', 'низкоуровневые операции', 'каждый callsite и контракт ABI', 'передать raw pointer без проверки'],
['slice', 'указатель и длина вместе', 'что slice не выходит за объект', 'довериться внешней length'],
]),
h2('Локальная проверка буфера'),
p('Вместо вызова реальной C-библиотеки сначала можно прогнать boundary checker на данных теста. Он принимает capacity, заявленную длину и признак проверки указателя. Результат разделяет отсутствие проверки, неверный диапазон и безопасный интерфейс. Это предметный пример входа в FFI: он проверяет именно опасную пару pointer/length, а не абстрактный статус карточки.'),
code(`import { checkSafeBoundary } from './upgrade-2027-03.mjs';
const calls = [
{ capacity: 16, declaredLength: 8, pointerChecked: true },
{ capacity: 16, declaredLength: 24, pointerChecked: true },
{ capacity: 16, declaredLength: 8, pointerChecked: false },
];
for (const call of calls) console.log(checkSafeBoundary(call));
// safe-interface; reject; system`),
p('Первый вход даёт диапазон внутри буфера. Второй останавливается до вызова: внешний контракт обещает 24 байта, а доступно 16. Третий не принимает решение за инженера, потому что адрес не прошёл проверку владельца. В D такой проверкой должен владеть маленький wrapper, а в тесте нужны граничные значения 0, capacity и capacity+1.'),
h2('Что означает @trusted'),
p('@trusted — не «проверено компилятором». Это обещание, что внешняя форма функции безопасна, хотя тело содержит операции, которые компилятор не может проверить. Поэтому у trusted-функции должны быть короткий исходник, явные preconditions и тесты на invalid length, null, пустой slice и повторный вызов. Не прячьте в ней преобразование формата, ownership и обработку ошибок одновременно.'),
p('Если внешняя C-функция возвращает указатель, проверка должна ответить на два вопроса: объект жив и его размер известен? При ответе «нет» безопасный интерфейс невозможен без копирования или дополнительного контракта. После вызова нельзя использовать старый slice, если C-функция освобождает память. Ошибка lifetime часто переживает тесты на успешном input, поэтому негативная матрица обязательна.'),
h2('Действия по порядку'),
ol([
'Прочитать C-прототип и зафиксировать смысл каждого указателя, длины, возвращаемого адреса и кода ошибки.',
'Выделить минимальный wrapper; не переносить внутрь @trusted парсинг, бизнес-правила и сетевой код.',
'Проверять указатель, диапазон, нулевую длину, overflow и время жизни до перехода в C.',
'Поставить unit tests на валидные и граничные значения, затем прогнать sanitizers или инструменты платформы.',
'Оставить публичную функцию @safe только при доказанном безопасном интерфейсе; остальную зону явно маркировать @system.',
]),
h2('Ограничения и следующий шаг'),
p('Проверка capacity в JavaScript — учебная модель числовой границы, а не анализ D-памяти. Она не видит aliasing, alignment, calling convention, null termination или освобождение в C. Даже корректный @safe wrapper может передать семантически неверный enum или структуру. Нужен compile-time и runtime тест именно тем компилятором и ABI, с которыми собирается продукт.'),
p('Следующий шаг — выбрать один extern(C) вызов и оформить для него таблицу: pointer, length, ownership, error, thread-safety. Напишите маленький wrapper, который принимает D slice, и отдельно проведите review trusted-тела. Если один из пунктов не имеет ответа, вызов нельзя считать готовым к безопасной границе.'),
], [
{ key: 'memory', use: 'Определения @safe, @trusted, @system, scope и границы memory safety.', boundary: 'Спецификация не проверяет контракт внешней C-библиотеки и не гарантирует portability или отсутствие логических ошибок.' },
{ key: 'functions', use: 'Правила function attributes и contract expressions используются для precondition и postcondition.', boundary: 'Документация не создаёт unit tests и не решает lifetime конкретного объекта.' },
{ key: 'cInterface', use: 'Интерфейс D/C и отдельные соглашения вызова используются для описания wrapper boundary.', boundary: 'Страница не подтверждает прототип, ABI и ownership неизвестной библиотеки.' },
]);
const field = revision({
slug: 'editorial-2027-03-field-d-lessons',
title: 'D и C ABI: разобрать пакет до вызова',
categories: ['D', 'Интеграции'],
cover: '/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg',
excerpt: 'Полевой разбор FFI-ошибки: размер структуры, порядок байтов и обязательные поля проверяются до вызова C.',
readingMinutes: 15,
}, [
p('Проблема C ABI редко выглядит как ошибка на строке вызова. D-программа передаёт структуру, C читает её и возвращает код, но значение поля оказывается неверным или процесс падает только на одной архитектуре. Цена — непредсказуемый сбой на границе, где обычный unit test видит только один компилятор и одну раскладку памяти. Чем дольше ошибка живёт, тем труднее отличить формат данных от ошибки бизнес-логики.'),
p('Полевой разбор начинается с пакета, который реально пересекает ABI: byte length, endianness, поля, alignment и calling convention. Дальше нужно сопоставить его с C header и настройками компилятора. Нельзя проверять только имя struct. Два типа с одинаковыми полями могут иметь разный padding, порядок байтов или размер указателя. Пакет — это физический контракт, а не только исходный текст.'),
h2('Размер структуры — первый стоп'),
p('ABI определяет, как типы и функции представлены для взаимодействия с машинным кодом. Для структуры важны не только поля, но и выравнивание. Добавленный int может изменить offsets следующего поля; на 32- и 64-битной платформе размер указателя различается. Если D и C собраны с разными ожиданиями, чтение смещается, а ошибка проявится как «неверное значение» далеко от причины.'),
p('Порядок байтов — отдельная ось. Файл может быть little-endian, а внешний протокол — big-endian; автоматическое копирование структуры не является преобразованием формата. Для числового поля запишите wire representation и проверяйте её на известном значении вроде 0x01020304. Так видно, поменялись байты или перепутана длина пакета.'),
figure('/assets/editorial/2027/d-lessons-2027-evidence-handoff-loop.svg', 'Цикл проверки D и C ABI: пакет, размер, поля и порядок байтов проходят сопоставление с контрактом до вызова функции.', 'Схема отделяет физическую проверку пакета от самого вызова. Красная ветка возвращает данные на границу, если размер или поле не совпали.'),
table('Проверка ABI-пакета до FFI-вызова', ['Проверка', 'Пример входа', 'Ожидаемое действие', 'Если пропустить'], [
['byte length', '24 байта', 'сверить sizeof на обеих сторонах', 'смещение полей'],
['endianness', 'little-endian', 'декодировать число явно', 'неверный id или размер'],
['field set', 'version, flags, payload', 'проверить обязательные поля', 'чтение мусора'],
['alignment', 'offset 8 вместо 4', 'сверить compiler layout', 'сбой на другой архитектуре'],
['error code', '0 или отрицательное значение', 'перевести в D-ошибку', 'успех при частичном чтении'],
]),
h2('Учебный валидатор пакета'),
p('Локальная функция ниже принимает пакет и описание ожидаемой структуры. Она проверяет три вещи, которые можно увидеть ещё до вызова: размер, порядок байтов и набор полей. Числа и названия в примере учебные, но сам порядок повторяет рабочую проверку. В реальном проекте contract строится из header и результатов компилятора, а не из догадки автора wrapper.'),
code(`import { validateCAbiPacket } from './upgrade-2027-03.mjs';
const contract = {
byteLength: 12,
endianness: 'little',
fields: [{ name: 'version' }, { name: 'flags' }, { name: 'payload' }],
};
const packet = { byteLength: 12, endianness: 'little', version: 2, flags: 1, payload: 4096 };
const broken = { byteLength: 16, endianness: 'big', version: 2, flags: 1 };
console.log(validateCAbiPacket(packet, contract));
console.log(validateCAbiPacket(broken, contract));
// { accepted: true, errors: [] }
// { accepted: false, errors: ['размер структуры', 'порядок байтов', 'поле payload'] }`),
p('Валидатор не вызывает C и поэтому не доказывает, что ABI корректен. Он делает видимыми три несовпадения до опасной операции. Для полноценного теста добавьте golden bytes, сборку маленького C helper и проверку результата на каждой целевой архитектуре. Смысл локального примера в том, что ошибка в contract table должна быть заметна раньше падения процесса.'),
h2('Где заканчивается автоматическая проверка'),
p('Размер и поля можно сравнить автоматически, но ownership и смысл flags требуют чтения C API. Поле payload может быть указателем, длиной или offset внутри того же пакета. Значение 0 может означать пусто, success или null. Не называйте результат «валидным пакетом», пока не проверены эти семантические значения и код ошибки.'),
p('Особенно опасен частичный успех. C-функция могла записать структуру, но вернуть ошибку; D-код видит заполненное поле и продолжает обработку. Обёртка должна сначала проверить код возврата, затем интерпретировать output по версии и только потом отдавать его доменному коду. Версия пакета должна быть частью ключа выбора декодера, а не обычным полем, которое можно проигнорировать.'),
h2('Действия по порядку'),
ol([
'Сохранить точный C header, compiler flags, target architecture и calling convention рядом с исходником wrapper.',
'Составить layout table с размером, offset, alignment, типом и смыслом каждого поля.',
'Проверить golden bytes для little/big-endian и граничных значений длины до вызова внешней функции.',
'Разделить код возврата, output и ошибку; не считать частично заполненную структуру успешным результатом.',
'Собрать тест на каждой поддерживаемой архитектуре и сохранить hex-пакет, версию контракта и итог проверки.',
]),
h2('Ограничения и следующий шаг'),
p('JavaScript-валидатор не моделирует padding, pointer alignment, compiler lowering и реальные байты. Он проверяет форму contract table, поэтому не заменяет C helper, D compiler и тест на целевом ABI. Документ D описывает правила языка, но конкретный vendor header может добавлять свои packing directives, версии и ownership соглашения.'),
p('Следующий шаг — выбрать один extern(C) вызов, получить маленький golden packet и сравнить layout D/C в автоматической проверке. В отчёте оставьте hex, размер, архитектуру и код возврата. Это позволит отличить изменение компилятора от изменения данных и быстро вернуть ошибку к физической границе.'),
], [
{ key: 'abi', use: 'Правила D ABI используются для объяснения layout, alignment и представления типов.', boundary: 'Спецификация не знает vendor header, compiler flags и архитектуру конкретного проекта.' },
{ key: 'cInterface', use: 'Правила extern(C) и взаимодействия с C используются для выбора contract table и calling convention.', boundary: 'Документация не гарантирует корректность неизвестного прототипа или ownership.' },
{ key: 'memory', use: 'Граница memory safety используется для отделения безопасного анализа пакета от raw pointer операций.', boundary: 'Проверка памяти не подтверждает семантику полей и код возврата внешней библиотеки.' },
]);
export const revisions = Object.freeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() {
const checks = revisions.map((item) => {
const body = bodyText(item.contentHtml);
return body.length >= 5000 && body.length <= 15000 && /