Haskell-gi: How to implement a GLib.CompareDataFunc

I have been having a problem finding my way though the Ptr a, ManagedPtr, GObject, and Object maze.

CompareDataFunc takes Ptr () arguments for the items to compare. How do I convert these to some Objects I would like to compare?
In my current case that would be Gtk.StringObjects.

I’m trying to use it in the context of a '#insertSorted" function on
a Gtk.ListStore.

The alternative seems to be to construct a Gtk.SortListModel on the Gtk.StringList, which requires a Gtk.Sorter which if I use a Gtk.StringSorter involves creating a Gtk.Expression which I have know idea how to do.

2 Likes

I searched StringObject in Hoogle for Ptr related stuff, found:
ManagedPtrNewtype StringObject
then searched for ManagerPtr related stuff, found:
withManagedPtr :: (HasCallStack, ManagedPtrNewtype a) => a -> (Ptr a -> IO c) -> IO c
then searched for Ptr a -> Ptr (), found:
castPtr :: Ptr a -> Ptr b

Putting it together:

f = withManagedPtr stringObject1 $ \ptr1 ->
    withManagedPtr stringObject2 $ \ptr2 ->
      let a = castPtr ptr1 :: Ptr ()
          b = castPtr ptr2 :: Ptr () 
      in compareDataFn a b -- takes two ptr () and returns IO Int32
1 Like

You seem to be going in the opposite directory. I was trying to ask how to write the compareDataFn and not how to call it with StringObjects.

You have already explained how to go from an Object to a StringObject using castTo. This answers most of my issues with gtk4 callbacks. However the GLib.CompareDataFunc API uses Ptr () rather than Object arguments, unlike the rest of the haskell-gi APIs.

My issue is getting from a Ptr () to a Object. I know I can castPtr to get to another kind of Ptr, but I’m not sure what it is. I then need to get a ManagedPtr and then a Object. Really a function:

ptrToObject :: Ptr () -> IO (Maybe Object)
1 Like

I finally got it. My code is:

