пятница, 1 апреля 2011 г.

Длинные пути файловой системы в .NET

Любимая всеми ОС Windows с не менее любимой ФС NTFS может поддерживать пути длинной до 32К символов. Однако  мало кто этим воспользовался, поскольку в обычной жизни DOS-пути (а ими являются все пути вида C:\Windows...) поддерживают всего лишь 260 символов. Частенько этого бывает мало. Скажем, необходимо восстановить инфраструктуру сайта, а пути там могут быть куда длиннее.
В таком случае хорошо бы воспользоваться обещанными 32К-длинными путями. Но вот незадача: при попытке сделать путь хотя бы немного длиннее дозволенного, мы схлопочем исключение:
The path is too long after being fully qualified. Make sure path is less than 260 characters.
Для использования длинных путей с помощью WinAPI делают так:
1. Используют UNICODE-версии функций: CreateFileW, CreateDirectoryW и т.д.
2. Формируют путь в стиле NT: "\\?\C:\Windows..."

Еще одна проблема в том, что пути NT никак не модерируются системой: если обычные DOS пути можно писать как попало (C:/Windows\\\\\system32), и система спокойно съест такое, то для NT путей это недопустимо. То есть вся ответственность за корректные пути ложится на программиста.
Вернемся к дотнету. Он, конечно же, не предоставит нам выбора какими функциями пользоваться для работы с файлом. Придется использовать P/Invoke.
Проблема не нова, и решения можно поискать например здесь.
Я же предлагаю сконцентрироваться на устранении последствий применения P/Invoke, которое:
а. Поломает архитектуру
б. Сделает программу ОС-зависимой

Пункт (б) побороть никак не получится (кроме как использовать условную компиляцию). А вот архитектуру надо бы сохранить. Если у вас готова уже половина кода проекта, то переписывать куски с учетом использования нестандартного механизма работы с файлами совсем не хочется. Поэтому напишем класс, который будет совместим с обычным FileStream, но будет работать с длинными путями файловой системы.
Для сохранения архитектуры (конечно если она построена на принципах dependency injection) достаточно будет заменить фабрики, создающие объект файла или внести поправки в инициализацию DI контейнера.

