Динамическая загрузка DLL во время выполнения


Введение

Иногда возникает потребность вызова процедуры, находящейся в DLL. Обычно это не является проблемой. Вы прототипируете вызов процедуры в MAP вашего приложения, компилируете и линкуете его, и затем запускаете приложение. Однако, важно знать что происходит во время линковки вашего приложения, и как решается проблема, когда DLL нет на месте при запуске вашей программы, или чем отличаются вызовы процедуры в зависимости от операционной системы.

Два типа .LIB?

1) Библиотека импорта (Import library)

Это файл .LIB, который не содержит выполнимого кода, но используется линкером для разрешения всех внешних вызываемых процедур. Если вы установили ваше 32-разрядное приложение использовать Clarion runtime DLLs, то линкер будет использовать C5RUNX.LIB (находящийся в каталоге C5EE/LIB ) для разрешения всех вызовов Clarion-функций периода выполнения, таких как Message, Accept, Open и т.д.

2) Статическая библиотека (Static library)

Это файл .LIB, который содержит выполнимый код. Редактор связей связывает .LIB файл в ваше приложение так, что вам не нужно поставлять DLL с вашим приложением. Для 32-разрядного статически-слинкованного приложения, компоновщик будет линковать C5RUNXL.LIB. Это не такой популярный способ сейчас, когда мы имеем Windows и DLL-и стали стандартным способом поставки функциональности, но во времена DOS это был единственный путь для третьих поставщиков предоставить требуемые функции, несмотря на то что они купили архитектуру DLL для Clarion DOS.

Когда вы работаете с большими приложениями, которые разделены на один EXE-файл + много DLL, то вы имеете дело с первым типом .LIB файлов. Ваше приложение линкуется с DLL: и если DLL отсутствует или он поврежден, то приложение не запустится. Это, обычно, не является проблемой, несмотря на то что вы хотите предоставить некоторые функции, которые зависят от чего-либо еще, установленного на машине. Если нужного DLL нет, то вместо того чтобы не предоставлять функций программа просто не запустится.

Почему мы беспокоимся?

Честно говоря, обычно мы не беспокоимся. Только если вы генерируете прямой код Clarion, то есть шансы, что вам никогда не нужно заботиться об этом. Библиотека периода выполнения Clarion будет все делать за вас и держать все эти вещи за сценой. Если, однако, вам нужно использовать вызовы Windows API для реализации чего-либо, то вам нужно очень хорошо заботиться об этом.  Microsoft, несмотря на свою мудрость, сделала вызовы Windows API несовместимыми между Windows NT и Windows 9x (не говоря уж о Windows 2000…)

Вопросы многоплатформености

Иногда вы способны написать две программы, по одной для каждой операционной системы, чтобы решить эту проблему. Вы "оборачиваете" ее всю внутрь прекрасного инсталл-визарда, и он заботится об инсталляции правильной программы в зависимости от операционной системы. Однако, вы не всегда можете сделать так, и поэтому вам нужно написать одну программу, которая работает везде и учитывает эти вопросы.

Хорошим примером этого является проблема, с которой я недавно столкнулся. Я хотел написать программу перечисления запущенных процессов.  API Win9x использует TOOLHELP.DLL, который не доступен под Windows NT. Windows NT, в свою очередь, использует библиотеку поддержки состояний процессов PSAPI.DLL, которой нет в системах Windows 9x. Написание кода перечисления процессов было легким, но написание программы для работы под обоими операционными системами было немного сложнее.

Использование LoadLibrary / GetProcAddress

К счастью для нас, имеется способ решения этой проблемы: это использование дополнительных Windows API.  Вы не прототипируете процедуру в MAP как обычно, что означает что компоновщику не нужен .LIB файл для разрешения имени процедуры. Вместо этого, вы динамически загружаете DLL и вызываете нужную процедуру во время выполнения.

Сначала нужно использовать вызов API  LoadLibrary для динамической загрузки нужной DLL в адресное пространство вашего приложения, затем при помощи вызова API  GetProcAddress получаете адреса нужной вам процедуры.

Вместо того чтобы делать это:

Map
 Module('FOO.DLL')
  BarProcedure(),Byte
 End
End
Code
 X# = BarProcedure()

Вы должны сделать это:

Map
 Module('Windows API')
  LoadLibrary(*Cstring),Ulong,Raw,Pascal
  GetProcAddress(Ulong,*Cstring),Ulong,Raw,Pascal
 End
