Skip to content

Commit

Permalink
Merge pull request #3 from well-typed/edsko/example-packages
Browse files Browse the repository at this point in the history
Example packages
  • Loading branch information
edsko authored Apr 24, 2024
2 parents 7b8af41 + fef1060 commit 4b0bee5
Show file tree
Hide file tree
Showing 28 changed files with 353 additions and 26 deletions.
36 changes: 25 additions & 11 deletions .github/workflows/haskell-ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# This GitHub workflow config has been generated by a script via
#
# haskell-ci 'github' 'cabal.project.ci'
# haskell-ci 'github' 'cabal.project'
#
# To regenerate the script (for example after adjusting tested-with) run
#
# haskell-ci regenerate
#
# For more information, see https://github.com/haskell-CI/haskell-ci
#
# version: 0.19.20240402
# version: 0.19.20240421
#
# REGENDATA ("0.19.20240402",["github","cabal.project.ci"])
# REGENDATA ("0.19.20240421",["github","cabal.project"])
#
name: Haskell-CI
on:
Expand All @@ -23,7 +23,7 @@ jobs:
timeout-minutes:
60
container:
image: buildpack-deps:bionic
image: buildpack-deps:jammy
continue-on-error: ${{ matrix.allow-failure }}
strategy:
matrix:
Expand Down Expand Up @@ -119,13 +119,15 @@ jobs:
chmod a+x $HOME/.cabal/bin/cabal-plan
cabal-plan --version
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: source
- name: initial cabal.project for sdist
run: |
touch cabal.project
echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
echo "packages: $GITHUB_WORKSPACE/source/trace-foreign-calls" >> cabal.project
echo "packages: $GITHUB_WORKSPACE/source/example-pkg-A" >> cabal.project
echo "packages: $GITHUB_WORKSPACE/source/example-pkg-B" >> cabal.project
cat cabal.project
- name: sdist
run: |
Expand All @@ -139,25 +141,33 @@ jobs:
run: |
PKGDIR_trace_foreign_calls="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/trace-foreign-calls-[0-9.]*')"
echo "PKGDIR_trace_foreign_calls=${PKGDIR_trace_foreign_calls}" >> "$GITHUB_ENV"
PKGDIR_example_pkg_A="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/example-pkg-A-[0-9.]*')"
echo "PKGDIR_example_pkg_A=${PKGDIR_example_pkg_A}" >> "$GITHUB_ENV"
PKGDIR_example_pkg_B="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/example-pkg-B-[0-9.]*')"
echo "PKGDIR_example_pkg_B=${PKGDIR_example_pkg_B}" >> "$GITHUB_ENV"
rm -f cabal.project cabal.project.local
touch cabal.project
touch cabal.project.local
echo "packages: ${PKGDIR_trace_foreign_calls}" >> cabal.project
echo "packages: ${PKGDIR_example_pkg_A}" >> cabal.project
echo "packages: ${PKGDIR_example_pkg_B}" >> cabal.project
echo "package trace-foreign-calls" >> cabal.project
echo " ghc-options: -Werror=missing-methods" >> cabal.project
echo "package example-pkg-A" >> cabal.project
echo " ghc-options: -Werror=missing-methods" >> cabal.project
echo "package example-pkg-B" >> cabal.project
echo " ghc-options: -Werror=missing-methods" >> cabal.project
cat >> cabal.project <<EOF
package trace-foreign-calls
tests: True
EOF
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(trace-foreign-calls)$/; }' >> cabal.project.local
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(example-pkg-A|example-pkg-B|trace-foreign-calls)$/; }' >> cabal.project.local
cat cabal.project
cat cabal.project.local
- name: dump install plan
run: |
$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
cabal-plan
- name: restore cache
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
path: ~/.cabal/store
Expand All @@ -179,6 +189,10 @@ jobs:
run: |
cd ${PKGDIR_trace_foreign_calls} || false
${CABAL} -vnormal check
cd ${PKGDIR_example_pkg_A} || false
${CABAL} -vnormal check
cd ${PKGDIR_example_pkg_B} || false
${CABAL} -vnormal check
- name: haddock
run: |
$CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
Expand All @@ -187,7 +201,7 @@ jobs:
rm -f cabal.project.local
$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
- name: save cache
uses: actions/cache/save@v3
uses: actions/cache/save@v4
if: always()
with:
key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
Expand Down
128 changes: 116 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,132 @@
# Eventlog tracing for foreign function calls
# Trace foreign calls

