среда, 8 апреля 2009 г.

BOOL, Boolean и Integer

Это перевод BOOL, Boolean and Integer. Автор: Christian Wimmer.

Кто-то может подумать, что типы BOOL и Boolean являются одним и тем же типом в Delphi, т.к. они могут содержать только true и false. Верно ведь? И да и нет. Они кажутся одинаковыми, но есть небольшая разница в трактовке этих типов при присваивании и сравнении. Чтобы увидеть разницу, нам нужно открыть окно CPU-отладчика и копнуть ассемблерный код. Не закрывайте страничку - это действительно не так сложно понять.

Вы также можете упаковать boolean-значение в integer. Фактически, заголовочники Windows определяют булевский тип именно так - так что мы посмотрим и на этот способ тоже.
Следующий ассемблерный код был снят с окна CPU в Delphi 7:

Integer
  value := Integer(True);
  mov [value],$00000001
BOOL/LongBool
  value := true;
  mov [value],$ffffffff
Boolean
  value := true;
  mov byte ptr [value],$01
Я не хочу философствовать о плохих или хороших сторонах этих строк. Главное тут в результате, который мы видим. Присваивание Integer и Boolean просто копирует единичку в переменную value. С другой стороны, присвоение для BOOL/LongBool выглядит иначе - и, фактически, с точностью до наоборот. Оно копирует значение $FFFFFFFF - что, очевидно, есть отрицательное значение для единицы. Так что результатом в переменной value будет -1. Вы можете проверить это с помощью следующего кода. Переменная i будет содержать отрицательное число.
var
  B: BOOL;
  I: Integer;
begin
  B := true;
  i := Integer(B);
end;
Почему важно понимать эту разницу? Ну, некоторые функции Win32 API проверяют свои аргументы и могут возвращать ошибку 87 (неверный параметр) или (в худшем случае) весьма странно себя вести. Как вы увидели выше, каждый раз, когда значению типа BOOL присваивают true, Delphi устанавливает его в -1. Если функция Win32 API приводит тип аргумента к обычному integer, то она получит -1 ($ffffffff) и может провалиться, если она ожидает только 0 или 1.
Здесь история только начинается... Все заголовочники JEDI API (и стандартные заголовочники в Delphi - прим. пер.) используют тип BOOL, приводя его к Delphi-скому LongBool, который - как мы уже видели ранее - определяет значение “-1″ как true. В большинстве случаев функции Win32 API проверяют на равенство нулю - и тогда мы в порядке. Они сравнивают аргумент с нулём (false) - а в противном случае считают его true. Поскольку я не хочу брать на себя ответственность за столь кардинальные изменения - я решил оставить везде тип BOOL. Кроме того, изменение BOOL на Integer означало бы, что вы не сможете больше присваивать true и false, без необходимости приведения типа к BOOL (или Integer).

В итоге всё упирается в совместимость и удобство. Однако, с другой стороны вы должны помнить, что тип BOOL в Delphi может быть источником ошибки №87 (неверный аргумент).

Вывод:
X BOOL в Delphi BOOL в C Boolean в Delphi
обычный тип LongBool int магия компилятора
размер в байтах 4 4 1
false = ? 0 0 0
true = ? -1 ($FFFFFFFF) 1 1

Следующий список содержит функции, которые могут конфликтовать с различными объявлениями, потому что они проверяют значения аргумента. Если вы найдёте функцию, которая ведёт себя странно с аргументом типа BOOL - отправляйте её мне.

# Имя Заголовочный файл Примечание
1. QueryServiceConfig2(W/A) JwaWinSVC.pas Проблема с TServiceDelayedAutoStartInfo (VISTA)

Прим. пер.: разумеется, этот список больше, но в оригинальном посте в списке была всего одна функция. Проблемы, например, имеются у такой известной функции как CreateMutex. Посмотрите, как она реализована в Windows.pas. А может тут речь идёт только о полях BOOL в записях.

