glean-hs-0.1.0: haskell/src/Glean/FFI.hs
-- | Foreign function interface to the glean-hs Rust substrate.
--
-- Binds the C-ABI functions exported by src/storage/rocksdb.rs.
-- The Rust library must be built before this module can link:
--
-- @
-- cargo build --release
-- cabal build
-- @
--
-- Error handling convention (matching Meta Glean):
-- Every function returns a CString error message.
-- Null pointer = success.
-- Non-null pointer = error message (must be freed via freeError).
module Glean.FFI
( -- * Cache
GleanCache
, newCache
, freeCache
, cacheCapacity
-- * Container
, GleanContainer
, OpenMode (..)
, openContainer
-- * Database
, GleanDatabase
, openDatabase
, freeDatabase
-- * Backup/Restore
, restore
-- * Fact storage
, store
, retrieve
, freeBytes
, getMeta
-- * Error handling
, GleanError
, checkError
) where
import Control.Exception (throwIO, Exception)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU
import Foreign
import Foreign.C.String
import Foreign.C.Types
-- ── Opaque types ──────────────────────────────────────────────────────────────
-- | Opaque handle to a RocksDB block cache.
data CCache
-- | Opaque handle to a RocksDB container (database files on disk).
data CContainer
-- | Opaque handle to a logical Glean database.
data CDatabase
-- | Type aliases for clarity at the Haskell level.
type GleanCache = ForeignPtr CCache
type GleanContainer = ForeignPtr CContainer
type GleanDatabase = ForeignPtr CDatabase
-- ── Error handling ────────────────────────────────────────────────────────────
-- | An error returned by the Rust substrate.
newtype GleanError = GleanError String
deriving (Show)
instance Exception GleanError
-- | Free an error string returned by the Rust substrate.
foreign import ccall unsafe "glean_rocksdb_free_error"
c_free_error :: CString -> IO ()
-- | Check a C error string. Null = success; non-null = throw GleanError.
checkError :: CString -> IO ()
checkError ptr
| ptr == nullPtr = return ()
| otherwise = do
msg <- peekCString ptr
c_free_error ptr
throwIO (GleanError msg)
-- ── Cache FFI ─────────────────────────────────────────────────────────────────
foreign import ccall unsafe "glean_rocksdb_new_cache"
c_new_cache :: CSize -> Ptr (Ptr CCache) -> IO CString
foreign import ccall unsafe "&glean_rocksdb_free_cache"
c_free_cache :: FinalizerPtr CCache
foreign import ccall unsafe "glean_rocksdb_cache_capacity"
c_cache_capacity :: Ptr CCache -> IO CSize
-- | Allocate a new RocksDB block cache of the given size in bytes.
newCache :: Int -> IO GleanCache
newCache size =
alloca $ \pptr -> do
err <- c_new_cache (fromIntegral size) pptr
checkError err
ptr <- peek pptr
newForeignPtr c_free_cache ptr
-- | Free a cache (called automatically by GC via ForeignPtr finalizer).
freeCache :: GleanCache -> IO ()
freeCache = finalizeForeignPtr
-- | Return the capacity of a cache in bytes.
cacheCapacity :: GleanCache -> IO Int
cacheCapacity cache =
withForeignPtr cache $ \ptr ->
fromIntegral <$> c_cache_capacity ptr
-- ── Container FFI ─────────────────────────────────────────────────────────────
foreign import ccall safe "glean_rocksdb_container_open"
c_container_open
:: CString -- path
-> CInt -- mode (0=ReadOnly, 1=ReadWrite, 2=Create)
-> Word8 -- cache_index (CBool)
-> Ptr CCache -- cache (nullable)
-> Ptr (Ptr CContainer)
-> IO CString
-- | How to open a RocksDB container.
data OpenMode
= ReadOnly
| ReadWrite
| Create
deriving (Show, Eq)
openModeToInt :: OpenMode -> CInt
openModeToInt ReadOnly = 0
openModeToInt ReadWrite = 1
openModeToInt Create = 2
-- | Open or create a RocksDB container at the given path.
openContainer
:: FilePath
-> OpenMode
-> Maybe GleanCache -- ^ Optional block cache
-> IO GleanContainer
openContainer path mode mCache =
withCString path $ \cpath ->
alloca $ \pptr -> do
err <- case mCache of
Nothing ->
c_container_open cpath (openModeToInt mode) 0 nullPtr pptr
Just cache ->
withForeignPtr cache $ \cptr ->
c_container_open cpath (openModeToInt mode) 1 cptr pptr
checkError err
ptr <- peek pptr
newForeignPtr_ ptr -- no finalizer yet — container lifetime managed manually
-- ── Database FFI ──────────────────────────────────────────────────────────────
foreign import ccall safe "glean_rocksdb_container_open_database"
c_open_database
:: Ptr CContainer
-> Word64 -- start_id (Fid)
-> Word32 -- first_unit_id (UsetId)
-> Int64 -- version
-> Ptr (Ptr CDatabase)
-> IO CString
foreign import ccall safe "&glean_rocksdb_database_free"
c_free_database :: FinalizerPtr CDatabase
-- | Open a logical database within a container.
openDatabase
:: GleanContainer
-> Word64 -- ^ Starting fact ID (Fid::LOWEST = 1024)
-> Word32 -- ^ First unit ID
-> Int64 -- ^ Schema version
-> IO GleanDatabase
openDatabase container startId firstUnitId version =
withForeignPtr container $ \cptr ->
alloca $ \pptr -> do
err <- c_open_database cptr startId firstUnitId version pptr
checkError err
ptr <- peek pptr
newForeignPtr c_free_database ptr
-- | Free a database handle.
freeDatabase :: GleanDatabase -> IO ()
freeDatabase = finalizeForeignPtr
-- ── Backup/Restore FFI ────────────────────────────────────────────────────────
foreign import ccall safe "glean_rocksdb_restore"
c_restore :: CString -> CString -> IO CString
-- | Restore a database from source path to target path.
restore :: FilePath -> FilePath -> IO ()
restore target source =
withCString target $ \ctarget ->
withCString source $ \csource -> do
err <- c_restore ctarget csource
checkError err
-- ── Fact storage FFI ──────────────────────────────────────────────────────────
foreign import ccall safe "glean_rocksdb_store"
c_store :: Ptr CDatabase -> Ptr Word8 -> CSize -> Word64 -> IO CString
foreign import ccall safe "glean_rocksdb_retrieve"
c_retrieve :: Ptr CDatabase -> Ptr (Ptr Word8) -> Ptr CSize -> IO CString
foreign import ccall unsafe "glean_rocksdb_free_bytes"
c_free_bytes :: Ptr Word8 -> CSize -> IO ()
-- | Store a serialized fact batch into the database.
-- fact_count is the exact number of facts in this batch.
store :: GleanDatabase -> ByteString -> Word64 -> IO ()
store db bytes factCount =
withForeignPtr db $ \dbptr ->
BSU.unsafeUseAsCStringLen bytes $ \(ptr, len) -> do
err <- c_store dbptr (castPtr ptr) (fromIntegral len) factCount
checkError err
-- | Retrieve serialized fact data from the database.
-- Returns Nothing if no facts are stored yet.
retrieve :: GleanDatabase -> IO (Maybe ByteString)
retrieve db =
withForeignPtr db $ \dbptr ->
alloca $ \pptr ->
alloca $ \plen -> do
err <- c_retrieve dbptr pptr plen
checkError err
ptr <- peek pptr
if ptr == nullPtr
then return Nothing
else do
len <- peek plen
bs <- BS.packCStringLen (castPtr ptr, fromIntegral len)
c_free_bytes ptr len
return (Just bs)
-- | Free bytes allocated by retrieve (called internally).
freeBytes :: Ptr Word8 -> Int -> IO ()
freeBytes ptr len = c_free_bytes ptr (fromIntegral len)
-- ── Metadata FFI ──────────────────────────────────────────────────────────────
foreign import ccall safe "glean_rocksdb_get_meta"
c_get_meta :: Ptr CDatabase -> CString -> Ptr Word64 -> IO CString
-- | Read a u64 metadata value by key from the database.
-- Returns 0 if the key does not exist.
getMeta :: GleanDatabase -> String -> IO Word64
getMeta db key =
withForeignPtr db $ \dbptr ->
withCString key $ \ckey ->
alloca $ \pval -> do
err <- c_get_meta dbptr ckey pval
checkError err
peek pval