Undefined behavior – это все, что явно не указано в документации.

June 30th, 2008

Навеяно постом про ExUuidCreate и в частности вот этой фразой:

Well, I suggest MSFT to documented this behavior, or at least explain this case in documentation.

Вкратце, суть статьи сводится к тому, что функция ExUuidCreate может изменять содержимое возвращаемого буфера даже в том случае, если она возвращает ошибку. Далее, в комментариях, завязался спор на тему имеет ли функция право трогать буфер в случае ошибки.

Для начала небольшое объяснение почему эта функция имеет право делать с буфером всё что угодно. ExUuidCreate объявлена следующим образом:

NTSTATUS
ExUuidCreate(
    OUT UUID *Uuid,
    );

Т.е. она пишет сгенерированный GUID в буфер, выделенный вызывающей стороной. Тут важно, что параметр объявлен как “OUT” параметр. Посмотрим, что говорит документация:

__out: The function will only write to the buffer. If used on the return value or with _deref, the function will provide the buffer and initialize it. Otherwise, the caller must provide the buffer, and the function will initialize it.

__out (это тоже самое, что и OUT), указывает на то, что:

  1. Вызываемая функция будет только писать в буфер;
  2. Вызывающая сторона отвечает за выделение буфера.

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

Uuid: Pointer to a caller-allocated UUID (GUID) structure that is set to a new UUID value.

Далее нам понадобиться немного дедукции. Фактически у нас осталось два случая:

  1. Функция возвращает ошибку, содержимое буфера не изменилось;
  2. Функция возвращает ошибку, содержимое буфера изменилось.

Если считать, что функция не должна изменять содержимое буфера в случае ошибки, то где-то в документации должно быть соответствующее требование. Однако же, такого требования там нет, соответственно правомочны оба варианта. Вывод – состояние “OUT” параметра неопределенно в случае неуспешного вызова.

Какой из этого следует вывод? Мне кажется, он достаточно очевиден: всё, что явно не описано в документации – не определено. В свете этого, можно определить критерии полноты документации: документация полна, если все задуманные аспекты поведения функции (модуля, класса и т.д.) описаны. Если документация описывает детали конкретной реализации, то такая документация избыточна и несколько вредна, так как усложняет внесение изменений в код в будущем.