Итак, ближе к делу. Сначала подготовим статический класс WinApi, в котором импортируем функции из kernel32.dll и определим константы. Также внимание надо уделить нормализации путей (ее сюда же добавим):
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace WindowsSpecific
{
    public static class WinApi
    {
        #region ECreationDisposition enum

        public static string NormalizePath(string path)
        {
            const int bufferSize = 32767;

            if (path.StartsWith(".") || !path.Contains(":"))
            {
                var stringBuilder = new StringBuilder {Length = bufferSize};
                GetCurrentDirectory(bufferSize-1, stringBuilder);

                path = stringBuilder + "/" + path.TrimStart(new[] { '.' });
            }

            path = path.Replace('/', '').Trim();
           
            while (path.Contains(@""))
                path = path.Replace(@"", @"");

            return @"?" + path;
        }

        public const uint ErrorAlreadyExist = 0x000000b7;

        public enum ECreationDisposition : uint
        {
            New = 1,
            CreateAlways = 2,
            OpenExisting = 3,
            OpenAlways = 4,
            TruncateExisting = 5,
        }
        #endregion

        #region EFileAccess enum
        [Flags]
        public enum EFileAccess : uint
        {
            GenericRead = 0x80000000,
            GenericWrite = 0x40000000,
            GenericExecute = 0x20000000,
            GenericAll = 0x10000000,
        }
        #endregion

        #region EFileAttributes enum
        [Flags]
        public enum EFileAttributes : uint
        {
            Readonly = 0x00000001,
            Hidden = 0x00000002,
            System = 0x00000004,
            Directory = 0x00000010,
            Archive = 0x00000020,
            Device = 0x00000040,
            Normal = 0x00000080,
            Temporary = 0x00000100,
            SparseFile = 0x00000200,
            ReparsePoint = 0x00000400,
            Compressed = 0x00000800,
            Offline = 0x00001000,
            NotContentIndexed = 0x00002000,
            Encrypted = 0x00004000,
            Write_Through = 0x80000000,
            Overlapped = 0x40000000,
            NoBuffering = 0x20000000,
            RandomAccess = 0x10000000,
            SequentialScan = 0x08000000,
            DeleteOnClose = 0x04000000,
            BackupSemantics = 0x02000000,
            PosixSemantics = 0x01000000,
            OpenReparsePoint = 0x00200000,
            OpenNoRecall = 0x00100000,
            FirstPipeInstance = 0x00080000
        }
        #endregion

        #region EFileShare enum
        [Flags]
        public enum EFileShare : uint
        {
            None = 0x00000000,
            Read = 0x00000001,
            Write = 0x00000002,
            Delete = 0x00000004,
        }
        #endregion

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern SafeFileHandle CreateFile
            (
            string lpFileName,
            EFileAccess dwDesiredAccess,
            EFileShare dwShareMode,
            IntPtr lpSecurityAttributes,
            ECreationDisposition dwCreationDisposition,
            EFileAttributes dwFlagsAndAttributes,
            IntPtr hTemplateFile
            );

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool CreateDirectory
            (
            string lpFileName,
            IntPtr lpSecurityAttributes
            );

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool CopyFile(
          string lpExistingFileName,
          string lpNewFileName,
          bool bFailIfExists
        );

        [DllImport("kernel32.dll")]
        public static extern uint GetCurrentDirectory(uint nBufferLength,
           [Out] StringBuilder lpBuffer);

        public static ECreationDisposition ECreationDispositionByFileMode(FileMode fileMode)
        {
            var winapiMode = ECreationDisposition.OpenAlways;
            switch (fileMode)
            {
                case FileMode.CreateNew:
                    winapiMode = ECreationDisposition.New;
                    break;

                case FileMode.Create:
                    winapiMode = ECreationDisposition.CreateAlways;
                    break;

                case FileMode.Open:
                    winapiMode = ECreationDisposition.OpenExisting;
                    break;

                case FileMode.OpenOrCreate:
                    winapiMode = ECreationDisposition.OpenAlways;
                    break;

                case FileMode.Truncate:
                    winapiMode = ECreationDisposition.TruncateExisting;
                    break;

                case FileMode.Append:
                    winapiMode = ECreationDisposition.OpenAlways;
                    break;
            }
            return winapiMode;
        }

        public static EFileAccess EFileAccessByFileAccess(FileAccess fileAccess)
        {
            var winapiAccess = EFileAccess.GenericAll;

            switch (fileAccess)
            {
                case FileAccess.Read:
                    winapiAccess = EFileAccess.GenericRead;
                    break;

                case FileAccess.Write:
                    winapiAccess = EFileAccess.GenericWrite;
                    break;

                case FileAccess.ReadWrite:
                    winapiAccess = EFileAccess.GenericRead | EFileAccess.GenericWrite;
                    break;
            }

            return winapiAccess;
        }
    }
}

Теперь напишем класс, перегрузив FileStream:
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace WindowsSpecific
{
    public class LongPathFileStream : FileStream
    {
        private static SafeFileHandle Initialize(string fileName, FileMode fileMode, FileAccess fileAccess)
        {
            var longFileName = WinApi.NormalizePath(fileName);

            var fileHandle = WinApi.CreateFile(longFileName,WinApi.EFileAccessByFileAccess(fileAccess),
                WinApi.EFileShare.Read,IntPtr.Zero,WinApi.ECreationDispositionByFileMode(fileMode),0,IntPtr.Zero);

            var lastWin32Error = Marshal.GetLastWin32Error();
            if (fileHandle.IsInvalid)
                throw new System.ComponentModel.Win32Exception(lastWin32Error);

            return fileHandle;
        }

        public LongPathFileStream(string fileName)
            :base(Initialize(fileName,FileMode.OpenOrCreate,FileAccess.ReadWrite),FileAccess.ReadWrite)
        {
        }

        public LongPathFileStream(string fileName,FileAccess fileAccess)
            :base(Initialize(fileName,FileMode.OpenOrCreate,fileAccess),fileAccess)
        {
        }

        public LongPathFileStream(string fileName, FileMode fileMode, FileAccess fileAccess)
            :base(Initialize(fileName,fileMode,fileAccess),FileAccess.ReadWrite)
        {
            if (fileMode == FileMode.Append)
                AppendMode();
        }

        private void AppendMode()
        {
            Position = Length;
        }
    }
}

