Haskell FFI: правильный способ передачи и возврата ByteStrings - PullRequest
0 голосов
/ 23 мая 2018

Настройка

Предположим, у вас есть типичный C API, где некоторая структура выделяется, затем инициализируется (выделяется память, создаются дескрипторы и т. Д.), Затем обрабатывается (преобразуется, запрашивается,и т. д.) с использованием различных функций и, наконец, освобождается снова.Т.е. стандартная OO-структура в стиле C, где структура - это «объект», а функции - «функции-члены».

В частности, позвольте API работать с байтовыми строками.Например, это может быть библиотека сжатия.

В Haskell Data.ByteString.ByteString, на самом низком уровне, является просто «закрепленной памятью в куче ГХ» (выделяется с помощью mallocPlainForeignPtrBytes), указатель на который является первым аргументом для конструктора ByteString PS.Базовая память ByteString может быть получена с помощью Data.ByteString.Internal.toForeignPtr.Пока все хорошо.

Передача ByteString

Удобно позволить Haskell управлять ByteString, на котором работает C API.В Си у нас может быть структура и функция инициализации, подобная этой:

   typedef struct { 
       uint8_t* buf; 
       size_t buflen; 
   } API_t;

   void API_init_as_writer(API_t* p, uint8_t* buf, size_t buflen);

buf и buflen содержат Haskell ByteString.Этот объект становится «писателем».Привязка Haskell будет выглядеть следующим образом:

   data API  -- empty decl (needs EmptyDataDecls extension)

   foreign import ccall unsafe "API.h API_init_as_writer"
       init :: Ptr API -> Ptr Word8 -> Int -> IO ()

, где Ptr Word8 получается с toForeignPtr и withForeignPtr.

Возвращая (передавая) a ByteString

Аналогично, мы можем позволить объекту (структуре) стать «читателем» и создать ByteString, как описано выше, используя mallocPlainForeignPtrBytes и PS.

Сборка мусора мешает!

Проблема в том, что сборщик мусора не знает, что ByteStringsиспользуется (через структуру, которая содержит указатель на память в куче GC) после вызова функций API_init_ *.В случае «писателя»:

    writer <- initWriter 1024     -- inits with buflen of 1024 via mallocPlainForeignPtrBytes
    writeSomething writer
    writeSomethingElse writer
    ...
    bs <- getByteString writer    -- construct ByteString with PS
    writeFile "Some.file" bs

и в случае «читателя»:

    bs <- ByteString.readFile("Some.file")
    reader <- initReader bs
    info1 <- readSomething reader
    info2 <- readSomethingElse reader

Все это происходит в монаде IO.

Очевидно, это не работает.ByteString может собирать мусор после initWriter или initReader, а функции writeSomething или readSomething будут продолжать использовать память, собираемую мусором.

И, несмотря на любые проблемы с сборкой мусора, какByteString был «небезопасен» из монады ввода-вывода, эти операции API ByteString не должны быть включены в монаду ввода-вывода, потому что на самом деле никакого ввода-вывода или взаимодействия с RealWorld не происходит.

Таким образом, вопрос: Как это правильно структурировано в Haskell?

Этот ТАК вопрос наводит на мысль об использовании IORef,что не очень приятно.

Я догадываюсь, что это нужно сделать работоспособным.Последовательный характер этих API-интерфейсов типа OO (alloc, init, opera, operating, ..., free) требует введения монады, отличной от монады IO, я полагаю.Но я не совсем понимаю, как правильно это настроить.Когда я смотрю на привязки базы данных, я вижу только монаду ввода-вывода.Например, функция db_get привязки BerkeleyDB возвращает IO (Maybe ByteString) - проблема GC не возникает, поскольку ByteString является результатом запроса.Я сейчас немного озадачен.Я предполагаю, что поражаю точку трения между ОО и ФП.Или, может быть, я смотрю на это совершенно неправильно.

Обновление 1

Как указывает @Alec, проблемы с ГХ могут быть разрешены (решены?)используя touchForeignPtr.У меня все еще есть сильное желание вывести это из монады ввода-вывода в сторону более чистого интерфейса.

...