Friday, September 29, 2006

Из жизни исключений

Вот некоторые интересные факты из жизни исключений (exceptions) в языке Delphi Pascal. Статья подразумевает, что читатель знает, что такое exception и как с ними работают. Здесь освещены некоторые любопытные и менее очевидные моменты.

1. Исключения в конструкторе

Если в конструкторе любого объекта произойдет исключение, будет автоматически вызван деструктор этого объекта. Это обеспечивается "магией" компилятора. Отсюда следует, что правильной идиомой для создания и уничтожения класса является:
Obj = TSomeClass.Create;
try
  // ... работа с Obj
finally
  Obj.Free;
end;
Т.о. если исключение произойдет в конструкторе, деструктор будет вызван автоматически, а если после завершения конструктора - то деструктор будет вызван через Free из блока finally.

Строго говоря, не является ошибочным и следующий вариант:

Obj = nil;
try
  Obj = TSomeClass.Create;
  // ... работа с Obj
finally
  Obj.Free;
end;
Однако, такой способ некрасив и избыточен (лишнее присвоение Obj = nil). К тому же, помещение конструктора в блок try-finally само по себе вовсе не помогает против исключения в конструкторе. В самом деле, посмотрим внимательнее на строчку "Obj = TSomeClass.Create;". Здесь сначала выполняется конструктор, а потом его результат присваевается переменной Obj. Если в конструкторе произойдет исключение, то до присвоения переменной дело просто не дойдет. Поэтому в блоке finally переменная Obj по-прежнему будет nil. Так что только автоматический вызов деструктора может предотвратить потерю ресурсов при исключении в конструкторе. Вероятно поэтому создатели Delphi его и сделали. Сложно предложить другой способ.

Другое важное следствие - деструктор должен быть готов к работе с не до конца созданным объектом. Например, рассмотрим следующий класс:

type
  TExample = class
  private
    FList : TList;
    // ...
  public
    constructor Create;
    destructor Destroy; override;
    // ...
  end;

constructor TExample.Create;
begin
  inherited;
  // ... иницилизация других private-полей
  FList := TList.Create;
end;

destructor TExample.Destroy;
var
  I : Integer;
begin
  for I := 0 to FList.Count - 1 do
    // что-то сделать с FList[I]
  FList.Free;
  inherited;
end;
Деструктор этого класса содержит потенциальную опасность. Если исключение произойдет в конструкторе до того, как FList был создан, то деструктор будет вызван автоматически, и при этом FList будет все еще nil. Т.о. обращение FList.Count вызовет access violation, которе "замаскирует" изначальное исключение, так что сложно будет понять, что именно случилось не так. Правильнее было написать:
destructor TExample.Destroy;
var
  I : Integer;
begin
  if FList <> nil then begin
    for I := 0 to FList.Count - 1 do
      // что-то сделать с FList[I]
    FList.Free;
  end;
  inherited;
end;

AfterConstruction и BeforeDestruction

