diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Revision history for ghc-stack-profiler
 
+## 0.3.0.0 -- 2026-06-23
+
+* Support GHC 10.1 [#29](https://github.com/well-typed/ghc-stack-profiler/pull/29)
+  * Adds supports for optional source locations in stack annotations
+* Add eventlog-socket tests to ghc-stack-profiler [#25](https://github.com/well-typed/ghc-stack-profiler/pull/25)
+
 ## 0.2.0.0 -- 2026-04-10
 
 * Backport stack decoding segmentation fault fix from GHC HEAD [#20](https://github.com/well-typed/ghc-stack-profiler/pull/20)
diff --git a/ghc-stack-profiler.cabal b/ghc-stack-profiler.cabal
--- a/ghc-stack-profiler.cabal
+++ b/ghc-stack-profiler.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.8
 name: ghc-stack-profiler
-version: 0.2.0.0
+version: 0.3.0.0
 license: BSD-3-Clause
 author: Hannes Siebenhandl, Wen Kokke, Matthew Pickering
 maintainer: hannes@well-typed.com
@@ -14,11 +14,13 @@
   To profile a program it needs to be compiled and instrumented with the 'ghc-stack-profiler' package via:
 
   @
-  import GHC.Stack.Profiler.Sampler
+  import GHC.Stack.Profiler
 
   main :: IO ()
-  main = 'withStackProfilerForMyThread' ('SampleIntervalMs' 10) $ do
-      ...
+  main = 
+    'withRootStackProfiler' True $ \ manager -> 
+      'withStackProfilerForMyThread' manager ('SampleIntervalMs' 10) $ do
+        ...
   @
 
   This will spawn a profiling thread that will periodically take a snapshot of the current RTS callstack of your program and serialises it to the eventlog.
@@ -54,9 +56,7 @@
 extra-doc-files: CHANGELOG.md
 category: Profiling, Benchmarking, Development
 tested-with:
-  ghc ==9.10.3
-  ghc ==9.12.2
-  ghc ==9.14.1
+  ghc ==10.1 || ==9.14.1 || ==9.12.2 || ==9.10.3
 
 common warnings
   ghc-options:
@@ -106,7 +106,7 @@
     GHC.Stack.Profiler
     GHC.Stack.Profiler.Commands
     GHC.Stack.Profiler.Decode
-    GHC.Stack.Profiler.FFI
+    GHC.Stack.Profiler.Eventlog.Socket
     GHC.Stack.Profiler.Manager
     GHC.Stack.Profiler.Stack.Compat
     GHC.Stack.Profiler.Stack.Decode
@@ -119,9 +119,9 @@
     binary >=0.8.9.3 && <0.11,
     bytestring >=0.11 && <0.13,
     containers >=0.6.8 && <0.9,
-    ghc-heap >=9.10.1 && <9.16,
-    ghc-internal >=9.1001 && <9.1600,
-    ghc-stack-profiler-core ==0.2.0.0,
+    ghc-heap >=9.10.1 && <10.2,
+    ghc-internal >=9.1001 && <10.200,
+    ghc-stack-profiler-core ==0.3.0.0,
     stm ^>=2.5.3.0 || ^>=2.5.0.0,
     text >=2 && <2.2,
 
@@ -129,7 +129,7 @@
     src
 
   if impl(ghc >=9.14)
-    build-depends: ghc-experimental >=9.1400 && <9.1600
+    build-depends: ghc-experimental >=9.1400 && <10.200
 
   if flag(use-ghc-trace-events) || impl(ghc <9.12)
     cpp-options:
diff --git a/src/GHC/Stack/Annotation/Experimental/Compat.hs b/src/GHC/Stack/Annotation/Experimental/Compat.hs
--- a/src/GHC/Stack/Annotation/Experimental/Compat.hs
+++ b/src/GHC/Stack/Annotation/Experimental/Compat.hs
@@ -3,21 +3,39 @@
 
 module GHC.Stack.Annotation.Experimental.Compat (
   SomeStackAnnotation (..),
-  CallStackAnnotation (..),
-  StringAnnotation (..),
+  showStackAnnotationLocation,
+  showStackAnnotationDescription,
 ) where
 
 #if MIN_VERSION_ghc_internal(9,1400,0)
 import GHC.Stack.Annotation.Experimental
 #else
 import Data.Typeable
-import GHC.Stack.Types (CallStack)
+#endif
+import GHC.Stack.Types (SrcLoc)
 
+
+#if !MIN_VERSION_ghc_internal(9,1400,0)
 data SomeStackAnnotation where
   SomeStackAnnotation :: forall a. (Typeable a) => a -> SomeStackAnnotation
+#endif
 
-data StringAnnotation where
-  StringAnnotation :: String -> StringAnnotation
 
-newtype CallStackAnnotation = CallStackAnnotation CallStack
+showStackAnnotationLocation :: SomeStackAnnotation -> Maybe SrcLoc
+showStackAnnotationLocation =
+#if MIN_VERSION_ghc_internal(9,1402,0)
+  stackAnnotationSourceLocation
+#else
+  \ _ann -> Nothing
+#endif
+
+
+showStackAnnotationDescription :: SomeStackAnnotation -> String
+showStackAnnotationDescription =
+#if MIN_VERSION_ghc_internal(9,1402,0)
+  displayStackAnnotationShort
+#elif MIN_VERSION_ghc_internal(9,1400,0)
+  displayStackAnnotation
+#else
+  \ _ann -> "showStackAnnotationLocation: Impossible, no value should be created with ghc-internal < 9.1400"
 #endif
diff --git a/src/GHC/Stack/Profiler.hs b/src/GHC/Stack/Profiler.hs
--- a/src/GHC/Stack/Profiler.hs
+++ b/src/GHC/Stack/Profiler.hs
@@ -49,7 +49,7 @@
 import GHC.Stack.Profiler.Core.Util
 import GHC.Stack.Profiler.Decode
 import qualified GHC.Stack.Profiler.Decode as Decode
-import qualified GHC.Stack.Profiler.FFI as FFI
+import qualified GHC.Stack.Profiler.Eventlog.Socket as EventlogSocket
 import GHC.Stack.Profiler.Manager
 import GHC.Stack.Profiler.SymbolTable (readSymbolTable)
 
@@ -117,7 +117,7 @@
 runNewStackProfilerManager shouldRun = do
   manager <- newStackProfilerManager shouldRun
   startEventLoopThread manager
-  FFI.installEventlogSocketHandlers manager `catches` FFI.defaultErrorHandlers
+  EventlogSocket.registerWithEventlogSocket manager
   pure manager
 
 shutdownStackProfilerManager :: StackProfilerManager -> IO ()
diff --git a/src/GHC/Stack/Profiler/Decode.hs b/src/GHC/Stack/Profiler/Decode.hs
--- a/src/GHC/Stack/Profiler/Decode.hs
+++ b/src/GHC/Stack/Profiler/Decode.hs
@@ -82,16 +82,13 @@
   go :: SourceLocationId -> SourceLocation -> BinarySourceLocationMessage
   go sid s =
     let
-      (funcId, newFuncName, _) = lookupOrInsertText table (writerTable table) (functionName s)
       (fileId, newFileName, _) = lookupOrInsertText table (writerTable table) (fileName s)
     in
       -- These should always be found
-      assert (not newFuncName) $
-        assert (not newFileName) $
-          MkBinarySourceLocationMessage
-            { binarySourceLocationMessageId = sid
-            , binarySourceLocationRow = line s
-            , binarySourceLocationColumn = column s
-            , binarySourceLocationFunctionId = funcId
-            , binarySourceLocationFilename = fileId
-            }
+      assert (not newFileName) $
+        MkBinarySourceLocationMessage
+          { binarySourceLocationMessageId = sid
+          , binarySourceLocationRow = line s
+          , binarySourceLocationColumn = column s
+          , binarySourceLocationFilename = fileId
+          }
diff --git a/src/GHC/Stack/Profiler/Eventlog/Socket.hs b/src/GHC/Stack/Profiler/Eventlog/Socket.hs
new file mode 100644
--- /dev/null
+++ b/src/GHC/Stack/Profiler/Eventlog/Socket.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE CPP #-}
+
+module GHC.Stack.Profiler.Eventlog.Socket (
+  registerWithEventlogSocket,
+) where
+
+import GHC.Stack.Profiler.Manager (StackProfilerManager)
+
+#ifdef EVENTLOG_SOCKET_SUPPORT
+import qualified Control.Monad.STM as STM
+import GHC.Eventlog.Socket (CommandId (..), Hook (..), registerCommand, registerHook, registerNamespace)
+import GHC.Stack.Profiler.Commands (startProfiling, stopProfiling, sendEnableEventlogMessage, sendDisableEventlogMessage, sendPublishInitEventMessages)
+import GHC.Stack.Profiler.Manager (disableEventLogging)
+import Debug.Trace (traceMarkerIO)
+#endif
+
+-- | Register the @eventlog-socket@ custom command handlers and lifecycle hooks.
+--
+-- This adds support for the following @eventlog-socket@ custom commands:
+--
+-- * @0x01@: Start profiling.
+-- * @0x02@: Stop profiling.
+--
+-- If built with @+control@, this may throw an [@EventlogSocketControlError@](https://hackage-content.haskell.org/package/eventlog-socket/docs/GHC-Eventlog-Socket.html#t:EventlogSocketControlError).
+registerWithEventlogSocket :: StackProfilerManager -> IO ()
+#ifdef EVENTLOG_SOCKET_SUPPORT
+registerWithEventlogSocket = registerWithEventlogSocketIfSupported
+#else
+registerWithEventlogSocket = const $ pure ()
+#endif
+
+#ifdef EVENTLOG_SOCKET_SUPPORT
+-- The real implementation of @registerWithEventlogSocket@.
+registerWithEventlogSocketIfSupported :: StackProfilerManager -> IO ()
+registerWithEventlogSocketIfSupported manager = do
+  -- Register the PostStartEventLogging and PreEndEventLogging hooks.
+  registerHook HookPostStartEventLogging $ startEventLoggingHook manager
+  registerHook HookPreEndEventLogging $ endEventLoggingHook manager
+
+  -- Register the custom commands under the ghc-stack-profiler namespace.
+  ns <- registerNamespace "ghc-stack-profiler"
+  registerCommand ns startProfilerCommandId (startProfilerCommand manager)
+  registerCommand ns stopProfilerCommandId (stopProfilerCommand manager)
+
+-- The @startProfiler@ command ID.
+startProfilerCommandId :: CommandId
+startProfilerCommandId = CommandId 0x1
+
+-- The @stopProfiler@ command ID.
+stopProfilerCommandId :: CommandId
+stopProfilerCommandId = CommandId 0x2
+
+-- | The handler for @eventlog-socket@'s @PostStartEventLogging@ hook.
+--
+-- This publishes the init events, flushes the eventlog, informs the manager
+-- that the eventlog is enabled, and blocks until this message is processed.
+startEventLoggingHook :: StackProfilerManager -> IO ()
+startEventLoggingHook manager = do
+  sendPublishInitEventMessages manager
+  sendEnableEventlogMessage manager
+
+-- | The handler for @eventlog-socket@'s @PreEndEventLogging@ hook.
+--
+-- This stops all profiler threads from writing to the eventlog, which stops
+-- all sampler threads, informs the manager that the eventlog is disabled, and
+-- blocks until this message is processed.
+endEventLoggingHook :: StackProfilerManager -> IO ()
+endEventLoggingHook manager = do
+  STM.atomically $ disableEventLogging manager
+  sendDisableEventlogMessage manager
+
+-- | The handler for the @StartProfiling@ custom command.
+startProfilerCommand :: StackProfilerManager -> IO ()
+startProfilerCommand manager = do
+  traceMarkerIO "ghc-stack-profiler: Start profiling"
+  startProfiling manager
+
+-- | The handler for the @StopProfiling@ custom command.
+stopProfilerCommand :: StackProfilerManager -> IO ()
+stopProfilerCommand manager = do
+  stopProfiling manager
+  traceMarkerIO "ghc-stack-profiler: Stop profiling"
+#endif
diff --git a/src/GHC/Stack/Profiler/FFI.hs b/src/GHC/Stack/Profiler/FFI.hs
deleted file mode 100644
--- a/src/GHC/Stack/Profiler/FFI.hs
+++ /dev/null
@@ -1,96 +0,0 @@
-{-# LANGUAGE CApiFFI #-}
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE ForeignFunctionInterface #-}
-
-module GHC.Stack.Profiler.FFI (
-  installEventlogSocketHandlers,
-  defaultErrorHandlers,
-) where
-
-#if defined(EVENTLOG_SOCKET_SUPPORT)
-import GHC.Eventlog.Socket
-import System.IO (hPutStrLn, stderr)
-#endif
-
-import Control.Exception
-import qualified Control.Monad.STM as STM
-import GHC.Stack.Profiler.Manager
-import GHC.Stack.Profiler.Commands
-
-#if defined(EVENTLOG_SOCKET_SUPPORT)
-startProfilerCommandId :: CommandId
-startProfilerCommandId = CommandId 0x1
-
-stopProfilerCommandId :: CommandId
-stopProfilerCommandId = CommandId 0x2
-#endif
-
--- | Install the @eventlog-socket@ custom command handlers and lifecycle hooks.
---
--- The supported custom commands are:
--- * Start the profiler
--- * Stop the profiler
---
--- We implement the lifecycle hooks for stopping the eventlog and starting
--- writing to the eventlog.
--- When we start eventlogging, we post the definitions of the existing callstack
--- definitions, e.g., string and source locations.
---
--- May throw 'EventlogSocketControlError' when registering @eventlog-socket@
--- hooks fails.
-installEventlogSocketHandlers :: StackProfilerManager -> IO ()
-installEventlogSocketHandlers =
-#if defined(EVENTLOG_SOCKET_SUPPORT)
-  \ manager -> do
-    registerEventlogSocketHooks manager
-  where
-    registerEventlogSocketHooks manager = do
-      registerHook HookPostStartEventLogging $
-        startEventLoggingHook manager
-      registerHook HookPreEndEventLogging $
-        endEventLoggingHook manager
-      ns <- registerNamespace "ghc-stack-profiler"
-      registerCommand ns startProfilerCommandId (startProfilerCommand manager)
-      registerCommand ns stopProfilerCommandId (stopProfilerCommand manager)
-#else
-  \ _manager ->
-    pure ()
-#endif
-
-defaultErrorHandlers :: [Handler ()]
-defaultErrorHandlers =
-  [
-#if defined(EVENTLOG_SOCKET_SUPPORT)
-    Handler $ \ (e :: EventlogSocketControlError) -> do
-      hPutStrLn stderr "Failed to register eventlog-socket commands"
-      hPutStrLn stderr (displayException e)
-#endif
-  ]
-
--- | Post-start EventLogging hook.
---
--- 1. Publish init events and flush the eventlog.
--- 2. Inform the main loop that the eventlog is ready for messages now.
-startEventLoggingHook :: StackProfilerManager -> IO ()
-startEventLoggingHook manager = do
-  sendPublishInitEventMessages manager
-  -- Block until start message has been processed
-  sendEnableEventlogMessage manager
-
--- | Pre-end EventLogging hook.
-endEventLoggingHook :: StackProfilerManager -> IO ()
-endEventLoggingHook manager = do
-  -- Disallow logging any more messages to the eventlog.
-  -- Stops the profiler sampling threads.
-  STM.atomically $ disableEventLogging manager
-
-  -- Block until all messages have been processed
-  sendDisableEventlogMessage manager
-
-startProfilerCommand :: StackProfilerManager -> IO ()
-startProfilerCommand manager = do
-  startProfiling manager
-
-stopProfilerCommand :: StackProfilerManager -> IO ()
-stopProfilerCommand manager = do
-  stopProfiling manager
diff --git a/src/GHC/Stack/Profiler/Stack/Decode.hs b/src/GHC/Stack/Profiler/Stack/Decode.hs
--- a/src/GHC/Stack/Profiler/Stack/Decode.hs
+++ b/src/GHC/Stack/Profiler/Stack/Decode.hs
@@ -6,7 +6,6 @@
 
 import Data.Maybe (catMaybes)
 import qualified Data.Text as Text
-import Data.Typeable (cast)
 import Unsafe.Coerce (unsafeCoerce)
 
 import GHC.Internal.ClosureTypes.Compat
@@ -55,28 +54,21 @@
       let
         Box annotation = getClosureBox stack# (index + offsetStgAnnFrameAnn)
       in
-        pure $ stackAnnotationToStackItem (unsafeCoerce annotation)
+        pure $ Just $ stackAnnotationToStackItem (unsafeCoerce annotation)
     _ ->
       fmap (IpeId . MkIpeId) <$> lookupIpeIdForStackFrame stackItbl
 
-stackAnnotationToStackItem :: SomeStackAnnotation -> Maybe StackItem
-stackAnnotationToStackItem = \case
-  SomeStackAnnotation ann ->
-    case cast ann of
-      Just (CallStackAnnotation cs) ->
-        case getCallStack cs of
-          [] -> Nothing
-          ((name, sourceLoc) : _) ->
-            Just $
-              SourceLocation $
-                MkSourceLocation
-                  { line = intToWord32 $ srcLocStartLine sourceLoc
-                  , column = intToWord32 $ srcLocStartCol sourceLoc
-                  , functionName = Text.pack $ name
-                  , fileName = Text.pack $ srcLocFile sourceLoc
-                  }
-      Nothing -> case cast ann of
-        Just (StringAnnotation msg) ->
-          Just $ UserMessage msg
-        Nothing ->
-          Nothing
+stackAnnotationToStackItem :: SomeStackAnnotation -> StackItem
+stackAnnotationToStackItem someStackAnnotation =
+  let
+    message = showStackAnnotationDescription someStackAnnotation
+    sourceLoc = do
+      srcLoc <- showStackAnnotationLocation someStackAnnotation
+      Just $
+        MkSourceLocation
+          { line = intToWord32 $ srcLocStartLine srcLoc
+          , column = intToWord32 $ srcLocStartCol srcLoc
+          , fileName = Text.pack $ srcLocFile srcLoc
+          }
+  in
+    UserAnnotation message sourceLoc