{-# LANGUAGE ImplicitParams      #-}
{-# LANGUAGE OverloadedLabels    #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings   #-}

import Data.GI.Base
import Data.Int (Int32)
import Foreign.Ptr (Ptr, castPtr)
import qualified GI.Gtk as Gtk

compareStringObject :: Ptr ()
                    -> Ptr ()
                    -> IO Int32
compareStringObject ptr1 ptr2 = do
  mbObj1 <- getStringObject ptr1
  mbObj2 <- getStringObject ptr2
  case (mbObj1, mbObj2) of
    (Just obj1, Just obj2) -> do
      str1 <- #getString obj1
      str2 <- #getString obj2
      -- print ("Comparing", str1, str2)
      if str1 < str2 then
        return (-1)
      else if str1 > str2 then
        return 1
      else do
        return 0
    _ -> do
          print "compareStringObject broken"
          return 0
  where
    getStringObject ::  Ptr () -> IO (Maybe Gtk.StringObject)
    getStringObject ptr = do
      let optr = castPtr ptr :: Ptr Object
      withNewObject optr $ \obj ->
        castTo Gtk.StringObject obj
2 Likes

The ptrToObject I was looking for could be written as:

    ptrToObject :: Ptr () -> IO Object
    ptrToObject ptr = do
      let optr = castPtr ptr :: Ptr Object
      withNewObject optr return

And seems more generally useful.

1 Like

Cool.
I noticed that (Maybe Object) and (Maybe StringObject) implement IsGValue so you can do

ptrToObject :: Ptr () -> IO (Maybe Object)
ptrToObject ptr = gvalueGet_ (castPtr ptr :: Ptr GValue)
1 Like

I think it returns a IO Object not a IO (Maybe Object). It seems that any code still working with foreign pointers is unsafe. I found that out trying to convert it to a Ptr String and getting a junk string for output.

BTW, I tried using Chat GPT to answer the question. Even after several hours of getting back broken code, and feeding it the error messages it didn’t get close to a proper answer or code that could compile. It eventually got in a loop feeding back code I already explained to it that it was wrong. I did get back a lot of Ptr, ManagedPtr, GObject conversions but nothing I could use even as a hint. Your reverse operation gave me much more help in figuring out what I need to do.

1 Like

Yes. A Ptr () can be anything. It’s like void*, so you better know what’s in the Ptr before casting, like in C, otherwise it’s garbage.

If you are working with GValues rather than raw C types (like CString) then castTo and checkInstanceType can give you some security with the Maybes so you don’t cast a StringObject to a Sorter.

Many Haskell topics are too obscure for ChatGPT, so it’s a good thing we talk to generate some Google search results.

Hoogle helps a lot playing type tetris, it’s a great tool.

You can check the instances in StringObject in Hoogle to see IsGValue (Maybe StringObject).

You can click on the small + boxes to see the class implementation and see gvalueGet_ :: Ptr GValue -> IO (Maybe StringObject).

1 Like

Yes.

In my above code I tried:

    ptrToObject':: Ptr () -> IO (Maybe Object)
    ptrToObject' ptr =
      gvalueGet_ (castPtr ptr :: Ptr GValue)

    ptrToObject :: Ptr () -> IO (Maybe Object)
    ptrToObject ptr = do
      let optr = castPtr ptr :: Ptr Object
      withNewObject optr (return . Just)

    getStringObject ::  Ptr () -> IO (Maybe Gtk.StringObject)
    getStringObject ptr = do
      mbobj <- ptrToObject ptr
      case mbobj of
        Just obj ->
          castTo Gtk.StringObject obj
        Nothing -> do
          print "ptrToObject failed"
          return Nothing

Everything compiled just fine. But your version ptrToObject' gave me a segmentation fault (core dumped). I can only conclude that the pointer passed to the compare function isn’t a Ptr GValue but a Ptr Object.

1 Like

Continuing my own experiments, I varied my ‘complex’ example to allow sorting of the list store (MyTypePrivate was made an instance of Ord):

{-# LANGUAGE ImplicitParams      #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE OverloadedLabels    #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main
  ( main
  ) where

import           Control.Monad ( void )
import           Control.Monad.IO.Class ( MonadIO (..))
import           Control.Monad.Extra ( whenJust )
import           Control.Monad.Trans.Maybe ( MaybeT (..) )
import           Data.MyTypeObject
import           Data.GI.Base
                   ( AttrOp (..), castTo, get, new, on, set, withNewObject )
import           Data.GI.Base.GObject
                   ( DerivedGObject (..), gobjectGetPrivateData
                   , gobjectSetPrivateData, registerGType
                   )
import           Data.Text ( Text, pack )
import           Foreign.Ptr ( Ptr, castPtr )
import           GI.GObject ( Object (..), toObject )
import qualified GI.Gio as Gio
import qualified GI.GLib as GLib
import qualified GI.Gtk as Gtk

activate :: Gtk.Application -> IO ()
activate app = do
  gType <- registerGType MyTypeObject
  listStore <- Gio.listStoreNew gType
  exampleList <- exampleObjects
  Gio.listStoreSplice listStore 0 0 exampleList
  #sort listStore (compareDataFunc MyTypeObject)
  singleSelection <- Gtk.singleSelectionNew (Just listStore)
  signalListItemFactory <- Gtk.signalListItemFactoryNew
  scrolledWindow <- Gtk.scrolledWindowNew

  let setupListItem :: Gtk.SignalListItemFactorySetupCallback
      setupListItem o = do
        mListItem <- castTo Gtk.ListItem o
        whenJust mListItem $ \listItem -> do
          label <- new Gtk.Label []
          set listItem [ #child := label ]

  let bindListItem :: Gtk.SignalListItemFactoryBindCallback
      bindListItem o = void $ runMaybeT $ do
          listItem     <- MaybeT $ castTo Gtk.ListItem o
          item         <- MaybeT $ get listItem #item
          myTypeObject <- MaybeT $ castTo MyTypeObject item
          widget       <- MaybeT $ get listItem #child
          label        <- MaybeT $ castTo Gtk.Label widget
          liftIO $ do
            myTypePrivate <- gobjectGetPrivateData myTypeObject
            let string = case myTypePrivate of
                  MyTypePrivate (Just f) (Just c) ->
                       f <> " (" <> pack (show c) <> ")"
                  MyTypePrivate (Just f) Nothing -> f
                  MyTypePrivate _ _ -> ""
            set label [ #label := string ]

  void $ on signalListItemFactory #setup setupListItem
  void $ on signalListItemFactory #bind bindListItem

  listView <-
    Gtk.listViewNew (Just singleSelection) (Just signalListItemFactory)

  set scrolledWindow [ #child := listView ]

  window <- new Gtk.ApplicationWindow
    [ #application := app
    , #title := "ListView test (complex)"
    , #child := scrolledWindow
    , #defaultHeight := 200
    ]
  window.show

exampleFruitJumbled :: [Text]
exampleFruitJumbled =
  [ "apple"
  , "tangerine"
  , "banana"
  , "strawberry"
  , "cherry"
  , "raspberry"
  , "damson"
  , "quince"
  , "elderberry"
  , "fig"
  , "peach"
  , "grape"
  , "orange"
  , "kiwi"
  , "nectarine"
  , "lemon"
  , "watermelon"
  , "mango"
  ]

exampleCount :: [Int]
exampleCount = [1 .. 18]

exampleObjects :: IO [Object]
exampleObjects = mapM (toObject =<<) exampleMyType
 where
  exampleMyType = zipWith go exampleFruitJumbled exampleCount
  go f c = do
    myTypeObject <- new MyTypeObject []
    gobjectSetPrivateData myTypeObject $ MyTypePrivate
      { fruit = Just f
      , count = Just c
      }
    pure myTypeObject

compareDataFunc ::
     forall o. (DerivedGObject o, Ord (GObjectPrivateData o))
  => (Gtk.ManagedPtr o -> o)
     -- ^ The data constructor for the type of the items to be sorted.
  -> GLib.CompareDataFunc
compareDataFunc _ ptrA ptrB =
  withNewObject ptrA' $ \myTypeObjectA ->
    withNewObject ptrB' $ \myTypeObjectB -> do
      myTypePrivateA <- gobjectGetPrivateData myTypeObjectA
      myTypePrivateB <- gobjectGetPrivateData myTypeObjectB
      pure $ case compare myTypePrivateA myTypePrivateB of
        LT -> -1
        EQ -> 0
        GT -> 1
 where
  ptrA' = castPtr ptrA :: Ptr o
  ptrB' = castPtr ptrB :: Ptr o

main :: IO ()
main = do
  app <- new Gtk.Application
    [ #applicationId := "com.pilgrem.testListView.complex"
    , On #activate (activate ?self)
    ]
  void $ app.run Nothing

with package.yaml (extract):

dependencies:
- base >= 4.7 && < 5
- extra
- gi-gio
- gi-glib
- gi-gobject
- gi-gtk >= 4.0
- haskell-gi-base
- text
- transformers
1 Like