Spec-Zone.ru › Haskell 9

6.17. Интерфейс внешних функций (FFI)

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, чтобы гарантировать, что аргумент не будет собран сборщиком мусора до завершения вызова.

Эти ограничения не проверяются во время компиляции. Их несоблюдение приводит к ошибкам времени выполнения, которые бывает очень сложно обнаружить. (Вероятно, ошибки проявятся только во время сборки мусора.) В табличной форме ограничения выглядят так:

Ограничения для неподнятых упакованных аргументов, передаваемых внешним вызовам C. Ячейки со значением «Небезопасно» соответствуют сочетаниям, которые приводят к неопределённому поведению во время выполнения. GHC не отклоняет такие небезопасные программы во время компиляции.

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

foreign import safe

foreign import unsafe

Тип аргумента

чтение

запись

чтение

запись

Array#

Небезопасно

Небезопасно

Безопасно

Небезопасно

MutableArray#

Небезопасно

Небезопасно

Безопасно

Небезопасно

SmallArray#

Небезопасно

Небезопасно

Безопасно

Небезопасно

MutableSmallArray#

Небезопасно

Небезопасно

Безопасно

Небезопасно

не закреплённый ByteArray#

Небезопасно

Небезопасно

Безопасно

Небезопасно

не закреплённый MutableByteArray#

Небезопасно

Небезопасно

Безопасно

Безопасно

закреплённый ByteArray#

Безопасно

Небезопасно

Безопасно

Небезопасно

закреплённый MutableByteArray#

Безопасно

Безопасно

Безопасно

Безопасно

При передаче любого из неподнятых типов массивов в качестве аргумента внешнего вызова 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. Теперь эти имена считаются устаревшими.

6.17.3. Использование FFI с GHC

В следующих разделах также приводятся некоторые подсказки и советы по использованию интерфейса внешних функций в GHC.

6.17.3.1. Использование foreign export и foreign import ccall "wrapper" с GHC

Когда GHC компилирует модуль (например, M.hs), в котором используется foreign export или foreign import "wrapper", он создает M_stub.h для использования программами на C.

Для обычного foreign export файл M_stub.h содержит прототип C для экспортируемой внешней функции. Например, если мы скомпилируем следующий модуль:

module Foo where

foreign export ccall foo :: Int -> IO Int

foo :: Int -> IO Int
foo n = return (length (f n))

f :: Int -> [Int]
f 0 = []
f n = n:(f (n-1))

Тогда Foo_stub.h будет содержать нечто подобное:

#include "HsFFI.h"
extern HsInt foo(HsInt a0);

Чтобы вызвать foo() из C, просто #include "Foo_stub.h" и вызовите foo().

Файл Foo_stub.h можно перенаправить с помощью параметра -stubdir; см. Перенаправление выходных данных компиляции.

6.17.3.1.1. Использование собственного main()

Обычно система времени выполнения GHC предоставляет main(), который обеспечивает вызов Main.main в программе на Haskell. Однако может потребоваться связать код на Haskell с программой, у которой функция main написана на другом языке, например на C. Для этого необходимо явно инициализировать систему времени выполнения Haskell.

Возьмем пример из предыдущего раздела и вызовем его из отдельной программы на C. Вот код на C:

#include <stdio.h>
#include "HsFFI.h"

#if defined(__GLASGOW_HASKELL__)
#include "Foo_stub.h"
#endif

int main(int argc, char *argv[])
{
  int i;

  hs_init(&argc, &argv);

  for (i = 0; i < 5; i++) {
    printf("%d\n", foo(2500));
  }

  hs_exit();
  return 0;
}

Мы заключили специфичные для GHC фрагменты в #if defined(__GLASGOW_HASKELL__); остальная часть кода должна быть переносимой между реализациями Haskell, поддерживающими стандарт FFI.

Вызов hs_init() инициализирует систему времени выполнения GHC. НЕ пытайтесь вызывать какие-либо функции Haskell до вызова hs_init(): это неизбежно приведет к проблемам.

Мы передаем ссылки на argc и argv в hs_init(), чтобы она могла отделить аргументы для RTS (то есть аргументы между +RTS...-RTS).

