FFI Haskell Callback с государством - PullRequest
7 голосов
/ 15 февраля 2012

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

Предположим, у нас есть код C, требующий обратных вызовов, а заголовок выглядит какследующее:

typedef int CallbackType(char* input, char* output, int outputMaxSize, void* userData)

int execution(CallbackType* caller);

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

Мы делаем подобные вещи в haskell, когда передаемвокруг замыканий с MVars, так что мы все еще можем общаться.Поэтому, когда мы пишем внешний интерфейс, мы хотели бы сохранить этот тип типа.

В частности, вот как может выглядеть код FFI:

type Callback = CString -> CString -> CInt -> Ptr () -> IO CInt

foreign import ccall safe "wrapper"
    wrap_callBack :: Callback -> IO (FunPtr Callback)

foreign import ccall safe "execution"
    execute :: FunPtr Callback -> IO CInt 

Пользователи должны иметь возможностьделать подобные вещи, но это похоже на плохой интерфейс, так как они должны писать обратные вызовы с типом Ptr ().Скорее мы хотели бы заменить это на MVars, которые кажутся более естественными.Итак, мы хотели бы написать функцию:

myCallback :: String -> Int -> MVar a -> (Int, String)
myCallback input maxOutLength data = ...

Чтобы преобразовать в C, мы хотели бы иметь такую ​​функцию:

castCallback :: ( String -> Int -> MVar a -> (Int, String) )
             -> ( CString -> CString -> CInt -> Ptr () -> IO CInt )

main = wrap_callBack (castCallback myCallback) >>= execute

В этом случае castCallback имеет видпо большей части несложно реализовать, преобразуйте строку -> cstring, Int -> CInt и скопируйте выходную строку.

Однако сложная часть заключается в преобразовании MVar в Ptr, который не обязательно сохраняется.

Мой вопрос - как лучше всего написать код обратного вызова в Haskell, с которым все еще можно связаться.

1 Ответ

10 голосов
/ 15 февраля 2012

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

В приложении с частичной функцией трюк состоит в том, чтобы создать частичную функцию с уже примененным MVar, и передать указатель на эту функцию в C. C затем вызовет ее обратно с объектом, который будет помещен в MVar. Пример кода ниже (весь приведенный ниже код получен из того, что я делал раньше - я модифицировал его для примеров здесь, но не тестировал модификации):

-- this is the function that C will call back
syncWithC :: MVar CInt -> CInt -> IO () 
syncWithC m x = do 
              putMVar m x
              return ()

foreign import ccall "wrapper"
  syncWithCWrap :: (CInt -> IO ()) -> IO (FunPtr (CInt  -> IO ()))

main = do
    m <- newEmptyMVar
    -- create a partial function with mvar m already applied. Pass to C. C will back with CInt
    f <- syncWithCWrap $ syncWithC m

Что если ваш объект MVar более сложный? Затем вам нужно создать Storable экземпляр объекта MVar, если он не существует. Например, если я хочу использовать MVar с массивом пар Ints, то сначала определим Storable экземпляр пар Int (SV is Storable Vector, MSV is Storable Mutable Vector):

data VCInt2 = IV2 {-# UNPACK #-} !CInt
                  {-# UNPACK #-} !CInt

instance SV.Storable VCInt2 where
  sizeOf _ = sizeOf (undefined :: CInt) * 2
  alignment _ = alignment (undefined :: CInt)
  peek p = do
             a <- peekElemOff q 0
             b <- peekElemOff q 1
             return (IV2 a b)
    where q = castPtr p
  {-# INLINE peek #-}
  poke p (IV2 a b) = do
             pokeElemOff q 0 a
             pokeElemOff q 1 b
    where q = castPtr p
  {-# INLINE poke #-}

Теперь вы можете просто передать указатель на вектор в C, заставить его обновить вектор и вызвать функцию void без аргументов (поскольку C уже заполняет вектор). Это также позволяет избежать дорогостоящей сортировки данных путем совместного использования памяти между Haskell и C.

-- a "wrapper" import is a converter for converting a Haskell function to a foreign function pointer
foreign import ccall "wrapper"
  syncWithCWrap :: IO () -> IO (FunPtr (IO ()))


-- call syncWithCWrap on syncWithC with both arguments applied
-- the result is a function with no arguments. Pass the function, and 
-- pointer to x to C. Have C fill in x first, and then call back syncWithC 
-- with no arguments
syncWithC :: MVar (SV.Vector VCInt2) -> MSV.IOVector VCInt2 -> IO ()
syncWithC m1 x = do
              SV.unsafeFreeze x >>= putMVar m1
              return ()

На стороне C вам потребуется объявление структуры для VCInt2, чтобы он знал, как его анализировать:

/** Haskell Storable Vector element with two int members **/
typedef struct vcint2{
  int a;
  int b;
} vcint2;

Итак, на стороне C вы передаете ему указатель vcint2 для объекта MVar.

...