Вот и все! Теперь если вместо FileStream задействовать LongPathFileStream, то можно создавать и открывать файлы используя путь длинной до 32К символов.

Есть еще правда вот такая проблема. Если создать файл, путь к которому длиннее 260 символов, то прочитать или удалить такой файл можно лишь программами, которые умеют работать с длинными путями, а таких куда меньше чем хотелось бы. Либо воспользоваться кривыми приемами от Microsoft (читаем тут, параграф 4).

Для полноты картины нужно бы обернуть еще WinAPI функции для создания и удаления директории, поиск файлов. Но это в другой раз.



Читать дальше...

четверг, 31 марта 2011 г.

Паттерн Service Locator на C++

Паттерн Service locator представляет собой хранилище сервисных объектов. Фактически это некоторого рода ассоциативный массив с экземплярами объектов-реализаций, которые определяются по ключу – типу или имени интерфейса.

Применяется для внедрения зависимостей (Dependency Injection). В целом очень полезная фича. Иногда этот паттерн реализуется DI контейнерами заодно с основной функциональностью.

Алгоритм работы с Service Locator:
1. Регистрация экземпляров реализаций сервисных объектов в ServiceLocator путем ассоциирования с типом или именем интерфейса
2. Передача экземпляра ServiceLocator в классы, которым необходимы сервисные объекты
3. Класс, которому необходим сервисный объект, запрашивает его по ключу у контенера

Получаем развязку интерфейса и реализации.

Спроектируем и реализуем Service Locator на C++.

Терминология:
Сервис – экземпляр класса, реализующего интерфейс
Интерфейс сервиса – класс, который декларирует методы взаимодействия с сервисом, но не реализовывает их
Тег – строковой идентификатор объекта в контейнере



Актерами выступают:
- конфигуратор – функционал, создающий ServiceLocator и настраивающий сервисы
- пользователь – функционал, использующий сервисы

Конфигуратор может:
- создать ServiceLocator
- регистрировать ассоциацию типа интерфейса с экземпляром объекта сервиса с указанием тега
- регистрировать ассоциацию типа интерфейса с экземпляром объекта сервиса без указания тега

Пользователь может:
- получать сервис по запросу типа интерфейса с указанием тега
- получать сервис по запросу типа интерфейса без указания тега
- получать сервис по запросу имени типа интерфейса с указанием тега
- получать сервис по запросу имени типа интерфейса без указания тега

Реализация

Интерфейс IServiceLocator

#include <string>
#include <list>

class IServiceLocator
{
public:
virtual ~IServiceLocator() {};

virtual void RegisterService(std::string name, void* serviceObject, std::string tag = "") = 0;
virtual void* Locate(std::string name, std::string tag = "") = 0;

template<typename T>
void RegisterService(T* serviceObject, std::string tag = "")
{
RegisterService(typeid(T).name(), reinterpret_cast<void*>(serviceObject), tag);
}

template<typename T>
T* Locate(std::string tag = "")
{
return (T*)Locate(typeid(T).name(),tag);
}
};


Декларация реализации ServiceLocator


#include <list>
#include <string>

class ServiceLocator : public IServiceLocator
{
public:
virtual void RegisterService(std::string name, void* serviceObject, std::string tag = "");
virtual void* Locate(std::string name, std::string tag = "");
virtual ~ServiceLocator();

protected:
struct Service
{
void* ServiceObject;
std::string Name;
std::string Tag;
};

private:
std::list<struct Service> services_;
typedef std::list<struct Service>::iterator ServiceIterator;
};


Реализация ServiceLocator

ServiceLocator::~ServiceLocator()
{
for (ServiceIterator i = services_.begin(); i != services_.end(); i++)
{
if (i->ServiceObject != NULL)
{
delete i->ServiceObject;
i->ServiceObject = NULL;
}
}
services_.clear();
}

