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