,

  1. July 1st, 2008 at 04:45 | #1

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

  2. July 2nd, 2008 at 07:27 | #2

    В таком случае параметр некорректно помечен, как OUT. Должно быть REF.

    • July 2nd, 2008 at 09:11 | #3

      Это вопрос терминологии. По разным причинам было выбрано имя OUT, а теперь никто его менять не будет. Тем более, что преимуществ у имени REF над OUT я не вижу никаких.

      • July 2nd, 2008 at 09:38 | #4

        А создатели многих современных языков увидели. Управляемых языков.

        • July 2nd, 2008 at 09:43 | #5

          Ну, а если серьезно, то одна из проблем:
          T MySuperFunction(OUT T1 v1, OUT T2 v2, …, OUT T100 v100)

          Что испортилось и что делать?

        • July 2nd, 2008 at 10:16 | #7

          Не надо мешать в кучу С и управляемые языки. И потом я всё равно не вижу, чтобы изменилось если бы “OUT” назвали “REF”. “Хоть горшком назови, только в печь не ставь.” (с) народ.

          • July 2nd, 2008 at 11:21 | #8

            Там есть и OUT и REF. Даже PL/SQL различает.

            • July 3rd, 2008 at 08:22 | #9

              Тот факт, что в C# есть in, out и ref, меня как-то не очень трогает, поскольку мы все-таки не об управляемых языках сейчах говорим.

            • amirul
              August 29th, 2008 at 15:48 | #10

              OUT появился в C (MS edition) задолго до того, как появился сам C#, в котором “есть и OUT и REF”. Если Вам не нравится различия в поведении OUT в C и C#, то пенять надо прежде всего разработчикам C#, а не C.

  3. July 2nd, 2008 at 21:42 | #11

    Хоть OUT, хоть REF, почему функция не имеет право использовать полученный буфер для повышения производительности и экономии памяти? Она не гарантирует, что буфер будет целым, она говорит, что в буфере будут определенные данные в случае успешного выполнения.

    • July 3rd, 2008 at 08:17 | #12

      Насколько я понял, Chabster пеняет на то, что Win32, Native API и прочие используют отличную от C# модель описания параметров. Тот факт, что речь вообще-то идет об обычном С его не смущает. Да и то, что SAL, в отличие от С#-овских in, out и ref параметров, не является частью языка – тоже, судя по всему.

  4. July 3rd, 2008 at 09:15 | #13

    Вообще, нет. Я думаю о функции, как о атомарной операции. И когда параметр OUT – это значит все, или ничего. Зачем мне функция, которая ничего не сделает, при этом испортив параметры?

    Для повышения производительности и экономии памяти? Конечно, правда приходится постоянно проверять, что буффер валидный, что в него можно писать, а это, полагаю, делают многие функции WinApi. Классно повышает производительность.

    • July 3rd, 2008 at 09:53 | #14

      Я думаю о функции, как о атомарной операции.

      Напрасно. Функция далеко не атомарная операция. Даже в управляемых языках.

      Конечно, правда приходится постоянно проверять, что буффер валидный, что в него можно писать, а это, полагаю, делают многие функции WinApi.

      Ошибаетесь. То, что в буфер можно писать никак специально не проверяется. Даже в случае перехода user в kernel mode. Сама запись данных и есть така “проверка”. Если буфер не доступен, то процессор сгенерирует исключение, которое будет (в случае системного вызова) или не обязятельно будет (в случае обычного user mode API) обработано.

    • Hackish Code aka Flenov
      July 4th, 2008 at 10:18 | #15

      Конечно, правда приходится постоянно проверять, что буффер валидный

      Не нужно проверять буфер, проверь результат работы фунции. Если он корректный, то в буфере валидная информация, иначе на буфер лучше не надеятся. Если не проверяешь резльтата работы функции, то это уже отдельная песня, в которой Microsoft не виновата.

  5. July 4th, 2008 at 01:42 | #16

    Еще раз: зачем мне функция, которая ничего не сделает, при этом испортив параметры?

    И еще нюанс.
    NTSTATUS ExUuidCreate(OUT UUID *Uuid);
    Уверен, статус возврата этой функции многие не проверяют. Вопрос: что лучше получить после неуспешного выполнения – то, что было до выполнения (зеро, например) или мусор?

    • July 4th, 2008 at 09:10 | #17

      Еще раз: зачем мне функция, которая ничего не сделает, при этом испортив параметры?

      Затем, что выполнение любой функции может завершиться ошибкой в любой момент?

      Уверен, статус возврата этой функции многие не проверяют.

      Многие и нулевой указатель разименовывают. Некоторые так вообще в голову себе стреляют. И что?

  6. July 6th, 2008 at 02:29 | #18

    Вероятно этот пост станет самым популярным в плане комментов :) )

    Советую всем обратить внимание на последний комментарий в моей посте, http://www.shcherbyna.com/?p=6#comments . Там описывается функция, которая явно декларирует что OUT параметр не будет тронут в случае ошибки.

    Кстати, по поводу проверок буфера на валидность, я в юзер модовских апи функциях такое частенько видел …

    • July 6th, 2008 at 21:06 | #19

      Советую всем обратить внимание на последний комментарий в моей посте

      В последнем комментарии говорится, что OUT буфер не будет тронут и дана ссылка на MSDN. Смотрим MSDN и видим:

      If an error occurs or S_FALSE is returned, this string will be empty.

      Здесь говориться, что в случае ошибки будет возвращено S_FALSE, а буфер будет пустым. Я не проверял, но мой плохой английский подсказывает мне о том, что если буфер был полным, то судя по документации, функция имеет права его грохнуть. Так что параметр не является неприкосновенным :( .

      • July 6th, 2008 at 22:10 | #20

        а буфер будет пустым

        Там в буфер пишется null terminated строка. Т.е. в случае ошибки в бувер будет записан нулевой символ.

        Тем не менее сути дела это не меняет. Просто в данном случае авторы функции решили зафиксировать состояние буфера с случае ошибки. Флаг им в руки и барабан на шею. Обычно же, таких оговорок в документации нет, соответственно и состояние буфера не определено в случае ошибки.

        • July 7th, 2008 at 00:24 | #21

          Там в буфер пишется null terminated строка

          Ясное дело, что буфер не освобождается, а обнуляется. Видимо народ требует, чтобы все функции Windows поступали так же. Поместить нолик в первый символ не так уж тяжело и скорость от этого пострадает не очень сильно, один mov никого не убивал.

          Microsoft не обязана приберать за собой по выходу из функции, но с точки зрения элегантности и красоты, я тоже хотел бы, чтобы мусора не было. Я согласен, что Microsoft не задокументировала и не обязана ничего чистить, но хотелось бы красоты. Помню вы выкладывали фото своего рабочего стола и там было вполне чистенько (бывает и хуже :) ). Приятно работать в чистоте? Мне да!

          • July 7th, 2008 at 10:08 | #22

            Т.е. Вы предлагаете:

            1. Ухудшить производительность и загадить процессорный кеш;
            2. Уменьшить вероятность обраружения любителей не проверять результат функции во время тестирования;
            3. Усложнить написание и сопровождение кода API.

            И всё ради чего?

  7. July 7th, 2008 at 21:34 | #23

    1. Ухудшить производительность и загадить процессорный кеш;
    2. Уменьшить вероятность обраружения любителей не проверять результат функции во время тестирования;
    3. Усложнить написание и сопровождение кода API.

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

    Третье – неужели написать одно присваивание так сложно?

    Ради чего? Ради красоты и простоты сопровождения. Мне кажеться, как раз сопровождение упростится.

    А теперь первое. Одно присваивание конечно очень сильно скажется на производительности и процессорном цеше. Если Microsoft так сильно боится потерять пару процессорных тактов ради красоты и удобства, то может оставить и так. На мой взгляд, данное улучшение стоит потери пары тактов. Да, оно практически бессмысленно, и кроме эстетических улучшений не несет в себе ничего особого. Но я же не зря привел пример с чистотой. Для меня в чистоте приятнее работать.

    • July 8th, 2008 at 08:00 | #24

      Они не проверяли и не будут проверять результат, поэтому любители не скроются.

      Это не так, к сожалению. Как показывает практика, если какая-то функция, которая всегда инициализировала буфер (даже если в этом не было необходимости, как в примере выше), вдруг перестает это делать – обязятельно кто-то с грохотом упадёт по Access Violation. И это в production коде.

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

      Третье – неужели написать одно присваивание так сложно?

      Одно присваивание – не сложно, но даже оно увеличивает объем кода, т.к. каждая функция должна это делать. Только кто вам сказал, что там будет только одно присваивание? Помимо строк функции могут возвращать и другие типы, включая вложенные буферы или полиморфные структуры. Их тоже, скажете, нужно инициализировать всегда и везде?

      Одно присваивание конечно очень сильно скажется на производительности и процессорном цеше

      Вы не поверите, но удлиннение кода перехода из user mode в kernel mode на несколько (2-4) команд уже заметно на тестах “общего назначения”. Вы предлагаете добавить несколько команд (2-4) в каждую функцию API, что эквивалентно по частоте вызовов.

      • July 8th, 2008 at 21:34 | #25

        Вы предлагаете добавить несколько команд (2-4) в каждую функцию API, что эквивалентно по частоте вызовов.

        Откуда там 2-4 вызова?

        • July 8th, 2008 at 22:52 | #26

          2-4 команды, не вызова. Например, чтобы проинициализировать вот такой параметр:

          __deref_out PVOID* Param

          нужно две команды:

          mov eax, [esp+Param]
          mov [eax], 0

          А вот чтобы проинициализировать вот такой:

          __deref_opt_out PVOID* Param

          Уже нужен условный переход или conditional mov:

          mov eax, [esp+Param]
          test eax, eax
          jz @f
          mov [eax], 0
          @@:

          Не говоря уже о том, что обычно нужно записать больше 4-х байт.

          • July 9th, 2008 at 01:10 | #27

            Я говорил, что я не против, чтобы осталось и так. Если стоит выбор – скорость или красота, конечно первое должно побеждать.

  8. July 7th, 2008 at 21:45 | #28

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

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

  9. July 22nd, 2008 at 06:14 | #29

    Ау, народ очнитесь, документации по WinAPI и ядру сколько лет не напомните, какие управляемые языки, ява в зачаточном состоянии только в 1996-ом появилась?
    Определениям OUT, REF в понимании Microsoft всё таки больше. Я не понимаю подхода “солнце жёлтое, а я хочу чтобы было зелёное” и появление в современных! языках отличной от WinAPI терминологии ничего не изменяет. Если я читаю MSDN пояснение как интерпретировать данные термины, я буду пытаться именно так их интерпретировать и если будет что-то не так, я трижды проверю не ошибся ли я в понимании, а после отправлю отчёт об ошибке в Майкрософт, а не буду притягивать за уши что мне хочется чтобы так было.

  1. June 30th, 2008 at 22:44 | #1
  2. July 21st, 2008 at 22:18 | #2
  3. July 21st, 2008 at 22:19 | #3
Comments are closed.