понедельник, 17 июня 2013 г.

Вот и мы перешли на Юникод

Однажды, наверное как и у многих, у меня возникло дикое желание перевести проекты в нашей компании на юникодную версию Delphi. Перевести с Delphi 7. Конечно же можно было оставить всё как есть, “оно же работает!”. Тем более у нас повсеместно использовались Tnt-компоненты, и Юникод в приложении уже был. Но как человек, который любит свою работу, мне было просто дико осознавать, что мы до сих пор используем то, что есть под рукой и не обновляем свой инструментарий.

Сложность вопроса ещё состояла в том, что проект у нас довольно большой – несколько приложений с общей кодовой базой более 1000 pas-файлов (сейчас это 332 pas-модуля + 812 экранных форм и фрейм, т.е. пар pas + dfm файлов). Это не считая сторонних библиотек и компонентов. Но время потихоньку бежит, часть компонентов перестаёт поддерживать старые версии Delphi, а в новых версиях Delphi появляются всякие полезняшки… одни дженерики чего стоят. И инлайны мне по душе. И много прочих мелочей.

Я уже плохо помню весь процесс перевода. И он оказался совсем не простым. Более того, он был у меня в два захода – сначала это была Delphi 2009 – я добился успешной компиляции приложений и местами даже их нормальной работы. Но это было внепланово, по выходным и в своё свободное время. На том первый этап и заглох.

И вот в прошлом году наконец-то дали добро плановому переходу. Здесь я уже выбрал Delphi 2010, как проверенный и не слишком навороченный нововведениями инструмент… но, постараюсь, по порядку.

Итак, для начала дам пару ссылок:

Официальный документ Embarcadero, на русском: http://www.delphi2009.ru/Unicode_Delphi-RUS.pdf (альтернативная ссылка). И блог Алексея Тимохина часть 1 и часть 2. Интересен факт, что у меня план перехода оказался один-в-один, как и у Алексея, причём на тот момент (2009 год), его поста ещё не было. И набор компонентов частично у нас пересекается, и требования такие же – приложение должно собираться и в старой Delphi и в новой. Поэтому далее я уделю внимание лишь только тем моментам, которые были наиболее нетривиальные.

  1. Библиотека TntComponents. Пока она была бесплатной, она существовала только для неюникодных версий делфи. Пришлось править вручную, но довольно просто, что-то типа такого:
{$ifdef UNICODE}
type
  TTntEdit = TEdit;
  TTntButton = TButton;
...
{$else}
// оригинальный код библиотеки
{$endif}
  1. Библиотека QStrings и некоторые другие. На первом этапе простая замена string –> AnsiString, Char –> AnsiChar, PChar –> PAnsiChar.
  2. Взаимодействие с БД. У нас используются самописные библиотеки. Со всеми вытекающими. Но мы работаем только с Oracle. Поэтому, вместо прямой (и корявой, по неопытности) работы с oci.dll, я внедрил использование библиотеки FreeDAC – той её части, которая работает с Oracle-клиентом. Но FreeDAC – библиотека старая, неюникодная… Пришлось покупать AnyDAC и адаптировать часть исходников.

Примерно на этом первая попытка перехода в конце 2009 года и завершилась. Приложения компилировались и худо-бедно запускались.

Во второй заход, уже переходя на Delphi 2010, явных проблем с компонентами и сторонними библиотеками на первом этапе не оказалось, т.к. все изменения первого захода были аккуратненько сохранены в репозиторий. Однако после тестирования у меня составился список проблем (уже скрытых) ещё аж в 28 пунктов. Вот некоторые из них:

  1. Самописные библиотеки. Их много. Очень много PChar-ов для легальной работы с адресной арифметикой. Очень много кода с верой, что Char = 1 байт. Скучно и неинтересно. Но надо.
  2. Библиотека FastCube – кириллица отображалась не корректно. Не вдаваясь в подробности – ошибку я нашёл быстро, отписался в саппорт, быстро исправили.
  3. Кодогенерация. Сгенерированный код, который без проблем компилировался в Delphi 7, в Delphi 2010 начал выдавать такую ошибку:
E2283 Too many local constants.  Use shorter procedures
One or more of your procedures contain so many string constant expressions that they exceed the compiler's internal storage limit. This can occur in code that is automatically generated. To fix this, you can shorten your procedures or declare contant identifiers instead of using so many literals in the code.

Решилось небольшой правкой в алгоритме кодогенератора. Новая версия даже больше нравится.

  1. Странное поведение контролов, “брошенных” на тулбары (TB2k) – свойства BevelKind = bkSoft/bkFlat у контролов приводят к зацикливанию в прорисовке (мерцание) и разъезжание границ тулбара. Причём даже в Design-Time. Причину не нашёл, тупо сбросил свойство в DFM-файлах.
  2. Наследие от старой версии TntUnicode – свойства Caption_UTF7 и Hint_UTF7 в старых DFM-файлах. Таких свойств у контролов просто нету, что приводит к ошибкам в RunTime. Решилось простой заменой на Caption и Hint соответственно. Тут же: свойство Lines.WideStrings. Ну и некоторые, типа PasswordCharW – это уже решилось путём переноса кода из DFM в PAS.
  3. Работа с UTF8 (например с HTML): UTF8Decode/UTF8Encode – работают с UTF8String, а это уже AnsiString, а не string, как раньше.
  4. Работа с SharedMemory. string вдруг стал UnicodeString – одновременный доступ неюникодного и юникодного приложений к разделяемой памяти приводили к сбою.
  5. array of const -- новый тип TVarRec.VType = vtUnicodeString. Сюда же: array of variant -- TVarData.VType = varUString.
  6. Работа с потоками (Stream) и файлами в частности. {$ifdef UNICODE}TEncoding.
  7. Чтение и запись в DBF. Суть проблемы точно уже не помню – ловилось часа два, исправилось за минуту.
  8. FormatBuf – оказалась не совместима с AnsiString. Заменил на Format.
  9. Печать RichEdit-текста стала уходить в бесконечный цикл печати. Оказывается, что сообщение EM_FORMATRANGE (которое используется методом GetTextLen) возвращает разные значения для ANSI/Unicode версий контрола. Заменил вызвов RichEdit.GetTextLen на вызов сообщения EM_GETTEXTLENGTHEX.

 

