We drop the tools specifically designed for Haskell and Rust together, in favour of general tools for using each with C. Namely, we use Mozilla's `cbindgen` for generating header files from the Rust source, and Well-Typed's new `hs-bindgen` tool for generating Haskell from those header files. The Rust code here is essentially the result of expanding the old macro, then inlining and renaming internals. The most important thing here is that we're now relying solely on robust well-maintained tools.
129 lines
3.7 KiB
Bash
Executable File
129 lines
3.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# TODO this is a complete vibe-coded hack, but the header patching at least is crucial
|
|
|
|
# Generate Haskell FFI bindings from Rust source code.
|
|
#
|
|
# Pipeline: cargo build -> cbindgen -> patch header -> hs-bindgen
|
|
#
|
|
# Prerequisites: run inside the Nix dev shell (provides gcc, cabal, etc.)
|
|
# cbindgen is fetched via `nix run nixpkgs#rust-cbindgen`.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
RUST_DIR="$SCRIPT_DIR/rust"
|
|
HASKELL_DIR="$SCRIPT_DIR/haskell"
|
|
HEADER_NAME="garnet_rs.h"
|
|
HEADER="$RUST_DIR/$HEADER_NAME"
|
|
|
|
# --- Step 1: Build Rust static library ---
|
|
echo "=== Building Rust library ==="
|
|
cargo build --manifest-path "$RUST_DIR/Cargo.toml"
|
|
|
|
# --- Step 2: Generate C header with cbindgen ---
|
|
echo "=== Running cbindgen ==="
|
|
nix run nixpkgs#rust-cbindgen -- \
|
|
--lang c \
|
|
--crate garnet-rs \
|
|
--output "$HEADER" \
|
|
"$RUST_DIR"
|
|
|
|
echo " Raw header written to $HEADER"
|
|
|
|
# --- Step 3: Patch the header for hs-bindgen compatibility ---
|
|
echo "=== Patching header ==="
|
|
|
|
# Two patches are needed:
|
|
#
|
|
# Patch 1: Replace cbindgen's enum pattern with typedef + #defines.
|
|
# cbindgen emits: enum Shape_Tag { Circle, Rectangle, }; typedef uint8_t Shape_Tag;
|
|
# hs-bindgen needs: typedef uint8_t Shape_Tag; #define Circle 0 ...
|
|
#
|
|
# Patch 2: Name anonymous unions inside structs.
|
|
# cbindgen emits: union { ... };
|
|
# hs-bindgen needs: union { ... } body;
|
|
|
|
awk '
|
|
# State machine for enum->typedef+define transformation
|
|
/^enum [A-Za-z_][A-Za-z0-9_]* \{$/ {
|
|
in_enum = 1
|
|
enum_name = $2
|
|
variant_count = 0
|
|
delete variants
|
|
next
|
|
}
|
|
in_enum && /^\};$/ {
|
|
# Next line should be: typedef <type> <enum_name>;
|
|
in_enum = 0
|
|
pending_enum = 1
|
|
next
|
|
}
|
|
in_enum {
|
|
# Collect variant names (strip trailing comma and whitespace)
|
|
v = $0
|
|
gsub(/^[[:space:]]+/, "", v)
|
|
gsub(/,[[:space:]]*$/, "", v)
|
|
if (v != "") {
|
|
variants[variant_count] = v
|
|
variant_count++
|
|
}
|
|
next
|
|
}
|
|
pending_enum && /^typedef [A-Za-z0-9_]+ / {
|
|
# Emit: typedef <type> <name>; then #define for each variant
|
|
print $0
|
|
for (i = 0; i < variant_count; i++) {
|
|
printf "#define %s %d\n", variants[i], i
|
|
}
|
|
pending_enum = 0
|
|
next
|
|
}
|
|
|
|
# Name anonymous unions: }; at end of union block inside struct -> } body;
|
|
/^ \};$/ && saw_union {
|
|
print " } body;"
|
|
saw_union = 0
|
|
next
|
|
}
|
|
/^ union \{$/ {
|
|
saw_union = 1
|
|
}
|
|
|
|
{ print }
|
|
' "$HEADER" > "${HEADER}.tmp" && mv "${HEADER}.tmp" "$HEADER"
|
|
|
|
echo " Patched header at $HEADER"
|
|
|
|
# --- Step 4: Derive system include paths for hs-bindgen's libclang ---
|
|
echo "=== Detecting system include paths ==="
|
|
|
|
# hs-bindgen uses libclang directly, which doesn't know about NixOS's
|
|
# non-standard include locations. We extract them from cpp -v and pass
|
|
# all of them — extra paths are harmless.
|
|
CLANG_OPTIONS=()
|
|
while IFS= read -r dir; do
|
|
CLANG_OPTIONS+=("--clang-option" "-isystem$dir")
|
|
done < <(echo | cpp -v 2>&1 | awk '/#include <\.\.\.> search starts here:/{f=1;next}/End of search list/{f=0}f{gsub(/^ +/,"");print}')
|
|
|
|
if [ ${#CLANG_OPTIONS[@]} -eq 0 ]; then
|
|
echo " WARNING: No system include paths detected. hs-bindgen may fail."
|
|
else
|
|
echo " Found ${#CLANG_OPTIONS[@]} clang options:"
|
|
for ((i=0; i<${#CLANG_OPTIONS[@]}; i+=2)); do
|
|
echo " ${CLANG_OPTIONS[i+1]}"
|
|
done
|
|
fi
|
|
|
|
# --- Step 5: Run hs-bindgen ---
|
|
echo "=== Running hs-bindgen ==="
|
|
cabal run -- hs-bindgen-cli preprocess \
|
|
--overwrite-files --create-output-dirs \
|
|
--unique-id com.garnet --enable-record-dot \
|
|
--hs-output-dir "$HASKELL_DIR/generated" --module GarnetRs \
|
|
"${CLANG_OPTIONS[@]}" \
|
|
-I "$RUST_DIR" "$HEADER_NAME"
|
|
|
|
echo "=== Done ==="
|
|
echo "Generated Haskell bindings in $HASKELL_DIR/generated/"
|
|
echo "Run 'cabal run garnet' to test."
|