Suppose we have a module containing
## Overview

Suppose we have a `foreign import` such as

```haskell
foreign import capi "test_cbits.h answer" c_answerIO :: IO CInt
foreign import capi "cbits.h xkcdRandomNumber" someForeignFunInA :: IO CInt
```

If we compile this module with the plugin from this package by adding this to
the module header:
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

```
{-# OPTIONS_GHC -fplugin=Plugin.TraceForeignCalls #-}
```bash
$ cabal run your-executable -- +RTS -l
```

then the foreign call will be wrapped, so that any call to this particular
foreign function will result in events such as this in the eventlog:
and then inspect the eventlog with
[`ghc-events`](https://hackage.haskell.org/package/ghc-events) `show`, you will
see something like this:

```
..
694754: cap 0: start foreign call c_answerIO
..
697444: cap 0: stop foreign call c_answerIO
379677: cap 0: running thread 1
446746: cap 0: trace-foreign-calls: call someForeignFunInA (capi safe "cbits.h xkcdRandomNumber")
447526: cap 0: stopping thread 1 (making a foreign call)
447746: cap 0: running thread 1
451726: cap 0: trace-foreign-calls: return someForeignFunInA
..
```

Of course any other tooling for the eventlog, such as
[`threadscope`](https://hackage.haskell.org/package/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

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

and then enable the module either globally by adding

```cabal
ghc-options:
-fplugin=Plugin.TraceForeignCalls
```

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

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

## Debugging

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

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

## Enabling the plugin on all (transitive) dependencies

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

```cabal
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](https://github.com/haskell/cabal/issues/6881),
[7901](https://github.com/haskell/cabal/issues/7901)), but for now we need to
use a workaround.

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

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

Create a `cabal.project.plugin` file with

```cabal
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

```bash
$ 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).
8 changes: 7 additions & 1 deletion cabal.project
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
packages: .
packages: trace-foreign-calls, example-pkg-A, example-pkg-B

package trace-foreign-calls
tests: True

package example-pkg-A
tests: True

package example-pkg-B
tests: True
10 changes: 10 additions & 0 deletions cabal.project.plugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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
30 changes: 30 additions & 0 deletions example-pkg-A/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Copyright (c) 2024, Edsko de Vries

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.

* Neither the name of Edsko de Vries nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3 changes: 3 additions & 0 deletions example-pkg-A/cbits/cbits.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
int xkcdRandomNumber() {
return 4;
}
2 changes: 2 additions & 0 deletions example-pkg-A/cbits/cbits.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// https://xkcd.com/221/
int xkcdRandomNumber();
47 changes: 47 additions & 0 deletions example-pkg-A/example-pkg-A.cabal
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
cabal-version: 3.0
name: example-pkg-A
version: 0.1.0
synopsis: Example package A
description: This is an example that imports a foreign function. The
function is used in example package B.
license: BSD-3-Clause
license-file: LICENSE
author: Edsko de Vries
maintainer: [email protected]
category: Development
build-type: Simple
extra-source-files: cbits/cbits.h
cbits/cbits.c
tested-with: GHC ==9.6.4

common lang
ghc-options:
-Wall
build-depends:
base >= 4.18 && < 4.19
default-language:
GHC2021

library
import:
lang
exposed-modules:
ExamplePkgA
hs-source-dirs:
src
include-dirs:
cbits
c-sources:
cbits/cbits.c

test-suite test-A
import:
lang
ghc-options:
-main-is TestA
type:
exitcode-stdio-1.0
main-is:
test/TestA.hs
build-depends:
example-pkg-A
7 changes: 7 additions & 0 deletions example-pkg-A/src/ExamplePkgA.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{-# LANGUAGE CApiFFI #-}

module ExamplePkgA (someForeignFunInA) where

import Foreign.C

foreign import capi "cbits.h xkcdRandomNumber" someForeignFunInA :: IO CInt
6 changes: 6 additions & 0 deletions example-pkg-A/test/TestA.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module TestA (main) where

import ExamplePkgA

main :: IO ()
main = print =<< someForeignFunInA
Loading

0 comments on commit 4b0bee5

Please sign in to comment.