Spec-Zone.ru › Haskell 9

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

ForeignFunctionInterface
С момента:

6.8.1

Статус:

Включено в GHC2024, GHC2021, Haskell2010

Разрешить использование интерфейса внешних функций Haskell.

GHC (в основном) соответствует спецификации Haskell Foreign Function Interface, указанной в Haskell Report. Для получения более подробной информации обратитесь к соответствующей главе Haskell Report.

Поддержка FFI включена по умолчанию, но её можно включить или выключить явно с помощью флага ForeignFunctionInterface.

GHC реализует ряд расширений, специфичных для GHC, к главе FFI в Haskell 2010 Report. Эти расширения описаны в Расширения GHC для главы FFI, но обратите внимание, что программы, использующие эти функции, не являются переносимыми. Поэтому эти функции следует избегать, где это возможно.

Документация библиотек FFI приведена в сопроводительной документации библиотеки; см., например, модуль Foreign.

6.17.1. Отличия GHC от главы FFI

6.17.1.1. Гарантированная безопасность вызовов

В Haskell 2010 Report указано, что safe вызовы FFI должны позволять внешним вызовам безопасно вызывать код Haskell. На практике это означает, что вызываемые функции также должны предполагать, что значения Haskell, размещенные в куче, могут перемещаться произвольно, чтобы разрешить работу сборщика мусора.

Это существенно ограничивает авторов библиотек, поскольку подразумевает, что передавать любые ссылки на объекты, размещенные в куче, внешней функции safe небезопасно. Например, часто желательно передавать непривязанные ByteArray# напрямую в нативный код, чтобы избежать в противном случае ненужного копирования. Однако это нельзя безопасно делать для вызовов safe , поскольку массив может быть перемещен сборщиком мусора в середине вызова.

В главе разрешено перемещать объекты во время вызовов unsafe . Таким образом, строго соответствующие Haskell 2010 программы также не могут передавать ссылки на объекты, размещенные в куче, вызовам unsafe FFI.

GHC, начиная с версии 8.4, гарантирует, что сборка мусора никогда не произойдет во время вызова unsafe, даже в интерпретаторе байткода, и гарантирует, что вызовы unsafe будут выполнены в потоке вызова. Это делает безопасным передачу объектов, размещенных в куче, небезопасным функциям.

В предыдущих версиях GHC использовал предоставляемую главой свободу, выполняя safe вызовы внешних функций вместо unsafe вызовов в интерпретаторе байткода. Это означало, что некоторые пакеты, работающие при компиляции, терпят неудачу в GHCi (например, #13730). Но в последних версиях этого уже нет.

6.17.1.2. Взаимодействие между вызовами safe и привязанными потоками

Вызов safe , вызывающий Haskell, выполняется в привязанном потоке RTS. Это означает, что любая вложенность вызовов safe будет выполняться в том же операционной системе потоке. Однако последовательные вызовы safe не имеют этой привилегии и могут выполняться в произвольных потоках ОС.

Это поведение считается деталью реализации, и код, зависящий от состояния локального потока, должен вместо этого использовать один из интерфейсов, предоставленных в Control.Concurrent, чтобы сделать это явным.

Дополнительную информацию о том, что такое привязанные потоки, см. в документации для Control.Concurrent.

Дополнительные сведения об реализации см. в документе: “Расширение интерфейса внешних функций Haskell с помощью одновременности”. Последний доступный по адресу здесь.

6.17.1.3. Varargs не поддерживаются соглашением о вызове ccall

Обратите внимание, что функции, требующие аргументов varargs, не поддерживаются соглашением о вызове 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
Since:

6.8.1

Следующие неподнятые необорачиваемые типы могут использоваться как базовые внешние типы (см. главу FFI, раздел 8.6) для safe и unsafe внешних вызовов: Int#, Word#, Char#, Float#, Double#, Addr#, и StablePtr# a. Кроме того, (# #) может быть использован, если это первый и единственный аргумент функции. Это позволяет более гибко импортировать функции, которые не требуют упорядочения через IO.

Несколько неподнятых оборачиваемых типов могут использоваться в качестве аргументов для вызовов FFI, с учетом следующих ограничений:

  • Действительные аргументы для foreign import unsafe вызовов FFI: Array#, SmallArray#, ByteArray#, и изменяемые аналоги этих типов.
  • Действительные аргументы для foreign import safe вызовов FFI: ByteArray# и MutableByteArray#. Массив байтов должен быть привязанным.
  • Изменение: в обоих foreign import unsafe и foreign import safe вызовах FFI безопасно изменять MutableByteArray. Изменение любого другого типа массива приводит к неопределённому поведению. Причина: Изменяемые массивы объектов кучи регистрируют записи для целей сбора мусора. Если массив объектов кучи передаётся внешней C-функции, среда выполнения не регистрирует никаких записей. Следовательно, запись в массив объектов кучи во внешней функции небезопасна. Поскольку среда выполнения не имеет средств для отслеживания изменений MutableByteArray#, их можно безопасно изменять в любой внешней функции.
  • Обратите внимание, что вызовы safe FFI не предпринимают никаких мер для сохранения своих аргументов в живом состоянии во время выполнения вызываемой 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.2 дополнения к 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
Since:

6.12.1

Status:

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. Более подробная информация находится на странице wiki разработчиков GHC.

6.17.2.5. Прерывимые внешние вызовы

InterruptibleFFI
Since:

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
Since:

7.6.1

Расширение CApiFFI позволяет использовать конвенцию вызова capi в объявлениях внешних функций, например:

foreign import capi "header.h f" f :: CInt -> IO CInt

Вместо генерации кода для вызова f в соответствии с ABI платформы, мы вызываем f с использованием API C, определённого в заголовке 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);

GHC выделяет небольшой объём памяти, связанной с потоком, когда поток вызывает функцию Haskell через foreign export. Эта память обычно не освобождается до 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_tables и hs_unlock_stable_tables вместо hs_lock_stable_ptr_table и hs_unlock_stable_ptr_table. Эти имена теперь устарели.

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 как закодированные в UTF8. Передача других кодировок может привести к неожиданным результатам. Передача 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⟩). Для установки этих параметров нужно вызвать специфичный для GHC API вместо 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, который определяет специфичный для GHC внешний интерфейс RTS, и мы вызвали 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 и более ранние) #included заголовочный файл в файле исходного кода C, сгенерированном из кода Haskell, и компилятор 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#, поэтому выделение и освобождение памяти происходит быстро: намного быстрее, чем C’s malloc/free, но не так быстро, как выделение стека в C. Используйте alloca всякий раз, когда это возможно.

mallocForeignPtr

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

В GHC mallocForeignPtr также реализовано с помощью MutableByteArray#. Хотя память указывает на ForeignPtr, никаких финализаторов фактически нет (если вы не добавите его с помощью addForeignPtrFinalizer), и освобождение выполняется с помощью GC, поэтому 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 import-функцию, которая аннотирована как safe (по умолчанию) в однопоточной среде выполнения (программа была скомпонована без использования -threaded), другие потоки Haskell будут заблокированы до возврата вызова.

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

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

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

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

Если вы делаете внешние вызовы из нескольких потоков Haskell и используете -threaded, убедитесь, что вызываемый вами внешний код является потокобезопасным. В частности, некоторые библиотеки графического интерфейса пользователя не потокобезопасны и требуют, чтобы вызывающий процесс вызывал методы GUI только из одного потока. В таком случае вам может потребоваться ограничить операции с GUI одним потоком 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 export-функции из нескольких потоков ОС параллельно. Система выполнения должна быть инициализирована обычным способом путем вызова hs_init(), и этот вызов должен завершиться до вызова любых foreign export-функций.

6.17.3.4.4. Об использовании hs_exit()

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

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

Следствием этого является то, что если у вас есть потоки Haskell, которые заблокированы в внешних вызовах, то hs_exit() может зависнуть (или, возможно, находится в состоянии ожидания), пока вызовы не вернутся. Поэтому рекомендуется убедиться, что у вас нет таких потоков в системе при вызове 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. Например, при использовании C API на основе обратных вызовов мы регистрируем обратный вызов на C, а затем нам нужно дождаться выполнения этого обратного вызова.

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

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

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

void hs_try_putmvar (int capability, HsStablePtr sp);

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

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

Пример. Предположим, у нас есть функция 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.
  • Мы узнаём текущий номер функциональности и передаём его в C. Этот номер возвращается в hs_try_putmvar, и помогает RTS узнать, какую функциональность нужно попробовать использовать tryPutMVar на. Если вам всё равно, вы можете передать -1 для функциональности hs_try_putmvar, и она выберет произвольную.

    Выбор правильной функциональности поможет избежать ненужных переключений контекста. В идеале вы должны передать функциональность, на которой последний раз работал поток, который будет разбужен, которую вы можете найти, вызвав 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#. Существует несколько примитивных функций в 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.12.1/docs/users_guide/exts/ffi.html

Spec-Zone.ru

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