void ServiceLocator::RegisterService(std::string name, void* serviceObject, std::string tag)
{
if (serviceObject == NULL)
throw std::invalid_argument("serviceObject");

for (ServiceIterator i = services_.begin(); i != services_.end(); i++)
{
if ((i->Name == name) && (i->Tag == tag))
{
throw std::runtime_error(std::string("Duplicate service registration for '") + name +
std::string("' and tag '") + tag + std::string("'"));
}
}

struct Service service;
service.Name = name;
service.ServiceObject = serviceObject;
service.Tag = tag;

services_.push_back(service);
}

void* ServiceLocator::Locate(std::string name, std::string tag)
{
for (ServiceIterator i = services_.begin(); i != services_.end(); i++)
{
if ((i->Name == name) && (i->Tag == tag))
{
return reinterpret_cast<void*>(i->ServiceObject);
}
}
throw std::runtime_error(std::string("Service not found for '") + name +
std::string("' and tag '") + tag + std::string("'"));
}


Пример использования:

IServiceLocator *serviceLocator = new ServiceLocator();

serviceLocator->RegisterService<IService>(new ServiceRelease());
serviceLocator->RegisterService<IService>(new OtherServiceRelease(),"svc");

IService* service = serviceLocator->Locate<IService>();
IService* otherService = serviceLocator->Locate<IService>("svc");


Читать дальше...

среда, 30 марта 2011 г.

Валидация параметров конструктора класса в секции инициализации

Принимая параметры-указатели в конструктор, иногда полезно их проверять на валидность:


MyClass::MyClass (A* a, B* b)
{
if (!a) throw std::invalid_argument("a");
if (!b) throw std::invalid_argument("b");
a_ = a;
b_ = b;
}

Красивее (да и правильнее) было бы выполнять присвоение в секции инициализации:

MyClass::MyClass (A* a, B* b)
:a_(a), b_(b)
{
if (!a) throw std::invalid_argument("a");
if (!b) throw std::invalid_argument("b");
}

Недостатки налицо: некрасиво и не очень функционально. Присвоение выполняется еще до проверок. Кроме того, бывает еще такая ситуация:


class MyClass
{
A* a_;
B& b_;

MyClass(A* a)
:b_(a->getRefB())
{
}
}

Проблема в том, что если в качестве параметра будет передан невалидный указатель, то будет сгенерировано системное исключение еще до вызова конструктора. А переносить инициализацию внутрь конструктора нельзя: ссылка может быть проинициализирована только в секции инициализации класса.
Возможный выход в том, чтобы не попадать в подобную ситуацию. То есть – не инициализировать ссылку через указатель на объект. Но если мы хотим использовать Dependency Injection вместе со средствами C++ на полную катушку, то такая ситуация рано или поздно возникнет.
Идея состоит в том, чтобы проверить валидность указателя именно в секции инициализации. Помогут функции с шаблонными параметрами.

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


template<typename T>
T NotNullChecker (T value, std::string variableName)
{
if (value == NULL)
throw std::invalid_argument(variableName + " cannot be NULL");

return value;
}

template<typename T>
T ConvertableTypeChecker(T value, std::string variableName)
{
if (dynamic_cast<T>(value) == NULL)
throw std::invalid_argument(variable + " must be convertible to type " + std::string(typeid(T).name()));

return value;
}

Для удобства допишем пару макросов для автоматической подстановки имен параметров:


#define NotNull(p) NotNullChecker(p,#p)
#define ConvertableType(p) ConvertableTypeChecker(p,#p)
#define ValidArgument(p) ConvertableTypeChecker(NotNullChecker(p,#p),#p)

Использование выглядит очень лаконично на мой взгляд:

class MyClass
{
A* a_;
B& b_;

MyClass(A* a)
:b_(ValidArgument(a) ->getRefB())
{
}
}

Вот так можно проверить аргумент на валидность еще в секции инициализации. Если развить идею дальше – можно написать таким же образом функцию проверки целочисленного аргумента на попадание в допустимый диапазон и т.д.

Читать дальше...

вторник, 14 сентября 2010 г.

AOP с Application Unity: практическое применение

