Автоматическое преобразование типов для вызовов FFI в Haskell

Я определил следующий модуль, чтобы помочь мне с экспортом функции FFI:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, TypeSynonymInstances #-}
module ExportFFI where

import Foreign
import Foreign.C


class FFI basic ffitype | basic -> ffitype where
    toFFI :: basic -> IO ffitype
    fromFFI :: ffitype -> IO basic
    freeFFI :: ffitype -> IO ()

instance FFI String CString where
    toFFI = newCString
    fromFFI = peekCString
    freeFFI = free

Я борюсь с экземпляром для функций. Кто-нибудь может мне помочь?


person Tener    schedule 26.07.2010    source источник
comment
Знаете ли вы о FunPtr? Если нет, то см. Foreign.Ptr и связанную с ним документацию.   -  person Thomas M. DuBuisson    schedule 27.07.2010
comment
Да, я знаю его. Идея здесь состоит в том, чтобы сделать автоматическое преобразование в/из FFI. Например, String -> String станет CString -> CString или что-то подобное.   -  person Tener    schedule 28.07.2010
comment
Пожалуйста, не используйте расширения Haskell в библиотеках привязки FFI!!! это действительно затрудняет сборку с другими компиляторами Haskell, которые не поддерживают эти используемые расширения.   -  person snk_kid    schedule 28.07.2010
comment
Извините, но то же самое верно и для любого другого кода. Зачем делать FFI особенным?   -  person Tener    schedule 28.07.2010
comment
Я уже упоминал почему, это затрудняет повторное использование библиотек привязок с другими компиляторами Haskell, нет необходимости использовать расширения для привязок. Вы в конечном итоге с дублированием усилий. Я пытался создать библиотеку привязок на jhc, чтобы использовать ее на Wii, но эта библиотека использует классы типов с несколькими параметрами, очень похожие на то, что вы делаете, и в этом даже не было необходимости, к счастью, мне удалось избавиться от него с одним классом типа параметра и ограничениями типа, на самом деле мои изменения фактически сделали код лучше, чем исходный код, оставаясь при этом совместимым с haskell98 кодом.   -  person snk_kid    schedule 28.07.2010
comment
Опять же, здесь нет ничего особенного в коде FFI. Если я использую определенные расширения, я знаю, что код не будет переносимым. Трудно повторно использовать любую библиотеку, если она использует расширения, которые не поддерживает ваш компилятор.   -  person Tener    schedule 28.07.2010
comment
Я хочу сказать, что если вы выпускаете библиотеку привязок для использования другими, библиотеки такого типа, скорее всего, будут использоваться гораздо большим количеством людей, да, конечно, это зависит от того, но в целом это так.   -  person snk_kid    schedule 28.07.2010
comment
@snk_kid Возможно, jhc следует улучшить поддержку расширений Haskell?   -  person alternative    schedule 09.07.2011


Ответы (1)


Есть две вещи, которые вы можете сделать с функциями, использующими FFI: 1) Маршалинг: это означает преобразование функции в тип, который можно экспортировать через FFI. Это выполнено FunPtr. 2) Экспорт: это означает создание средства для кода, отличного от Haskell, для вызова функции Haskell.

Ваш класс FFI помогает с маршалингом, и сначала я создаю несколько примеров того, как маршалировать функции.

Это не проверено, но компилируется, и я ожидаю, что это сработает. Во-первых, давайте немного изменим класс:

class FFI basic ffitype | basic -> ffitype, ffitype -> basic where
    toFFI :: basic -> IO ffitype
    fromFFI :: ffitype -> IO basic
    freeFFI :: ffitype -> IO ()

Это говорит о том, что при заданном типе «базовый» или «ffitype» другой является фиксированным[1]. Это означает, что больше невозможно маршалировать два разных значения в один и тот же тип, например. вы больше не можете иметь оба

instance FFI Int CInt where

instance FFI Int32 CInt where

Причина этого в том, что freeFFI нельзя использовать так, как вы его определили; нет способа определить, какой экземпляр выбрать только из ffitype. В качестве альтернативы вы можете изменить тип на freeFFI :: ffitype -> basic -> IO () или (лучше?) freeFFI :: ffitype -> IO basic. Тогда вам вообще не понадобится фундепс.

Единственный способ выделить FunPtr — это оператор «внешнего импорта», который работает только с полностью созданными типами. Вам также необходимо включить расширение ForeignFunctionInterface. В результате функция toFFI, которая должна возвращать IO (FunPtr x), не может быть полиморфной по типам функций. Другими словами, вам понадобится следующее:

foreign import ccall "wrapper"
  mkIntFn :: (Int32 -> Int32) -> IO (FunPtr (Int32 -> Int32))

foreign import ccall "dynamic"
  dynIntFn :: FunPtr (Int32 -> Int32) -> (Int32 -> Int32)

instance FFI (Int32 -> Int32) (FunPtr (Int32 -> Int32)) where
    toFFI = mkIntFn
    fromFFI = return . dynIntFn
    freeFFI = freeHaskellFunPtr

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

Для неупорядочиваемых типов (например, строк) вам нужно что-то более сложное. Прежде всего, поскольку маршалинг происходит в IO, вы можете маршалировать только те функции, которые приводят к действию IO. Если вы хотите маршалировать чистые функции, например. (String -> String), нужно поднять их до вида (String -> IO String).[2] Определим двух помощников:

wrapFn :: (FFI a ca, FFI b cb) => (a -> IO b) -> (ca -> IO cb)
wrapFn fn = fromFFI >=> fn >=> toFFI

