VerQueryValue Access Violation

Столкнулся с интересным багом при получении информации о версии из ресурсов (Visual Studio 10, C++).

Access Violation возникает при выполнении VerQueryValue (см. картинку).
Причем, ошибка возникает только если используется Multi-Byte Character Set. А если выставить в свойствах проекта Use Unicode Character Set, то ошибка пропадает. Из этого можно сделать вывод, что баг именно в VerQueryValueA, а в VerQueryValueW его нет.
Что ж, решение проблемы заключается в переписывании функции чтения информации о версии таким образом, чтобы всегда использовалась функция VerQueryValueW. Ниже приведен листинг готовой функции.

UPD 30.01.2014. Спустя полтора года выяснилось, что и с использованием VerQueryValueW возможен Access Violation. Что же, лучше поздно чем никогда. Дело в том, что эта функция рассчитана на то, чтобы быть использованной совместно GetFileVersionInfo. Возможно, VerQueryValue может в каких-то случаях модифицировать ресурс. В общем, фикс заключается в том, что нужно выделить участок памяти (например, с помощью LocalAlloc), скопировать в него информацию о версии из ресурса (CopyMemory), и использовать его при вызове VerQueryValue. Реализация приведена ниже.

void ReadVersion(_TCHAR * szVer, int cbVer, const _TCHAR * szEntry)
{
_tcscpy(szVer, _T("\n"));
HRSRC hVersion = FindResource(
NULL,
MAKEINTRESOURCE(VS_VERSION_INFO),
RT_VERSION);
if (hVersion)
{
DWORD dwSize = SizeofResource(NULL, hVersion);
HGLOBAL hGlobal = LoadResource(NULL, hVersion);
if (hGlobal != NULL)
{
LPVOID versionInfoGlobal = LockResource(hGlobal);
if (versionInfoGlobal != NULL)
{
LPVOID versionInfoLocal = LocalAlloc(LMEM_FIXED, dwSize);
CopyMemory(versionInfoLocal, versionInfoGlobal, dwSize);

DWORD vLen, langD;
BOOL retVal;

LPVOID retbuf = NULL;

#ifdef _UNICODE
const _TCHAR * entry = szEntry;
#else
WCHAR entry[64]; //I assume here that length of szEntry string is less than 64
MultiByteToWideChar(CP_ACP, 0, szEntry, -1, entry, 64);
#endif

WCHAR fileEntry[1024];

_swprintf(fileEntry, L"\\VarFileInfo\\Translation");
retVal = VerQueryValueW(versionInfoLocal, fileEntry, &retbuf, (UINT*)&vLen);
if (retVal && vLen == 4)
{
memcpy(&langD, retbuf, 4);
_swprintf(fileEntry, L"\\StringFileInfo\\%02X%02X%02X%02X\\%s",
(langD & 0xff00)>>8,langD & 0xff,(langD & 0xff000000)>>24,
(langD & 0xff0000)>>16, entry);
}
else
_swprintf(fileEntry, L"\\StringFileInfo\\%04X04B0\\%s",
GetUserDefaultLangID(), entry);

retVal = VerQueryValueW(versionInfoLocal, fileEntry, &retbuf, (UINT*)&vLen);
if (retVal)
{
#ifdef _UNICODE
_sntprintf(szVer, cbVer-1, _T(" %s\n"), (_TCHAR*)retbuf);
szVer[cbVer-1] = '\0';
#else
szVer[0] = '\0';
int n = WideCharToMultiByte(CP_ACP, 0, (LPWSTR)retbuf, -1, szVer+1, cbVer-2, NULL, NULL);
if (n > 0)
{
szVer[0] = ' ';
szVer[n] = '\n';
szVer[n+1] = '\0';
}
#endif
}

LocalFree(versionInfoLocal);
}
FreeResource(hGlobal);
}
}
}

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Qt и Visual Studio 2008