Сколько это дело заняло у меня по времени точно сказать не могу. Две рабочие недели плановой рутины. Потом примерно месяц неспешного тестирования и выявления неявных проблем (совмещая с другими задачами). Плюс внеплановое время первого захода.

Как итог: одна кодовая база, приложения собираются и в Delphi 7, и в Delphi 2010. Интересный момент: скорость сборки (build) приложений и там, и там совпадает до секунды. Скорость обработки табличных данных в памяти приложения в среднем не изменилась (местами чуть быстрее, местами чуть медленнее). Для пользователя, кроме размера экзешника (который вырос примерно в полтора раза), никаких изменений и не заметно. В общем, я доволен.

Теперь у нас в планах: плавный отказ от Delphi 7, неспешный рефакторинг наших библиотек с переходом на новые возможности компилятора. Есть желание освоить 64 бита (актуально для одного из наших заказчиков, который любит строить FastReport-отчёт  на 40 с лишним тысяч страниц в оперативной памяти – такое тупо не помещается в 2Гб предел для Win32 приложения). Но сначала надо подчистить некоторые технические долги…

 

А Вы перешли на Юникод?

10 коммент.:

Unknown комментирует...

Если я правильно понял, вы используете TB2k на Delphi 2010. Когда переводил проект с Delphi 7, прочитал на оффсайте "Toolbar2000 version 2.2.2 for Delphi 4.0 through 2009" и с сожалением отказался от использования TB2k.

Николай Зверев комментирует...

Сергей, Вы правильно поняли. Причём в связке с SpTBX. Но оно без проблем заработало в Delphi 2010, и без проблем будет работать и в следующих версиях.

Вообще со сторонними библиотеками и компонентами главное, чтобы на руках были исходники. По другому мы их не приобретаем. А с исходниками заставить их работать в более новой версии делфи, как правило - дело техники и не занимает много времени. Единственное рутинно - это компоненты доступа к БД, их проще купить.

Всеволод Леонов комментирует...

Отлично, Николай!
Примите мои поздравления :)

Slike комментирует...

2Гб не предел для Win32 приложений. Есть возможность расширить до 3. Правда необходимо немого по колдовать на ОС и запускаемым exe-приложением.

Николай Зверев комментирует...

Всеволод, спасибо )
Slike, Вы правы. Есть даже такая интересная вещь:
http://www.ntcore.com/4gb_patch.php
Но есть парочка но:
а) попробуйте объяснить рядовому пользователю,
с чего это вдруг ему вносить правку в boot.ini
б) если отчёт уже достигает 2Гб, то рано или поздно (ну пусть даже в теории) он достигнет и 4Гб.
в) FastReport (а отчёт именно на нём строится), умеет строить отчёт не в оперативной памяти, а во временном файле на диске. Таким образом, это ограничение мы сейчас и обошли. Но это сильно замедляет построение отчёта и работу с ним.
В общем, 64бита особо и не горит, но попробовать (хотя бы ради такого вот случая) хочется.

Slike комментирует...

Кстати, если не секрет, почему для подключения к БД оракл используете AnyDAC, а не ADO с OBDC драйверами?

Николай Зверев комментирует...

Не, ODBC это не вариант. Официальный Oracle Instant Client весит всего 17 Мб в архиве, и включение его в дистрибутив приложения не составляет никаких сложностей.
А почему именно AnyDAC - ну потому что сначала была библиотека NCOCi7, потом NCOCi8, а потом FreeDAC (AnyDAC v1) - все они были бесплатными и хорошо себя показали в действии (в разных проектах).
Ну и есть такой момент, что мы не используем стандартные DataSet'ы, у нас свои Mem-таблицы, а у AnyDAC открыт Phys-layer - нативная обёртка над API Oracle-клиента - мы получаем максимальную производительность.

Анонимный комментирует...

Как Вы избавились от E2283 без правки кода - можете описать подробнее? Столкнулся с этой же проблемой и не могу её решить :-\

Спасибо.

Николай Зверев комментирует...

Без правки кода - не избавился.
У меня ошибка возникала в исходнике, который генерировался по некоторому описанию полей и таблиц. Там был код из большого кол-ва строк вида:
TableAFieldA := TableA['FieldA'];

Я внёс две правки в кодогенератор (согласно совету по описанию ошибки).
1. Все строковые литералы вынес в раздел const модуля, получилось что-то в виде:
const
SFieldA = 'FieldA';
...
TableAFieldA := TableA[SFieldA];

2. Одну большую процедуру вида:
TableAFieldA := TableA[SFieldA];
...
TableBFieldA := TableB[SFieldA];
...
TableZFieldA := TableZ[SFieldA];
разделил на много маленьких процедур - по кол-ву таблиц.

Николай Зверев комментирует...

x64 - быть!

Отправить комментарий

.

.