End
Code
 HandleToDll = LoadLibrary('FOO.DLL')
 If HandleToDll = 0 
  ! Unable to load DLL for whatever reason
 Else 
  AddressOfProcedure = GetProcAddress( HandleToDll, 'BarProcedure')
  If AddressOfProcedure = 0
   ! Unable to find the procedure in the DLL
  End
 End

Однако, этот код еще не делает вызова нашей процедуры. Пока мы только имеем адрес в памяти процедуры BarProcedure. Мы еще не вызвали ее. К сожалению, у нас появилась другая проблема - в языке, который является вызовом функциональности, предоставляемой указателями, таким как Clarion, как можно сделать вызов по адресу в памяти?

To C or not to C?

Имеется 2 решения этой проблемы - одно ведет к написанию кода на C, а другое нет. В прошлом, для решения этой проблемы, я всегда прибегал к написанию кода на C, потому что C не порождает таких проблем как Clarion. Вы можете успешно написать код C для вызова по адресу - любому адресу - без проблем. (Хорошая ли это идея или нет - это будет ясно позже. Много позже). И только недавно, благодаря Bernard Grosperrin я нашел способ сделать это все на Clarion. Bernie твердо обещает, что он напишет статью для Clarion Magazine, но пока он этого не сделал, вы должны понять мою грубую попытку объяснить эту технику.

Решение на C

Создайте новый исходный модуль с любым названием - fred.c кажется всегда у меня работает (вы заметили как легко набирать слово "fred"?). Новый исходний модуль должен содержать что то подобное следующему - я говорю "подобное" потому что мой код C не самый великий в мире, и я уверен, что кто-нибудь найдет в нем что либо плохое. Тем не менее, он достаточен чтобы понять идею.

long CallAddr(long procaddress) 
{ 
 DllFunc fpFuncAddress; 
 fpFuncAddress = procaddress; 
 if (fpFuncAddress != 0) 
 { 
  (*fpFuncAddress)(); 
  return 0; 
 } 
 return 1; 
} 

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

Map
 Module('fred.c')
  CallAddr(LONG),LONG,RAW,PROC,Name('_CallAddr')
 End
End
Code
 ! Insert code to load the DLL using LoadLibrary / GetProcAddress here
 If AddressOfProcedure
  X# = CallAddr(AddressOfProcedure)
 End

Решение на Clarion

Решение Bernie затрагивает компилятор и компоновщик, и работает только если вы вызываете функцию - вы должны возвратить что-либо чтобы это работало.

Вы также прототипируете процедуру с именем CallAddr в MAP, но вместо реализации ее как внешней C процедуры, вы делаете ее процедурой на Clarion во внешнем модуле. Должен признаться, что я не очень уверен почему он должен быть внешним (external) модулем - достаточно сказать, если это не так, то это не кажется работающим. Единственная разница между кодом на C и кодом на Clarion - это прототип CallAddr в MAP - остальной код остается неизменным.

Map
 Module('LoadPipe')
  CallAddr(LONG),BYTE,PROC,Name('CallAddr')    
 End
End
Code
 ! Insert code to load the DLL using LoadLibrary / GetProcAddress here
 If AddressOfProcedure <> 0
  X# = CallAddr(AddressOfProcedure )
 End

Волшебство заключается в внешнем модуле Clarion (LOADPIPE.CLW). Как вы можете видить, вы описываете внешнюю вызываемую процедуру (BarProcedure) точно также как если бы вы вызывали ее напрямую, но с указанием атрибута TYPE. Затем вы прототипируете процедуру CallAddr снова, на этот раз принимающую *BarProcedure как параметр.

Member()
  ! Этот файл создан для преодоления
  ! неспособности Clarion-а вызывать процедуру по адресу.
  ! Каждый "настоящий" внешний прототип должен быть объявлен как TYPE, 
  ! затем Callprocedure принимает type как первый параметр
  MAP
    BarProcedure(),BYTE,TYPE
    CallAddr(*BarProcedure),BYTE,PROC,NAME('CallAddr')
  END

CallAddr Function(Ptr)
  CODE
  RETURN Ptr()

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

Загрузить

LOADER.ZIP (8K)


Вернуться к моей домашней странице, к странице Clarion, или послать мне письмо paula@attglobal.net

Последнее изменение 22Dec99