вторник, 25 января 2011 г.

Деструкторы COM объектов являются очень хрупкими функциями

Это перевод COM object destructors are very sensitive functions. Автор: Реймонд Чен.

Если вы попробуете делать там слишком много - у вас могут быть неприятности.

К примеру, если ваш деструктор передаёт ссылки на себя другим функциям, то эти функции могут решить вызывать ваши IUnknown._AddRef и IUnknown._Release во время своей работы. Посмотрите на этот код:
function TMyObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;

destructor TMyObject.Destroy;
begin
  if FNeedSave then
    Save;
  inherited;
end;
Это не выглядит очень уж страшным, не так ли? Объект просто сохраняет себя перед разрушением.

Но метод Save мог бы выглядеть примерно так:
function TMyObject.Save: HRESULT;
var
  spstm: IStream;
  spows: IObjectWithSite;
begin
  Result := GetSaveStream(spstm);
  if SUCCEEDED(hr) then
  begin
    Supports(spstm, IObjectWithSite, spows);
    if Assigned(spows) then
      spows.SetSite(Self);
    Result := SaveToStream(spstm);
    if Assigned(spows) then
      spows.SetSite(nil);
  end;
end;
Сам по себе он выглядит нормально. Мы получаем поток (stream) и сохраняем себя в него, дополнительно устанавливая сведения о контексте (site) - на случай если потоку нужна будет дополнительная информация.

Но этот простой код в сочетании с тем фактом, что он запущен из деструктора, даёт нам рецепт для катастрофы. Посмотрите что при этом происходит:
  • Метод _Release уменьшает счётчик ссылок до нуля и выполняет удаление Self.
  • Деструктор пытается сохранить объект.
  • Метод Save получает поток для сохранения и устанавливает Self в качестве контекста. Это увелививает счётчик ссылок с нуля до единицы.
  • Метод SaveToStream сохраняет объект в поток.
  • Метод Save очищает контекст потока. Это приводит к уменьшению счётчика ссылок нашего объекта с единицы до нуля.
  • Поэтому метод _Release вызывает деструктор объекта второй раз.
Разрушение объекта второй раз приводит к полномасштабному хаосу. Если вам повезет, вы вылетите внутри рекурсивного уничтожения и сможете определить его источник, но если вам не повезет, то может произойти повреждение кучи, которое останется не обнаруженным в течении некоторого времени, после чего вы будете просто чесать голову.

Поэтому, как минимум, вы должны вставить Assert в ваш метод AddRef , чтобы гарантировать, что вы не увеличиваете счётчик ссылок с нуля:
function TMyObject._AddRef: Integer;
begin
  Assert(FRefCount > 0);
  Result := InterlockedIncrement(FRefCount);
end;
Это поможет вам легко отлавливать "случаи загадочного двойного вызова деструктора объекта". Но когда вы идентифицируете проблему, то что же вам с ней делать? Мы поговорим об этом в следующий раз.

Примечание переводчика:
Сказанное здесь в точности применимо и к объектам, реализующим интерфейсы в Delphi (просто потому, что интерфейсы COM - это единственные интерфейсы в Delphi). Однако в Delphi объект может существовать, даже когда его счётчик равен 0. Например:
var
  MyObj: TMyObject;
begin
  MyObj := TMyObject.Create;
  FreeAndNil(MyObj);
end;
В этом примере счётчик ссылок будет равен 1 во время работы конструктора и опустится до 0 после выхода из Create, после чего счётчик будет равен 0 до конца жизни объекта.

Сравните это с:
var
  MyObj: IMyObject;
begin
  MyObj := TMyObject.Create;
  MyObj := nil;
end;
В этом примере счётчик ссылок будет равен 1 во время работы конструктора и опустится до 0 после выхода из Create, после чего снова поднимется до 1 из-за копирования интерфейса в переменную MyObj.

С учётом этого момента код выше нужно немного изменить. Например, так:
function TMyObject._AddRef: Integer;
begin
  Assert(FRefCount >= 0);
  Result := InterlockedIncrement(FRefCount);
end;

function TMyObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;

procedure TMyObject.BeforeDestruction;
begin
  if RefCount <> 0 then
    System.Error(reInvalidPtr);
  FRefCount := -1;
end;
См. также этот запрос в QC.

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

  1. В вызове TMyObject._Release не нужно добавлять строку FRefCount := -1, она уже добавлена в BeforeDestruction, который будет вызван при вызове Destroy.

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

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

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

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

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

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