Динамическая загрузка — это механизм, с помощью которого компьютерная программа может во время выполнения загружать библиотеку (или другой двоичный файл ) в память, извлекать адреса функций и переменных, содержащихся в библиотеке, выполнять эти функции или получать доступ к этим переменным и выгружать библиотеку из памяти. Это один из трех механизмов, с помощью которых компьютерная программа может использовать некоторое другое программное обеспечение в программе; другие — статическое связывание и динамическое связывание . В отличие от статического связывания и динамического связывания, динамическое связывание позволяет компьютерной программе запускаться при отсутствии этих библиотек, обнаруживать доступные библиотеки и потенциально получать дополнительную функциональность. [1] [2]
Динамическая загрузка была распространенной техникой для операционных систем IBM для System/360 , таких как OS/360 , в частности для подпрограмм ввода -вывода , и для библиотек времени выполнения COBOL и PL/I , и продолжает использоваться в операционных системах IBM для z/Architecture , таких как z/OS . Что касается прикладного программиста, загрузка в значительной степени прозрачна, поскольку она в основном обрабатывается операционной системой (или ее подсистемой ввода-вывода). Основные преимущества:
Стратегическая система обработки транзакций IBM , CICS (1970-е годы и далее) широко использует динамическую загрузку как для своего ядра , так и для обычной загрузки прикладных программ . Исправления в прикладных программах можно было вносить в автономном режиме, а новые копии измененных программ загружались динамически без необходимости перезапуска CICS [3] [4] (которая может работать круглосуточно и часто работает ).
Общие библиотеки были добавлены в Unix в 1980-х годах, но изначально не позволяли программе загружать дополнительные библиотеки после запуска. [5]
Динамическая загрузка чаще всего используется при реализации программных плагинов . [1] Например, файлы плагинов «динамических общих объектов» веб-сервера Apache представляют собой библиотеки , которые загружаются во время выполнения с помощью динамической загрузки. [6] Динамическая загрузка также используется при реализации компьютерных программ , где несколько различных библиотек могут предоставлять требуемую функциональность и где пользователь имеет возможность выбрать, какую библиотеку или библиотеки предоставить.*.dso
Не все системы поддерживают динамическую загрузку. Unix-подобные операционные системы, такие как macOS , Linux и Solaris, обеспечивают динамическую загрузку с помощью библиотеки "dl" языка программирования C. Операционная система Windows обеспечивает динамическую загрузку через Windows API .
Загрузка библиотеки осуществляется с помощью LoadLibrary
или LoadLibraryEx
на Windows и на dlopen
Unix -подобных операционных системах . Примеры приведены ниже:
void * sdl_library = dlopen ( "libSDL.so" , RTLD_LAZY ); if ( sdl_library == NULL ) { // сообщить об ошибке ... } else { // использовать результат в вызове dlsym }
Как библиотека Unix :
void * sdl_library = dlopen ( "libSDL.dylib" , RTLD_LAZY ); if ( sdl_library == NULL ) { // сообщить об ошибке ... } else { // использовать результат в вызове dlsym }
В качестве фреймворка macOS :
void * sdl_library = dlopen ( "/Library/Frameworks/SDL.framework/SDL" , RTLD_LAZY ); if ( sdl_library == NULL ) { // сообщить об ошибке ... } else { // использовать результат в вызове dlsym }
Или если фреймворк или пакет содержит код Objective-C:
NSBundle * bundle = [ NSBundle bundleWithPath : @"/Library/Plugins/Plugin.bundle" ]; NSError * err = nil ; if ([ bundle loadAndReturnError :& err ]) { // Использовать классы и функции в пакете. } else { // Обработать ошибку. }
HMODULE sdl_library = LoadLibrary ( TEXT ( "SDL.dll" )); if ( sdl_library == NULL ) { // сообщить об ошибке ... } else { // использовать результат в вызове GetProcAddress }
Извлечение содержимого динамически загружаемой библиотеки осуществляется GetProcAddress
в операционных системах Windows и Unix .dlsym
void * initializer = dlsym ( sdl_library , "SDL_Init" ); if ( initializer == NULL ) { // сообщить об ошибке ... } else { // привести инициализатор к правильному типу и использовать }
В macOS при использовании пакетов Objective-C также можно:
Class rootClass = [ bundle principalClass ]; // В качестве альтернативы можно использовать NSClassFromString() для получения класса по имени. if ( rootClass ) { id object = [[ rootClass alloc ] init ]; // Использовать объект. } else { // Сообщить об ошибке. }
FARPROC initializer = GetProcAddress ( sdl_library , "SDL_Init" ); if ( initializer == NULL ) { // сообщить об ошибке ... } else { // привести инициализатор к правильному типу и использовать }
Результат dlsym()
или GetProcAddress()
должен быть преобразован в указатель соответствующего типа, прежде чем его можно будет использовать.
В Windows преобразование выполняется просто, поскольку FARPROC по сути уже является указателем на функцию :
typedef INT_PTR ( * FARPROC )( void );
Это может быть проблематично, когда необходимо извлечь адрес объекта, а не функцию. Однако обычно в любом случае требуется извлечь функции, поэтому это обычно не является проблемой.
typedef void ( * sdl_init_function_type ) ( void ); sdl_init_function_type init_func = ( sdl_init_function_type ) инициализатор ;
Согласно спецификации POSIX, результатом dlsym()
является void
указатель. Однако указатель на функцию не обязан иметь тот же размер, что и указатель на объект данных, и поэтому допустимое преобразование между типом void*
и указателем на функцию может оказаться непростым для реализации на всех платформах.
В большинстве современных систем указатели на функции и объекты де-факто конвертируемы. Следующий фрагмент кода демонстрирует один обходной путь, который позволяет выполнять преобразование в любом случае на многих системах:
typedef void ( * sdl_init_function_type ) ( void ); sdl_init_function_type init_func = ( sdl_init_function_type ) инициализатор ;
Приведенный выше фрагмент выдаст предупреждение на некоторых компиляторах: warning: dereferencing type-punned pointer will break strict-aliasing rules
. Другой способ решения проблемы:
typedef void ( * sdl_init_function_type ) ( void ); union { sdl_init_function_type func ; void * obj ; } alias ; alias . obj = инициализатор ; sdl_init_function_type init_func = alias . func ;
что отключает предупреждение, даже если действует строгое алиасинг. Это использует тот факт, что чтение из другого члена объединения, чем тот, в который была сделана последняя запись (называемый « каламбуром типа »), является обычным явлением и явно разрешено, даже если действует строгое алиасинг, при условии, что доступ к памяти осуществляется напрямую через тип объединения. [7] Однако в данном случае это не строго так, поскольку указатель функции копируется для использования вне объединения. Обратите внимание, что этот трюк может не работать на платформах, где размер указателей данных и размер указателей функций не совпадают.
Фактом остается то, что любое преобразование между указателями функций и объектов данных следует рассматривать как (по сути непереносимое) расширение реализации, и что не существует «правильного» способа прямого преобразования, поскольку в этом отношении стандарты POSIX и ISO противоречат друг другу.
Из-за этой проблемы в документации POSIX dlsym()
для устаревшего выпуска 6 указано, что «будущая версия может либо добавить новую функцию для возврата указателей на функции, либо текущий интерфейс может быть объявлен устаревшим в пользу двух новых функций: одной, которая возвращает указатели на данные, и другой, которая возвращает указатели на функции». [8]
Для последующей версии стандарта (выпуск 7, 2008 г.) эта проблема обсуждалась, и был сделан вывод, что указатели функций должны быть преобразуемыми для void*
соответствия POSIX. [8] Это требует от создателей компиляторов реализации рабочего приведения для этого случая.
Если содержимое библиотеки может быть изменено (т. е. в случае пользовательской библиотеки), в дополнение к самой функции можно экспортировать указатель на нее. Поскольку указатель на указатель функции сам по себе является указателем на объект, этот указатель всегда можно законно получить вызовом dlsym()
и последующим преобразованием. Однако этот подход требует поддержания отдельных указателей на все функции, которые должны использоваться извне, а выгоды обычно невелики.
Загрузка библиотеки приводит к выделению памяти; библиотека должна быть освобождена, чтобы избежать утечки памяти . Кроме того, сбой при выгрузке библиотеки может помешать операциям файловой системы с файлом , содержащим библиотеку. Выгрузка библиотеки выполняется с помощью FreeLibrary
в Windows и dlclose
в Unix-подобных операционных системах . Однако выгрузка DLL может привести к сбоям программы, если объекты в основном приложении ссылаются на память, выделенную в DLL. Например, если DLL вводит новый класс и DLL закрывается, дальнейшие операции с экземплярами этого класса из основного приложения, скорее всего, вызовут нарушение доступа к памяти. Аналогично, если DLL вводит фабричную функцию для создания экземпляров динамически загружаемых классов, вызов или разыменование этой функции после закрытия DLL приводит к неопределенному поведению.
dlclose ( sdl_library );
FreeLibrary ( sdl_library );
Реализации динамической загрузки в операционных системах типа Unix и Windows позволяют программистам извлекать символы из текущего выполняемого процесса.
Unix-подобные операционные системы позволяют программистам получать доступ к глобальной таблице символов, которая включает как основной исполняемый файл, так и впоследствии загружаемые динамические библиотеки.
Windows позволяет программистам получать доступ к символам, экспортируемым основным исполняемым файлом. Windows не использует глобальную таблицу символов и не имеет API для поиска по нескольким модулям, чтобы найти символ по имени.
void * this_process = dlopen ( NULL , 0 );
HMODULE this_process = GetModuleHandle ( NULL ); HMODULE этот_процесс_снова ; GetModuleHandleEx ( 0 , 0 , & этот_процесс_снова );
В языке программирования Java классы могут быть динамически загружены с помощью ClassLoader
объекта. Например:
Тип класса = ClassLoader.getSystemClassLoader ( ) . loadClass ( name ) ; Объект obj = type.newInstance ( ) ;
Механизм Reflection также предоставляет средства для загрузки класса, если он еще не загружен. Он использует загрузчик классов текущего класса:
Тип класса = Class.forName ( name ) ; Объект obj = type.newInstance ( ) ;
Однако не существует простого способа выгрузить класс контролируемым образом. Загруженные классы могут быть выгружены только контролируемым образом, т. е. когда программист хочет, чтобы это произошло, если загрузчик классов, используемый для загрузки класса, не является системным загрузчиком классов и сам выгружен. При этом необходимо соблюдать различные детали, чтобы убедиться, что класс действительно выгружен. Это делает выгрузку классов утомительной.
Неявная выгрузка классов, т. е. неконтролируемая сборщиком мусора, несколько раз менялась в Java. До Java 1.2 сборщик мусора мог выгружать класс всякий раз, когда считал, что ему нужно место, независимо от того, какой загрузчик классов использовался для загрузки класса. Начиная с Java 1.2 классы, загруженные через системный загрузчик классов, никогда не выгружались, а классы, загруженные через другие загрузчики классов, только когда этот другой загрузчик классов был выгружен. Начиная с Java 6 классы могут содержать внутренний маркер, указывающий сборщику мусора, что они могут быть выгружены, если сборщик мусора захочет это сделать, независимо от загрузчика классов, использованного для загрузки класса. Сборщик мусора может игнорировать эту подсказку.
Аналогично библиотеки, реализующие собственные методы, динамически загружаются с помощью System.loadLibrary
метода. Метод отсутствует System.unloadLibrary
.
Несмотря на его распространение в 1980-х годах через Unix и Windows, некоторые системы все еще решили не добавлять — или даже удалять — динамическую загрузку. Например, Plan 9 от Bell Labs и его преемник 9front намеренно избегают динамического связывания, поскольку считают его «вредным». [9] Язык программирования Go , созданный некоторыми из тех же разработчиков, что и Plan 9, также не поддерживал динамическое связывание, но загрузка плагинов доступна с Go 1.8 (февраль 2017 г.). Среда выполнения Go и любые библиотечные функции статически связаны в скомпилированном двоичном файле. [10]