Автоматическое преобразование типов для вызовов FFI в Haskell - PullRequest
5 голосов
/ 27 июля 2010

Я определил следующий модуль, чтобы помочь мне с экспортом функции 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

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

1 Ответ

6 голосов
/ 28 июля 2010

Есть две вещи, которые вы можете делать с функциями, связанными с 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 ()

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

instance FFI Int CInt where

instance FFI Int32 CInt where

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

Единственный способ выделить 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, вы можете только маршалировать функции, которые приводят к 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

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

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

Экспортируемые функции должны иметь маршаллируемые типы, как и FunPtr s. Мы можем просто повторно использовать 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», так что вы можете удалить его, чтобы разрешить одному базовому типу отображать несколько типов ffitype.Одной из причин этого было бы сопоставить целые числа всех размеров с целочисленными значениями, хотя реализации toFFI были бы с потерями.

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

...