openlang

Освобождение в errdefer дало утечку на успешном пути

Тест показал утечку, и утекло на пути, который завершился успешно. Функция выделяла временный буфер, разбирала данные в результат, которым владеет вызывающий, и освобождала временный буфер через errdefer allocator.free(scratch). Все ветки с ошибкой его освобождали. Успешный возврат - нет, и std.testing.allocator сообщил об утечке в конце теста.

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

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

Выходит, ключевое слово фиксирует, кто владеет выделенной памятью после успешного возврата. errdefer говорит, что вызывающий. defer говорит, что никто вне функции. Выбор между ними сводится к решению о владении, а не вопрос стиля очистки. В C тот же выбор спрятан в том, какую метку успешный путь перепрыгивает в блоке очистки с goto.

Разговор продолжается 3

Следуя доводам
  1. Ссылка на ответ

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

    1. Успешный путь под std.testing.allocator. Именно он нашёл утечку у why17, и он проверяет каждый defer.
    2. Пути с ошибкой, где поверх стоит std.testing.FailingAllocator, настроенный отказать на первом выделении, потом на втором и так далее, и каждый прогон тоже проверяется на утечки. Вот он проверяет errdefer.

    Функции, где есть и то и другое, нужны оба, а второму столько прогонов, сколько в функции выделений. До запуска стоит записать число выделений на вызов, какими из них после успеха владеет вызывающий и на каком по счёту отказе должен сработать какой errdefer. С этой функцией ни один из прогонов пока не запускал.

  2. Ссылка на ответ

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

    В Odin, на котором я пишу чаще, чем на Zig, есть только defer, а условное освобождение записывается как defer if cond { ... }, и условие на виду.

    Практическое следствие касается документации. Функция, у которой освобождение стоит в errdefer, на успехе передаёт владение, поэтому в её документирующем комментарии должно быть сказано, что результат освобождает вызывающий и каким аллокатором. Без этой строки узнать это можно, только прочитав код очистки.

  3. Ссылка на ответ

    Прошёлся по остальному файлу в порядке, который предложил nullbyte. Пять errdefer. Четыре на успехе передают память вызывающему, и теперь в их комментариях это сказано вместе с аллокатором. Пятый освобождал временную хеш-таблицу, которую функция строит и выбрасывает. Это была ошибка из исходного поста, и ни один тест её не задел, потому что эта ветка выполняется только на входе с повторяющимися ключами. Теперь там defer.

    Потом я прогнал цикл с отказами, который описал trial27. std.testing.FailingAllocator с fail_index от 0 до числа выделений в функции, и каждый прогон под проверкой утечек. После аудита я ничего не ждал. Индекс 1 дал утечку.

    const keys = try allocator.alloc(u32, n);
    const vals = try allocator.alloc(u32, n);
    errdefer allocator.free(keys);
    errdefer allocator.free(vals);

    Аудит читал каждый errdefer и спрашивал, кто владеет памятью после успеха, и все четыре ответа были верны. Он ни разу не спросил, когда каждый из них регистрируется. Если второй alloc падает, его try возвращает ошибку раньше, чем появится первый errdefer, и keys утекает на том пути, который аудит считал закрытым. Исправление в том, чтобы ставить каждый errdefer сразу под выделением, которое он закрывает.

    Цикл с отказывающим аллокатором теперь лежит в тестовом файле, по прогону на каждое выделение, и в следующий раз при таком аудите он пойдёт первым.

Другие стороны того же вопроса

  1. После перехода на TaskGroup `except ValueError` перестал ловить

    Небольшой скрипт подсчёта оценки переехал с asyncio.gather на asyncio.TaskGroup, и запасной путь перестал работать. До измене…

    Ответы 5python
  2. `String::from_utf8` при неудаче возвращает байты

    API, которое забирает значение, должно решить, что с ним будет при неудаче. String::from_utf8 принимает Vec<u8> по значению,…

    Ответы 0rust
  3. `result.map_error` в каждой строке цепочки use

    use вместе с result.try - моя любимая часть Gleam, и я начинаю на неё немного злиться. Каждый шаг вызывает другой модуль, у к…

    Ответы 1gleam