Чтобы использовать Qt совместно со средой разработки Visual Studio нужно выполнить следующие шаги:

1. Скачать и установить Qt for Visual Studio 2008: http://qt.nokia.com/downloads/windows-cpp-vs2008 (каталог установки не должен содержать пробелов, например это может быть C:\qt\4.8.2)

2. Скачать и установить Visual Studio Add-in: http://qt.nokia.com/downloads/visual-studio-add-in

3. Добавить в Переменную среды Path (Пуск->Панель инструментов->Система->Дополнительно->Переменные среды) путь к bin каталогу Qt (например, C:\\qt\\4.8.2\\bin)
Вот, в принципе, все. В Microsoft Visual Studio IDE должен появиться пункт меню Qt. А в диалоге New Project - типы проектов Qt4 Projects.

Есть только два нюанса:
1. Если хочется избавиться от Qt Runtime (статически прилинковать к exe файлу все необходимые библиотеки), то нужно пересобрать Qt с ключом -static:
  • Запустить Visual Studio 2008 Command Prompt
  • Выполнить команду: cd  C:\qt\4.8.2
  • Выполнить команду: configure -platform win32-msvc2008 -static
  • Выполнить команду: nmake

Сборка Qt у меня заняла несколько часов.

2. По-умолчанию библиотеки Qt компилируются в режиме Multi-threaded DLL - /MD (для Debug -  Multi-threaded Debug DLL - /MDd). Что означает наличие зависимостей от msvcr90.dll и msvcp90.dll. Т.е. при установке программы возможно потребуется устанавливать Microsoft Visual C++ 2008 Redistributable Package. Чтобы избавиться от этой зависимости нужно перед сборкой Qt поправить qmake.conf ( C:\qt\4.8.2 \mkspecs\win32-msvc2008\qmake.conf) следующим образом:

заменить строки

  QMAKE_CFLAGS_RELEASE    = -O2 -MD

  QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO += -O2 -MD -Zi

  QMAKE_CFLAGS_DEBUG      = -Zi -MDd

на

  QMAKE_CFLAGS_RELEASE    = -O2 -MT

  QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO += -O2 -MT -Zi

  QMAKE_CFLAGS_DEBUG      = -Zi -MTd

===

Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Зависимость от Microsoft Visual C++ Runtime

Чтобы убрать зависимость от Microsoft Visual C++ Runtime нужно в свойствах проекта выбрать Runtime Library: Multi-threaded (/MT). Получающийся при компиляции exe/dll будет несколько больше, зато не нужно будет устанавливать Visual C++ Redistributable Package.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Проверка серверного сертификата и Enhanced Security

В моем посте про проверку серверного сертификата SSL в Delphi 7 проверка этого самого сертификата выполнялась вызовом функции CryptoAPI CertGetIssuerCertificateFromStore. Я столкнулся на практике со следующим: оказывается, что на эту функцию оказывает влияние Internet Explorer Enhanced Security (по умолчанию оно включено на всех серверах Windows 2003). Т.е. до тех пор, пока хост не будет включен в список доверенных, его сертификат тоже может не проходить проверку. Как-то так (слишком глубоко эту тему не копал). В общем, если на одном компьютере проверка сертификата работает, а на другом - нет (ошибка “Error connecting with SSL”), то следует проверить - включен ли IE Enhanced Security, и если да, то либо отключить его, либо добавить хост в список доверенных.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Delphi XE 2 & ASProtect

ASProtect, к сожалению, не поддерживает приложения, сделанные в Delphi XE 2. Упакованные им exe-ки не запускаются. В Windows Application Log-е появляется соответствующая запись об ошибке в приложении.

Сообщение о несовместимости Delphi XE 2 и ASProtect на официальном форуме датировано 18 ноября 2011, но до сих пор решения нет. Последний ответ от 26 марта 2012 гласит:

Your problem in the queue for the decision and will be solved in the next release of ASProtect (will be released in the nearest future).