Аспектно-ориентированное программирование (проектирование) – относительно новая методология, применяемая при разработке клиентского и серверного ПО. Несмотря на это, в основе ее лежит старый принцип «перехвата» методов – код как бы ижектируется до и после вызываемого метода.
К сожалению, C# не поддерживат АОП на языковом уровне (а жаль, мог бы). Однако есть расширения для DI контейнера Application Unity (который входит в состав Microsoft Enterprise Library), позволяющий реализовывать АОП.

Есть хорошие статьи для начала по использованию Application Unity для АОП. Читаем:
http://habrahabr.ru/blogs/net/50845
http://codebetter.com/blogs/david.hayden/archive/2008/11/07/unity-and-aop-example.aspx
http://www.codewrecks.com/blog/index.php/2009/01/31/unity-and-aop-in-enterprise-library/

Application Unity генерирует класс-прозрачный прокси, который вызывает методы целевого класса. Он позволяет выполнить перехват методов либо на уровне связи интерфейса с реализацией, либо у самого класса, если он наследует MarshalByRefObject.
В обоих случаях выглядит это так: вы пишете атрибуты, ставите их над методом, и атрибут обрабатывает вызов метода, позволяя выполнять код до и после метода, или не вызывать настоящий метод вообще.
Каждый из случаев имеет свои особенности, имеет свои преимущества и недостатки.
Перехват на уровне связи интерфейса с классом-реализацией:
+ выполняется быстро
+ регистрируется только на интерфейс
- атрибуты ставятся только в интерфейсе; как следствие, АОП может применяться только к публичным методам
- из-за атрибутов в интерфейсе частично раскрывает особенности реализации
- AOP не будет работать в реализации без интерфейса

Перехват методов у класса реализации:
+ позволяет расставить атрибуты над любыми методами (в т.ч. private и protected)
+ атрибуты расставляются только в классе-реализации: не раскрываются особенности АОП в интерфейсе
- работает медленно из за MarshalByRefObject
- требует регистрации на все классы-реализации интерфейса

Оба случая завязывают АОП на использование DI контейнера Application Unity, поскольку требуют получения экземпляра класса его средствами.

Как показала практика, в реальной жизни АОП удобно использовать для:
1. Логгирования на входе и выходе из метода (классический пример, чаще всего встречается в публикациях)
2. Многопоточной синхронизации
3. Проверки входных аргументов метода
4. Обработки исключений
5. Реализация ступенчатой инициализации

Кроме логгирования, практически все остальные случаи требуют привязки АОП к реализации, поэтому выберем метод с наследованием MarshalByRefObject.

Например, возьмем пример реализации многоступенчатой инициализации. После вызова конструктора перед вызовом всех методов должен быть единажды вызван метод Initialize. Если была произведена попытка вызова метода до инициализации, должно быть сгенерировано исключение, как и в случае повторного вызова Initialize. Как это выглядит без АОП:


public interface IInitializable
{
bool IsInitialized { get; }
}

public class MyClass : IInitializable, IDisposable
{
public bool IsInitialized { get; private set; }

public void Initialize()
{
if (IsInitialized)
throw new Exception("duplicate initialization");

IsInitialized = true;
}

public void FirstUsefulMethod()
{
if (!IsInitialized)
throw new Exception("object must be initialized first");
}

public void SecondUsefulMethod()
{
if (!IsInitialized)
throw new Exception("object must be initialized first");
}

public void Dispose()
{
IsInitialized = false;
}
}

Недостатки очевидны: в каждом методе нужно добавлять проверку на инициализацию.
Используем декларативный подход с АОП:


public class MyClass : MarshalByRefObject, IInitializable, IDisposable
{
public bool IsInitialized { get; private set; }

[Initializer]
public void Initialize()
{
IsInitialized = true;
}

[AfterInitialize]
public void FirstUsefulMethod()
{
}

[AfterInitialize]
public void SecondUsefulMethod()
{
}

public void Dispose()
{
IsInitialized = false;
}
}

Положительные стороны налицо: теперь полезные методы содержат только полезный код. Кроме того, декларативный подход более нагляден и краток.
Пример, конечно же, очень простой. Но если подумать о том, что кроме инициализаторов у класса может быть еще многопоточная синхронизация, проверка параметров, обработка исключений и логгирование, то применение АОП весьма оправдано.
Сравните код без АОП:

public void FirstUsefulMethod(object parameter)
{
if (!IsInitialized)
throw new Exception("object must be initialized before using this method");

if (parameter == null)
throw new ArgumentNullException("parameter");

_logger.Debug("Enter to FirstUsefulMethod");

try
{
_locker.EnterWriteLock();

<ПОЛЕЗНЫЙ КОД>
}
catch(Exception exception)
{
DisplayMessageBox(exception.Message);
throw;
}
finally
{
_locker.ExitWriteLock();
_logger.Debug("Exit from FirstUsefulMethod");
}
}

и с АОП:

[AfterInitialize]
[ArgumentsNotNull]
[DisplayError]
[Log]
[ExclusiveLock]
public void UsefulMethod(object parameter)
{
<ПОЛЕЗНЫЙ КОД>
}

Для примера приведу исходный код пары атрибутов, Initializer и AfterInitialize:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class InitializerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new InitializerCallHandler();
}
}

public class InitializerCallHandler : ICallHandler
{
public int Order { get; set; }

public InitializerCallHandler()
{
}

public InitializerCallHandler(int order)
{
Order = order;
}

#region ICallHandler Members

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var initializableObject = (IInitializable)input.Target;

if (initializableObject == null)
{
throw new Exception(
"[Initializer] attribute can be used only for classes that released IInitializable interface");
}

if (initializableObject.IsInitialized)
{
throw new InvalidOperationException(
string.Format("Object of type '{0}' already initialized",
initializableObject.GetType().FullName));
}

return getNext().Invoke(input, getNext);
}

#endregion
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class AfterInitializeAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new AfterInitializeCallHandler();
}
}

public class AfterInitializeCallHandler : ICallHandler
{
public int Order { get; set; }

public AfterInitializeCallHandler()
{
}

public AfterInitializeCallHandler(int order)
{
Order = order;
}

#region ICallHandler Members

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var initializableObject = (IInitializable) input.Target;

if (initializableObject == null)
{
throw new Exception(
"[AfterInitialize] attribute can be used only for classes that released IInitializable interface");
}

if (!initializableObject.IsInitialized)
{
throw new InvalidOperationException(
string.Format("Object of type '{0}' must be initialized before using",
initializableObject.GetType().FullName));
}

return getNext().Invoke(input, getNext);
}

#endregion
}

Регистрация типа в контейнере:

unityContainer.RegisterType<IMyClass, MyClass>();
unityContainer.Configure<Interception>()
.SetInterceptorFor<MyClass>(new TransparentProxyInterceptor())

Перед этим нужно включить расширение Interception

unityContainer.AddNewExtension<Interception>();


P.S. Атрибуты АОП не работают для конструкторов

Читать дальше...

суббота, 21 августа 2010 г.

Интерфейсы и виртуальные деструкторы

Почему деструктор нужно делать виртуальным, описано на многих блогах и форумах (даже линки приводить не буду, гуглите – материала полно).
Но это только вершина айсберга... оказывается, всем интерфейсам нужно добавить виртуальные деструкторы, иначе велик шанс, что деструктор реализации вызван не будет.

Вот почему.

Смотрим на обычный интерфейс и его реализацию:


class IFile
{
public:
virtual void Process() = 0;
};

class FileImplementation : public IFile
{
public:
FileImplementation()
{
// тут открываем файл
}

virtual ~FileImplementation()
{
// а тут закрываем файл
}

virtual void Process()
{
// какие-то действия
}
};


Что мы видим? Стандартная имплементация в стиле RAII.
Создаем экземпляр имплементирующего класса и проходимся отладчиком:


{
std::auto_ptr<FileImplementation> file(new FileImplementation());
file->Process();
}


При выходе из области видимости деструктор вызовется и файл закроется.
А теперь попробуем воспроизвести Dependency Injection:


{
std::auto_ptr<IFile> file(new FileImplementation());
file->Process();
}


И видим, что деструктор FileImplementation не будет вызван!
Объяснение простое. Интеллектуальный указатель будет удалять экземпляр IFile, который ничего не знает о своей реализации, кроме виртуальных методов. И про деструктор у имплементации он тоже ничего не знает, даже не смотря на то, что он – виртуальный. Причина: он становится виртуальным уже НИЖЕ по архитектуре.

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

