C dllimport пример

В статье рассмотрена полная интеграция C++ библиотеки в managed окружение с использованием Platform Invoke. Под полной интеграцией подразумевается возможность наследования классов библиотеки, реализации её интерфейсов (интерфейсы будут представлены в managed коде как абстрактные классы). Экземпляры наследников можно будет «передавать» в unmanaged окружение.
Вопрос интеграции уже не раз поднимался на хабре, но, как правило, он посвящен интеграции пары-тройки методов, которые нет возможности реализовать в managed коде. Перед нами же стояла задача взять модуль из C++ и заставить его работать в .NET. Вариант написать заново, по ряду причин, не рассматривался, так что мы приступили к интеграции.
Эта статья не раскрывает всех вопросов интеграции unmanaged модуля в .NET. Есть еще нюансы с передачей строк, логических значений и т.п… По этим вопросам есть документация и несколько статей на хабре, так что здесь эти вопросы не рассматривались.
Стоит отметить, что .NET обёртка на базе Platform Invoke кроссплатформенна, её можно собрать на Mono + gcc.

Интеграция sealed класса

Первое, что приходится осознать при интеграции с помощью Platform Invoke это то, что этот инструмент позволяет интегрировать лишь отдельные функции. Нельзя просто так взять и интегрировать класс. Решение проблемы выглядит просто:
На стороне Unmanaged пишем функцию:
SomeType ClassName_methodName(ClassName * instance, SomeOtherType someArgument) { instance->methodName(someArgument); }
Не забываем к подобным функциям добавить extern «C», чтобы их имена не декорировались C++ компилятором. Это помешало бы нам при интеграции этих функций в .NET.
Далее повторяем процедуру для всех публичных методов класса и интегрируем полученные функции в класс, написанный в .NET. Получившийся класс нельзя наследовать, поэтому в .NET такой класс объявляется как sealed. Как обойти это ограничение и с чем оно связано — смотрите ниже.
А пока вот вам небольшой пример:
Unmanaged class:
class A { int mField; public: A( int someArgument); int someMethod( int someArgument); };
Функции для интеграции:
A * A_createInstance(int someArgument) { return new A(someArgument); } int A_someMethod(A *instance, int someArgument) { return instance->someMethod( someArgument); } void A_deleteInstance(A *instance) { delete instance; }
Реализация в .Net:
public sealed class A { private IntPtr mInstance; private bool mDelete; private static extern IntPtr A_createInstance( int someArgument); private static extern int A_someMethod( IntPtr instance, int someArgument); private static extern void A_deleteInstance( IntPtr instance); internal A( IntPtr instance) { Debug.Assert(instance != IntPtr.Zero); mInstance = instance; mDelete = false; } public A( int someArgument) { mInstance = A_createInstance(someArgument); mDelete = true; } public int someMethod( int someArgument) { return A_someMethod(mInstance, someArgument); } internal IntPtr getUnmanaged() { return mInstance; } ~A() { if (mDelete) A_deleteInstance(mInstance); } }
Internal конструктор и метод нужны, чтобы получать экземпляры класса из unmanaged кода и передавать их обратно. Именно с передачей экземпляра класса обратно в unmanaged среду связана проблема наследования. Если класс A отнаследовать в .NET и переопределить ряд его методов (представим, что someMethod объявлен с ключевым словом virtual), мы не сможем обеспечить вызов переопределённого кода из unmanaged среды.

Интеграция интерфейса