Ждем новую версию… Если в ближайшее время не появится, придется переходить на какой-нибудь другой протектор.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Coolsoftware on github

Завел аккаунт в github: https://github.com/coolsoftware/

Это не первый мой аккаунт в github, поэтому потребовалось как-то организовывать одновременный доступ к нему из под разных учеток, с разными SSHKey. По этому поводу маленький “трик” - как я все это сделал:

1. При установке Git for Windows создает такой ярлык для Git Bash:

Как мы видим текущий каталог Git Bash - %HOMEDRIVE%%HOMEPATH%. В нем хранятся все настройки git, каталог .ssh с SSHKey и проекты обычно тоже там.

Я создал в %HOMEDRIVE%%HOMEPATH% каталог Coolsoftware и в нем git.cmd следующего содержания:

@set HOME=%HOMEDRIVE%%HOMEPATH%\Coolsoftware

C:\Windows\SysWOW64\cmd.exe /c “”C:\Program Files (x86)\Git\bin\sh.exe” –login -i”

Затем создал ярлык на этот git.cmd и вынес его на рабочий стол, обозвав “Git Bash - Coolsoftware”. Теперь при его запуске текущий каталог пользователя в “баше” %HOMEDRIVE%%HOMEPATH%\Coolsoftware, а при запуске старого ярлыка “Git Bash” текущий каталог %HOMEDRIVE%%HOMEPATH%. Таким образом получилось два “баша”, в каждом - свои настройки.

Далее запустил “Git Bash - Coolsoftware” произвел установку github в соответствии с инструкцией и выложил проект VHashedStringList - оптимизированный список строк, доступ к элементам которого организован с помощью хеш-индекса.

TVHashedStringList - аналог стандартного THashedStringList (фактически, он сделан на его основе, хотя наследуется не от него, а от TStringList).

Существенное отличие от THashedStringList заключается в том, что оптимизированы операции перестроения индекса при изменении списка.

В THashedStringList полное перестроение индекса требуется после любого изменения, что приводит к значительным задержкам в реальных приложениях.

Например, в следующем примере полное перестроение индекса будет произведено 3 раза:

procedure ChangeList(lst: THashedStringList);
begin
lst.Values['key1'] := 'Value1';
lst.Values['key2'] := 'Value2'; //неявный вызов IndexOfName и перестроение индекса
lst.Values['key3'] := 'Value3'; //неявный вызов IndexOfName и перестроение индекса
lst.Values['key4'] := 'Value4'; //неявный вызов IndexOfName и перестроение индекса
end;

В отличие от THashedStringList, TVHashedStringList не требует полного перестроения индекса после добавления нового элемента в конец списка или

при изменении значений элементов списка. Перестроение происходит только после вставки нового элемента в список или удаления из списка.

Поэтому в следующем примере полного перестроения индекса не произойдет ни разу.

procedure ChangeList(lst: TVHashedStringList);
begin
lst.Values['key1'] := 'Value1'; //индекс модифицирован и не требует перестроения
lst.Values['key2'] := 'Value2'; //индекс модифицирован и не требует перестроения
lst.Values['key3'] := 'Value3'; //индекс модифицирован и не требует перестроения
lst.Values['key4'] := 'Value4'; //индекс модифицирован и не требует перестроения
end;

===

Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Много читателей и один писатель

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

Ниже приведена моя реализация “читателя” и “писателя”. В этой реализации есть допущение о том, что нам известно максимальное количество читателей MAX_READERS.