Закончив вызовы функций Haskell, мы можем вызвать hs_exit(), который завершит работу RTS.

Вызовов hs_init() может быть несколько, но каждому из них должен соответствовать ровно один вызов hs_exit(). Фактически деинициализирует систему самый внешний hs_exit(). Обратите внимание, что в настоящее время система времени выполнения GHC не может надежно повторно инициализироваться после этого; см. Интерфейс внешних функций.

Примечание

При компоновке итоговой программы обычно проще всего выполнить компоновку с помощью GHC, хотя это и не обязательно. Если вы используете GHC, не забудьте указать флаг -no-hs-main, иначе GHC попытается скомпоновать модуль Haskell Main.

Примечание

В Windows hs_init считает, что argv закодирован в UTF-8. Передача данных в другой кодировке может привести к неожиданным результатам. Передавать NULL в качестве argv допустимо, но в сообщениях об ошибках вместо имени исполняемого файла может появиться <unknown>.

Чтобы использовать флаги +RTS с hs_init(), нужно немного изменить пример. По умолчанию RTS GHC принимает только «безопасные» флаги +RTS (см. Параметры, влияющие на компоновку), а флаг компоновки -rtsopts[=⟨none|some|all|ignore|ignoreAll⟩] переопределяет это поведение. Однако -rtsopts[=⟨none|some|all|ignore|ignoreAll⟩] не действует, если используется -no-hs-main (то же касается -with-rtsopts=⟨opts⟩). Чтобы задать эти параметры, нужно вызвать API, специфичный для GHC, вместо hs_init():

#include <stdio.h>
#include "HsFFI.h"

#if defined(__GLASGOW_HASKELL__)
#include "Foo_stub.h"
#include "Rts.h"
#endif

int main(int argc, char *argv[])
{
  int i;

#if __GLASGOW_HASKELL__ >= 703
  {
      RtsConfig conf = defaultRtsConfig;
      conf.rts_opts_enabled = RtsOptsAll;
      hs_init_ghc(&argc, &argv, conf);
  }
#else
  hs_init(&argc, &argv);
#endif

  for (i = 0; i < 5; i++) {
    printf("%d\n", foo(2500));
  }

  hs_exit();
  return 0;
}

Обратите внимание на два изменения: мы добавили Rts.h, который определяет внешний интерфейс RTS, специфичный для GHC, и вызвали hs_init_ghc() вместо hs_init(), передав аргумент типа RtsConfig. RtsConfig — это структура с различными полями, влияющими на поведение системы времени выполнения. Ее определение:

typedef struct {
    RtsOptsEnabledEnum rts_opts_enabled;
    const char *rts_opts;
} RtsConfig;

extern const RtsConfig defaultRtsConfig;

typedef enum {
    RtsOptsNone,         // +RTS causes an error
    RtsOptsSafeOnly,     // safe RTS options allowed; others cause an error
    RtsOptsAll           // all RTS options allowed
  } RtsOptsEnabledEnum;

Существует значение по умолчанию defaultRtsConfig, которое следует использовать для инициализации переменных типа RtsConfig. В будущем в RtsConfig наверняка будут добавлены новые поля, поэтому для обеспечения совместимости кода с будущими версиями лучше инициализировать структуру значением defaultRtsConfig, а затем изменять необходимые поля, как в примере кода выше.

6.17.3.1.2. Создание библиотеки Haskell, вызываемой из внешнего кода

Этот сценарий во многом похож на описанный в разделе Использование собственного main(), за исключением того, что цель состоит не в компоновке законченной программы, а в создании библиотеки из кода Haskell, которую можно распространять так же, как библиотеку кода на C.

Главное требование здесь состоит в том, что система времени выполнения должна быть инициализирована до вызова любого кода Haskell, поэтому библиотека должна предоставлять точки входа для инициализации и деинициализации, реализованные на C или C++. Например:

#include <stdlib.h>
#include "HsFFI.h"