Теперь вспомним, что все классы в Delphi имеют два унаследованных от TObject "магических" метода AfterConstruction и BeforeDestruction. Первый из них вызывается сразу после конструктора, второй - непосредственно перед деструктором. Интересно отметить, что если исключение произойдет в AfterConstruction, деструктор будет вызван автоматически точно также, как если бы исключение произошло бы в конструкторе. Еще более интересно то, что при возникновении исключения в конструкторе ни AfterConstruction ни BeforeDestruction не вызываются (при возникновении исключения в AfterConstruction метод BeforeDestruction тоже не вызывается). Можно только догадываться, почему Delphi ведет себя именно так, но, по-видимому, вызывать AfterConstruction в таком случае представлялось разработчикам Delphi нелогичным - раз "Construction" не завершено, то и "AfterConstruction" быть не должно. А вызов BeforeDestruction при том, что AfterConstruction не сработал, привел бы к ряду проблем. Например, для классов, унаследованных от TInterfacedObject. Дело в том, что InterfacedObject.InitInstance устанавливает счетчик ссылок RefCount а 1, а TInterfacedObject.AfterConstruction уменьшает RefCount на 1. Делается это для того, чтобы в процессе работы конструктора объект вдруг не уничтожился, это если RefCount увеличится, а потом уменьшится до 0 (В help'e пишут по этому поводу: "When an instance of TInterfacedObject is allocated, RefCount is incremented to prevent its destruction while processing the constructor. AfterConstruction decrements that RefCount to re-establish the correct reference count after all constructors have executed."). Так что, если в конструкторе такого объекта произойдет исключение, то при вызове деструктора RefCount будет равен 1. Если бы в этот момент сработал BeforeDestruction, чей код имеет вид
if RefCount <> 0 then Error(reInvalidPtr);
то мы получили бы run-time error "Invalid pointer operation" вместо "нормального" исключения.

Так или иначе, BeforeDestruction вызван не будет. Помимо упомянутого обхода потенциальной проблемы с TInterfacedObject, это имеет еще такие последствия (которые иногда полезно иметь в виду)

  • Если объект унаследован от TCustomForm или TDataModule, то обработчик события OnDestroy вызван не будет (что логично, т.к. OnCreate тоже не вызывался)
  • Если объект унаследован от TComponent, то при вызове деструктора в ComponentState не будет флага csDestroying

2. Исключения в OnCreate и OnDestroy

Классы, порожденные от TCustomForm и TDataModule имеют события OnCreate и OnDestroy. События эти вызываются из методов DoCreate и DoDestroy этих классов. Эти методы перехватывают любое исключение, которые могло бы произойти в обработчиках этих событий и обрабатывают его с помощью Application.HandleException. Таким образом, если в событии OnCreate вашей формы произойдет исключение, оно будет тут же обработано через Application.HandleException, конструктор не будет прерван, и форма будет создана успешно (т.е. автоматического вызова деструктора, описанного в предыдущем пункте не произойдет). Насколько форма будет при этом функциональна - другой вопрос. Поэтому если при создании формы нужно проделать какую-то критическую операцию - т.е. такую операцию, что при ее неудаче форме вообще нет смысла создаваться - то надо делать ее в конструкторе, а не в событии OnCreate.

3. Исключения в initialization и finalization

Если исключение произойдет в секции initialization или finalization какого-либо из модулей, то оно вообще не будет обработано и программа завершится аварийно с run-time error 217. Казалось бы, исключение должно быть обработано с помощью ExceptProc из SysUtils, но почему-то этого не происходит.

4. Исключения при создании COM-объектов

Исключения при создании COM-объектов связаны с еще большим количеством проблем, чем исключения при создании других объектов. Как известно, если метод COM-объекта объявлен как safecall, то компилятор Delphi неявно "заворачивает" его в блок try-except, причем в блоке except вызывается метод SafeCallException, который гасит исключение, зато обставляет все таким образом, чтобы клиент, вызывающий данный метод, мог восстановить исключение. Таким образом исключение передаются практически "прозрачно" от COM-сервера к его клиенту ("практически", потому что класс исключения у клиента будет уже не тем, что на сервере, а всегда EOleException).

К сожалению, все это не работает при создании COM-объекта. Когда клиент создает COM-сервер (с помощью CreateCOMObject или чего-то аналотичного), COM-объект создается своей фабрикой классов. Если в конструкторе COM-объекта или его методе Initialize произойдет исключение, оно будет перехвачено в методе фабрики классов TComObjectFactory.CreateInstanceLic. При этом, если свойство фабрики классов ShowErrors имеет значение True (а это значение по умолчанию), сервер выдаст MessageBox с текстом ошибки. У клиента же вызов CreateCOMObject завершится маловразумительной ошибкой "Критический сбой" (код E_UNEXPECTED). Все это еще ничего, если сервер является inprocess (DLL) или локальным. Если же сервер вызывается удаленно (через DCOM), то скорей всего он вообще не имеет доступа к экрану серверного компьютера, так что показанный из TComObjectFactory.CreateInstanceLic MessageBox с текстом ошибки не будет виден и нажать на нем "ОК" будет невозможно. В результате сервер намертво зависнет. Даже если серверный процесс имеет доступ к экрану (что будет иметь место, если на сервере Windows 95/98, или же DCOM сконфигурирован соответствующим образом), то все равно сообщение об ошибке на экране сервера мало поможет клиенту. Выводы из этого такие:

  • Если предполагается использование COM-сервера через DCOM, то необходимо выставлять ShowErrors в False у всех фабрик классов.
  • Конструктор и метод Initialize COM-объектов не должны вызывать исключения. Все равно клиент не поймет в чем дело.
Если же при создании COM-объекта необхоодимо проделать какие-либо потенциально опасные (могущие привести к исключению) действия (например, связаться с базой данных), то возможны такие обходные варианты:
  1. Потенциально опасные действия делать в отдельном методе, который клиент вызывает сразу после создания объекта
  2. Обрамить потенциально опасные действия блоком try-except, причем в except погасить исключение и выставить некое свойство "инициализация неуспешна". Клиент в таком случае сразу после создания объекта должен проверить это свойство.
Оба варианта не очень красивы - требуют от клиента лишнего вызова сервера (потенциально по сети). Это отнимает лишнее время, к тому же клиент может просто забыть вызвать дополнительный метод. Но ничего лучше автор предложить пока не может. Призываю читателей этого текста придумать лучшее решение!

NetBIOS still alive

Recently I set up Remote Access Service in a Windows Server 2003 machine. After the installation (which went well) discovered that the computer is no longer visible in Network Neighbourhood of other network computers, and on that computer itself Network Neighbourhood is empty. Investigation revealed that NetBIOS over TCP/IP was disabled on the computer's NIC. Why it happenned I don't know. Enabling NetBIOS helped. And I thought NetBIOS was obsolete technology that we didn't need anymore? Apparently not.

Thursday, September 28, 2006

Getting Group Policy Editor to Work

In hopes to better administrate our Windows network at work I tried to get started with Group Policy. Discovered that attempting to edit the Group Policy from Active Directory Users and Computer or Group Policy Management result in error: "Failed to open the Group Policy Object. You may not have appropriate rights. Details: The parameter is incorrect". What a surprise. A quick Google search revealed an answer here: http://help.lockergnome.com/windows2/Failed-Open-Object-Parameter-incorrect-ftopict187780.html:

Check to see whether the path for "Default" under HKEY_CLASSES_ROOT\MSCFile\Shell\Open\Command is correct. This value should be a REG_EXPAND_SZ (not REG_SZ) and should have the value: %SystemRoot%\system32\mmc.exe "%1" %*

So, I fired up my Regedit and indeed, the type of the default value of the key in question was REG_SZ. But how do I change it to REG_EXPAND_SZ? Fiddled with Regedit for a while to no avail. You can't delete the default value to recreate it, and there's no option to change the value's type. So I ended up writing a small program in Delphi to do it. The program (with its trivial source in Delphi) is here. Just click the "Fix it" button.

Monday, September 25, 2006

Потемкинские евреи

Интересная статья из истории евреев в Российской империи: http://www.kackad.com/article.asp?article=1152. Помимо прочего, автор упоминает некоего Иошуа Цейтлина, моего однофамильца (или предка? едва ли):

Ключевую роль при потемкинском «дворе» играла личность, отмеченная исследователями давно, но значение которой, по-видимому, еще предстоит полностью оценить.

Иошуа Цейтлин, крупный купец и ученый гебраист, путешествовал с князем, управлял его имениями, строил города, заключал займы для снабжения армии, и даже управлял монетным двором в Крыму. По описаниям современников, он «расхаживал вместе с Потемкиным как его брат и друг», с гордостью сохраняя традиционную одежду, набожность, и на глазах у окружающих вел беседы с раввинами. В талмудических дискуссиях иногда участвовал и лично Светлейший. При нем, правда, находились также поп и мулла. Такое зрелище было поразительным не только для России, но и для Европы, получавшей от осведомителей отчеты о происходившем вокруг одного из самых непредсказуемых властителей.

Получив титул надворного советника, а значит, дворянское достоинство, Иошуа Цейтлин в 1791 году вступил во владение имением в Могилевской губернии. Некрещеный еврей по воле своего покровителя стал владельцем крепостных.

и дальше:

Цейтлин добивался, по возможности, реальных позитивных перемен. По протекции князя, он в 1787 году, во время поездки Екатерины на Юг, был принят ею и подал петицию о прекращении употребления в официальных документах унизительного слова «жиды». Императрица дала согласие на это, предписав использовать только слово «евреи».

Имеются подтверждения связей, установленных Цейтлиным с берлинским философом Мозесом Мендельсоном, центральной фигурой еврейского Просвещения - «Хаскалы». Была между ними духовная общность, но не могли не сказаться и различия, - как жизненных условий, так и концепций.

По-видимому, в общении с князем Таврическим И. Цейтлин сделал рискованную попытку связать «стратегические» еврейские интересы с имперским визионерством Потемкина. По мере нарастания военного превосходства над турками, Светлейший прогнозировал близкое крушение Оттоманской Порты. В этой связи он не исключал и возможности придать новый статус Иерусалиму, разместив там еврейское войско.

Начало этому войску должен был положить формировавшийся с 1786 года «Израилевский» конный полк. Есть сведения, что в местечке Кричев, входившем в одно из потемкинских имений, был образован первый эскадрон легкой кавалерии этого полка. Обучение евреев-конников велось под руководством немецкого офицера. Скоро к ним присоединился второй эскадрон, и еще новые части создавались в Польше. Хотя работа была приостановлена и не окончена при жизни Потемкина, остается фактом, что это была первая попытка иностранного государства вооружить евреев со времен, когда Тит разрушил Храм.

Также высказывается утверждение, что черта оседлости была введена не с целью обезопасить русских купцов от конкуренции, но "в пику" революционной Франции, где сразу после революции евреев уровняли в правах. Интересно.

Thursday, September 21, 2006

Fun with web page images

Open any web site, then copy the following to the address bar:

javascript:R=0; x1=.1; y1=.05; x2=.25; y2=.24; x3=1.6; y3=.24; x4=300; y4=200; x5=300; y5=200; DI=document.images; DIL=DI.length; function A(){for(i=0; i<DIL; i++){DIS=DI[ i ].style; DIS.position='absolute'; DIS.left=Math.sin(R*x1+i*x2+x3)*x4+x5; DIS.top=Math.cos(R*y1+i*y2+y3)*y4+y5}R++}setInterval('A()',5); void(0)

Make sure you copy the entire text above as one line. Enjoy the show :-)