Решения, которые вы найдёте в JEDI API.
Вы можете найти различные решения этой проблемы в исходниках JEDI. Почему решений несколько? Потому что код был написан многими людьми, с разным уровнем знаний, и в различное время. Так что если вы увидите объявление записи (struct), которая использует объявление BOOL как в SERVICE_DELAYED_AUTO_START_INFO - вам нужно убедиться, что функция, которая использует аргументы такого типа, ведёт себя корректно.
typedef struct _SERVICE_DELAYED_AUTO_START_INFO {
  BOOL fDelayedAutostart;
} SERVICE_DELAYED_AUTO_START_INFO;
И здесь может быть два возможных решения... ну или вы так думаете.
type
  _SERVICE_DELAYED_AUTO_START_INFO = record
    fDelayedAutostart : BOOL;
  end;

  // или

  _SERVICE_DELAYED_AUTO_START_INFO = record
    fDelayedAutostart : Boolean;
  end;
Первое объявление кажется корректным, но, как вы уже поняли, оно не всегда будет таким (например, оно не является верным для _SERVICE_DELAYED_AUTO_START_INFO в Vista). Второе может работать, но только благодаря случаю. Учтите, что тип BOOL в C имеет размер 4 байта (= sizeof(int)), а тип Boolean состоит только из одного. Получается, что мы обрезали у записи три байта. Поэтому целевая функция (здесь: ChangeServiceConfig2) “думает”, что поле записи fDelayedAutoStart состоит из 4-х байт (sizeof(int)) и читает три байта за структурой. Поскольку мы не знаем их содержания - они могут быть заполненными случайными данными. В результате получим число, которое не является ни нулём, ни +/-1. Т.к. ChangeServiceConfig2 может быть успешно выполнена только для 1 или 0, то мы получаем ошибку 87 (неверный аргумент).
Поэтому, на самом деле, здесь могут быть два таких решения:
type
  _SERVICE_DELAYED_AUTO_START_INFO = record
    fDelayedAutostart : Boolean;
    Pad : Array[0..2] of Byte;
  end;

  _SERVICE_DELAYED_AUTO_START_INFO = record
    fDelayedAutostart : Integer;
  end;
Я предпочитаю второе решение и вам того же советую. Почему? Сначала вы можете подумать, что первый вариант - это идеальное решение, потому что вы можете просто присваивать true или false - прямо как в C. Однако, как я только что сказал, три байта в поле Pad тоже должны быть инициализированны. Поэтому правильный способ использования был бы таким:
  ZeroMemory(@myRecord, SizeOf(myRecord));
Проблема в том, что это не будет работать. Почему? Многие Delphi-программисты, работающие напрямую с API, находят решения своих проблем в исходниках на C. Они просто переводят исходники слово-в-слово с C на Delphi, не разбираясь в деталях. А дело в том, что вы не найдёте вызова ZeroMemory в таком C коде! Программист C может просто присвоить полю fDelayedAutoStart TRUE и всё: проблемные три байта инициализируются в момент присваивания. Ну, не кажется ли вам теперь второе решение лучшим?
Именно по этой причине вы увидите в последних версиях JEDI API только такую запись:
type
  _SERVICE_DELAYED_AUTO_START_INFO = record
    fDelayedAutostart : Integer;
  end;
Да, компилятор не даст присвоить значение true напрямую, но это лучшее решение, что пришло мне в голову.

Нет ничего лучше, чем просто работающий код...
  info.fDelayedAutostart := Integer(true);

Читать далее: BOOL vs. VARIANT_BOOL vs. BOOLEAN vs. bool.

4 комментария:

  1. Отправил на QC предложение добавить новый Boolean тип: http://qc.embarcadero.com/wc/qcmain.aspx?d=72852
    По идее, они сами должны быть в курсе, но вдруг прокатит :D

    ОтветитьУдалить
  2. Предполагаю, что проверка на равенство 0 или 1 выполняется, т.к. в будущем вероятно изменение типа параметра на Enum или Set с дополнительными значениями.

    Такое, например, наблюдается с твиками в реестре: 0 - выключено, 1 - включено, 2 - какой-то особый режим.

    ОтветитьУдалить

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

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

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

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

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