Настройка
Предположим, у вас есть типичный 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
.У меня все еще есть сильное желание вывести это из монады ввода-вывода в сторону более чистого интерфейса.