Проблема FFI-кода появляется там, где D вызывает C-функцию с указателем и отдельной длиной буфера. Если длина пришла из другого источника, вызов может прочитать за пределами памяти, даже когда внешний метод выглядит коротким. Цена ошибки — повреждение памяти, падение процесса или уязвимость, которую трудно воспроизвести по обычному input. Один атрибут на публичной функции не исправляет неверное условие границы.
В D для этой границы различаются @safe, @trusted и @system. @safe-код ограничивает операции, которые могут привести к memory corruption. @trusted разрешает узкий участок, но ответственность за его интерфейс остаётся у автора. @system не даёт компилятору такого обещания. Механизм работает, если unsafe-код короткий, его входы проверены, а наружу выходит безопасное представление данных.
Сначала проверяем размер, потом вызываем C
C-функция часто получает pointer + length. Сам указатель не содержит длину, поэтому компилятор не может вывести, что заявленный диапазон действителен. В D безопасный wrapper должен принять массив или slice, проверить нужное условие и передать только диапазон, размер которого известен. Если C API требует null-terminated string, одного массива байт тоже недостаточно: нужна отдельная проверка завершающего байта.
Изолируйте правила владельца. Если C-функция сохраняет указатель после возврата, wrapper должен либо запретить такой вызов, либо передать копию с понятным временем жизни. Атрибут scope помогает выражать ограничения escape там, где включена соответствующая проверка, но он не заменяет договорённость с внешней библиотекой. Любая функция, которая сохраняет адрес, требует отдельного чтения API и теста.
| Уровень | Что разрешает | Что обязан проверить инженер | Типичная ошибка |
|---|---|---|---|
| @safe | ограниченный набор операций | что вызовы и значения остаются безопасными | считать весь вызванный C безопасным |
| @trusted | узкая ручная обёртка | инвариант указателя, длины и lifetime | поместить большой модуль в trusted |
| @system | низкоуровневые операции | каждый callsite и контракт ABI | передать raw pointer без проверки |
| slice | указатель и длина вместе | что slice не выходит за объект | довериться внешней length |
Локальная проверка буфера
Вместо вызова реальной C-библиотеки сначала можно прогнать boundary checker на данных теста. Он принимает capacity, заявленную длину и признак проверки указателя. Результат разделяет отсутствие проверки, неверный диапазон и безопасный интерфейс. Это предметный пример входа в FFI: он проверяет именно опасную пару pointer/length, а не абстрактный статус карточки.
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Первый вход даёт диапазон внутри буфера. Второй останавливается до вызова: внешний контракт обещает 24 байта, а доступно 16. Третий не принимает решение за инженера, потому что адрес не прошёл проверку владельца. В D такой проверкой должен владеть маленький wrapper, а в тесте нужны граничные значения 0, capacity и capacity+1.
Что означает @trusted
@trusted — не «проверено компилятором». Это обещание, что внешняя форма функции безопасна, хотя тело содержит операции, которые компилятор не может проверить. Поэтому у trusted-функции должны быть короткий исходник, явные preconditions и тесты на invalid length, null, пустой slice и повторный вызов. Не прячьте в ней преобразование формата, ownership и обработку ошибок одновременно.
Если внешняя C-функция возвращает указатель, проверка должна ответить на два вопроса: объект жив и его размер известен? При ответе «нет» безопасный интерфейс невозможен без копирования или дополнительного контракта. После вызова нельзя использовать старый slice, если C-функция освобождает память. Ошибка lifetime часто переживает тесты на успешном input, поэтому негативная матрица обязательна.
Действия по порядку
- Прочитать C-прототип и зафиксировать смысл каждого указателя, длины, возвращаемого адреса и кода ошибки.
- Выделить минимальный wrapper; не переносить внутрь @trusted парсинг, бизнес-правила и сетевой код.
- Проверять указатель, диапазон, нулевую длину, overflow и время жизни до перехода в C.
- Поставить unit tests на валидные и граничные значения, затем прогнать sanitizers или инструменты платформы.
- Оставить публичную функцию @safe только при доказанном безопасном интерфейсе; остальную зону явно маркировать @system.
Ограничения и следующий шаг
Проверка capacity в JavaScript — учебная модель числовой границы, а не анализ D-памяти. Она не видит aliasing, alignment, calling convention, null termination или освобождение в C. Даже корректный @safe wrapper может передать семантически неверный enum или структуру. Нужен compile-time и runtime тест именно тем компилятором и ABI, с которыми собирается продукт.
Следующий шаг — выбрать один extern(C) вызов и оформить для него таблицу: pointer, length, ownership, error, thread-safety. Напишите маленький wrapper, который принимает D slice, и отдельно проведите review trusted-тела. Если один из пунктов не имеет ответа, вызов нельзя считать готовым к безопасной границе.
Проверяемые источники
- D Language Specification: Memory Safety — версия и дата: D language specification, page generated 23 July 2026. Применение: Определения @safe, @trusted, @system, scope и границы memory safety. Граница: Спецификация не проверяет контракт внешней C-библиотеки и не гарантирует portability или отсутствие логических ошибок.
- D Language Specification: Functions and Function Safety — версия и дата: D language specification, page generated 25 July 2026. Применение: Правила function attributes и contract expressions используются для precondition и postcondition. Граница: Документация не создаёт unit tests и не решает lifetime конкретного объекта.
- D Language Specification: Interfacing to C — версия и дата: D language specification, page checked 31 July 2026. Применение: Интерфейс D/C и отдельные соглашения вызова используются для описания wrapper boundary. Граница: Страница не подтверждает прототип, ABI и ownership неизвестной библиотеки.