Botan bindings devlog
Monday Update
No meeting notes today, I was the only one in attendance. Last week saw the start of the botan-low + memalloc refactor, mostly getting our feet planted beneath us by dissecting the existing RNG interface, and discussing what we wanted to preserve and what we wanted to replace. Major points of were the complexity of the Internal.ByteString, Make, and Remake modules, the resulting mkBindings as a method of constructing context objects, and a quick review of the RNG interface. Later, in the memory thread we presented an in-depth discussion of the new Memory interface that will be handling the allocation and object construction moving forwards.
Continuing where we left off:
A look at mkBindings and handling context objects - BotanRNG vs RNG
One of the things that I have been mulling over is the necessity of unwrapping and wrapping pointers. The mkBindings function is obviously a critical component, it performs a vital duty, so we cannot get rid of it - and yet it is ugly and terrible. If anything could be improved - here it is. So let us look closer to understand why it exists; then we can replace it.
Botan.Bindings.RNG defines BotanRNG in the following manner:
-- | Opaque RNG struct
data {-# CTYPE "botan/ffi.h" "struct botan_rng_struct" #-} BotanRNGStruct
-- | Botan RNG object
newtype {-# CTYPE "botan/ffi.h" "botan_rng_t" #-} BotanRNG
= MkBotanRNG { runBotanRNG :: Ptr BotanRNGStruct }
deriving newtype (Eq, Ord, Storable)
foreign import capi safe "botan/ffi.h botan_rng_init"
botan_rng_init
:: Ptr BotanRNG
-> ConstPtr CChar
-> IO CInt
foreign import capi safe "botan/ffi.h &botan_rng_destroy"
botan_rng_destroy
:: FinalizerPtr BotanRNGStruct
Then, Botan.Low.RNG defines RNG in the following manner:
newtype RNG = MkRNG { getRNGForeignPtr :: ForeignPtr BotanRNGStruct }
withRNG :: RNG -> (BotanRNG -> IO a) -> IO a
rngDestroy :: RNG -> IO ()
createRNG :: (Ptr BotanRNG -> IO CInt) -> IO RNG
(withRNG, rngDestroy, createRNG)
= mkBindings MkBotanRNG (.runBotanRNG) MkRNG (.getRNGForeignPtr) botan_rng_destroy
rngInit
:: RNGType
-> IO RNG
rngInit = mkCreateObjectCString createRNG botan_rng_init
If we proceed through this step by step (and ignore how awkwards mkBindings is), we note that it is not all that complicated, and that we have several reasons for doing what we did. Making improvements here is valuable, because most all Botan context objects follow this pattern, so whatever we do here probably will affect every other module in a similar manner (we do have a high degree of consistency in that regard).
- We have an opaque Botan struct type signified by an empty data type
BotanRNGStruct- this makes sense because as an opaque type, we can never actually see an instance of it. - We have a newtype wrapper around a
Ptrto aBotanRNGStruct- we can actually have an instance of this, it is just a pointer to an opaque struct. - The empty
data BotanRNGStructandnewtype BotanRNGare necessary in order to useCApiFFIandCTYPE, due to type safety requirements. - This requires boilerplate / wrapping / unwrapping to use the pointer, but is a zero-cost abstraction
- The
Ptris unmanaged, and we must allocate and provide one to be filled - There is a
FinalizerPtrin the form ofbotan_rng_destroy, butPtrneeds to be aForeignPtrto use it- Maybe we should have both
botan_rng_destroyandbotan_rng_destroy_finalizer
- Maybe we should have both
botan-lowlifts theBotanRNGfrom aPtr BotanRNGStructto aForeignPtr BotanRNGStructso we can finalize the memory when no references remain- The
RNGnewtype isn’t strictly necessary, unlikeBotanRNGStructandBotanRNG; it provides value but could be reduced to atypealias. - The Botan API requires that we allocate a temporary pointer
Ptr BotanRNG(that is, aPtr (Ptr BotanRNGStruct)) for the ‘create’ function to populate - Then,
mkBindingstakes that unmanagedPtrand stuffs it into aForeignPtrfor the GC to track - The temporary pointer uses
allocaso it is good to know we are not leaking that pointer either
- The
mkBindingsand egmkCreateObjectCStringare mostly just thin wrappers for converting to and interacting with the inner foreign pointer- The
mkCreateObjectFoofunctions sort of combine this lifting while also handling initialization arguments, but may no longer provide as much / any convenience due to the newMemoryclasses - We didnt have
Memory.Pointerlast time to handle things, so mkBindings filled the gap - The new
Memory.MemoryandMemory.Pointerclasses allow us to provide the same functionality more naturally - I can just access the inner pointer usingwithMem(which also replaces thewithFoothatmkBindingsgenerates)
So, it turns out that some of the complexity is unavoidable - especially the BotanRNGStruct vs BotanRNG vs RNG stuff. We really do need to unwrap the Ptr and pack it into a ForeignPtr - to allow it to be tracked by the GC, and because a ForeignPtr BotanRNG is a ForeignPtr (Ptr BotanRNGStruct) and not a ForeignPtr BotanRNGStruct. We could turn RNG into a type instead of a newtype but type safety would suffer and I’d rather take advantage of the new Memory classes.
But, it seems that we should be able to more or less get rid of mkBindings, if we don’t really need any of its functions anymore! The new memory classes can handle both allocation (create and destroy) and pointer access (withPtr), so all that complexity can just be tossed out.
This does mean that our context objects will have to conform to some classes, and the botan-low library will have slightly more responsiblities, but I think upkeep will be more tolerable, and as a bonus we’ll be polymorphic over the return type of various functions so we can use anything that conforms to the new memory classes instead of being stuck with ByteString. That, I think, makes all this worthwhile.
Discussion in the ATmosphere