Автоматично преобразуване на типове за 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 ()

Това казва, че като се има предвид типът "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 действие, което връща маршалируем чужд тип.

За немаршалируеми типове (напр. Strings) ви трябва нещо малко по-сложно. Първо, тъй като маршалингът се случва в 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“, така че можете да го премахнете, за да позволите на един основен тип да се съпостави с множество ffitypes. Една от причините да го направите би била да картографирате всички int-ове с размери към Integers, въпреки че имплементациите 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