-
ForeignFunctionInterface -
- Начиная с:
-
6.8.1
- Статус:
-
Включено в
GHC2024,GHC2021,Haskell2010
Разрешает использовать интерфейс внешних функций Haskell.
GHC (в основном) соответствует интерфейсу внешних функций Haskell, описанному в отчёте Haskell. Дополнительные сведения см. в соответствующей главе отчёта Haskell.
Поддержка FFI включена по умолчанию, но её можно явно включить или отключить с помощью флага ForeignFunctionInterface.
GHC реализует ряд расширений, специфичных для GHC, к главе FFI отчёта Haskell 2010. Эти расширения описаны в разделе Расширения GHC для главы FFI, однако обратите внимание, что программы, использующие эти возможности, непереносимы. Поэтому по возможности следует избегать их использования.
Библиотеки FFI описаны в сопутствующей документации библиотек; см., например, модуль Foreign. Документацию по FFI для WebAssembly см. в главе о WebAssembly.
6.17.1. Отличия GHC от главы FFI
6.17.1.1. Гарантированная безопасность вызовов
В отчёте Haskell 2010 указано, что вызовы FFI safe должны позволять внешнему коду безопасно вызывать код Haskell. На практике это означает, что вызываемые функции также должны исходить из того, что размещённые в куче значения Haskell могут произвольно перемещаться, чтобы допускать сборку мусора.
Это значительно ограничивает авторов библиотек, поскольку подразумевает, что небезопасно передавать ссылку на любой объект в куче в вызов внешней функции safe. Например, часто желательно передавать не закреплённые ByteArray# напрямую в машинный код, чтобы избежать ненужного копирования. Однако для вызовов safe это нельзя сделать безопасно, поскольку сборщик мусора может переместить массив во время вызова.
Глава также допускает перемещение объектов во время вызовов unsafe. Поэтому программы, строго соответствующие Haskell 2010, тоже не могут передавать ссылки на объекты в куче в вызовы FFI unsafe.
Начиная с версии 8.4, GHC гарантирует, что сборка мусора никогда не произойдёт во время вызова unsafe, даже в интерпретаторе байт-кода, а также гарантирует, что вызовы unsafe будут выполняться в вызывающем потоке. Это делает безопасной передачу объектов в куче небезопасным функциям.
В предыдущих выпусках GHC использовал свободу, предоставляемую главой, и выполнял внешние вызовы safe вместо вызовов unsafe в интерпретаторе байт-кода. Это означало, что некоторые пакеты, которые работали после компиляции, завершались с ошибкой в GHCi (например, #13730). В современных выпусках это больше не происходит.
6.17.1.2. Взаимодействие вызовов safe и привязанных потоков
Вызов safe, обращающийся к Haskell, выполняется RTS в привязанном потоке. Это означает, что все вложенные вызовы safe выполняются в одном и том же потоке операционной системы. Однако последовательные вызовы safe такого преимущества не имеют и могут выполняться в произвольных потоках ОС.
Такое поведение считается деталью реализации. Вместо того чтобы полагаться на локальное состояние потока, следует использовать один из интерфейсов, предоставляемых в Control.Concurrent, чтобы явно задать это поведение.
Сведения о привязанных потоках см. в документации к Control.Concurrent.
Дополнительные сведения о реализации см. в статье «Extending the Haskell Foreign Function Interface with Concurrency». Последняя известная ссылка для доступа к ней находится здесь.
6.17.1.3. Вариативные аргументы не поддерживаются соглашением о вызовах ccall
Обратите внимание, что функции, которым требуются вариативные аргументы, не поддерживаются соглашением о вызовах ccall. Для вызова таких функций во внешних импортах следует использовать соглашение capi, явно задавая сигнатуру необходимого шаблона вызова. Например, можно написать:
foreign import "capi" "printf"
my_printf :: Ptr CChar -> CInt -> IO ()
printInt :: CInt -> IO ()
printInt n = my_printf "printed number %d" n
6.17.2. Расширения GHC для главы FFI
Описанные в этом разделе возможности FFI специфичны для GHC. Если вы будете их использовать, ваш код не будет переносимым на другие компиляторы.
6.17.2.1. Неподнятые типы FFI
-
UnliftedFFITypes -
- Начиная с:
-
6.8.1
Следующие неподнятые неупакованные типы можно использовать как базовые внешние типы (см. главу FFI, раздел 8.6) как для внешних вызовов safe, так и для unsafe: Int#, Word#, Char#, Float#, Double#, Addr# и StablePtr# a. Кроме того, (# #) можно использовать, если это единственный аргумент функции и он стоит первым. Это позволяет более гибко импортировать функции, которым не требуется упорядочивание посредством IO.
Несколько неподнятых упакованных типов можно использовать в качестве аргументов вызовов FFI при соблюдении следующих ограничений:
- Допустимые аргументы вызовов FFI
foreign import unsafe:Array#,SmallArray#,ByteArray#и изменяемые аналоги этих типов. - Допустимые аргументы вызовов FFI
foreign import safe:ByteArray#иMutableByteArray#. Массив байтов должен быть закреплён. - Изменение: как в вызовах FFI
foreign import unsafe, так и вforeign import safeбезопасно изменятьMutableByteArray. Изменение массивов любого другого типа приводит к неопределённому поведению. Причина: изменяемые массивы объектов в куче регистрируют записи для нужд сборки мусора. Если массив объектов в куче передаётся внешней функции C, среда выполнения не регистрирует записи. Следовательно, изменять массив объектов в куче во внешней функции небезопасно. Поскольку в среде выполнения нет средств отслеживания измененийMutableByteArray#, такие массивы можно безопасно изменять в любой внешней функции. - Обратите внимание, что вызовы FFI
safeне принимают мер для сохранения аргументов в живом состоянии во время выполнения вызываемой функции C. Для аргументов, время жизни которых не выходит за пределы вызова FFI, следует использоватьkeepAlive#илиStablePtr, чтобы гарантировать, что аргумент не будет собран сборщиком мусора до завершения вызова.
Эти ограничения не проверяются во время компиляции. Их несоблюдение приводит к ошибкам времени выполнения, которые бывает очень сложно обнаружить. (Вероятно, ошибки проявятся только во время сборки мусора.) В табличной форме ограничения выглядят так:
Когда значение используется как аргумент вызова FFI, который | ||||
|---|---|---|---|---|
|
| |||
Тип аргумента | чтение | запись | чтение | запись |
| Небезопасно | Небезопасно | Безопасно | Небезопасно |
| Небезопасно | Небезопасно | Безопасно | Небезопасно |
| Небезопасно | Небезопасно | Безопасно | Небезопасно |
| Небезопасно | Небезопасно | Безопасно | Небезопасно |
не закреплённый | Небезопасно | Небезопасно | Безопасно | Небезопасно |
не закреплённый | Небезопасно | Небезопасно | Безопасно | Безопасно |
закреплённый | Безопасно | Небезопасно | Безопасно | Небезопасно |
закреплённый | Безопасно | Безопасно | Безопасно | Безопасно |
При передаче любого из неподнятых типов массивов в качестве аргумента внешнего вызова C внешняя функция видит указатель на полезную нагрузку массива, а не на содержащий её объект в куче StgArrBytes/StgMutArrPtrs/StgSmallMutArrPtrs [1]. В отличие от этого, внешний вызов Cmm, введённый с помощью foreign import prim, видит объект в куче, а не только его полезную нагрузку. Это означает, что в некоторых случаях внешней функции C может не потребоваться знание типов замыканий RTS. Следующий пример суммирует первые три байта в MutableByteArray# [2], не используя ничего из Rts.h:
// C source
uint8_t add_triplet(uint8_t* arr) {
return (arr[0] + arr[1] + arr[2]);
}
-- Haskell source
foreign import ccall unsafe "add_triplet"
addTriplet :: MutableByteArray# RealWorld -> IO Word8
В других случаях функции C может потребоваться знание типов замыканий RTS. Следующий пример суммирует первый элемент каждого ByteArray# (интерпретируя байты как массив CInt) элемента Array# ByteArray# [3]:
// C source, must include the RTS to make the struct StgArrBytes
// available along with its fields, such as `payload`.
#include "Rts.h"
int sum_first (StgArrBytes **bufs, StgWord sz) {
int res = 0;
for(StgWord ix = 0; ix < sz; ix++) {
res = res + ((int*)(bufs[ix]->payload))[0];
}
return res;
}
-- Haskell source
foreign import ccall unsafe "sum_first"
sumFirst :: Array# ByteArray# -> CInt -> IO CInt
sumFirst' :: Array# ByteArray# -> IO CInt
sumFirst' arr = sumFirst arr (sizeofArray# arr)
Хотя GHC позволяет пользователю передавать внешним функциям все неподнятые упакованные типы, некоторые из них не подходят для полезной работы. Несмотря на то что Array# является неподнятым, элементы его полезной нагрузки могут быть поднятыми, а внешняя функция C не может безопасно вычислять отсроченные вычисления. Следовательно, внешняя функция C не должна разыменовывать адреса, составляющие полезную нагрузку Array# a, если a имеет поднятое представление.
6.17.2.2. Обёртывание монады IO с помощью newtype
Спецификация FFI требует, чтобы монада IO встречалась в различных местах, однако иногда бывает удобно обернуть монаду IO в newtype, например так:
newtype MyIO a = MIO (IO a)
(Например, это может быть нужно, чтобы запретить программисту вызывать произвольные процедуры IO в некоторой части программы.)
В спецификации Haskell FFI уже указано, что аргументы и результаты внешнего импорта и экспорта автоматически разворачиваются, если они являются newtype (раздел 3 дополнения FFI). GHC расширяет FFI, автоматически разворачивая любые newtype, оборачивающие саму монаду IO. Точнее, везде, где спецификация FFI требует тип IO, GHC принимает любой newtype, оборачивающий тип IO. Например, следующие объявления допустимы:
foreign import foo :: Int -> MyIO Int foreign import "dynamic" baz :: (Int -> MyIO Int) -> CInt -> MyIO Int
6.17.2.3. Явные «forall» во внешних типах
Переменные типа в типе внешнего объявления можно квантифицировать с помощью явного forall, используя языковое расширение ExplicitForAll, как в следующем примере:
{-# LANGUAGE ExplicitForAll #-}
foreign import ccall "mmap" c_mmap :: forall a. CSize -> IO (Ptr a)
Обратите внимание, что явный forall должен находиться в начале сигнатуры типа и не может быть вложен в тип, как в следующих (ошибочных) примерах:
foreign import ccall "mmap" c_mmap' :: CSize -> forall a. IO (Ptr a) foreign import ccall quux :: (forall a. Ptr a) -> IO ()
6.17.2.4. Примитивный импорт
-
GHCForeignImportPrim -
- Начиная с:
-
6.12.1
- Статус:
-
InternalUseOnly
С помощью GHCForeignImportPrim GHC расширяет FFI, добавляя соглашение о вызовах prim, например:
foreign import prim "foo" foo :: ByteArray# -> (# Int#, Int# #)
Это используется для импорта функций, написанных на Cmm и следующих внутреннему соглашению о вызовах GHC. Аргументы и результаты должны быть неупакованными типами, за исключением того, что аргумент может иметь тип Any :: Type или Any :: UnliftedType (что можно обеспечить с помощью unsafeCoerce#), а тип результата может быть неупакованным кортежем или одним из типов Any :: Type или Any :: UnliftedType.
Эта возможность не предназначена для использования за пределами основных библиотек, поставляемых с GHC. Дополнительные сведения см. в вики разработчиков GHC.
6.17.2.5. Прерываемые внешние вызовы
-
InterruptibleFFI -
- Начиная с:
-
7.2.1
Этот раздел посвящён взаимодействию внешних вызовов с Control.Concurrent.throwTo. Обычно, если цель throwTo участвует во внешнем вызове, исключение не возникает до возврата из вызова, а вызывающий поток всё это время блокируется. Это может привести к потере отзывчивости, что особенно нежелательно при пользовательском прерывании (например, Control-C). По умолчанию при получении сигнала Control-C (SIGINT в Unix) в главном потоке возникает исключение UserInterrupt; если в этот момент главный поток заблокирован во внешнем вызове, программа не отреагирует на пользовательское прерывание.
Проблема заключается в том, что в общем случае безопасно прервать внешний вызов невозможно. Однако GHC предоставляет способ прерывать блокирующие системные вызовы, который работает для большинства системных вызовов как в Unix, так и в Windows.
Если включено расширение InterruptibleFFI, внешний вызов можно пометить как interruptible вместо safe или unsafe:
foreign import ccall interruptible "sleep" sleepBlock :: CUint -> IO CUint
interruptible работает точно так же, как safe, за исключением того, что при направлении throwTo потоку, выполняющему прерываемый внешний вызов, независимо от состояния маскирования исключение добавляется в очередь заблокированных исключений целевого потока, а для попытки вернуть управление из внешнего вызова используется механизм, специфичный для ОС:
- Системы Unix
-
Потоку, выполняющему внешний вызов, посылается сигнал
SIGPIPEс помощьюpthread_kill(). Обычно этого достаточно, чтобы блокирующий системный вызов завершился сEINTR(по умолчанию GHC устанавливает пустой обработчик сигнала дляSIGPIPE, переопределяя поведение по умолчанию, при котором процесс немедленно завершается). - Системы Windows
-
[Только Vista и новее] RTS вызывает функцию Win32
CancelSynchronousIo, которая приводит к завершению блокирующей операции ввода-вывода с ошибкойERROR_OPERATION_ABORTED.
После успешного прерывания системного вызова окружающий код должен вернуть управление из foreign import обратно в код Haskell, чтобы можно было вызвать заблокированное исключение, если это допускает состояние маскирования потока. Маскирование даёт коду Haskell возможность обнаружить код ошибки прерывания, возвращённый вызовом C, и отреагировать на него.
Если внешний код просто повторяет системный вызов напрямую, не возвращаясь в Haskell, предполагаемый эффект interruptible исчезает, и такие функции, как System.Timeout.timeout, работать не будут.
Наконец, после возврата внешнего вызова interruptible в Haskell код Haskell должен разрешить возникновение исключений (Control.Exception’s allowInterrupt или interruptible yield для не--threaded, см. #8684) и реализовать повтор вызова при EINTR в Haskell (например, с помощью Foreign.C.Error.throwErrnoIfMinus1Retry).
При использовании interruptible особенно важно убедиться, что вызываемая внешняя функция готова к последствиям прерывания вызова. В Unix считается хорошей практикой всегда проверять EINTR после системных вызовов, поэтому можно ожидать, что программа не завершится с ошибкой (но в таком случае interruptible не сработает как задумано, если только код затем не вернётся до самого Haskell, как описано выше). Однако в Windows обычно не принято обрабатывать ERROR_OPERATION_ABORTED.
Этот подход работает только для внешнего кода, выполняющего ввод-вывод (системные вызовы), а не для вычислений с интенсивной загрузкой процессора, не выполняющих системных вызовов. Это связано с тем, что внешний код может обнаружить прерывание только при получении кодов ошибки прерывания от системных вызовов. Чтобы иметь возможность прерывать длительно выполняющийся внешний код, не использующий системные вызовы, скорее всего, потребуется изменить его и явно проверять наличие запроса на досрочное завершение.
6.17.2.6. Соглашение о вызовах CAPI
-
CApiFFI -
- Начиная с:
-
7.6.1
Расширение CApiFFI позволяет использовать в объявлениях внешних функций соглашение о вызовах capi, например:
foreign import capi "header.h f" f :: CInt -> IO CInt
Вместо генерации кода для вызова f в соответствии с ABI платформы мы вызываем f с помощью C API, определённого в заголовочном файле header.h. Таким образом, f можно вызвать, даже если оно определено как макрос CPP #define, а не как настоящая функция.
При использовании capi можно импортировать и значения, а не только функции. Например,
foreign import capi "pi.h value pi" c_pi :: CDouble
будет работать независимо от того, определено ли pi как
const double pi = 3.14;
или так:
#define pi 3.14
Чтобы сообщить GHC, какому типу C соответствует тип Haskell при использовании CAPI, в определении типа можно использовать прагму CTYPE. При необходимости также можно указать заголовочный файл, в котором определён тип. Синтаксис выглядит так:
data {-# CTYPE "unistd.h" "useconds_t" #-} T = ...
newtype {-# CTYPE "useconds_t" #-} T = ...
Если внешние объявления содержат тип возвращаемого указателя с квалификатором const, для его кодирования можно использовать ConstPtr из Foreign.C.ConstPtr, например:
foreign import capi "header.h f" f :: CInt -> ConstPtr CInt
что соответствует
const *int f(int);
6.17.2.7. hs_thread_done()
void hs_thread_done(void);
Когда поток вызывает функцию Haskell через foreign export, GHC выделяет небольшой объём локальной для потока памяти. Обычно эта память освобождается только при hs_exit(); она кэшируется, чтобы последующие вызовы Haskell выполнялись быстро. Однако если ваше приложение работает долго и регулярно создаёт новые потоки, вызывающие Haskell, вероятно, следует обеспечить освобождение этой памяти в потоках, завершивших вызовы функций Haskell. Для этого вызовите hs_thread_done() из потока, память которого нужно освободить.
Вызов hs_thread_done() необязателен. Вы можете вызывать его как часто, так и редко, как захотите. Его безопасно вызывать из потока, который никогда не вызывал и не будет вызывать функции Haskell. Если вы забудете вызвать эту функцию, в худшем случае часть памяти останется выделенной до вызова hs_exit(). Если вы будете вызывать её слишком часто, в худшем случае следующий вызов функции Haskell потребует дополнительных накладных расходов.
6.17.2.8. Эффективное освобождение множества стабильных указателей
Стандартная функция hs_free_stable_ptr блокирует таблицу стабильных указателей, освобождает заданный стабильный указатель, а затем снова разблокирует таблицу. При одновременном освобождении множества стабильных указателей обычно эффективнее заблокировать и разблокировать таблицу только один раз.
extern void hs_lock_stable_ptr_table (void); extern void hs_unlock_stable_ptr_table (void); extern void hs_free_stable_ptr_unsafe (HsStablePtr sp);
Функцию hs_free_stable_ptr_unsafe следует использовать только после блокировки таблицы с помощью hs_lock_stable_ptr_table. После этого таблицу необходимо разблокировать с помощью hs_unlock_stable_ptr_table. Сборщик мусора Haskell не может работать, пока таблица заблокирована, поэтому её следует разблокировать как можно скорее. Пока таблица стабильных указателей заблокирована, запрещены следующие операции:
- Вызов любой функции Haskell, независимо от того, работает ли она со стабильными указателями.
- Вызов любой функции FFI, работающей с таблицей стабильных указателей, за исключением произвольного числа вызовов
hs_free_stable_ptr_unsafeи заключительного вызоваhs_unlock_stable_ptr_table. - Вызов
hs_free_fun_ptr.
Примечание
В версиях GHC до 8.8 вместо hs_lock_stable_ptr_table и hs_unlock_stable_ptr_table были определены недокументированные функции hs_lock_stable_tables и hs_unlock_stable_tables. Теперь эти имена считаются устаревшими.