На некоторых платформах GHC поддерживает компиляцию кода Haskell в общие библиотеки. Общие библиотеки иногда также называют динамическими библиотеками; в частности, в Windows их называют библиотеками динамической компоновки (DLL).
Общие библиотеки позволяют нескольким программам использовать один экземпляр предварительно скомпилированного кода. В отличие от этого, при статической компоновке код копируется в каждую программу. Таким образом, использование общих библиотек может сэкономить место на диске. Кроме того, они позволяют нескольким программам совместно использовать одну копию кода в памяти. Общие библиотеки часто применяются для структурирования крупных проектов, особенно если разные их части написаны на разных языках программирования. Кроме того, различные приложения часто используют общие библиотеки в качестве механизма подключаемых модулей. Особенно распространён этот подход в Windows при использовании COM.
Начиная с версии 6.12 GHC поддерживает сборку общих библиотек для Linux (на архитектурах x86 и x86-64). В версии 7.0 добавлена поддержка Windows (см. Сборка и использование DLL Win32), FreeBSD и OpenBSD (x86 и x86-64), Solaris (x86) и Mac OS X (x86 и PowerPC).
Сборка и использование общих библиотек немного сложнее, чем сборка и использование статических библиотек. При использовании Cabal большая часть деталей скрыта: достаточно указать --enable-shared при настройке пакета, чтобы собрать его в общую библиотеку или скомпоновать его с другими пакетами, собранными в виде общих библиотек. Дополнительная сложность при сборке кода заключается в том, что необходимо различать, будет ли код использоваться в общей библиотеке или использовать общие версии библиотек других пакетов, от которых он зависит. Дополнительные сложности возникают при установке и распространении общих библиотек или программ, использующих общие библиотеки: необходимо убедиться, что все требуемые во время выполнения общие библиотеки находятся в подходящих местах.
5.12.1. Сборка программ, использующих общие библиотеки
Чтобы собрать простую программу, использующую общие библиотеки для системы времени выполнения и базовых библиотек, укажите флаг -dynamic:
ghc --make -dynamic Main.hs
Это даёт два эффекта. Во-первых, код компилируется так, чтобы его можно было скомпоновать с общими версиями библиотек Haskell (например, base). Во-вторых, при компоновке используются общие, а не статические версии библиотек пакетов. Разумеется, для этого пакеты должны быть собраны с общими библиотеками. На поддерживаемых платформах GHC поставляется с общими библиотеками для всех основных пакетов, но если вы устанавливаете дополнительные пакеты (например, с помощью Cabal), их также необходимо собирать с общими библиотеками (--enable-shared для Cabal).
5.12.2. Общие библиотеки для пакетов Haskell
Вы можете скомпилировать код Haskell в общую библиотеку и создать пакет, который смогут использовать другие программы на Haskell. Проще всего это сделать с помощью Cabal: достаточно настроить пакет Cabal, указав флаг --enable-shared.
Если вы хотите выполнить эти шаги вручную или пишете собственную систему сборки, необходимо соблюдать определённые соглашения. Создание общей библиотеки, экспортирующей код Haskell для использования другим кодом Haskell, немного сложнее, чем создание библиотеки, экспортирующей API C для использования кодом C. Если допустить ошибку, обычно возникают ошибки компоновщика.
В частности, общие библиотеки Haskell обязательно должны быть оформлены как пакеты. Нельзя произвольно выбирать, какие модули входят в какие общие библиотеки. Общие библиотеки Haskell должны соответствовать границам пакетов. Причина в том, что GHC по-разному обрабатывает ссылки на символы внутри одной и той же общей библиотеки (или основного исполняемого файла) и ссылки на символы между разными общими библиотеками. GHC должен знать для каждого импортированного модуля, находится ли он локально в той же общей библиотеке или в отдельной общей библиотеке. Для этого используются пакеты. При использовании -dynamic предполагается, что модуль из отдельного пакета находится в отдельной общей библиотеке, а модули из того же пакета (или пакета «main» по умолчанию) находятся в той же общей библиотеке (или основном исполняемом файле).
Большая часть соглашений, соблюдения которых GHC ожидает при использовании пакетов, описана в разделе Сборка пакета из исходного кода Haskell. Кроме того, обратите внимание: GHC ожидает, что файлы .hi будут иметь расширение .dyn_hi. Остальные требования такие же, как для библиотек C, и описаны ниже; в частности, это относится к использованию флагов -dynamic, -fPIC и -shared.
5.12.3. Общие библиотеки, экспортирующие API C
Компиляция кода Haskell в общую библиотеку — хороший способ включить код Haskell в крупный проект, использующий несколько языков. При статической компоновке рекомендуется использовать GHC для выполнения заключительного этапа компоновки, а общую библиотеку Haskell можно использовать так же, как любую другую общую библиотеку. Компоновку можно выполнять с помощью обычного системного компилятора или компоновщика C.
Общие библиотеки, созданные GHC, можно загружать в другие программы, написанные не на Haskell, поэтому они подходят для использования в качестве подключаемых модулей. Разумеется, для создания подключаемого модуля необходимо использовать FFI для экспорта функций C и соблюдать правила инициализации RTS. См. раздел Создание библиотеки Haskell, вызываемой из внешнего кода. В частности, вероятно, потребуется экспортировать из общей библиотеки функцию C для инициализации подключаемого модуля до вызова любых функций Haskell.
Чтобы скомпилировать модули Haskell, экспортирующие API C, в общую библиотеку, используйте флаги -dynamic, -fPIC и -shared:
ghc --make -dynamic -shared -fPIC -flink-rts Foo.hs -o libfoo.so
Как и прежде, флаг -dynamic указывает, что эта библиотека компонуется с общими версиями библиотек пакета base. Флаг -flink-rts дополнительно выполняет компоновку с общей версией библиотеки пакета rts (компоновка с пакетом rts по умолчанию не включена при сборке общих библиотек). Можно также не указывать -flink-rts и скомпоновать библиотеку RTS с конечным исполняемым файлом.
Флаг -fPIC необходим для всего кода, который попадёт в общую библиотеку. Флаг -shared указывает, что нужно создать общую библиотеку, а не программу. Для наглядности разделим процесс на отдельные этапы компиляции и компоновки:
ghc -dynamic -fPIC -c Foo.hs ghc -dynamic -shared -flink-rts Foo.o -o libfoo.so
В принципе, на этапе компоновки можно использовать -shared без -dynamic. Это означает, что система времени выполнения и все базовые библиотеки будут статически скомпонованы с новой общей библиотекой. В результате получится очень большая, но автономная общая библиотека. Однако на большинстве платформ для этого потребуется, чтобы все статические библиотеки были собраны с флагом -fPIC, обеспечивающим пригодность кода для включения в общую библиотеку. В настоящее время мы этого не делаем.
Предупреждение
Если ваша общая библиотека экспортирует API Haskell, нельзя напрямую скомпоновать её с другой программой на Haskell и использовать этот API Haskell. Это приведёт к ошибкам компоновщика. Вместо этого необходимо оформить библиотеку как пакет, как описано в предыдущем разделе.
5.12.4. Поиск общих библиотек во время выполнения
Основная сложность управления общими библиотеками заключается в том, чтобы программы могли находить необходимые им библиотеки во время выполнения. Принцип работы различается в зависимости от платформы, в частности, в трёх основных системах: Unix на базе ELF, Windows и Mac OS X.
5.12.4.1. Unix
В Unix существуют два механизма. Общие библиотеки можно устанавливать в стандартные каталоги, известные динамическому компоновщику. Например, /usr/lib или /usr/local/lib в большинстве систем. Другой механизм — использовать «путь выполнения» (rpath), встроенный непосредственно в программы и библиотеки. Эти пути могут быть абсолютными или, как минимум в Linux и Solaris, относительными к самой программе или библиотеке. В принципе, это позволяет создавать полностью переносимые наборы программ и библиотек.
В GHC есть флаг компоновки -dynload, выбирающий способ поиска общих библиотек во время выполнения. Сейчас предусмотрено два режима:
-
sysdep -
Режим, зависящий от системы. Он используется по умолчанию. В системах Unix на базе ELF в общую библиотеку или исполняемый файл встраиваются записи
RPATH/RUNPATH. В частности, используются абсолютные пути к расположению общих библиотек RTS и каждого пакета. Это означает, что программу можно сразу запустить, и она сможет найти необходимые библиотеки. Однако такой вариант может оказаться неподходящим для развёртывания, если на другом компьютере библиотеки установлены в другом месте. -
deploy -
При этом режиме пути выполнения не встраиваются. Он предполагает, что общие библиотеки доступны в стандартном каталоге или в каталоге, указанном переменной окружения
LD_LIBRARY_PATH.
Чтобы использовать относительные пути к зависимым библиотекам в Linux и Solaris, передайте компоновщику подходящий флаг -rpath:
ghc -dynamic Main.hs -o main -lfoo -L. -optl-Wl,-rpath,'$ORIGIN'
Предполагается, что библиотека libfoo.so находится в текущем каталоге и после развёртывания программы будет доступна в том же каталоге, что и исполняемый файл main. Аналогичным образом можно использовать подкаталог относительно исполняемого файла, например -optl-Wl,-rpath,'$ORIGIN/lib'.
Этот способ с относительными путями можно использовать в любом из двух режимов -dynload, хотя он наиболее уместен в режиме deploy. Разница в том, что при режиме deploy в приведённом примере в ELF RUNPATH окажется только $ORIGIN, а при режиме sysdep в RUNPATH будут указаны $ORIGIN, а также каталоги библиотек всех пакетов, от которых зависит программа (например, пакетов base и rts и т. д.); обычно это абсолютные пути. Утилита Unix readelf --dynamic удобна для просмотра записей RPATH/RUNPATH в общих библиотеках ELF и исполняемых файлах.
На большинстве платформ UNIX также можно собирать исполняемые файлы, которые можно dlopen как общие библиотеки, используя при компоновке флаг -pie.
5.12.4.2. Mac OS X
В Darwin/Mac OS X обычно предполагается, что динамические библиотеки при сборке получают «имя установки» — полный конечный путь установки файла библиотеки. Любые библиотеки или исполняемые файлы, которые впоследствии компонуются с ней (даже если она ещё не установлена), получают этот путь как место поиска библиотеки во время выполнения. При непосредственной компиляции с помощью ghc имя установки по умолчанию совпадает с местом сборки. Его можно переопределить параметром -dylib-install-name ⟨path⟩ (который передаёт -install_name компоновщику Apple). Cabal выполняет это за вас. Он автоматически задаёт для динамических библиотек имя установки, равное абсолютному пути конечного места установки.