Для интеграции интерфейсов нам потребуется обратная связь. Т.е. для полноценного использования интегрируемого модуля нам нужна возможность реализации его интерфейсов. Реализация связана с определением методов в managed среде. Эти методы нужно будет вызывать из unmanaged кода. Тут нам на помощь придут Callback Methods, описанные в документации к Platform Invoke.
На стороне unmanaged среды Callback представляется в виде указателя на функцию:
typedef void (*PFN_MYCALLBACK )(); int _MyFunction(PFN_MYCALLBACK callback);
А в .NET его роль будет играть делегат:
public delegate void MyCallback (); public static extern void MyFunction( MyCallback callback);
Имея инструмент для обратной связи мы легко сможем обеспечить вызов переопределённых методов.
Но чтобы передать экземпляр реализации интерфейса, в unmanaged среде нам его тоже придётся представить как экземпляр реализации. Так что придётся написать ещё одну реализацию в unmanaged среде. В этой реализации мы, кстати говоря, заложим вызовы Callback функций.
К сожалению, такой подход не позволит нам обойтись без логики в managed интерфейсах, так что нам придётся представить их в виде абстрактных классов. Давайте посмотрим на код:
Unmanaged interface:
class IB { public: virtual int method( int arg) = 0; virtual ~IB() {}; };
Unmanaged реализация
typedef int (*IB_method_ptr)(int arg); class UnmanagedB : public IB { IB_method_ptr mIB_method_ptr; public: void setMethodHandler( IB_method_ptr ptr); virtual int method( int arg); //… конструктор/деструктор }; void UnmanagedB ::setMethodHandler(IB_method_ptr ptr) { mIB_method_ptr = ptr; } int UnmanagedB ::method(int arg ) { return mIB_method_ptr( arg); }
Методы UnmanagedB просто вызывают коллбэки, которые ему выдает managed класс. Здесь нас поджидает еще одна неприятность. До тех пор, пока в unmanaged коде у кого-то есть указатель на UnmanagedB, мы не имеем права удалять экземпляр класса в managed коде, реагирующий на вызов коллбэков. Решению этой проблемы будет посвящена последняя часть статьи.
Функции для интеграции:
UnmanagedB *UnmanagedB_createInstance() { return new UnmanagedB(); } void UnmanagedB_setMethodHandler(UnmanagedB *instance, IB_method_ptr ptr) { instance->setMethodHandler( ptr); } void UnmanagedB_deleteInstance(UnmanagedB *instance) { delete instance; }
А вот и представление интерфейса в managed коде:
public abstract class AB { private IntPtr mInstance; private static extern IntPtr UnmanagedB_createInstance(); private static extern IntPtr UnmanagedB_setMethodHandler( IntPtr instance, MethodHandler ptr); private static extern void UnmanagedB_deleteInstance( IntPtr instance); private delegate int MethodHandler( int arg); private int impl_method( int arg) { return method(arg); } public abstract int method(int arg); public AB() { mInstance = UnmanagedB_createInstance(); UnmanagedB_setMethodHandler(mInstance, impl_method); } ~AB() { UnmanagedB_deleteInstance(mInstance); } internal virtual IntPtr getUnmanaged() { return mInstance; } }

Каждому методу интерфейса соответствует пара:

  1. Публичный абстрактный метод, который мы будем переопределять
  2. «Вызыватель» абстрактного метода (приватный метод с приставкой impl). Может показаться, что он не имеет смысла, но это не так. Этот метод может содержать дополнительные преобразования аргументов и результатов выполнения. Так же в нём может быть заложена дополнительная логика для передачи исключений (как вы уже догадались, просто передать исключение из среды в среду не получится, исключения тоже надо интегрировать)

Вот и всё. Теперь мы можем отнаследовать класс AB и переопределить его метод method. Если нам потребуется передать наследника в unmanaged код мы отдадим вместо него mInstance, который вызовет переопределённый метод через указатель на функцию/делегат. Если же мы получим указатель на интерфейс IB из unmanaged окружения, его потребуется представить в виде экземпляра AB в managed среде. Для этого мы реализуем наследника AB «по умолчанию»:
internal sealed class BImpl : AB { private static extern int BImpl_method( IntPtr instance, int arg); private IntPtr mInstance; internal BImpl( IntPtr instance) { Debug.Assert(instance != IntPtr.Zero); mInstance = instance; } public override int method(int arg) { return BImpl_method(mInstance, arg); } internal override IntPtr getUnmanaged() { return mInstance; } }
Функции для интеграции:
int BImpl_method(IB *instance , int arg ) { instance->method( arg); }
По большому счёту это та же интеграция класса без поддержки наследования, описанная выше. Не сложно заметить, что создавая экземпляр BImpl, мы также создаём экземпляр UnmanagedB и делаем не нужные привязки коллбэков. При желании этого можно избежать, но это уже тонкости, здесь мы их описывать не будем.

Интеграция классов с поддержкой наследования

Задача — интегрировать класс и предоставить возможность переопределения его методов. Указатель на класс мы будем отдавать в unmanaged, так что надо обеспечить класс коллбэками, чтобы иметь возможность вызвать переопределённые методы.
Рассмотрим класс C, имеющий реализацию в unmanaged коде:
class C { public: virtual int method(int arg); virtual ~C() {}; };
Для начала мы сделаем вид, что это интерфейс. Интегрируем его также, как это было сделано выше:
Unmanaged наследник для коллбэков:
typedef int (*С_method_ptr )(int arg); class UnmanagedC : public cpp::C { С_method_ptr mС_method_ptr; public: void setMethodHandler( С_method_ptr ptr); virtual int method( int arg); }; void UnmanagedC ::setMethodHandler(С_method_ptr ptr) { mС_method_ptr = ptr; } int UnmanagedC ::method(int arg ) { return mС_method_ptr( arg); }
Функции для интеграции:
//… опустим методы createInstance и deleteInstance void UnmanagedC_setMethodHandler(UnmanagedC *instance , С_method_ptr ptr ) { instance->setMethodHandler( ptr); }
И реализация в .Net:
public class C { private IntPtr mHandlerInstance; private static extern IntPtr UnmanagedC_setMethodHandler( IntPtr instance, MethodHandler ptr); private delegate int MethodHandler( int arg); //… также импортируем функции для создания/удаления экземпляра класса private int impl_method( int arg) { return method(arg); } public virtual int method(int arg) { throw new NotImplementedException(); } public C() { mHandlerInstance = UnmanagedC_createInstance(); UnmanagedC_setMethodHandler(mHandlerInstance, impl_method); } ~C() { UnmanagedC_deleteInstance(mHandlerInstance); } internal IntPtr getUnmanaged() { return mHandlerInstance; } }
Итак, мы можем переопределять метод C.method и он будет корректно вызван из unmanaged среды. Но мы не обеспечили вызов реализации по умолчанию. Здесь нам поможет код из первой части статьи:
Для вызова реализации по умолчанию нам потребуется её интегрировать. Также для её работы нам нужен соответствующий экземпляр класса, который придётся создавать и удалять. Получаем уже знакомый код:
//… опять же опускаем createInstance и deleteInstance int C_method(C *instance, int arg) { return instance->method( arg); }
Допилим .Net реализацию:
public class C { //… private static extern int C_method(IntPtr instance, int arg); public virtual int method(int arg) { return C_method(mInstance, arg); } public C() { mHandlerInstance = UnmanagedC_createInstance(); UnmanagedC_setMethodHandler(mHandlerInstance, impl_method); mInstance = C_createInstance(); } ~C() { UnmanagedC_deleteInstance(mHandlerInstance); C_deleteInstance(mInstance); } //… }

Такой класс можно смело применять в managed коде, наследовать, переопределять его методы, передавать указатель на него в unmanaged среду. Даже если мы не переопределяли никаких методов, мы всё равно передадим указатель на UnmanagedC. Это не очень рационально, учитывая, что unmanaged код будет вызывать методы unmanaged класса C транслируя вызовы через managed код. Но такова цена за возможность переопределения методов. В примере, прикреплённом к статье, этот случай продемонстрирован, с помощью вызова метода method у класса D. Если посмотреть на callstack, можно увидеть такую последовательность:

Исключения

Platform Invoke не позволяет передавать исключения и для обхода этой проблемы мы перехватываем все исключения перед переходом из среды в среду, обёртываем информацию об исключении в специальный класс и передаём. На той стороне генерируем исключение на основе полученной информации.
Нам повезло. Наш C++ модуль генерирует только исключения типа ModuleException или его наследников. Так что нам достаточно перехватывать это исключение во всех методах, в которых оно может быть сгенерировано. Чтобы пробросить объект исключения в managed среду нам потребуется интегрировать класс ModuleException. По идее исключение должно содержать текстовое сообщение, но я не хочу заморачиваться с темой маршалинга строк в этой статье, так что в примере будут «коды ошибок»:
public sealed class ModuleException : Exception { IntPtr mInstance; bool mDelete; //… пропущено create/delete instance private static extern int ModuleException_getCode( IntPtr instance); public int Code { get { return ModuleException_getCode(mInstance); } } public ModuleException( int code) { mInstance = ModuleException_createInstance(code); mDelete = true; } internal ModuleException( IntPtr instance) { Debug.Assert(instance != IntPtr.Zero); mInstance = instance; mDelete = false; } ~ModuleException() { if (mDelete) ModuleException_deleteInstance(mInstance); } //… пропущено getUnmanaged }
Теперь предположим, что метод C::method может генерировать исключение ModuleException. Перепишем класс с поддержкой исключений:

//Весь класс описывать не будем, ниже приведены только изменения typedef int (*С_method_ptr )(int arg, ModuleException **error); int UnmanagedC ::method(int arg ) { ModuleException *error = nullptr; int result = mС_method_ptr( arg, &error); if (error != nullptr) { int code = error->getCode(); //… управление удалением экземпляра error описано ниже и в сэмпле throw ModuleException(code); } return result; }
int C_method(C *instance, int arg, ModuleException ** error) { try { return instance->method( arg); } catch ( ModuleException& ex) { *error = new ModuleException(ex.getCode()); return 0; } }
public class C { //… private static extern int C_method(IntPtr instance, int arg, ref IntPtr error); private delegate int MethodHandler( int arg, ref IntPtr error); private int impl_method( int arg, ref IntPtr error) { try { return method(arg); } catch (ModuleException ex) { error = ex.getUnmanaged(); return 0; } } public virtual int method(int arg) { IntPtr error = IntPtr.Zero; int result = C_method(mInstance, arg, ref error); if (error != IntPtr.Zero) throw ModuleException(error); return result; } //… }
Здесь нас тоже ждут неприятности с управлением памятью. В методе impl_method мы передаем указатель на ошибку, но Garbage Collector может удалить её раньше, чем она будет обработана в unmanaged коде. Пора уже разобраться с этой проблемой!

Сборщик мусора против коллбэков

Тут надо сказать, что нам более-менее повезло. Все классы и интерфейсы интегрируемого модуля наследовались от некоего интерфейса IObject, содержащего методы addRef и release. Мы знали, что везде в модуле при передаче указателя производился вызов addRef. И всякий раз, когда потребность в указателе исчезала, производился вызов release. За счёт такого подхода мы легко могли отследить нужен ли указатель unmanaged модулю или колбеки уже можно удалить.
Чтобы избежать удаления managed объектов, используемых в unmanaged среде, нам потребуется менеджер этих объектов. Он будет считать вызовы addRef и release из unmanaged кода и освобождать managed объекты, когда они больше не будут нужны.
Вызовы addRef и release будут пробрасываться из unmanaged кода в managed, так что первое, что нам понадобится — это класс, который обеспечит такой проброс:
typedef long (*UnmanagedObjectManager_remove )(void * instance); typedef void (*UnmanagedObjectManager_add )(void * instance); class UnmanagedObjectManager { static UnmanagedObjectManager mInstance; UnmanagedObjectManager_remove mRemove; UnmanagedObjectManager_add mAdd; public: static void add( void *instance); static long remove( void *instance); static void setAdd( UnmanagedObjectManager_add ptr); static void setRemove( UnmanagedObjectManager_remove ptr); }; UnmanagedObjectManager UnmanagedObjectManager ::mInstance; void UnmanagedObjectManager ::add(void * instance ) { if (mInstance.mAdd == nullptr) return; mInstance.mAdd( instance); } long UnmanagedObjectManager ::remove(void * instance ) { if (mInstance.mRemove == nullptr) return 0; return mInstance.mRemove( instance); } void UnmanagedObjectManager ::setAdd(UnmanagedObjectManager_add ptr ) { mInstance.mAdd = ptr; } void UnmanagedObjectManager ::setRemove(UnmanagedObjectManager_remove ptr) { mInstance.mRemove = ptr; }
Второе, что мы должны сделать, это переопределить addRef и release интерфейса IObject так, чтобы они меняли значения счётчика нашего менеджера, хранящегося в managed коде:
template <typename T > class TObjectManagerObjectImpl : public T { mutable bool mManagedObjectReleased; public: TObjectManagerObjectImpl() : mManagedObjectReleased( false) { } virtual ~TObjectManagerObjectImpl() { UnmanagedObjectManager::remove(getInstance()); } void *getInstance() const { return ( void *) this; } virtual void addRef() const { UnmanagedObjectManager::add(getInstance()); } virtual bool release() const { long result = UnmanagedObjectManager::remove(getInstance()); if (result == 0) if (mManagedObjectReleased) delete this; return result == 0; } void resetManagedObject() const { mManagedObjectReleased = true; } };
Теперь классы UnmanagedB и UnmanagedC необходимо отнаследовать от класса TObjectManagerObjectImpl. Рассмотрим на примере UnmanagedC:
class UnmanagedC : public TObjectManagerObjectImpl <C> { С_method_ptr mС_method_ptr; public: UnmanagedC(); void setMethodHandler( С_method_ptr ptr); virtual int method( int arg); virtual ~UnmanagedC(); };
Класс C реализует интерфейс IObject, но теперь методы addRef и release переопределены классом TObjectManagerObjectImpl, так что подсчётом количества указателей будет заниматься менеджер объектов в managed среде.
Пора бы уже взглянуть на код самого менеджера:
internal static class ObjectManager { //… импортируем всё, что необходимо, см. сэмпл private static AddHandler mAddHandler; private static RemoveHandler mRemoveHandler; private class Holder { internal int count; internal Object ptr; } private static Dictionary< IntPtr, Holder> mObjectMap; private static long removeImpl( IntPtr instance) { return remove(instance); } private static void addImpl(IntPtr instance) { add(instance); } static ObjectManager() { mAddHandler = new AddHandler(addImpl); UnmanagedObjectManager_setAdd(mAddHandler); mRemoveHandler = new RemoveHandler(removeImpl); UnmanagedObjectManager_setRemove(mRemoveHandler); mObjectMap = new Dictionary<IntPtr , Holder >(); } internal static void add(IntPtr instance, Object ptr = null) { Holder holder; if (!mObjectMap.TryGetValue(instance, out holder)) { holder = new Holder(); holder.count = 1; holder.ptr = ptr; mObjectMap.Add(instance, holder); } else { if (holder.ptr == null && ptr != null) holder.ptr = ptr; holder.count++; } } internal static long remove(IntPtr instance) { long result = 0; Holder holder; if (mObjectMap.TryGetValue(instance, out holder)) { holder.count—; if (holder.count == 0) mObjectMap.Remove(instance); result = holder.count; } return result; } }
Теперь у нас есть менеджер объектов. Перед передачей экземпляра managed объекта в unmanaged среду, мы должны добавить его в менеджер. Так что метод getUnmanaged у классов AB и C необходимо изменить. Приведу код для класса C:
internal IntPtr getUnmanaged() { ObjectManager.add(mHandlerInstance, this); return mHandlerInstance; }
Теперь мы можем быть уверены, что коллбэки будут работать настолько долго, насколько это необходимо.

Учитывая специфику модуля, потребуется переписать классы, заменив все вызовы ClassName_deleteInstance на вызовы IObject::release, а также не забывать делать IObject::addRef там, где это потребуется. В частности, это позволит избежать преждевременного удаления ModuleException, даже если сборщик мусора удалит managed обёртку, unmanaged экземпляр, будучи наследником IObject, не будет удалён, пока unmanaged модуль не обработает ошибку и не вызовет для неё IObject_release.

На самом деле, пока мы занимались интеграцией модуля, мы испытали огромное количество эмоций, выучили немало нецензурных слов и научились спать стоя. Наверно мы должны хотеть, чтобы эта статья кому-нибудь пригодилась, но не дай бог. Конечно решать проблемы управления памятью, наследования и передачи исключений было весело. Но мы интегрировали далеко не три класса и было в них далеко не по одному методу. Это был тест на выносливость.
Если вы, всё же, столкнётесь с такой задачей, то вот вам совет: любите Sublime Text, регулярные выражения и сниппеты. Этот небольшой набор уберёг нас от алкоголизма.
P.S. Рабочий пример интеграции библиотеки доступен по ссылке github.com/simbirsoft-public/pinvoke_example

Delphi , Файловая система , DLL и PlugIns

Прежде чем начать использование какой-либо процедуры или функции, находящейся в динамической библиотеке,

вам необходимо загрузить DLL в оперативную память. Загрузка библиотеки может быть осуществлена

одним из двух способов: статическая загрузка и динамическая загрузка.

Оба метода имеют как преимущества, так и недостатки.

Статическая загрузка означает, что динамическая библиотека загружается автоматически

при запуске на выполнение использующего ее приложения. Для того чтобы использовать такой способ загрузки,

вам необходимо воспользоваться ключевым словом external при описании экспортируемой из

динамической библиотеки функции или процедуры. DLL автоматически загружается при старте программы,

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

как если бы они были описаны внутри модулей приложения.

Это наиболее легкий способ использования кода, помещенного в DLL .

Недостаток метода заключается в том, что если файл библиотеки, на который

имеется ссылка в приложении, отсутствует, программа откажется загружаться.

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

а в тот момент, когда вам это действительно необходимо. Сами посудите, ведь если функция, описанная

в динамической библиотеке, используется только при 10% запусков программы, то совершенно нет

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

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

загрузки DLL — это уменьшение (по понятным причинам) времени старта вашего приложения.

А какие же у этого способа имеются недостатки? Основной, как мне кажется, — это то, что использование

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

Сначала вам необходимо воспользоваться функцией Windows API LoadLibrary .

Для получения указателя на экспортируемой процедуры или функции должна

использоваться функция GetProcAddress. После завершения использования библиотеки DLL

должна быть выгружена с применением FreeLibrary.

Вызов процедур и функций, загруженных из DLL.

Способ вызова процедур и функций зависит от того, каким образом вы загрузили динамическую библиотеку,

в которой эти подпрограммы находятся.

Вызов функций и процедур из статически загруженных DLL достаточно прост. Первоначально в приложении

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

точно так же, как если бы они были описаны в одном из модулей вашего приложения.

Для импорта функции или процедуры, содержащейся в DLL , необходимо использовать

модификатор external в их объявлении. К примеру, для рассмотренной нами выше процедуры HelloWorld

в вызывающем приложении должна быть помещена следующая строка:

procedure SayHello(AForm : TForm); external myfirstdll.dll’;

Ключевое слово external сообщает компилятору, что данная процедура может быть найдена в

динамической библиотеке (в нашем случае — myfirstdll.dll).

Далее вызов этой процедуры выглядит следующим образом:


HelloWorld(self);

При импорте функции и процедур будьте особенно внимательны при написании их имен и интерфейсов!

Дело в том, что в процессе компиляции приложения не производится проверки на правильность имен объектов,

экспортируемых из DLL, осуществляться не будет, и если вы неправильно описали какую-нибудь функцию,

то исключение будет сгенерировано только на этапе выполнения приложения.

Импорт из DLL может проводиться по имени процедуры (функции), порядковому номеру или

с присвоением другого имени.

В первом случае вы просто объявляете имя процедуры и библиотеку, из которой ее импортируете

(мы это рассмотрели чуть выше). Импорт по порядковому номеру требует от вас указание этого самого номера:

procedure HelloWorld(AForm : TForm); external myfirstdll.dll index 15;

В этом случае имя, которое вы даете процедуре при импорте не обязательно должно совпадать с тем,

которое было указано для нее в самой DLL. Т.е. приведенная выше запись означает,

что вы импортируете из динамической библиотеки myfirstdll.dll процедуру, которая в ней экспортировалась

пятнадцатой, и при этом в рамках вашего приложения этой процедуре дается имя SayHello.

Если вы по каким-то причинам не применяете описанный выше способ импорта,

но тем не менее хотите изменить имя импортируемой функции (процедуры), то можно воспользоваться третьим методом:

procedure CoolProcedure; external myfirstdll.dll name ‘DoSomethingReallyCool’;

Здесь импортируемой процедуре CoolProcedure дается имя DoSomethingReallyCool.

Вызов процедур и функций, импортируемых из динамически загружаемых библиотек

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

указатель на функцию или процедуру, которую вы собираетесь использовать.

Помните процедуру HelloWorld? Давайте посмотрим, что необходимо сделать для того,

чтобы вызвать ее на выполнение в случае динамической загрузки DLL. Во-первых, вам

необходимо объявить тип, который описывал бы эту процедуру:

type
THelloWorld = procedure(AForm : TForm);

Теперь вы должны загрузить динамическую библиотеку, с помощью GetProcAddress получить

указатель на процедуру, вызвать эту процедуру на выполнение, и, наконец, выгрузить DLL из памяти.

Ниже приведен код, демонстрирующий, как это можно сделать:

Как уже говорилось выше, одним из недостатков статической загрузки DLL является невозможность

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

загрузкой у вас появляется возможность программно обрабатывать такие ситуации и не допускать, чтобы программа

вываливалась» самостоятельно. По возвращаемому функциями LoadLibrary и GetProcAddress значениям можно

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

Приведенный ниже код демонстрирует это.

В DLL можно хранить не только код, но и формы.

Причем создание и помещение форм в динамическую библиотеку не слишком сильно отличается от работы

с формами в обычном проекте. Сначала мы рассмотрим, каким образом можно написать библиотеку,

содержащую формы, а затем мы поговорим об использовании технологии MDI в DLL.

Разработку DLL, содержащую форму, я продемонстрирую на примере.

Итак, во-первых, создадим новый проект динамической библиотеки.

Для этого выберем пункт меню File|New, а затем дважды щелкнем на иконку DLL .

После этого вы увидите примерно следующий код:

library Project2;
{здесь были комментарии}
uses
SysUtils,
Classes;
{$R *.RES}
begin
end.

Сохраните полученный проект. Назовем его DllForms.dpr.

Теперь следует создать новую форму. Это можно сделать по-разному.

Например, выбрав пункт меню File|New Form. Добавьте на форму какие-нибудь компоненты.

Назовем форму DllForm и сохраним получившийся модуль под именем DllFormUnit.pas .

Вернемся к главному модулю проекта и поместим в него функцию ShowForm, в задачу которой будет входить

создание формы и ее вывод на экран. Используйте для этого приведенный ниже код.

Обращаю внимание, что для того, чтобы проект был скомпилирован без ошибок, необходимо добавить в секцию uses модуль Forms .

Экспортируем нашу функцию с использованием ключевого слова exports :

exports
ShowForm;

Компилируем проект и получаем файл dllforms.dll. Эти простые шаги — все,

что необходимо сделать для сОбратите внимание, что функция ShowForm объявлена с использованием ключевого слова stdcall .

Оно сигнализирует компилятору использовать при экспорте функции соглашение

по стандартному вызову (standard call calling convention). Экспорт функции таким образом создает

возможность использования разработанной DLL не только в приложениях, созданных в Delphi.

Соглашение по вызову (Calling conventions) определяет, каким образом передаются аргументы при вызове функции.

Существует пять основных соглашений: stdcall, cdecl, pascal, register и safecall.

Подробнее об этом можно узнать, посмотрев раздел » Calling Conventions » в файле помощи Delphi.

Также обратите внимание, что значение, возвращаемое функцией ShowForm ,

соответствует значению ShowModal. Таким образом вы можете передавать некоторую информацию

о состоянии формы вызывающему приложению.

Ниже представлено два листинга, первый из которых содержит полный код файла

проекта DLL (модуль с формой здесь не приводится), а второй — модуль вызывающего приложения,

в котором используется только что разработанная нами библиотека.

Прошу заметить, что при экспорте функции также было использовано ключевое слово stdcall.

Следует обратить особое внимание на работу с дочерними формами в DLL. Если, к примеру,

в вызывающем приложении главная форма имеет значение свойства FormStyle, равным MDIForm,

то при попытке вызова из DLL MDIChild-формы, на экране появится сообщение об ошибке,

в котором будет говориться, что нет ни одной активной MDI-формы.

В тот момент, когда вы пытаетесь показать ваше дочернее окно, VCL проверяет корректность

свойства FormStyle главной формы приложения. Однако в нашем случае все вроде бы верно.

Так в чем же дело? Проблема в том, что при проведении такой проверки, рассматривается объект Application,

принадлежащий не вызывающему приложению, а собственно динамической библиотеке.

Ну, и естественно, поскольку в DLL нет главной формы, проверка выдает ошибку.

Для того чтобы избежать такой ситуации, надо назначить объекту Application динамической библиотеки

объект Application вызывающего приложения. Естественно, это заработает только в том случае,

когда вызывающая программа — VCL-приложение. Кроме того, перед выгрузкой библиотеки из памяти

необходимо вернуть значение объекта Application библиотеки в первоначальное состояние.

Это позволит менеджеру памяти очистить оперативную память, занимаемую библиотекой.

Следовательно, вам нужно сохранить указатель на «родной» для библиотеки объект Application

в глобальной переменной, которая может быть использована при восстановлении его значения.

Итак, вернемся немного назад и перечислим шаги, необходимые нам для работы с помещенным

в DLL MDIChild-формами.

В динамической библиотеке создаем глобальную переменную типа TApplication.

Сохраняем указатель на объект Application DLL в глобальной переменной.

Объекту Application динамической библиотеки ставим в соответствие указатель на Application

вызывающего приложения.

Создаем MDIChild-форму и работаем с ней.

Возвращаем в первоначальное состояние значение объекта Application динамической библиотеки

и выгружаем DLL из памяти.

Первый шаг прост. Просто помещаем следующий код в верхней части модуля DLL:

var
DllApp : TApplication;

Затем создаем процедуру, которая будет изменять значение объекта Application и создавать дочернюю форму.

Процедура может выглядеть примерно так:

Все, что нам теперь необходимо сделать, — это предусмотреть возвращение значения объекта Application

в исходное состояние. Делаем это с помощью процедуры MyDllProc:

procedure MyDLLProc(Reason: Integer);
begin
if Reason = DLL_PROCESS_DETACH then
{ DLL is выгружается. Восстанавливаем значение указателя Application}
if Assigned(DllApp) then
Application := DllApp;
end;

Вместо заключения.

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

DLL предоставляют широчайшие возможности для оптимизации работы приложений,

а также работы самих программистов. Используйте DLL и, возможно, ваша жизнь станет легче!

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

DLL предоставляют широчайшие возможности для оптимизации работы приложений,

а также работы самих программистов. Используйте DLL и, возможно, ваша жизнь станет легче!

Использование dll-библиотеки

Прежде чем вызывать подпрограммы, включённые в dll-библиотеку, их надо импортировать в приложение. Это можно сделать двумя способами – путём объявления внешней процедуры или функции или динамически с помощью Win32 API функций. Но в любом случае подпрограммы из dll-библиотеки будут импортированы только во время работы приложения.

3.1. Поиск dll-библиотек

Чтобы загрузить dll-библиотеку, операционная система должна найти её. Поиск осуществляется в следующих местах:

  • директория, из которой загружено приложение, требующее dll-библиотеку;
  • текущая директория;
  • системная директория (обычно C:\Windows\System32);
  • системная директория для 16-битных приложений (обычно C:\Windows\System);
  • Windows-директория;
  • директории, указанные в переменной окружения PATH.

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

3.2. Статическое подключение

Самый простой способ импортировать процедуру или функцию – тем или иным способом объявить её как внешнюю. Необходимо также указать имя подключаемой dll-библиотеки.

3.2.1. Статическое подключение dll-библиотеки в программе на языке С++

Для статического подключения dll-библиотеки в программе на языке С++ необходимо скопировать файл .lib в директорию с исходным кодом приложения и указать имя этого файла в настройках компоновщика – Project → Properties → Configuration Properties → Linker → Input → Additional Dependencies. Кроме того, поскольку функции должны иметь прототипы, потребуется заголовочный файл с прототипами функций, который необходимо вставить с помощью директивы препроцессора #include в файлы с исходным кодом, в которых предполагается использование функций dll-библиотеки. Заголовочный файл поставляется разработчиком библиотеки. Функции должны быть объявлены со спецификацией extern «C» __declspec(dllimport), которая указывает, что функция будет импортирована из внешнего модуля.

3.2.2. Статическое подключение dll-библиотеки в программе на языке Паскаль

Если включить в код приложения подобное объявления, библиотека, имя которой задано после ключевого слова external, будет загружаться при запуске приложения. Имя импортируемой процедуры или функции будет означать одну и ту же подпрограмму из одной и той библиотеки всё время выполнения приложения.

3.2.3. Статическое подключение dll-библиотеки в программе на языке Ассемблер

Для статического подключения dll-библиотеки в программе на языке Ассемблер потребуется файл .lib, а также файл с расширением inc, который аналогичен заголовочному файлу языка С++. В исходный код программы нужно включить файл .inc с помощью директивы include и файл .lib с помощью директивы includelib. После этого можно вызывать процедуры dll-библиотеки.

; Использование dll-библиотеки. Пример библиотеки см. в разделе 2.4.2 ; Файл ExampleDll.inc ; Файл содержит прототип процедуры, экспортируемой из библиотеки, что указывает ключевое слово PROTO. ; Ключевое слово CDECL указывает используемое соглашение о вызовах. Далее описываются параметры, которые необходимо передать в процедуру. ; В связи со спецификой передачи параметров в программах на языке Ассемблер это описание является необязательным. ExampleProc PROTO CDECL :DWORD, :DWORD ; Файл DllTest.asm .686 .model flat, c option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include ExampleDll.inc includelib \masm32\lib\kernel32.lib includelib ExampleDll.lib .data source db ‘I am a testing string’,0 dest db 25 dup(0) .code program: push offset dest push offset source call ExampleProc add esp, 8 push 0 call ExitProcess end program

3.3. Динамическое подключение

Доступ к подпрограммам dll-библиотеки можно получить с помощью Win32 API функций LoadLibrary, FreeLibrary и GetProcAddress. При импорте подпрограмм данным способом, библиотека загружается только при вызове функции LoadLibrary и выгружается при вызове функции FreeLibrary. Это позволяет уменьшать требуемое количество памяти и запускать приложение, даже если какие-то библиотеки отсутствуют на компьютере.

Функция LoadLibrary ищет и загружает dll-библиотеку по переданному ей имени. Поиск осуществляется в тех же директориях, что и при статическом подключении (см. раздел 3.1). Функция возвращает идентификатор библиотеки, если библиотека была найдена, и значение 0 в противном случае.

Функция GetProcAddress осуществляет поиск нужной процедуры в dll-библиотеке. Функция получает идентификатор библиотеки и имя или номер процедуры и возвращает указатель на процедуру, если она была найдена, и 0 в противном случае.

Функция FreeLibrary освобождает dll-библиотеку. Однако библиотека может не выгружаться из памяти, если она используется другими приложениями.

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

Динамическое подключение dll-библиотек более трудоёмко, чем статическое, т.к. нужно самостоятельно вызывать все необходимые функции и проверять возвращаемые ими значения. Кроме того, при динамическом подключении генерируется чуть более объёмный код. Но динамическое подключение является более гибким механизмом. При статическом подключении в случае отсутствия необходимой dll-библиотеки приложение просто не запустится. При динамическом подключении в случае отсутствия dll-библиотеки программист может предпринять какие-либо действия, например, попытаться загрузить другую библиотеку или позволить пользователю работать с теми функциями приложения, которые определены не в отсутствующей библиотеке.

3.3.1. Динамическое подключение dll-библиотеки в программе на языке С++

Для использования функций динамического подключения dll-библиотек и соответствующих типов нужно включить заголовочный файл windows.h.

3.3.2. Динамическое подключение dll-библиотеки в программе на языке Паскаль

Функции для динамического подключения dll-библиотек определены в модуле Windows.

3.3.3. Динамическое подключение dll-библиотеки в программе на языке Ассемблер

Для динамического подключения используются те же функции с теми же параметрами. Вызов функций осуществляется с учётом специфики языка Ассемблер. Для вызова функций Win32 API необходимо включить файл kernel32.inc и подключить библиотеку импорта kernel32.lib.

; Использование dll-библиотеки. Пример библиотеки см. в разделе 2.4.2 .686 .model flat, c option casemap:none include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .data source db ‘I am a testing string’,0 dest db 25 dup(0) .data? hLib dd ? fAddr dd ? .const libName db ‘ExampleDll.dll’,0 fName db ‘ExampleProc’,0 lnf db ‘Library »ExampleDll.dll» not found’,13,10 fnf db ‘Function »ExampleProc» not found’,13,10 .code program: ; Получаем идентификатор вывода push -11 call GetStdHandle mov hStdOut, eax ; Получаем идентификатор библиотеки push offset libName call LoadLibrary ; Проверяем полученный идентификатор test eax, eax jz L0 mov hLib, eax ; Получаем указатель на функцию push offset fName push hLib call GetProcAddress ; Проверяем полученный указатель test eax, eax jz L1 ; Вызываем функцию push offset dest push offset source mov fAddr, eax call add esp, 8 ; Освобождаем библиотеку push hLib call FreeLibrary push 0 call ExitProcess L0: push 0 push 0 push 36d push offset lnf push hStdOut call WriteConsoleA ; Задержка push 1000h call Sleep push 0 call ExitProcess L1: push 0 push 0 push 34d push offset fnf push hStdOut call WriteConsoleA ; Задержка push 1000h call Sleep ; Освобождаем библиотеку push hLib call FreeLibrary push 0 call ExitProcess end program