HsBool mylib_init(void){
  int argc = 3;
  char *argv[] = { "mylib", "+RTS", "-A32m", NULL };
  char **pargv = argv;

  // Initialize Haskell runtime
  hs_init(&argc, &pargv);

  // do any other initialization here and
  // return false if there was a problem
  return HS_BOOL_TRUE;
}

void mylib_end(void){
  hs_exit();
}

Процедура инициализации mylib_init обычно вызывает hs_init() для инициализации системы времени выполнения Haskell, а соответствующая функция деинициализации mylib_end() вызывает hs_exit() для завершения работы системы времени выполнения.

6.17.3.2. Использование заголовочных файлов

Функции C обычно объявляются с помощью прототипов в заголовочном файле C. В более ранних версиях GHC (6.8.3 и предыдущих) в исходный файл C, сгенерированный из кода Haskell, #include заголовочный файл, поэтому компилятор C мог проверить, что функция C, вызываемая через FFI, вызывается с правильным типом.

GHC больше не подключает внешние заголовочные файлы при компиляции через C, поэтому такая проверка не выполняется. Это изменение было внесено для совместимости с генератором машинного кода (-fasm) и для строгого соблюдения спецификации FFI, требующей, чтобы вызовы FFI не подвергались макроподстановке и другим преобразованиям CPP, которые могут применяться при использовании заголовочных файлов C. Такой подход также упрощает встраивание внешних вызовов через границы модулей и пакетов: при компиляции встроенной версии внешнего вызова заголовочный файл не требуется, поэтому компилятор может встраивать внешние вызовы в любом контексте.

Параметр -#include теперь объявлен устаревшим, а поле includes в спецификации пакета Cabal игнорируется.

6.17.3.3. Выделение памяти

Библиотеки FFI предоставляют несколько способов выделения памяти для использования с FFI, и не всегда понятно, какой из них лучше. На выбор может влиять эффективность конкретного вида выделения в данном компиляторе или на данной платформе, поэтому в этом разделе рассматривается эффективность различных способов выделения памяти в GHC.

alloca

Подходит для кратковременного выделения, когда выделенная память должна быть доступна в пределах заданного вычисления IO. Этот вид выделения часто используется при преобразовании данных при передаче в функции FFI и получении данных от них.

В GHC alloca реализован с помощью MutableByteArray#, поэтому выделение и освобождение памяти выполняются быстро: гораздо быстрее, чем malloc/free в C, хотя и не так быстро, как выделение памяти в стеке C. По возможности используйте alloca.

mallocForeignPtr

Подходит для выделения памяти на более длительный срок, когда требуется сборка мусора. Однако если вы собираетесь сохранить указатель на эту память во внешней структуре данных, mallocForeignPtr — не лучший выбор.

В GHC mallocForeignPtr также реализован с помощью MutableByteArray#. Хотя на память указывает ForeignPtr, фактические финализаторы не задействуются (если только вы не добавите их с помощью addForeignPtrFinalizer), а освобождение памяти выполняется сборщиком мусора, поэтому mallocForeignPtr обычно очень дешев.

malloc/free

Если остальные способы не подходят, придется прибегнуть к Foreign.malloc и Foreign.free. Это всего лишь обертки над одноименными функциями C, а их эффективность в конечном счете зависит от реализации этих функций в библиотеке C вашей платформы. Как правило, malloc и free значительно медленнее описанных выше способов выделения памяти.

Foreign.Marshal.Pool

Пулы могут быть более удобным способом организации выделения памяти, чем другие рассмотренные варианты. Они используют внутреннюю арену RTS, а не malloc/free.

6.17.3.4. Многопоточность и FFI

Чтобы использовать FFI в многопоточной среде, необходимо задать параметр -threaded (см. Параметры, влияющие на компоновку).

6.17.3.4.1. Внешний импорт и многопоточность

При вызове функции foreign imported, помеченной как safe (значение по умолчанию), в однопоточной системе времени выполнения (программа скомпонована без параметра -threaded) остальные потоки Haskell будут заблокированы до возврата из вызова.

