SAL аннотации.
В последнее время я немного забросил 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 аннотаций параметров функций, типов и членов структур. Но об этом в следующий раз.
[...] from blog.not-a-kernel-guy.com. Filed under: C, Win32.Utf8, [...]
Да, такие нотации помогут делать привязки для других языков. Тонкие привязки ещё можно сделать автоматически, но от них не будет того кайфа, который даёт язык. Для толстых привязок нужна именно такая информация. Если Вы будете позиционировать это как утилиту, помогающую поддерживать привязки в актуальном состоянии, к Вам могут присоединиться энтузиасты с самых разных языков. Привязки нужны всем.
Win32.Utf8 отложен в долгий ящик. Шансов на продолжение проекта не очень много.
А что вы имеете в виду под “тонкими” и “толстыми” привязками?
Разница между чистым WinAPI и MFC чувствуется? Вот MFC — это пример толстой привязки.
Толстая привязка может являться ОО обёрткой, может конвертировать коды ошибок в языковые исключения, может предоставлять возможность использовать замыкания и функторы там, где в C был callback с void* параметром. А тонкая — это просто возможность вызывать функции, в не зависимости от того, насколько это комфортно по сравнению с родными библиотеками языка.
А, понятно. Спасибо.