There seems to be a lot of references to this hack on the web, mostly in blogs, but no reference to who first created it. I wonder.

Linguistic notes after visiting Catalonia

Catalan language has some unusual reading rules
  1. The letter "x" denotes a "sh" sound, e.g. "xocolate"
  2. Consequently, "tx" becomes "ch" sound
  3. The letter "j" is like in French (the "s" sound in "pleasure"; Russian "ж"). This is not so unusual.
  4. The letter combinations "tj" and "tg" become what "j" is in English. As in "Platja" (=beach) or "Viatges" (=voyages, travels)
  5. The letter combination "ig" denotes, unusually enough, "ch" sound, at least in the end of a word. As in the word "Passeig" (=avenue) or the name of the famous Catalan architect Puig i Cadafalch
  6. Double "l" denotes a soft "l" (Russian "ль") sound and is common in the beginning of words, as in the name of the town Lloret de Mar, name Llorenc or words like Lleo, Llimones, Llibertat
  7. When double "l" has to denote just a double "l" and not the special "ll" sound, it is spelled with the dot in the middle, as in words Paral.lel or Instal.lacion. This is very uniquely Catalan and looks so unusual to a foreigner.

How to configure /etc/krb5.conf for Samba

Recently I've set up a Samba server to be an AD domain member. A question came up: do I need to configure Kerberos? I am using Fedora Core 5 with MIT Kerberos. The answer can be found in Samba docs, but it is scattered around several unrelated parts of the Official Samba Howto and is not easily accessible. In my experience, the answer is as follows:
  • make sure dns_lookup_kdc = true in [libdefaults] section
  • optionally set default_realm in [libdefaults] section to the DNS name of your AD domain. I have a suspicion that this only provides a default for Kerberos command-line tools and is not important for Samba
So, have in your krb5.conf: [libdefaults] dns_lookup_kdc = true default_realm = YOUR.DOMAIN and don't change anything else.