В многопоточной системе времени выполнения (программа скомпонована с параметром -threaded) функции foreign imported выполняются параллельно (как safe, так и unsafe), однако аналогичная ситуация может возникнуть при вызове функции unsafe, если в другом потоке запустится глобальная сборка мусора. В этом случае сборщик мусора не может продолжить работу, что может привести к снижению производительности. Такая проблема часто проявляется при высокой нагрузке: остальные потоки активнее и поэтому чаще запускают глобальную сборку мусора.

Это означает, что если нужно вызвать внешнюю функцию, которая выполняется долго или может заблокироваться, ее следует пометить как safe и использовать -threaded. Некоторые библиотечные функции сами выполняют такие вызовы; в документации к ним это должно быть указано.

С другой стороны, внешний вызов функции, которая гарантированно выполняется быстро и не вызывает обратный вызов в Haskell, можно пометить как unsafe. Это работает как в однопоточной, так и в многопоточной системе времени выполнения. Хорошим кандидатом будет внешняя функция, выполняющая примерно столько же работы, сколько выполняет код Haskell между выделениями памяти в куче (то есть совсем немного).

За пределами этих двух очевидных случаев для внешних функций safe и unsafe приходится искать компромисс между пропускной способностью всей программы и эффективностью отдельного внешнего вызова.

Если вы выполняете внешние вызовы из нескольких потоков Haskell и используете -threaded, убедитесь, что вызываемый внешний код потокобезопасен. В частности, некоторые графические библиотеки не являются потокобезопасными и требуют, чтобы вызывающая сторона вызывала методы графического интерфейса только из одного потока. В этом случае может понадобиться ограничить операции с графическим интерфейсом одним потоком Haskell и, возможно, также использовать привязанный поток (см. Связь между потоками Haskell и потоками ОС).

Обратите внимание, что внешние вызовы, выполняемые разными потоками Haskell, могут выполняться параллельно, даже если флаг +RTS -N не используется (Параметры RTS для параллелизма SMP). Флаг -N ⟨x⟩ управляет параллельным выполнением потоков Haskell, но число одновременно выполняющихся внешних вызовов может быть любым независимо от значения +RTS -N.

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

6.17.3.4.2. Связь между потоками Haskell и потоками ОС

Обычно между потоками Haskell и потоками ОС нет фиксированного соответствия. Это означает, что внешний вызов может выполняться в неопределенном потоке ОС. Кроме того, нет гарантии, что несколько вызовов, сделанных одним потоком Haskell, будут выполняться в одном и том же потоке ОС.

Обычно это не создает проблем и позволяет системе времени выполнения GHC эффективно использовать ресурсы потоков ОС. Однако в некоторых случаях полезно лучше контролировать, какой именно поток ОС используется, например при вызове внешнего кода, использующего локальное состояние потока. Для таких случаев предусмотрены привязанные потоки — потоки Haskell, закрепленные за определенным потоком ОС. Информацию о привязанных потоках см. в документации к модулю Control.Concurrent.

6.17.3.4.3. Внешний экспорт и многопоточность

Если программа скомпонована с параметром -threaded, можно одновременно вызывать функции foreign exported из нескольких потоков ОС. Систему времени выполнения необходимо инициализировать обычным способом, вызвав hs_init(); этот вызов должен завершиться до вызова любых функций foreign exported.

6.17.3.4.4. Использование hs_exit()

hs_exit() обычно приводит к завершению всех работающих в системе потоков Haskell; после возврата из hs_exit() работающих потоков Haskell не останется. Затем система времени выполнения корректно завершит работу: при необходимости сформирует данные профилирования и статистику, а также освободит всю принадлежащую ей память.

Принудительно завершить поток Haskell удается не всегда: например, поток может в данный момент выполнять внешний вызов, а способа принудительно завершить этот вызов у нас нет. Кроме того, система времени выполнения должна исходить из наихудшего сценария, при котором код Haskell и система времени выполнения вскоре будут удалены из памяти (например, если это DLL Windows, hs_exit() обычно вызывается перед выгрузкой DLL). Поэтому hs_exit() должна дождаться возврата всех выполняющихся внешних вызовов, прежде чем сама сможет вернуться.