Я борюсь с этим через объявление тривиального публичного виртуального конструктора в интерфейсе. Чисто виртуальный деструктор не годится, а прятать его в protected смысла нет. Да и нехорошо в интерфейсном классе объявлять protected, пусть даже деструктор.


class IFile
{
public:
virtual void Process() = 0;
virtual ~IFile() {};
};


Мораль сей басни такова. Добавьте во все классы виртуальный деструктор, хотя бы тривиальный. И будут они жить долго и счастливо...

Читать дальше...

суббота, 24 июля 2010 г.

Remote debugging with MS Visual Studio

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

Что нам расскажет Microsoft про удаленную отладку? Читаем: http://msdn.microsoft.com/ru-ru/library/bt727f1t%28v=VS.90%29.aspx

Устанавливать среду разработки, или отдельные ее части, на тестовый ПК необязательно. Достаточно скопировать Remote Debugger Monitor с основного компьютера (он обычно лежит в директории \Program Files\Microsoft Visual Studio 9.0\Common7\IDE\Remote Debugger, если он установлен).
На тестовом компьютере запускаем монитор – файл msvsmon.exe из директории x86.

После этого нужно настроить тип транспорта (Tools - Options). По умолчанию для связи используются именованные пайпы. На практике мне удалось соединиться через них только в доменной сети. Поэтому если вы являетесь счастливым пользователем контроллера домена, то, немного поплясав с бубном, возможно у вас все получится. Положительный момент соединения через пайпы – Windows-авторизация для доступа к удаленной отладке.
Я же пошел более простым путем, настроив соединение через TCP. К сожалению, с авторизацией в этом случае все совсем плохо: нет даже примитивной парольной защиты. Ну да ладно, мы же в отладке.

Проверяем, что все настроено правильно. На машине с Visual Studio пытаемся подключиться к удаленному процессу (Tools – Attach to process).
Если все выполнено правильно, то появится список удаленных процессов:

Теперь уже можно отлаживаться: копируем на удаленную машину собранное приложение (с отладочной информацией, естественно), запускаем его там и отсюда подключаемся к процессу. Но это не очень удобно.
Хорошо бы автоматизировать копирование и запуск.
Расшариваем директорию на тестовой машине, например C:\Debug. Следует только не забыть разрешить запись для всех пользователей и включить гостевую учетную запись из «Локальной политики безопасности», если она отключена (Пуск – Панель управления – Администрирование – Локальная политика безопасности):

У меня получился адрес \\192.168.92.1\Debug, путь на тестовой машине C:\Debug
Проверяем доступность директории: переходим по адресу \\192.168.92.1\Debug и закидываем туда файл. Если все получилось, то директория настроена правильно.

Копировать можно двумя способами:
1. Настроить Ouput Directory проекта на \\192.168.92.1\Debug
2. Настроить через Post Build Events копирование из командной строки

Рекомендую второй способ, поскольку если у вас в проекте используются сторонние DLL или другие бинарники, то копировать их все равно придется Post Build-ом.

Заходим в свойства проекта и настраиваем копирование:

и параметры запуска:

Вот теперь все как надо. Делаем Rebuild и отлаживаемся удаленно :)
Читать дальше...

пятница, 23 июля 2010 г.

Идиома RAII

Resource Acquisition Is Initialization – «Завладение ресурсом есть его инициализация». Если вдуматься в смысл, то чем-то напоминает ленивую инициализацию.
Однако для нас, для C++ программистов, наиболее важным является следствие из этой идиомы: «Освобождение ресурса есть его деинициализация».

К примеру, нужно скопировать содержимое одного файла в другой:

FILE* file1 = fopen("first.bin","rb");
if (!file1)
throw;

FILE* file2 = fopen("second.bin","wb");
if (!file2)
{
fclose(file1);
throw;
}

fseek(file1,0,SEEK_END);
unsigned long size = ftell(file1);
rewind(file1);

char* buffer = (char*)malloc(sizeof(char)*size);
if (!buffer)
{
fclose(file2);
fclose(file1);
throw;
}

