Skip to content

Commit 63ace18

Browse files
committed
Fourth attempt
1 parent 374c661 commit 63ace18

19 files changed

Lines changed: 1683 additions & 873 deletions

File tree

examples/c-qrcode/generate-and-run.sh

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ echo "# "
1010
echo "# Building qrcode C bindings"
1111
echo "# "
1212

13-
git submodule init QR-Code-generator
14-
git submodule update QR-Code-generator
15-
16-
cd "$SCRIPT_DIR/QR-Code-generator/c"
17-
make
13+
# Check out the submodule and build the C library only if it is not already
14+
# built. Delete libqrcodegen.a to force a rebuild.
15+
if [ -e "$SCRIPT_DIR/QR-Code-generator/c/libqrcodegen.a" ]; then
16+
echo "# libqrcodegen.a already built, skipping submodule checkout and make"
17+
else
18+
git submodule update --init "$SCRIPT_DIR/QR-Code-generator"
19+
( cd "$SCRIPT_DIR/QR-Code-generator/c" && make )
20+
fi
1821

1922
echo "# "
2023
echo "# Generating Haskell bindings"
@@ -32,28 +35,27 @@ cabal run hs-bindgen-cli -- \
3235
"$SCRIPT_DIR/QR-Code-generator/c/qrcodegen.h"
3336

3437
echo "# "
35-
echo "# Updating cabal.project.local"
38+
echo "# Writing cabal.project.local"
3639
echo "# "
3740

38-
LINE=$(
39-
cat <<-EOF
41+
# cabal.project.local is gitignored and machine-specific, so rewrite it from
42+
# scratch each run rather than appending (which would accumulate stale paths).
43+
cat > "$SCRIPT_DIR/hs-project/cabal.project.local" <<EOF
4044
package c-qrcode
4145
extra-include-dirs:
4246
$SCRIPT_DIR/QR-Code-generator/c/
4347
extra-lib-dirs:
4448
$SCRIPT_DIR/QR-Code-generator/c/
4549
EOF
46-
)
47-
grep -qxF "$LINE" "$SCRIPT_DIR/hs-project/cabal.project.local" || echo "$LINE" >>"$SCRIPT_DIR/hs-project/cabal.project.local"
4850
cat "$SCRIPT_DIR/hs-project/cabal.project.local"
4951

5052
echo "# "
5153
echo "# Done!"
5254
echo "# "
5355
echo "Running the project"
5456

55-
cd $SCRIPT_DIR/hs-project
56-
export LD_LIBRARY_PATH=$SCRIPT_DIR/QR-Code-generator/c/:\$LD_LIBRARY_PATH
57+
cd "$SCRIPT_DIR/hs-project"
58+
export LD_LIBRARY_PATH="$SCRIPT_DIR/QR-Code-generator/c/:$LD_LIBRARY_PATH"
5759