В результате hs_exit() может зависнуть (или, возможно, активно ожидать), если потоки Haskell заблокированы во внешних вызовах, пока эти вызовы не вернутся. Поэтому перед вызовом hs_exit() рекомендуется убедиться, что в системе нет таких потоков. Это относится и к потокам, выполняющим операции ввода-вывода, поскольку ввод-вывод может (в зависимости от его типа и платформы) выполняться с помощью блокирующих внешних вызовов.

Система времени выполнения GHC обрабатывает завершение программы особым образом, чтобы при выходе отдельного исполняемого файла не приходилось ждать разблокировки потоков. Поскольку программа и все ее потоки вот-вот завершатся одновременно с удалением кода из памяти, предварительно дожидаться завершения потоков не требуется. Если вам нужна такая быстрая и упрощенная версия hs_exit(), можно вызвать:

void hs_exit_nowait(void);

вместо этого. Это особенно полезно, если внешним библиотекам необходимо вызвать hs_exit() при завершении программы (возможно, через деструктор C++): в этом случае следует использовать hs_exit_nowait(), поскольку поток, вызвавший exit() и выполняющий деструкторы C++, находится во внешнем вызове из Haskell, который никогда не вернется, поэтому hs_exit() приведет к взаимной блокировке.

6.17.3.4.5. Пробуждение потоков Haskell из C

Иногда требуется иметь возможность разбудить поток Haskell из кода на C. Например, при использовании API C с обратными вызовами мы регистрируем обратный вызов C, а затем ждем его выполнения.

Один из способов сделать это — создать foreign export, который выполнит необходимые действия для пробуждения потока Haskell, например putMVar, а затем вызвать его из обратного вызова C. Однако у такого подхода есть несколько недостатков:

  1. Вызов внешней экспортированной функции требует больших накладных расходов: например, он создает целый новый поток Haskell.
  2. Вызов может надолго заблокироваться, если выполняется сборка мусора. Этот способ нельзя использовать, если вызываемый API C не допускает блокировку в обратном вызове.

По этим причинам GHC предоставляет внешний API для tryPutMVar, hs_try_putmvar, с помощью которого можно недорого и асинхронно разбудить поток Haskell из C/C++.

void hs_try_putmvar (int capability, HsStablePtr sp);

void hs_try_putmvar_with_value (int capability, HsStablePtr sp, StgClosure *value);

Вызов C hs_try_putmvar(cap, mvar) эквивалентен вызову Haskell tryPutMVar mvar (), за исключением того, что он

  • не блокируется: занимает ограниченное и короткое время
  • асинхронен: фактический putMVar может быть выполнен после возврата из вызова (например, если в данный момент RTS выполняет сборку мусора). Поэтому hs_try_putmvar() не возвращает результат, указывающий, удалось ли выполнить put. Вы должны самостоятельно убедиться, что MVar пуста; если она заполнена, hs_try_putmvar() не даст эффекта.

Вызов C hs_try_putmvar_with_value(cap, mvar, value) принимает дополнительный аргумент value — указатель на замыкание RTS, представляющее значение, которое нужно поместить в MVar. Он работает так же, как hs_try_putmvar, но предоставляет немного больше гибкости: чтобы передать значение C в Haskell, можно напрямую вызвать одну из функций rts_mk, которая обернет значение C и поместит его в MVar, вместо того чтобы записывать его в область кучи, а затем считывать из указателя в Haskell.

Пример. Предположим, что у нас есть функция C/C++, которая возвращает управление, а затем через некоторое время вызывает обратный вызов, передавая нам данные. Мы хотим дождаться вызова обратного вызова в Haskell и получить данные. Это можно сделать так:

import GHC.Conc (newStablePtrPrimMVar, PrimMVar)

makeExternalCall = mask_ $ do
  mvar <- newEmptyMVar
  sp <- newStablePtrPrimMVar mvar
  fp <- mallocForeignPtr
  withForeignPtr fp $ \presult -> do
    (cap, _) <- threadCapability =<< myThreadId
    scheduleCallback sp cap presult
    takeMVar mvar `onException`
      forkIO (do takeMVar mvar; touchForeignPtr fp)
    peek presult

foreign import ccall "scheduleCallback"
    scheduleCallback :: StablePtr PrimMVar
                     -> Int
                     -> Ptr Result
                     -> IO ()

А внутри scheduleCallback мы создаем обратный вызов, который со временем сохранит данные результата в Ptr Result, а затем вызовет hs_try_putmvar().

Здесь следует отметить несколько моментов.

  • Для создания StablePtr предусмотрена специальная функция — newStablePtrPrimMVar, поскольку RTS требуется StablePtr на примитивный объект MVar#, а создать его напрямую мы не можем. Не используйте newStablePtr для MVar: программа завершится с ошибкой.
  • Освобождение StablePtr выполняется функцией hs_try_putmvar(). Иначе было бы трудно надежно организовать освобождение StablePtr: в Haskell освободить его нельзя, потому что если takeMVar будет прервано асинхронным исключением, обратный вызов сработает позднее. В C его освободить тоже нельзя, потому что неизвестно, когда это сделать (не при возврате из hs_try_putmvar(), поскольку это асинхронный вызов, который использует StablePtr в будущем).
  • mask_ нужен, чтобы предотвратить асинхронные исключения до вызова scheduleCallback, которые привели бы к утечке StablePtr.
  • Мы получаем номер текущей capability и передаем его в C. Этот номер передается обратно в hs_try_putmvar и помогает RTS определить, на какой capability следует выполнить tryPutMVar. Если это не важно, можно передать -1 в качестве capability функции hs_try_putmvar, и она выберет произвольную.

    Выбор подходящей capability поможет избежать ненужных переключений контекста. В идеале следует передать capability, на которой поток, который нужно разбудить, выполнялся в последний раз; узнать ее можно, вызвав threadCapability в Haskell.

  • Если вместе с этим нужно передать данные из обратного вызова C в Haskell, лучше всего сначала выделить в Haskell память для получения данных и передать в C ее адрес, как в примере выше.
  • Вызов takeMVar может быть прерван асинхронным исключением. В этом случае обратный вызов в C все равно выполнится позднее, запишет результат и вызовет hs_try_putmvar(). Поэтому необходимо обеспечить, чтобы память для результата оставалась доступной до выполнения обратного вызова. Если во время takeMVar возникает исключение, мы создаем еще один поток, который ожидает обратного вызова и удерживает память доступной с помощью touchForeignPtr.

Полностью работающий пример см. в testsuite/tests/concurrent/should_run/hs_try_putmvar001.hs в дереве исходного кода GHC.

6.17.3.5. Числа с плавающей точкой и FFI

Стандартный заголовочный файл C99 fenv.h предоставляет операции для проверки и изменения состояния модуля обработки чисел с плавающей точкой. В частности, можно изменить режим округления, используемый операциями с плавающей точкой, и проверить флаги исключений.

В Haskell операции с плавающей точкой имеют чистые типы, а порядок вычисления не определен. Поэтому, строго говоря, поскольку функции fenv.h позволяют изменять результаты операций с плавающей точкой или наблюдать их эффекты, использование fenv.h делает неопределенным поведение операций с плавающей точкой в любой части программы.

Тем не менее мы можем точно описать поведение GHC в отношении состояния модуля обработки чисел с плавающей точкой, чтобы вы могли использовать fenv.h, если это действительно необходимо, зная обо всех подводных камнях:

  • GHC полностью игнорирует среду вычислений с плавающей точкой: система времени выполнения не изменяет и не считывает ее.
  • Среда вычислений с плавающей точкой не сохраняется при обычном переключении контекста потока. Поэтому изменения состояния модуля обработки чисел с плавающей точкой, сделанные в одном потоке, могут быть видны в других потоках. Кроме того, проверка состояния исключений ненадежна, поскольку переключение контекста может изменить его. Если вам нужно изменять или проверять состояние модуля обработки чисел с плавающей точкой и использовать потоки, необходимо использовать привязанные потоки (Control.Concurrent.forkOS): привязанный поток имеет собственный поток ОС, а потоки ОС сохраняют и восстанавливают состояние модуля обработки чисел с плавающей точкой.
  • Во время внешнего вызова безопасно временно изменять состояние модуля обработки чисел с плавающей точкой, поскольку GHC никогда не прерывает внешние вызовы.