unsigned long result = fread (buffer,1,size,file1);
if (result != size)
{
free(buffer);
fclose(file2);
fclose(file1);
throw;
}

result = fwrite(buffer,1,size,file2);
if (result != size)
{
free(buffer);
fclose(file2);
fclose(file1);
throw;
}

free(buffer);
fclose(file2);
fclose(file1);

Если на миг забыть то, что этот код исправно работает, во всем остальном он просто ужасен. Невооруженным взглядом видно, что большей частью он состоит из проверки результатов операций и откатов. Притом что, в конце концов, делается запланированный откат.

Давайте подумаем над недостатками такого кода. Вносить изменения в него крайне опасно: что-нибудь пропустишь – и ресурс (память или файл) останется неосвобожденным. Кроме того, если по каким-либо причинам исключение произойдет при работе с файлом через fread или fwrite, то все ресурсы останутся жить в зомбиленде. Кроме того, присутствует дублирование кода, читабельность стремиться к нулю со всеми вытекающими.

И тут приходят на помощь особенности языка C++. Давайте вспомним локальные переменные и области видимости.

{
int i;
MyClass myClass;

...
} // выход из области видимости

Что произойдет с переменными i и myClass по достижению конца области видимости? Переменная i будет вытолкнута из стека, а для myClass будет вызван деструктор класса. А ведь это уже интересно...

{
int i;
MyClass myClass;

...
if (i==1)
return 0;

if (i==2)
throw;
}

Когда в этом случае будет вызван деструктор myClass? Правильно, в каждом из случаев выхода из области видимости: если i=1, то перед return, если i=2 – перед генерацией исключения. При всех остальных значениях i – естественным путем, так как достигнута закрывающая скобка.
Это напоминает первый пример. Если написать откаты в деструкторах объектов, то не нужно их специально вызывать: они будут вызваны автоматически, когда это понадобится. И не останется зомби-файлов и утечек памяти, даже случайно. Ну, и по аналогии, в конструкторе нужно инициализировать объекты. Это и есть RAII.
Пишем код, лишенный всех недостатков. Сначала определяем классы, которые будут служить RAII-врапперами:

class File // RAII обертка для файла
{
public:
File(char* name, char* mode)
{
file_ = fopen(name,mode);
if (!file_)
throw;
}

~File()
{
if (file_)
fclose(file_);
}

void fread(char* buffer, unsigned long elements, unsigned long bytes)
{
if (::fread(buffer,elements,bytes,file_) != bytes)
throw;
}

void fwrite(char* buffer, unsigned long elements, unsigned long bytes)
{
if (::fwrite(buffer,elements,bytes,file_) != bytes)
throw;
}

unsigned long GetSize()
{
fseek(file_,0,SEEK_END);
unsigned long size = ftell(file_);
rewind(file_);

return size;
}

private:
FILE* file_;
};

class Memory // RAII обертка для памяти
{
public:
Memory(unsigned long size)
{
memory_ = (char*) malloc (size);

if (!memory_)
throw;
}

~Memory()
{
if (memory_)
free(memory_);
}

char* Get()
{
return memory_;
}
};

И, собственно, рефакторинг участка кода:

{
File file1("file1.bin","rb");
File file2("file2.bin","wb");

unsigned long fileSize = file1.GetSize();

Memory buffer(fileSize);

file1.fread(buffer.Get(),1,fileSize);
file2.fwrite(buffer.Get(),1,fileSize);
}

Вот и все! Память больше освобождать не нужно: она освободится автоматически, когда переменная buffer станет недоступна. И с закрытиями файлов также: все будет закрыто при выходе из области видимости переменных file1 и file2.
Стоит ли использовать идиому RAII? Поверьте, стоит. Даже если у вас феноменальная память и bounds checker в голове, рано или поздно вы допустите ошибку. Так почему бы не использовать RAII, хуже то от этого не становится.

P.S. Обмозговав класс Memory, можно прийти к выводу, что механизм автоматического освобождения памяти через RAII очень даже хорош. Но не спешите писать свой класс Memory: он уже есть в библиотеке STL, правильное его название «интеллектуальный указатель», а класс называется std::auto_ptr.

Читать дальше...