Botan
uses a double-pointer method of initializing objects, as the return value is already used for a success / error code.
Given the following C++
declarations:
typedef struct botan_foo_struct* botan_foo_t; // The opaque type
int botan_foo_init(botan_foo_t* foo, const char* foo_name, uint32_t flags); // The initializer
int botan_foo_destroy(botan_foo_t foo); // The destructor
int botan_foo_do_something(botan_foo_t foo); // An action
We can declare the following types and foreign imports to match:
data FooStruct -- botan_foo_struct*
type FooPtr = Ptr FooStruct -- botan_foo_t
foreign import ccall unsafe botan_foo_init :: Ptr FooObj -> IO BotanErrorCode
foreign import ccall unsafe "&botan_foo_destroy" botan_foo_destroy :: FinalizerPtr FooStruct
foreign import ccall unsafe botan_foo_do_something :: FooPtr -> IO BotanErrorCode
NOTE: I have opted to adopt glguy's terminology. Note that
FooPtr
representsbotan_foo_t
, which isbotan_foo_struct*
, but is confusing when foreign calls only mentionbotan_foo_t
. I am considering changingtype FooPtr = Ptr FooStruct
toFooInst
orFooRef
orFooObj
to reflect this. For now, this terminology is fine.
This allows us to define the following type and initializer, which will create objects that are safely destroyed when garbage-collected:
-- TODO: Maybe rename FooCtx?
newtype Foo = Foo { getFooForeignPtr :: ForeignPtr FooStruct } -- Haskell newtype wrapper
withFooPtr :: Foo -> (FooPtr -> IO a) -> IO a -- Convenience function
withFooPtr = withForeignPtr . getFooForeignPtr
fooInit :: IO Foo -- Initializer AND destructor
fooInit = do
alloca $ \ outPtr -> do
throwBotanIfNegative_ $ botan_foo_init outPtr
out <- peek outPtr
fooForeignPtr <- newForeignPtr botan_foo_destroy out
return $ Foo fooForeignPtr
fooDoSomething :: Foo -> IO () -- An action
fooDoSomething foo = withFooPtr foo $ \ fooPtr -> do
throwBotanIfNegative_ $ botan_foo_do_something fooPtr
This pattern should be followed, unless we come across an construct that is initialized in a different manner. If any portion of it is found unsafe, the fix may then be readily applied to all constructs.
The above pattern creates objects that will stay alive as long as they are referenced, and will eventually destroy themselves safely when they are not. However, they do not guarantee prompt cleanup.
For temporary objects with guaranteed immediate cleanup, we may simply wrap them as such.
-- TODO: Generalize / reuse pattern
withCipher :: CipherName -> CipherInitFlags -> (Cipher -> IO a) -> IO a
withCipher name flags act = do
cipher <- cipherInit name flags
act cipher <* cipherDestroy cipher
-- or withCipher = mkWithTemp2 cipherInit cipherDestroy
cipherDestroy :: Cipher -> IO ()
cipherDestroy cipher = finalizeForeignPtr (getCipherForeignPtr cipher)