58-
cabal build
59-
cabal run c-qrcode
60+
cabal build exe:c-qrcode
61+
cabal run exe:c-qrcode
Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
{-# LANGUAGE TypeApplications #-}
2+
{-# LANGUAGE TypeFamilies #-}
3+
4+
-- The DefaultIn instances for the generated enums below are orphans: the enum types
5+
-- and the class are both defined elsewhere. The natural home is the generated module
6+
-- (ideally hs-bindgen would emit them), so silence the expected warning here.
7+
{-# OPTIONS_GHC -Wno-orphans #-}
28

39
-- | High-level wrappers around the generated qrcodegen bindings.
410
--
@@ -11,21 +17,33 @@ module HighLevel (
1117
import Data.Word (Word8)
1218
import Foreign.Ptr (Ptr)
1319

14-
import HsBindgen.Runtime.HighLevel.ToHighLevel (OutMarshaller, input, output,
15-
pureIn, scratchArray,
16-
toHighLevel)
17-
import HsBindgen.Runtime.HighLevel.ToHighLevel.Defaults (defaultIn, defaultRes)
18-
import HsBindgen.Runtime.HighLevel.ToHighLevel.Marshallers (peekIncompleteArrayOut,
19-
withCStringIn,
20-
withConstIncompleteArrayIn)
20+
import HsBindgen.Runtime.HighLevel (output, scratchArray, toHighLevel)
21+
import HsBindgen.Runtime.HighLevel.Defaults (DefaultIn (..), auto)
22+
import HsBindgen.Runtime.HighLevel.Marshaller (Unmarshaller, scalar)
23+
import HsBindgen.Runtime.HighLevel.Marshaller.Utils (peekIncompleteArrayOut)
2124
import HsBindgen.Runtime.IncompleteArray (IncompleteArray)
2225

2326
import QRCodeGenerator.Generated qualified as QR
2427
import QRCodeGenerator.Generated.Safe qualified as QR
2528

26-
-- | Lift @qrcodegen_encodeText@. @tempBuffer@ is a 'scratchArray', so it adds
27-
-- nothing to the result: only the QR code and the success flag come back. The
28-
-- scalar inputs and the result use default marshallers.
29+
-- | Passthrough defaults for the generated enums: each enum is its own C argument
30+
-- type, so the default marshaller is @scalar id@. With these in scope, 'auto' can
31+
-- fill the @ecc@ and @mask@ arguments of 'encodeText'. These are orphan instances;
32+
-- the natural home is alongside the enum in the generated module, so ideally
33+
-- hs-bindgen would emit them.
34+
instance DefaultIn QR.Qrcodegen_Ecc where
35+
type DefInArrow QR.Qrcodegen_Ecc lo = QR.Qrcodegen_Ecc -> lo
36+
defaultIn = scalar id
37+
38+
instance DefaultIn QR.Qrcodegen_Mask where
39+
type DefInArrow QR.Qrcodegen_Mask lo = QR.Qrcodegen_Mask -> lo
40+
defaultIn = scalar id
41+
42+
-- | Lift @qrcodegen_encodeText@. Only the two positions that need a human decision
43+
-- are explicit: @tempBuffer@ is a 'scratchArray' (the callee writes it, the caller
44+
-- never sees it) and @qrcode@ is the 'output' we keep. 'auto' fills the rest from
45+
-- the signature: the leading 'String', then the two enums, the version 'Int's, the
46+
-- boost 'Bool', and the result.
2947
encodeText
3048
:: String
3149
-> QR.Qrcodegen_Ecc
@@ -35,33 +53,22 @@ encodeText
3553
-> Bool
3654
-> IO (IncompleteArray Word8, Bool)
3755
encodeText = toHighLevel
38-
( input withCStringIn
56+
( auto -- text (String)
3957
$ scratchArray @Word8 maxLen -- tempBuffer: written, never read
40-
$ output qrCodeOut
41-
$ input (pureIn id) -- ecc level (enum, passthrough)
42-
$ input defaultIn -- minVersion (Int -> CInt)
43-
$ input defaultIn -- maxVersion (Int -> CInt)
44-
$ input (pureIn id) -- mask (enum, passthrough)
45-
$ input defaultIn -- boostEcl (Bool -> CBool)
46-
$ defaultRes -- CBool -> Bool
58+
$ output qrCodeOut -- qrcode: the out-parameter we keep
59+
$ auto -- ecc, minVersion, maxVersion, mask, boostEcl, result
4760
) QR.qrcodegen_encodeText
4861
where
4962
maxLen :: Int
5063
maxLen = fromIntegral QR.qrcodegen_BUFFER_LEN_MAX
5164

52-
qrCodeOut :: OutMarshaller (Ptr Word8) (IncompleteArray Word8)
65+
qrCodeOut :: Unmarshaller (Ptr Word8) (IncompleteArray Word8)
5366
qrCodeOut = peekIncompleteArrayOut maxLen
5467

68+
-- | Every position is a default ('IncompleteArray' marshals as a @const@ pointer,
69+
-- 'CInt' \/ 'CBool' scalars), so the whole wrapper is 'auto'.
5570
getSize :: IncompleteArray Word8 -> IO Int
56-
getSize = toHighLevel
57-
( input withConstIncompleteArrayIn
58-
$ defaultRes
59-
) QR.qrcodegen_getSize
71+
getSize = toHighLevel auto QR.qrcodegen_getSize
6072

6173
getModule :: IncompleteArray Word8 -> Int -> Int -> IO Bool
62-
getModule = toHighLevel
63-
( input withConstIncompleteArrayIn
64-
$ input defaultIn -- x (Int -> CInt)
65-
$ input defaultIn -- y (Int -> CInt)
66-
$ defaultRes -- CBool -> Bool
67-
) QR.qrcodegen_getModule
74+
getModule = toHighLevel auto QR.qrcodegen_getModule

examples/libpcap/generate-and-run.sh

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,33 @@ set -e
55
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
77
export PROJECT_ROOT
8+
cd "$SCRIPT_DIR"
89

9-
(
10+
# Download and build libpcap only if it is not already built. Delete the
11+
# ./libpcap directory to force a fresh download and rebuild. (The previous
12+
# version unconditionally deleted and re-downloaded, which fails offline and
13+
# throws away an existing build.)
14+
if [ -e libpcap/libpcap.so ]; then
1015
echo "# "
11-
echo "# Building libpcap"
16+
echo "# libpcap already built in ./libpcap, skipping download"
1217
echo "# "
18+
else
19+
(
20+
echo "# "
21+
echo "# Building libpcap"
22+
echo "# "
1323

14-
rm -rfv libpcap*
15-
wget https://www.tcpdump.org/release/libpcap-1.10.5.tar.xz
16-
tar -xf libpcap-1.10.5.tar.xz
17-
mv libpcap-1.10.5 libpcap
18-
rm libpcap-1.10.5.tar.xz
24+
rm -rfv libpcap libpcap-1.10.5 libpcap-1.10.5.tar.xz
25+
wget https://www.tcpdump.org/release/libpcap-1.10.5.tar.xz
26+
tar -xf libpcap-1.10.5.tar.xz
27+
mv libpcap-1.10.5 libpcap
28+
rm libpcap-1.10.5.tar.xz
1929

20-
cd "libpcap"
21-
cmake .
22-
make
23-
)
30+
cd "libpcap"
31+
cmake .
32+
make
33+
)
34+
fi
2435

2536
LIBPCAP_DIR=$(realpath libpcap)
2637

@@ -31,18 +42,18 @@ echo "# "
3142
./generate.sh
3243

3344
echo "# "
34-
echo "# Updating cabal.project.local"
45+
echo "# Writing cabal.project.local"
3546
echo "# "
3647

37-
LINE=$(cat <<-EOF
48+
# cabal.project.local is gitignored and machine-specific, so rewrite it from
49+
# scratch each run rather than appending (which would accumulate stale paths).
50+
cat > "$SCRIPT_DIR/hs-project/cabal.project.local" <<EOF
3851
package libpcap
3952
extra-include-dirs:
4053
$LIBPCAP_DIR
4154
extra-lib-dirs:
4255
$LIBPCAP_DIR
4356
EOF
44-
)
45-
grep -qxF "$LINE" "$SCRIPT_DIR/hs-project/cabal.project.local" || echo "$LINE" >> "$SCRIPT_DIR/hs-project/cabal.project.local"
4657
cat "$SCRIPT_DIR/hs-project/cabal.project.local"
4758

4859
echo "# "

examples/libpcap/hs-project/app/HighLevel.hs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,28 @@ module HighLevel (
1010
import Control.Exception (Exception, throwIO)
1111
import Control.Monad (when)
1212
import Foreign.C.String qualified as C
13-
import Foreign.C.Types (CInt)
14-
import Foreign.Marshal.Alloc (alloca)
13+
import Foreign.C.Types (CChar, CInt)
14+
import Foreign.Marshal.Alloc (alloca, allocaBytes)
1515
import Foreign.Ptr (Ptr, nullPtr)
1616
import Foreign.Storable (peek)
1717

18-
import HsBindgen.Runtime.HighLevel.ToHighLevel (OutMarshaller, mkOut, output,
19-
resultPure, toHighLevel)
20-
import HsBindgen.Runtime.HighLevel.ToHighLevel.Marshallers (peekCStringOut)
18+
import HsBindgen.Runtime.HighLevel (output, resultIO, scratch, toHighLevel)
19+
import HsBindgen.Runtime.HighLevel.Marshaller (Unmarshaller, unmarshalOutWith)
2120

2221
import Generated.Pcap qualified as Pcap
2322
import Generated.Pcap.Safe qualified as Pcap
2423

2524
-- | Thrown by 'findAllDevNames' when @pcap_findalldevs@ reports failure.
2625
--
27-
data PcapError = PcapError { msg :: String, code :: CInt }
26+
data PcapError = PcapError { msg :: String, code :: Int }
2827
deriving stock (Show)
2928
instance Exception PcapError
3029

3130
-- | Allocate a @pcap_if_t **@, walk the linked list after the call, free it.
3231
--
3332
peekPcapDeviceNames
34-
:: OutMarshaller (Ptr (Ptr Pcap.Pcap_if_t)) [String]
35-
peekPcapDeviceNames = mkOut alloca $ \pp -> do
33+
:: Unmarshaller (Ptr (Ptr Pcap.Pcap_if_t)) [String]
34+
peekPcapDeviceNames = unmarshalOutWith alloca $ \pp -> do
3635
headPtr <- peek pp
3736
names <- collect [] headPtr
3837
Pcap.pcap_freealldevs headPtr
@@ -45,16 +44,24 @@ peekPcapDeviceNames = mkOut alloca $ \pp -> do
4544
name <- C.peekCString (Pcap.pcap_if_t_name dev)
4645
collect (name : acc) (Pcap.pcap_if_t_next dev)
4746

48-
-- | Collect the names of all devices visible to libpcap. The status check
49-
-- runs after the spec rather than inside a 'throwOn' because the error
50-
-- buffer lives in a separate output position.
47+
-- | Collect the names of all devices visible to libpcap. @pcap_findalldevs@
48+
-- signals failure with a non-zero status and writes a message into a separate
49+
-- error buffer. The buffer is pre-allocated and passed as 'scratch', so the
50+
-- 'resultIO' closer can read it and throw on failure, keeping the check inside
51+
-- the spec.
5152
--
5253
findAllDevNames :: IO [String]
53-
findAllDevNames = do
54-
(names, errMsg, status) <- toHighLevel
55-
( output peekPcapDeviceNames
56-
$ output (peekCStringOut (fromIntegral Pcap.pCAP_ERRBUF_SIZE))
57-
$ resultPure id
58-
) Pcap.pcap_findalldevs
59-
when (status /= 0) $ throwIO (PcapError errMsg status)
60-
pure names
54+
findAllDevNames =
55+
allocaBytes (fromIntegral Pcap.pCAP_ERRBUF_SIZE) $ \errbuf -> do
56+
(names, ()) <- toHighLevel
57+
( output peekPcapDeviceNames -- pcap_if_t ** : device names (kept)
58+
$ scratch (\k -> k errbuf) -- char * : pre-allocated errbuf
59+
$ resultIO (throwOnStatus errbuf) -- int : throw on failure
60+
) Pcap.pcap_findalldevs
61+
pure names
62+
where
63+
throwOnStatus :: Ptr CChar -> CInt -> IO ()
64+
throwOnStatus errbuf status =
65+
when (status /= 0) $ do
66+
errMsg <- C.peekCString errbuf
67+
throwIO (PcapError errMsg (fromIntegral status))

hs-bindgen-runtime/CHANGELOG.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,35 @@
55
### New features
66

77
* Add a combinator library for hand-writing high-level wrappers over the
8-
generated low-level FFI bindings.
8+
generated low-level FFI bindings, in two modules under
9+
`HsBindgen.Runtime.HighLevel`. `Marshaller` is the marshaller vocabulary: a
10+
single `Marshal` arrow marshals a Haskell value into the C argument(s) it fills
11+
(build one with `scalar` or `bracket`, aim it with `at`, compose fields with
12+
`>>>` from the `Category` instance, and embed a sub-struct with `marshalNested`). A
13+
`MarshalStruct` seals a field chain into a whole-struct marshaller; `Unmarshaller`
14+
handles out-parameters; and `UnmarshalStruct` is the dual that reads C back into
15+
Haskell.
16+
Optional values cross the boundary with `marshalOptional` (write) and `unmarshalOptional`
17+
(read). `HighLevel` itself lifts a low-level function into a wrapper (`input` /
18+
`output` / `scratch` / result closers) and drops a struct marshaller into a
19+
wrapper position with `asArgument` / `asOutput` / `asResult`; a by-value struct
20+
argument is written into a zeroed slot, so its padding reaches C as zeros. Kept
21+
outputs accumulate into a flat result tuple (result last, up to eight
22+
components). Ready-made
23+
marshallers live in `Marshaller.Utils` and per-type defaults in `Defaults`, where
24+
`auto` is a single combinator that fills the mundane input positions and the
25+
closer of a wrapper from its high-level signature. It covers multi-C-argument
26+
defaults (such as a `ByteString` filling a `(const char *, size_t)` pair) and any
27+
user-defined `DefaultIn`, so only the interesting positions are written by hand;
28+
outputs stay explicit via `output`. The built-in defaults convert the idiomatic
29+
scalars (`Int`, `Word`, `Bool`, `Double`, `Float`) and the `String` / `ByteString`
30+
/ `IncompleteArray` compounds, and pass through unchanged every raw C scalar type,
31+
every fixed-width type, and a raw pointer or function pointer (`Ptr` / `FunPtr`).
32+
In every position, including the result, the Haskell type in the signature picks the
33+
representation, so `auto` closes a wrapper that keeps the C return type (`IO CInt`)
34+
as readily as one that converts it (`IO Int`).
35+
The combinators (including `auto`) carry
36+
`INLINE` pragmas and fuse to the raw `alloca` / `poke` / `peek` at `-O2`.
937

1038
## 0.1.0-alpha2 -- 2026-03-27
1139

hs-bindgen-runtime/hs-bindgen-runtime.cabal

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@ library
7777
HsBindgen.Runtime.FLAM
7878
HsBindgen.Runtime.HasCBitfield
7979
HsBindgen.Runtime.HasCField
80-
HsBindgen.Runtime.HighLevel.ToHighLevel
81-
HsBindgen.Runtime.HighLevel.ToHighLevel.Defaults
82-
HsBindgen.Runtime.HighLevel.ToHighLevel.Errors
83-
HsBindgen.Runtime.HighLevel.ToHighLevel.Marshallers
80+
HsBindgen.Runtime.HighLevel
81+
HsBindgen.Runtime.HighLevel.Defaults
82+
HsBindgen.Runtime.HighLevel.Internal.Errors
83+
HsBindgen.Runtime.HighLevel.Internal.Threading
84+
HsBindgen.Runtime.HighLevel.Marshaller
85+
HsBindgen.Runtime.HighLevel.Marshaller.Utils
8486
HsBindgen.Runtime.IncompleteArray
8587
HsBindgen.Runtime.Internal.Bitfield
8688
HsBindgen.Runtime.Internal.ByteArray
@@ -133,7 +135,7 @@ test-suite test-hs-bindgen-runtime
133135
Test.HsBindgen.Runtime.CEnum
134136
Test.HsBindgen.Runtime.CEnumArbitrary
135137
Test.HsBindgen.Runtime.ConstantArray
136-
Test.HsBindgen.Runtime.HighLevel.CoreDump
138+
Test.HsBindgen.Runtime.HighLevel.Struct
137139
Test.HsBindgen.Runtime.HighLevel.Test
138140
Test.HsBindgen.Runtime.IncompleteArray
139141
Test.HsBindgen.Runtime.SizedByteArray

0 commit comments

Comments
 (0)