LONG gCounter = 0;
//алгоритм читателя
for (;;) //бесконечный цикл ожидания освобождения ресурса
{
LONG n = InterlockedIncrement(&gCounter);
//в n - значение gCounter после инкремента
if (n <= MAX_READERS) break; //писатель ничего не пишет - можно читать
InterlockedDecrement(&gCounter);
}
// здесь читаем данные
...
//
InterlockedDecrement(&gCounter); //освобождаем блокировку читателем
// алгоритм писателя
for (;;) //бесконечный цикл освобождения ресурса читателями/писателями
{
LONG n = InterlockedCompareExchange(&gCounter, (MAX_READERS+1), 0);
//в n - предыдущее значение gCounter, которое было ДО попытки заменить его на MAX_READERS+1 в InterlockedCompareExchange;
//если там был 0, то никаких читателей/писателей не было, новое значение в gCounter будет MAX_READERS+1;
//если в gCounter был не 0, то это значение НЕ будет заменено на MAX_READERS+1, а останется прежним
if (n == 0) break;
}
// здесь пишем данные
...
//
InterlockedExchangeAdd(&gCounter, -(MAX_READERS+1)); //освобождаем блокировку писателем

Обращаю внимание на использование Interlocked-функций. Это такие функции, которые обеспечивают атомарность.
Например, InterlockedIncrement(&gCounter) - это атомарное увеличение на 1 (инкремент) значения gCounter.
Вообще, операция инкремента gCounter не атомарна, она состоит из следующих 3-х операций:

1. прочитать значение из памяти по адресу &gCounter
2. увеличить значение на 1
3. записать результат в память по адресу &gCounter

Если выполняется параллельно 2 потока, то возможна такая ситуация :

[поток 1] читает gCounter, прочитан 0
[поток 2] читает gCounter, прочитан 0
[поток 1] увеличивает значение на 1, получается 1
[поток 2] увеличивает значение на 1, получается 1
[поток 1] записывает 1 в gCounter
[поток 2] записывает 1 в gCounter

В итоге в gCounter будет 1, а не 2, как можно было бы предположить.

UPD. LockLib - классы C++ для блокировки разделяемых ресурсов: http://blog.coolsoftware.ru/2013/12/locklib.html

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Свой ГСЧ

Стандартный генератор случайных чисел (ГСЧ) в C++ - функция rand(). Он работает по так-называемому конгруэнтному линейному алгоритму. Для инициализации генератора используется srand(seed).

У этого генератора есть несколько неприятных особенностей.

1. Он более-менее сносно работает, когда в своей программе вы генерируете небольшое количество независимых между собой случайных величин. Но когда с помощью одного и того же датчика генерируется с десяток различных переменных (A, B, C, D, E, F, G, H, I, J), то последовательность псевдослучайных значений, например, переменной A: A1, A2, A3, A4, A5, … уже не будет выглядеть как случайная.

2. Требуется производить инициализацию счетчика с помощью функции srand(seed) в каждом потоке, в котором вызывается rand().

В общем, столкнувшись на практике со странностями работы стандартного ГСЧ, я решил написать свой. Основные требования - скорость работы и возможность обращаться к одному и тому же ГСЧ из разных потоков. И вот, что у меня получилось:


class CRnd
{
private:
unsigned long m_iran;
public:
void Init(unsigned long seed);
unsigned long Rand();
}
void CRnd::Init(unsigned long seed)
{
m_iran = seed;
}
unsigned long CRnd::Rand()
{
unsigned long old_iran = m_iran;
unsigned long new_iran, cur_iran;
for (;;)
{
new_iran = 1664525L*old_iran+1013904223L;
cur_iran = InterlockedCompareExchange((volatile long*)&m_iran, new_iran, old_iran);
if (cur_iran == old_iran) break;
old_iran = cur_iran;
}
return new_iran;
}

UPD. Множитель a = 1664525 и слагаемое c = 1013904223 были взяты из статьи в вики. На практике, однако, выяснилось, что получаемая последовательность чисел имеет короткий период, поэтому имеет смысл использовать другие параметры:
a = 1103515245, c = 12345, m = 2^31.

unsigned long CRnd::Rand()

