Spec-Zone.ru › Haskell 8

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

ForeignFunctionInterface
Since

6.8.1

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

GHC (в основном) соответствует Haskell интерфейсу внешних функций, как указано в отчете Haskell. Для получения более подробной информации см. соответствующий раздел отчета Haskell.

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

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

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

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

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

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

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

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

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

Однако с версии 8.4 это больше не так: GHC гарантирует, что сборка мусора никогда не произойдёт во время вызова unsafe, даже в интерпретаторе байткода, и далее гарантирует, что unsafe вызовы будут выполняться в потоке вызывающей функции.

11.2. Расширения GHC для главы FFI

Функции FFI, описанные в этом разделе, специфичны для GHC. Ваш код не будет переносимым на другие компиляторы, если вы их используете.

11.2.1. Неподнятые типы FFI

UnliftedFFITypes
Since

6.8.1

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

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

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

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

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

foreign import safe

foreign import unsafe

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

чтения

записи

чтения

записи

Array# MutableArray# SmallArray# MutableSmallArray# ArrayArray# MutableArrayArray# непривязанные 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) элемента ArrayArray## 3:

// C source, must include the RTS to make the struct StgArrBytes
// available along with its fields: ptrs and payload.
#include "Rts.h"
int sum_first (StgArrBytes **bufs) {
  StgArrBytes **bufs = (StgArrBytes**)bufsTmp;
  int res = 0;
  for(StgWord ix = 0;ix < arr->ptrs;ix++) {
    res = res + ((int*)(bufs[ix]->payload))[0];
  }
  return res;
}

-- Haskell source, all elements in the argument array must be
-- either ByteArray# or MutableByteArray#. This is not enforced
-- by the type system in this example since ArrayArray is untyped.
foreign import ccall unsafe "sum_first"
  sumFirst :: ArrayArray# -> IO CInt

Хотя GHC позволяет пользователю передавать все неподнятые упакованные типы внешним функциям, некоторые из них не подходят для полезной работы. Хотя Array# неподнятый, элементы в его содержимом подняты, и внешняя функция C не может безопасно принуждать к выполнению функций. Следовательно, внешняя функция C не может де-референсировать ни один из адресов, составляющих содержимое Array#.

11.2.2. Обёртка типа newtype над монадой IO

Спецификация 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

11.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 ()

11.2.4. Примитивные импорты

GHC расширяет FFI дополнительной конвенцией вызова prim, например:

foreign import prim "foo" foo :: ByteArray# -> (# Int#, Int# #)

Это используется для импорта функций, написанных на языке Cmm, которые следуют внутренней конвенции вызова GHC. Аргументы и результаты должны быть невыраженными типами, за исключением того, что аргумент может быть типа Any (через unsafeCoerce#) , а тип результата может быть невыраженным кортежем или типом Any.

Эта функция не предназначена для использования за пределами основных библиотек, которые поставляются с GHC. Для получения более подробной информации см. вики-сайт разработчика GHC.

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

InterruptibleFFI
Since

7.2.1

Это касается взаимодействия внешних вызовов с Control.Concurrent.throwTo. Обычно, когда целевой объект throwTo участвует во внешнем вызове, исключение не поднимается до возврата вызова, и в это время вызывающая сторона заблокирована. Это может привести к потере отклика, что особенно нежелательно в случае прерывания пользователя (например, Ctrl+C). По умолчанию, при получении сигнала Ctrl+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.

Если системный вызов успешно прерван, он вернётся в Haskell, где может быть поднято исключение. Следует быть особенно внимательным при использовании interruptible, убедившись, что вызывающая сторона внешней функции готова справиться с последствиями прерывания вызова; в Unix рекомендуется всегда проверять EINTR, а в Windows обычно нет необходимости обрабатывать ERROR_OPERATION_ABORTED.

11.2.6. Конвенция вызова CAPI

CApiFFI
Since

7.10.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 = ...

11.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 понесёт некоторую дополнительную нагрузку.

11.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 соответственно. Эти имена теперь устарели.

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

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

11.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; см. Перенаправление выходного(ых) результата компиляции.

11.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 и затем изменить необходимые поля, как показано в примере кода выше.

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

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

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

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

HsBool mylib_init(void){
  int argc = 2;
  char *argv[] = { "+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() для завершения системы выполнения.

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

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

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

Опция -#include теперь устарела, и поле include-files в спецификации пакета Cabal игнорируется.

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

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

alloca

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

В GHC alloca реализуется с помощью MutableByteArray#, поэтому выделение и освобождение памяти быстрые: намного быстрее, чем выделение в C 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

Пулы в настоящее время реализуются с помощью malloc/free, поэтому, хотя они могут быть более удобным способом структурировать выделение памяти, чем использование одного из других типов выделения, они не будут более эффективными. Однако мы планируем предоставить улучшенную реализацию пулов с большей производительностью в будущем.

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

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

11.3.4.1. Внешние импорты и многопоточность

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

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

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

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

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

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

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

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

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

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

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

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

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

11.3.4.5. Разбуживание потоков Haskell из C

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

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

  1. Вызов внешнего экспорта имеет большой объем накладных расходов: он создает, например, совершенно новый поток 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 может быть выполнен после возвращения вызова (например, если в данный момент выполняется сборка мусора RTS). Вот почему 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.

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

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

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

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

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

11.3.6. Привязанные массивы байтов

Привязанный массив байтов — это массив, перемещение которого запрещено сборщиком мусора. Следовательно, у него есть стабильный адрес, который можно безопасно запросить с помощью byteArrayContents#. Существует несколько примитивных функций в GHC.Prim <GHC-Prim.html>, используемых для принудительного применения или проверки привязанности: isByteArrayPinned#, isMutableByteArrayPinned#, и newPinnedByteArray#. Массив байтов может быть привязан по трём причинам:

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

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

2(1,2)

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

3

Как и в 2, FFI фактически не требуется для этого. GHC.Exts содержит примитивы для чтения из ArrayArray#.

© 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/8.10.2/docs/html/users_guide/ffi-chap.html

Spec-Zone.ru

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