unwrapFn :: (FFI a ca, FFI b cb) => (ca -> IO cb) -> (a -> IO b)
unwrapFn fn a = bracket (toFFI a) freeFFI (fn >=> fromFFI)

Они преобразуют типы функций в соответствующие упорядоченные значения, например. wrapStrFn :: (String -> IO String) -> (CString -> IO CString); wrapStrFn = wrapFn. Обратите внимание, что unwrapFn использует «Control.Exception.bracket», чтобы гарантировать освобождение ресурса в случае исключений. Игнорируя это, вы можете написать unwrapFn fn = toFFI >=> fn >=> fromFFI; см. сходство с wrapFn.

Теперь, когда у нас есть эти помощники, мы можем начать писать экземпляры:

foreign import ccall "wrapper"
  mkStrFn :: (CString -> IO CString) -> IO (FunPtr (CString -> IO CString))

foreign import ccall "dynamic"
  dynStrFn :: FunPtr (CString -> IO CString) -> (CString -> IO CString)

instance FFI (String -> IO String) (FunPtr (CString -> IO CString)) where
    toFFI = mkStrFn . wrapFn
    fromFFI = return . unwrapFn . dynStrFn
    freeFFI = freeHaskellFunPtr

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

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

Экспортируемые функции должны иметь маршаллируемые типы, как и FunPtrs. Мы можем просто повторно использовать wrapFn для этого. Чтобы экспортировать несколько функций, все, что вам нужно сделать, это обернуть их wrapFn и экспортировать завернутые версии:

f1 :: Int -> Int
f1 = (+2)

f2 :: String -> String
f2 = reverse

f3 :: String -> IO Int
f3 = return . length

foreign export ccall f1Wrapped :: CInt -> IO CInt
f1Wrapped = wrapFn (return . f1)

foreign export ccall f2Wrapped :: CString -> IO CString
f2Wrapped = wrapFn (return . f2)

foreign export ccall f3Wrapped :: CString -> IO CInt
f3Wrapped = wrapFn f3

К сожалению, эта настройка работает только для функций с одним аргументом. Для поддержки всех функций создадим еще один класс:

class ExportFunction a b where
  exportFunction :: a -> b

instance (FFI a ca, FFI b cb) => ExportFunction (a->b) (ca -> IO cb) where
  exportFunction fn = (wrapFn (return . fn))

instance (FFI a ca, FFI b cb, FFI d cd) => ExportFunction (a->b->d) (ca->cb->IO cd) where
  exportFunction fn = \ca cb -> do
    a <- fromFFI ca
    b <- fromFFI cb
    toFFI $ fn a b

Теперь мы можем использовать exportFunction для функций с 1 и 2 аргументами:

f4 :: Int -> Int -> Int
f4 = (+)

f4Wrapped :: CInt -> CInt -> IO CInt
f4Wrapped = exportFunction f4

foreign export ccall f4Wrapped :: CInt -> CInt -> IO CInt

f3Wrapped2 = :: CString -> IO CInt
f3Wrapped2 = exportFunction f3

foreign export ccall f3Wrapped2 :: CString -> IO CInt
f3Wrapped2 = exportFunction f3

Теперь вам просто нужно написать больше экземпляров ExportFunction для автоматического преобразования любой функции в соответствующий тип для экспорта. Я думаю, что это лучшее, что вы можете сделать без использования какого-либо препроцессора или без unsafePerformIO.

[1] Технически, я не думаю, что есть необходимость в fundep «basic -> ffitype», поэтому вы можете удалить его, чтобы один базовый тип мог сопоставляться с несколькими fftypes. Одной из причин для этого может быть сопоставление целых чисел всех размеров с целыми числами, хотя реализации toFFI будут с потерями.

[2] Небольшое упрощение. Вы можете маршалировать функцию String -> String в тип FFI CString -> IO CString. Но теперь вы не можете преобразовать функцию CString -> IO CString обратно в String -> String из-за IO в возвращаемом типе.

person John L    schedule 28.07.2010
comment
Забыл упомянуть, что это относится только к экспорту функций Haskell. Импорт функций через FFI лучше всего выполнять с помощью чего-то вроде c2hs или GreenCard. - person John L; 28.07.2010
comment
Мое основное внимание сосредоточено на foreign export. Я не уверен, как ваше решение будет сопоставляться с экспортом функций. Из-за этого FunPtr здесь не так важен. - person Tener; 28.07.2010
comment
Вам нужен FunPtr для маршалинга функции, что и делает ваш класс FFI (т.е. он маршалирует). Я добавил немного, чтобы объяснить, как использовать его для экспорта функций, но еще есть над чем поработать. - person John L; 28.07.2010
comment
Возродить это вечность позже, когда я пытаюсь сделать что-то подобное.... Но я не думаю, что ваш класс ExportFunction будет работать так, как вы описали. Ваши два экземпляра кажутся перекрывающимися, так как я думаю, что функция (a -> b -> c) будет соответствовать первому экземпляру, где a :: a и b :: (b -> c) приводят к сигнатуре (ca -> IO (cb -> cc)), что не является желаемым результатом. - person Brandon Ogle; 15.04.2016
comment
Я подозреваю, что это работало в то время. Я думаю, что это все равно будет работать, если вы включите OverlappingInstances (или соответствующие прагмы уровня экземпляра), потому что второй экземпляр строго более конкретен, чем первый. OverlappingInstances не очень хорош, но в ограниченных обстоятельствах это тоже не страшно. - person John L; 20.04.2016