Undefined behavior – это все, что явно не указано в документации.
Навеяно постом про 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), указывает на то, что:
- Вызываемая функция будет только писать в буфер;
- Вызывающая сторона отвечает за выделение буфера.
Ни слова о состоянии буфера в случае успешного или неуспешного вызова. Но, состояние буфера в случае успешного выполнения описано в документации на саму функцию:
Uuid: Pointer to a caller-allocated UUID (GUID) structure that is set to a new UUID value.
Далее нам понадобиться немного дедукции. Фактически у нас осталось два случая:
- Функция возвращает ошибку, содержимое буфера не изменилось;
- Функция возвращает ошибку, содержимое буфера изменилось.
Если считать, что функция не должна изменять содержимое буфера в случае ошибки, то где-то в документации должно быть соответствующее требование. Однако же, такого требования там нет, соответственно правомочны оба варианта. Вывод – состояние “OUT” параметра неопределенно в случае неуспешного вызова.
Какой из этого следует вывод? Мне кажется, он достаточно очевиден: всё, что явно не описано в документации - не определено. В свете этого, можно определить критерии полноты документации: документация полна, если все задуманные аспекты поведения функции (модуля, класса и т.д.) описаны. Если документация описывает детали конкретной реализации, то такая документация избыточна и несколько вредна, так как усложняет внесение изменений в код в будущем.
Абсолютно согласен. Буфер передается функции для того, чтобы она его использовала, а не для того, чтобы она на него смотрела. Если функция будет выделять свой буфер, и копировать в выходной только при выходе, то это будут лишние расходы (выделение памяти, копирование, проверка корректности), которые мне не нужны. Если кому-то нужны, то пусть сам сделает отдельный резервный буфер и сохраняет данные перед вызовом.
В таком случае параметр некорректно помечен, как OUT. Должно быть REF.
Это вопрос терминологии. По разным причинам было выбрано имя OUT, а теперь никто его менять не будет. Тем более, что преимуществ у имени REF над OUT я не вижу никаких.
А создатели многих современных языков увидели. Управляемых языков.
Ну, а если серьезно, то одна из проблем:
T MySuperFunction(OUT T1 v1, OUT T2 v2, …, OUT T100 v100)
Что испортилось и что делать?
А по-подробнее?
Не надо мешать в кучу С и управляемые языки. И потом я всё равно не вижу, чтобы изменилось если бы “OUT” назвали “REF”. “Хоть горшком назови, только в печь не ставь.” (с) народ.
Там есть и OUT и REF. Даже PL/SQL различает.
Тот факт, что в C# есть in, out и ref, меня как-то не очень трогает, поскольку мы все-таки не об управляемых языках сейчах говорим.
OUT появился в C (MS edition) задолго до того, как появился сам C#, в котором “есть и OUT и REF”. Если Вам не нравится различия в поведении OUT в C и C#, то пенять надо прежде всего разработчикам C#, а не C.
Хоть OUT, хоть REF, почему функция не имеет право использовать полученный буфер для повышения производительности и экономии памяти? Она не гарантирует, что буфер будет целым, она говорит, что в буфере будут определенные данные в случае успешного выполнения.
Насколько я понял, Chabster пеняет на то, что Win32, Native API и прочие используют отличную от C# модель описания параметров. Тот факт, что речь вообще-то идет об обычном С его не смущает. Да и то, что SAL, в отличие от С#-овских in, out и ref параметров, не является частью языка - тоже, судя по всему.
Вообще, нет. Я думаю о функции, как о атомарной операции. И когда параметр OUT - это значит все, или ничего. Зачем мне функция, которая ничего не сделает, при этом испортив параметры?
Для повышения производительности и экономии памяти? Конечно, правда приходится постоянно проверять, что буффер валидный, что в него можно писать, а это, полагаю, делают многие функции WinApi. Классно повышает производительность.
Напрасно. Функция далеко не атомарная операция. Даже в управляемых языках.
Ошибаетесь. То, что в буфер можно писать никак специально не проверяется. Даже в случае перехода user в kernel mode. Сама запись данных и есть така “проверка”. Если буфер не доступен, то процессор сгенерирует исключение, которое будет (в случае системного вызова) или не обязятельно будет (в случае обычного user mode API) обработано.
Не нужно проверять буфер, проверь результат работы фунции. Если он корректный, то в буфере валидная информация, иначе на буфер лучше не надеятся. Если не проверяешь резльтата работы функции, то это уже отдельная песня, в которой Microsoft не виновата.
Еще раз: зачем мне функция, которая ничего не сделает, при этом испортив параметры?
И еще нюанс.
NTSTATUS ExUuidCreate(OUT UUID *Uuid);
Уверен, статус возврата этой функции многие не проверяют. Вопрос: что лучше получить после неуспешного выполнения - то, что было до выполнения (зеро, например) или мусор?
Затем, что выполнение любой функции может завершиться ошибкой в любой момент?
Многие и нулевой указатель разименовывают. Некоторые так вообще в голову себе стреляют. И что?
Вероятно этот пост станет самым популярным в плане комментов :))
Советую всем обратить внимание на последний комментарий в моей посте, http://www.shcherbyna.com/?p=6#comments . Там описывается функция, которая явно декларирует что OUT параметр не будет тронут в случае ошибки.
Кстати, по поводу проверок буфера на валидность, я в юзер модовских апи функциях такое частенько видел …
Советую всем обратить внимание на последний комментарий в моей посте
В последнем комментарии говорится, что OUT буфер не будет тронут и дана ссылка на MSDN. Смотрим MSDN и видим:
If an error occurs or S_FALSE is returned, this string will be empty.
Здесь говориться, что в случае ошибки будет возвращено S_FALSE, а буфер будет пустым. Я не проверял, но мой плохой английский подсказывает мне о том, что если буфер был полным, то судя по документации, функция имеет права его грохнуть. Так что параметр не является неприкосновенным :(.
Там в буфер пишется null terminated строка. Т.е. в случае ошибки в бувер будет записан нулевой символ.
Тем не менее сути дела это не меняет. Просто в данном случае авторы функции решили зафиксировать состояние буфера с случае ошибки. Флаг им в руки и барабан на шею. Обычно же, таких оговорок в документации нет, соответственно и состояние буфера не определено в случае ошибки.
Там в буфер пишется null terminated строка
Ясное дело, что буфер не освобождается, а обнуляется. Видимо народ требует, чтобы все функции Windows поступали так же. Поместить нолик в первый символ не так уж тяжело и скорость от этого пострадает не очень сильно, один mov никого не убивал.
Microsoft не обязана приберать за собой по выходу из функции, но с точки зрения элегантности и красоты, я тоже хотел бы, чтобы мусора не было. Я согласен, что Microsoft не задокументировала и не обязана ничего чистить, но хотелось бы красоты. Помню вы выкладывали фото своего рабочего стола и там было вполне чистенько (бывает и хуже
). Приятно работать в чистоте? Мне да!
Т.е. Вы предлагаете:
1. Ухудшить производительность и загадить процессорный кеш;
2. Уменьшить вероятность обраружения любителей не проверять результат функции во время тестирования;
3. Усложнить написание и сопровождение кода API.
И всё ради чего?
1. Ухудшить производительность и загадить процессорный кеш;
2. Уменьшить вероятность обраружения любителей не проверять результат функции во время тестирования;
3. Усложнить написание и сопровождение кода API.
Начнем со второго. Они не проверяли и не будут проверять результат, поэтому любители не скроются.
Третье - неужели написать одно присваивание так сложно?
Ради чего? Ради красоты и простоты сопровождения. Мне кажеться, как раз сопровождение упростится.
А теперь первое. Одно присваивание конечно очень сильно скажется на производительности и процессорном цеше. Если Microsoft так сильно боится потерять пару процессорных тактов ради красоты и удобства, то может оставить и так. На мой взгляд, данное улучшение стоит потери пары тактов. Да, оно практически бессмысленно, и кроме эстетических улучшений не несет в себе ничего особого. Но я же не зря привел пример с чистотой. Для меня в чистоте приятнее работать.
Это не так, к сожалению. Как показывает практика, если какая-то функция, которая всегда инициализировала буфер (даже если в этом не было необходимости, как в примере выше), вдруг перестает это делать - обязятельно кто-то с грохотом упадёт по Access Violation. И это в production коде.
Так что безусловная инициализация буфера (нулями) - это хороший способ протащить несколько багов в production. Другое дело, если инициализировать буфер случайным мусором. Но это можно делать только в отладочном коде.
Одно присваивание - не сложно, но даже оно увеличивает объем кода, т.к. каждая функция должна это делать. Только кто вам сказал, что там будет только одно присваивание? Помимо строк функции могут возвращать и другие типы, включая вложенные буферы или полиморфные структуры. Их тоже, скажете, нужно инициализировать всегда и везде?
Вы не поверите, но удлиннение кода перехода из user mode в kernel mode на несколько (2-4) команд уже заметно на тестах “общего назначения”. Вы предлагаете добавить несколько команд (2-4) в каждую функцию API, что эквивалентно по частоте вызовов.
Вы предлагаете добавить несколько команд (2-4) в каждую функцию API, что эквивалентно по частоте вызовов.
Откуда там 2-4 вызова?
2-4 команды, не вызова. Например, чтобы проинициализировать вот такой параметр:
нужно две команды:
А вот чтобы проинициализировать вот такой:
Уже нужен условный переход или conditional mov:
Не говоря уже о том, что обычно нужно записать больше 4-х байт.
Я говорил, что я не против, чтобы осталось и так. Если стоит выбор - скорость или красота, конечно первое должно побеждать.
Подведу такой итог, ибо спор здесь бессмысленен:
Наезды на MS необоснованы, потому что нарушения нет и функция ведет себя корректно. Но хотелось бы красоты.
Ау, народ очнитесь, документации по WinAPI и ядру сколько лет не напомните, какие управляемые языки, ява в зачаточном состоянии только в 1996-ом появилась?
Определениям OUT, REF в понимании Microsoft всё таки больше. Я не понимаю подхода “солнце жёлтое, а я хочу чтобы было зелёное” и появление в современных! языках отличной от WinAPI терминологии ничего не изменяет. Если я читаю MSDN пояснение как интерпретировать данные термины, я буду пытаться именно так их интерпретировать и если будет что-то не так, я трижды проверю не ошибся ли я в понимании, а после отправлю отчёт об ошибке в Майкрософт, а не буду притягивать за уши что мне хочется чтобы так было.