четверг, 20 апреля 2017 г.

Как создать "сигнальные" значения для указателей в Windows

Это перевод On generating sentinel pointer values in Windows. Автор: Реймонд Чен.

Предположим, что вам нужно несколько т.н. "сигнальных" значений. К примеру, пусть ваша функция работает с указателями на тип Widget, а вам требуется способ передавать значения с особым смыслом, к примеру: "Не указан Widget", или "Используй значение по умолчанию", или "Возьми Widget из родительского объекта", или "Все известные Widget-ы".

Ну, почти любой язык уже даёт вам как минимум одно значение, которое можно использовать в качестве сигнального: это nil, null, Nothing, nullptr или аналогичное "пустое" значение в вашем языке. Если вам нужно только одно значение, то вот вам простой вариант.

С другой стороны, это "пустое" значение является настолько общим, что оно может быть получено по ошибке. К примеру, это значение может быть значением по умолчанию для указателей. Значение может поступить от предыдущей (неудачной) операции (вроде выделение или поиска). Вы можете захотеть использовать какое-то другое значение, чтобы по ошибке не перепутать его с такими случаями.

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

2а). Другая идея состоит в создании частного региона памяти и использовании указателей из него. Вы можете вызвать Virtual­Alloc(MEM_RESERVE) для резервирования региона. В таком регионе никто не сможет выделить память/создать объекты. Если вы зарезервируете регион и целенаправленно не будете его использовать, то любой адрес из этого региона можно использовать в качестве сигнального значения.

2б). Аналогичную работу делает за вас и Windows: при инициализации адресного пространства процесса ядро резервирует нижние 64 Кб адресного пространства, чтобы в них нельзя было выделить память. Это даёт вам 65'536 потенциальных значений (ну, одно из них занято nil, поэтому у вас появляется только 65'535 новых значений). В примеру, эта техника используется макросами MAKE­INT­RESOURCE и MAKE­INT­ATOM для передачи числовых значений вместо строк.

До Windows 8 у приложений был способ "разрезервировать" этот регион памяти в 64 Кб и начать выделять в нём память - представьте себе хаос, к которому это может привести! В счастью, в Windows 8 это уже невозможно.

3). Если ваши Widget имеют требования по выравниванию (а если они состоят из чего угодно, кроме простого массива байт - то, вероятно, такие требования имеются), вы можете использовать любой указатель, который не удовлетворяет этим требованиям. К примеру, если Widget - это указатель (к примеру, в Delphi любой объект является указателем), то он, следовательно, должен быть выровнен на границу 4 байт (в 32-битной программе). Таким образом, любое значение указателя, не делящееся нацело на 4, может быть использовано в качестве сигнального значения - поскольку такое значение никогда не будет закреплено за допустимым объектом.

Примечание переводчика: строго говоря, если объект Delphi выделяется стандартным образом (через стандартный менеджер памяти), то он будет выровнен как минимум на границу 8 байт - это характеристика менеджера памяти Delphi, закреплённая в официальной документации. Опционально вы можете переключить менеджер памяти в режим выравнивания на 16 байт.

Даже если требования к выравниванию всего только два байта (например, запись с полями типа Word и меньше), то даже это даст вам два миллиарда значений (все нечётные 32-битные значения) - что очень много. Вы можете использовать f(n) := n * 2 + 1, чтобы создать сигнальное значение по номеру и g(n) := (n − 1) div 2, чтобы преобразовать сигнальное значение в его номер.

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

Упражнение: Что не так с этим советом: "Возьмите любое значение большее $80000000"?

7 комментариев:

  1. Лол, только что прочитал в оригинальном блоге (вы меня подсадили).

    Я как-то делал велосипед для сериализации «старых» объектов по TypeOf, но с возможностью работать с произвольными типами, идентифицируя их фиктивными TypeOf. И в качестве такого фиктивного TypeOf использовал указатель на любой метод объекта:

    const Type_Vec3: pointer = @Vec3.GetLength;
    ...
    RegisterType(Type_Vec3, @SerializeVec3, @DeserializeVec3);
    ...
    Serialize(vec, Type_Vec3).

    В FPC это работает, но может сломаться, если однажды компилятор научат объединять одинаковые функции (внезапно тоже было, https://blogs.msdn.microsoft.com/oldnewthing/20050322-00/?p=36113). Вот бы их как-нибудь последовательно распределять в compile-time, начиная с pointer(1).

    Хотя сейчас подумал, что можно использовать уникальные строковые константы, приведённые к указателям — они «наверное точно» не пересекутся ни друг с другом, ни с настоящими VMT.

    ОтветитьУдалить
    Ответы
    1. > вы меня подсадили

      Так было задумано :)

      > в качестве такого фиктивного TypeOf использовал указатель на любой метод объекта

      Эээ... перекомпилируем программу, меняются адреса - что раньше сохранили, теперь не прочитать. Не?

      Удалить
    2. Нет, они только для идентификации типа в пределах программы, как и «настоящие» TypeOf, а в поток сохраняются под именами и/или порядковыми номерами, конечно.

      Удалить
  2. "sentinel" в качестве существительного "часовой" выглядит нормально, а вот как прилагательное - не очень (например, по заголовку совершенно непонятно, о чём речь). Может быть, "охранный, сигнальный"?

    ОтветитьУдалить
  3. Анонимный4 мая 2017 г., 16:40

    У дяди 1-апреля что ли?

    Посту больше подходит название
    "Вредные советы от бывалых или как ковыряться в зубах дулом пистолета"

    И судя по тамошнему диалогу - народ в восторге!

    ОтветитьУдалить
    Ответы
    1. Ну, это смотря как программить. Чен пишет для нормальных программеров.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.