6.17.3.6. Закрепленные байтовые массивы

Закрепленный байтовый массив — это массив, который сборщику мусора запрещено перемещать. Поэтому у него есть стабильный адрес, который можно безопасно получить с помощью byteArrayContents#. Пока массив остается живым, адрес, возвращенный byteArrayContents#, будет действителен. Обратите внимание, что закрепление не мешает сборке мусора для byteArray: он может быть собран так же, как обычный байтовый массив, если на ByteArray# больше нет ссылок. В модуле GHC.Exts есть несколько примитивных функций для закрепления массива и проверки того, закреплен ли он: isByteArrayPinned#, isMutableByteArrayPinned#, isByteArrayWeaklyPinned#, isMutableByteArrayWeaklyPinned# и newPinnedByteArray#. Байтовый массив может быть закрепленным или слабо закрепленным по одной из трех причин:

  1. Он был выделен с помощью newPinnedByteArray#. В результате получается обычный закрепленный байтовый массив.
  2. Он имеет большой размер; в этом случае массив слабо закреплен. В настоящее время GHC считает большим объект размером не менее 80 % блока объемом 4 КБ (то есть не менее 3277 байт).
  3. Он был скопирован в компактную область, в результате чего массив стал слабо закрепленным. Этот процесс описан в документации к ghc-compact и compact.

Разница между закрепленным и слабо закрепленным массивом заключается только в том, что попытка уплотнить закрепленный массив приводит к исключению, а попытка уплотнить слабо закрепленный массив завершается успешно. Однако результаты предыдущих вызовов byteArrayContents# при уплотнении не обновляются. Это означает, что они по-прежнему указывают на исходный адрес массива, а не на его новый адрес внутри компактной области.

Это особенно опасно, если адрес содержимого байтового массива хранится в структуре данных вместе со ссылкой на сам байтовый массив. Если впоследствии структура данных будет уплотнена, указатель не обновится, но ссылка на байтовый массив будет указывать на его копию в компактной области. Тип данных, особенно подверженный этой проблеме, — ForeignPtr при использовании для представления ByteArray#.

Рассмотрим следующий пример:

workWithArrayContents :: (ByteArray, Ptr Word8) -> (Ptr Word8 -> IO ()) -> IO ()
workWithArrayContents (arr@(ByteArray uarr),ptr) worker =
    case () of
      _
        -- Conservative but safe
        | isByteArrayPinned arr -> keepAliveUnlifted uarr (worker ptr)
        -- Potentially dangerous, the program needs to ensures the Ptr points into the array.
        | isByteArrayWeaklyPinned arr -> keepAliveUnlifted uarr (worker ptr)
        | otherwise -> ... -- Otherwise we can't directly use it for safe FFI calls directly at all.

main :: IO ()
main = do
    -- We create a large array, which causes it to be implicitly pinned
    arr <- newByteArray 5000
    arr@(ByteArray uarr) <- freezeByteArray arr 0 5000 -- Make it immutable
    let ptr = byteArrayContents arr

    -- Compacting a data structure that contains both an array and a ptr to
    -- the arrays content's is dangerous and usually the wrong thing to do.
    let foo = (arr, ptr)
    foo_compacted <- compact foo

    -- This is fine
    workWithArrayContents foo do_work
    -- This is unsound
    workWithArrayContents (getCompact foo_compacted) do_work
[1]

До GHC 8.10 при передаче аргумента ArrayArray# внешней функции она получала указатель на StgMutArrPtrs, а не только на полезную нагрузку.

[2] (1,2)

На практике не следует использовать FFI для такой простой задачи, как чтение байтов из MutableByteArray#. Для этого предпочтительнее использовать GHC.Exts.readWord8Array#.

[3]

Как и в случае с [2], для этого FFI на самом деле не нужен. GHC.Exts содержит примитивы для чтения из Array# a, например GHC.Exts.indexArray#.

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.14.1/docs/users_guide/exts/ffi.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API