Как создать два ByteStrings вызова этой внешней библиотеки API?


В настоящее время я пишу привязки к криптографической библиотеке, которая предоставляет функцию для генерации ключевых пар:

const size_t PUBLICKEYBYTES = 32;
const size_t SECRETKEYBYTES = 32;
int random_keypair(unsigned char pk[PUBLICKEYBYTES],
                   unsigned char sk[SECRETKEYBYTES]);
Эта функция случайным образом генерирует секретный ключ, вычисляет соответствующий открытый ключ и помещает результаты в pk и sk.

Когда я просто возвращаю один ByteString, я обнаружил, что самый простой способ-использовать create :: Int -> (Ptr Word8 -> IO ()) -> IO ByteString из Data.ByteString.Internal. Однако эта функция не может создать два ByteStrings одновременно.

Мой первый подход состоял в том, чтобы написать что-то например:

newtype PublicKey = PublicKey ByteString
newtype SecretKey = SecretKey ByteString
randomKeypair :: IO (PublicKey, SecretKey)
randomKeypair = do
    let pk = B.replicate 0 publicKeyBytes
        sk = B.replicate 0 secretKeyBytes
    B.unsafeUseAsCString pk $ ppk ->
        B.unsafeUseAsCString sk $ psk ->
        c_random_keypair ppk psk
    return (PublicKey pk, SecretKey sk)

Однако это, похоже, не работает с GHC 7.10.2. При запуске набора тестов я обнаруживаю, что у меня, кажется, есть совместное использование ByteStrings между вызовами функций, что приводит к сбою шифрования/дешифрования и дает неправильные результаты.

Мне удалось обойти эту проблему, определив свою собственную функцию:

createWithResult :: Int -> (Ptr Word8 -> IO a) -> IO (ByteString, a)
createWithResult i f = do
    fp <- B.mallocByteString i
    r <- withForeignPtr fp f
    return (B.fromForeignPtr fp 0 i, r)

И использовать его как:

randomKeypair = fmap (PublicKey *** SecretKey) $
    createWithResult publicKeyBytes $ ppk ->
    B.create secretKeyBytes $ psk ->
    void $ c_random_keypair ppk psk
Это, кажется, работает, все тесты проходят.

Мой вопрос в том, что же такое семантика когда речь заходит о совместном использовании и ссылочной прозрачности, когда речь заходит о монаде IO?

Моя интуиция подсказывала мне (неверно), что я могу решить проблему первым способом, но, по-видимому, я не мог. я считаю, что происходит то, что оптимизатор увидел, что let - утверждения могут быть переведены в определения верхнего уровня, и это было причиной, по которой я получил эти проблемы.
2 5

2 ответа:

Проблема с вашим первым подходом заключается в том, что вы пытаетесь изменить неизменяемое значение (pk и sk в вашей функции). Документы для unsafeUseAsCString говорят:

Изменение строки C, либо в C, либо с помощью poke, приведет к изменению содержимого ByteString, нарушая ссылочную прозрачность

Монада IO не имеет другой семантики, когда речь заходит о совместном использовании и ссылочной прозрачности. На самом деле, let в do блок никоим образом не связан с IO монадой; ваш код эквивалентен:

randomKeypair :: IO (PublicKey, SecretKey)
randomKeypair =
    let pk = B.replicate 0 publicKeyBytes
        sk = B.replicate 0 secretKeyBytes
    in B.unsafeUseAsCString pk (\ppk ->
        B.unsafeUseAsCString sk $ \psk ->
        c_random_keypair ppk psk) >>
    return (PublicKey pk, SecretKey sk)
Теперь ясно видно, что pk и sk могут быть подняты на верхний уровень.

Это не ответ на ваш вопрос, но это слишком долго, чтобы быть помещенным в комментарий.

В качестве Хака, если вы хотите избежать ручного распределения, вы можете использовать два вложенных вызова create и IORef ByteString для хранения байтестринга, созданного самым внутренним create. Например (псевдокод)

secRef <- newIORef ""
pubB <- create publicKeyBytes (\pub -> do
   secB <- create secretKeyBytes (\sec -> void $ c_random_keypair pub sec)
   writeIORef secRef secB)
secB <- readIORef secRef
return (pubB, secB)
Однако я предпочитаю ваш createWithResult этому подходу.