{
unsigned long old_iran = m_iran;
unsigned long new_iran, cur_iran;
for (;;)
{
new_iran = 1103515245L*old_iran+12345L;
cur_iran = InterlockedCompareExchange((volatile long*)&m_iran, new_iran, old_iran);
if (cur_iran == old_iran) break;
old_iran = cur_iran;
}
return ((new_iran >> 16) & 32767L);
}

===

Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Вызов кода на C# из программы на Delphi

Обычно решают противоположную задачу - вызов внешних модулей (DLL, COM), написанных на C++, Delphi и т.п., из программы на C#. У меня же возникла необходимость вызвать из программы написанной на Delphi функцию библиотеки, написанной на C#. В этом посте я расскажу о том, как это делается.

Итак, нужно сделать на C# COM объект, к которому затем мы сможем обращаться из нашей программы на Delphi.

  1. В Visual Studio создаем новый проект New/Project/Visual C#/Class Library.

  2. В свойствах проекта на вкладке Application жмем кнопку “Assembly Information” и отмечаем “Make assembly COM-visible”.

  3. В свойствах проекта на вкладке Build отмечаем Register for COM interop.

  4. Для регистрации библиотеки вместо regsvr32 нужно использовать regasm. Вызов будет типа такого:

    regasm.exe vcomclasslibrary.dll /tlb:vcomclasslibrary.tlb

    regasm.exe обычно расположен тут:

    c:\Windows\Microsoft.NET\Framework\v2.0.50727\

  5. Код на C#:


    using System;
    using System.Drawing;
    using System.Runtime.InteropServices;
    namespace VCOMClassLibrary
    {
    [ComVisible(true)]
    public interface IVCOMInterface
    {
    string LoadImageFromStream(System.IO.Stream stream);
    }
    [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
    public class VCOMClass : IVCOMInterface
    {
    public string LoadImageFromStream(System.IO.Stream stream)
    {
    Image img = Image.FromStream(stream);
    //put code to load image here
    return "OK";
    }
    }
    }
  6. В проекте на Delphi импортируем VCOMClassLibrary (Project/Import Type Library). Пример вызова (в котором я заодно показал как передавать Stream):


    procedure TForm1.Button1Click(Sender: TObject);
    var
    cls: IVCOMInterface;
    Stream: IStream;
    FileStream: TFileStream;
    begin
    cls := CreateComObject(CLASS_VCOMClass) as IVCOMInterface;
    FileStream := TFileStream.Create('c:\temp\1.jpg', fmOpenRead);
    Stream := TStreamAdapter.Create(FileStream, soOwned) as IStream;
    cls.LoadImageFromStream(Stream);
    FileStream.Free;
    end;

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

Быстрая загрузка данных в Sqlite

Sqlite замечательная локальная база данных. Не требует установки, легко встраивается в приложения благодаря API. Шустрая благодаря тому, что данные не гоняются ни по сети, ни даже между процессами. У меня загрузка листа в миллион записей в Sqlite занимает порядка 30 секунд. Для сравнения - та же самая процедура загружает те же данные в локальный MySQL в 10 раз дольше.
Однако, для того, чтобы загрузка данных в Sqlite происходила с максимально возможной скоростью нужно использовать две вещи:

1. Параметризованный Sql, типа такого: INSERT INTO `table1` (`A`, `B`) VALUES (:param1, :param2). В начале процедуры загрузки выполнять “prepare” (Sqlite3_Prepare_v2). Перед вставкой “биндить” переменные с помощью sqlite3_bind_text, sqlite3_bind_int64 и т.п.,
а затем выполнять вставку вызовом функции sqlite3_step.

2. Транзакции: вставлять данные большими порциями, например, по 1000 записей. Перед вставкой очередной порции выполнять BEGIN TRANSACTION. В конце вставки - COMMIT TRANSACTION. Если этого не делать, то после каждой операции вставки данные будут немедленно сбрасываться на диск, что замедлит процесс во много раз.

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru