/trace-foreign-calls

GHC plugin to add eventlog tracing for foreign function calls

Primary LanguageHaskellOtherNOASSERTION

Trace foreign calls

Overview

Suppose we have a foreign import such as

foreign import capi "cbits.h xkcdRandomNumber" someForeignFunInA :: IO CInt

If the module containing the import is compiled with this plugin enabled, this foreign function will be wrapped in a function that emits custom events to the eventlog before and after the foreign call is made. If you run your executable with

$ cabal run your-executable -- +RTS -l

and then inspect the eventlog with ghc-events show, you will see something like this:

..
397876: cap 0: running thread 1
491265: cap 0: trace-foreign-calls: call someForeignFunInA (capi safe "cbits.h xkcdRandomNumber") at CallStack (from HasCallStack):
  someForeignFunInA, called at src/ExamplePkgB.hs:11:21 in example-pkg-B-0.1.0-inplace:ExamplePkgB
491815: cap 0: stopping thread 1 (making a foreign call)
492165: cap 0: running thread 1
500755: cap 0: trace-foreign-calls: return someForeignFunInA
..

Of course any other tooling for the eventlog, such as threadscope, will be able to see these events as well.

Enabling the plugin for your package

Add a dependency to the build-depends of your .cabal file

  build-depends:
      ..
      trace-foreign-calls
      ..

and then enable the module either globally by adding

  ghc-options:
      -fplugin=Plugin.TraceForeignCalls

to your .cabal file, or on a per-module basis by adding this pragma to the module header:

{-# OPTIONS_GHC -fplugin=Plugin.TraceForeignCalls #-}

Plugin options

If you want to see how the plugin transforms your code, you can add a plugin option

{-# OPTIONS_GHC -fplugin=Plugin.TraceForeignCalls
                -fplugin-opt Plugin.TraceForeignCalls:dump-generated #-}

You can disable HasCallStack support by setting

{-# OPTIONS_GHC -fplugin-opt Plugin.TraceForeignCalls:disable-callstack #-}

Enabling the plugin on all (transitive) dependencies

In an ideal world, we could just create a cabal.project file containing

package *
  ghc-options:
    -fplugin-trustworthy
    -plugin-package=trace-foreign-calls
    -fplugin=Plugin.TraceForeignCalls

The first open ensures that if we have dependencies that rely on Safe Haskell, compiling modules with the plugin does not mark them as unsafe, the second line declares which package the plugin comes from, and finally the third line enables the plugin.

Unfortunately, this is not quite sufficient. The problem is that we have not edited the .cabal files of all packages and declared trace-foreign-calls to be a dependency. We could do that, but of course that would be extremely laborious. There are some cabal tickets open about solving this properly (#6881, #7901), but for now we need to use a workaround.

First, we will install the plugin in a fresh cabal store:

$ cabal --store-dir=/tmp/cabal-plugin-store install --lib trace-foreign-calls

Create a cabal.project.plugin file with

import: cabal.project

package *
  ghc-options:
    -package-db=/tmp/cabal-plugin-store/ghc-9.6.4/package.db
    -fplugin-trustworthy
    -plugin-package=trace-foreign-calls
    -fplugin=Plugin.TraceForeignCalls

store-dir: /tmp/cabal-plugin-store

You should then be able to build or run your executable, rebuilding (almost) all of its dependencies, with

$ cabal run --project-file cabal.project.plugin

Upgrading the plugin

When you install a new version of the plugin, cabal will not try to rebuild any dependencies (it does not include the hash of the plugin in the hash of the packages). So wipe your cabal-plugin-store as well as your dist-newstyle directory each time you update your plugin (another good reason for using a separate store for the plugin).

libphread

For reasons currently unclear, enabling the plugin on packages that declare

extra-libraries: pthread

in their .cabal file will cause a compilation failure:

<command line>: User-specified static library could not be loaded (/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/libpthread.a)
Loading static libraries is not supported in this configuration.
Try using a dynamic library instead.

Currently the only known workaround is patch such packages; it many cases it may be possible to simply remove pthread from extra-libraries; alternatively, it may be possible to instead use cc-options:

cc-options: -pthread

An example used to be crypton; see crypton#32 and crypton#33 for examples of both of these options, and see https://stackoverflow.com/a/62561519/742991 for a discussion of the difference between -lphread and -pthread.