SAL аннотации
Oct 31, 2007 · CommentsC/C++SALWin32.Utf8
В последнее время я немного забросил Win32.Utf8 - на работе завал, так что, приходя домой, делать ничего особенного не хочется. Тем не менее, проект потихоньку движется и, на сегодняшний день, я добрался до разбора SAL аннотаций.
Основной источник информации, которым я пользуюсь - это заголовок sal.h из поставки Visual C++. В нем содержится достаточно подробное описание того, как составляются конструкции вроде __deref_inout_ecount_part_opt
. Кроме того, там же определены макросы, преобразующие SAL аннотации в набор директив для статического анализатора PREfast. По ним достаточно просто понять как SAL аннотации «работают на самом деле».
Аналогичный заголовок есть в Platform SDK для Windows 2003 - specstrings.h. В Windows SDK для Vista все еще более запутано.
SAL использует следующую модель для описания аргументов функций и членов структур. Каждый аргумент функции/член структуры условно представляется одним или несколькими уровнями «вложенности»:
-
0-ой уровень относится к самому аргументу, т.е. непосредственно к значению в стеке или регистре процессора;
-
1-ый уровень относится к значению или массиву значений, на который указывает аргумент функции. Т.е. аргумент должен быть указателем;
-
2-ой уровень относится к значению, на которое ссылается указатель, на который указывает аргумент функции. Учитывая, что 1-ый уровень может быть массивом значений, то 2-ой уровень может быть представлен множеством значений/массивов значений.
-
и т.д.
Например, для параметра LPSTR* arg
0-ой уровень это сам arg
(указатель на указатель), 1-ый - указатель на строку, на которую ссылается arg и 2-ой - это сама строка, вернее буфер, выделенный под строку.
Для каждого из уровней вложенности могут быть заданы следующие параметры:
-
_in
- значение должно действительно (valid) перед вызовом функции; -
_out
- значение должно быть действительно после вызова функции; -
_opt
- значение может быть нулевым; -
_ecount
или_bcount
- длина буфера заданная в элементах или байтах; -
_part
- буферу разрешается быть частично инициализированным; -
_z
(или_nz
) - указывает, что последний инициализированный элемент буфера должен быть нулевым (или не должен).
Не все параметры применимы ко всем уровням. Например, для 0-ого уровня имеют смысл только _in
, _out
и _opt
, поскольку длина «буфера» на нулевом уровне всегда равняется одному элементу. Текущая реализация SAL накладывает ещё больше ограничений. К примеру, она не даёт возможности описывать аргументы типа «массив указателей на массив» и работать задавать параметры уровней глубже 2-ого. Фактически SAL позволяет описать только наиболее распространенные комбинации указателей в параметрах, которые, тем не менее, позволяют описать 99% случаев, встречающихся в реальной жизни.
С учетом всего вышесказанного можно приступать к конструированию SAL аннотации. Для этого используется следующая таблица (из sal.h):
Level | Usage | Size | Output | Null-terminated | Optional | Parameters |
---|---|---|---|---|---|---|
- | - | - | - | - | - | - |
_deref | _in | _ecount | _full | _z | _opt | (size) |
_deref_opt | _out | _bcount | _part | _nz | (size, length) | |
_inout |
Порядок элементов в таблице совпадает с тем, как элементы должны указываться в аннотации.
Level: выбирает уровни которые определяет данная аннотация:
-
Если уровень не указан, то SAL аннотация определяет сам аргумент и, если это указатель, - буфер, на который он указывает. (Уровень 0 и 1);
-
_deref
указывает, что описываемый аргумент - это указатель на указатель на буфер (Уровни 0, 1 и 2). При этом подразумевается, что размер буфера на уровне 1 - один элемент; -
_deref_opt
дополнительно указывает, что указатель на уровне 1 может быть нулевым.
Usage: указывает, что аргумент должен быть действителен до/после вызова функции.
Size: определяет размер буфера на уровне, выбранном с помощью _deref
. Размер указывается в скобках после SAL директивы. _ecount
/_bcount
выбирает единицу изменения длины буфера: элементы/байты.
Output: позволяет описывать не до конца проинициализированный буфер.
-
По-умолчанию, считается проинициализирована только часть буфера отведенная под данные. Хвост буфера может содержать мусор;
-
_full
говорит о том, что буфер должен быть проинициализирован до конца, например нулями; -
_part
позволяет явно задавать длину проинициализированной части буфера.
Null-terminated: явно указывает, что данные завершаются нулевым элементом.
Optional: наличие _opt
говорит о том, что аргумент функции может быть нулевым. В отличии от _deref_opt
, _opt
относится к 0-у уровню вложенности. Это позволяет комбинировать _deref_opt
и _opt
, указывая, что оба указателя могут быть нулевыми.
Parameters: параметры задают длину буфера и длину его инициализированной части.
Итого:
-
_deref
,_deref_opt
и_opt
- выбирают уровни вложенности аргумента; -
_in
,_out
и_inout
- определяют время жизни аргумента на всех уровнях; -
_bcount
,_ecount
,_full
,_part
,_z
и_nz
- описывают характеристики самого буфера.
Пример: параметр вида LPSTR* Arg
, который используется для получения строки из функции может быть описан следующим образом:
__deref_out_z LPSTR* Arg;
Такой объявление означает, что по завершению функции указатель на который указывает Arg
будет проинициализирован указателем на строку, завершающуюся нулём. Обратите внимание, что для того чтобы это работало Arg
должен быть действительным ненулевым указателем во время вызова функции, т.е. Arg
должен быть описан как _in
параметр на нулевом и первом уровнях вложенности, однако никаких явных _in
в примере нет. Причина в том, что этот _in
неявно подразумевается при указании _deref
, иначе аннотация противоречила бы самой себе.
Другие подводные камни, стоящие упоминания, - это константные параметры (подразумевают _in
) и правила комбинирования SAL аннотаций параметров функций, типов и членов структур. Но об этом в следующий раз.