garnet/generate-bindings

147 lines
4.5 KiB
Plaintext
Raw Normal View History

#!/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.
#
2026-02-19 15:00:46 +00:00
# Pipeline:
# 1. cargo build - build the Rust static library
# 2. cbindgen - generate a C header from the Rust source
# 3. awk - patch the header for hs-bindgen compatibility
# 4. hs-bindgen-cli - generate Haskell FFI modules from the C header
# 5. cabal configure - point Cabal at the Rust build artifacts
#
# Prerequisites: run inside the Nix dev shell (provides gcc, cabal, cbindgen, hs-bindgen-cli).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RUST_DIR="$SCRIPT_DIR/rust"
2026-02-19 14:15:14 +00:00
HASKELL_DIR="$SCRIPT_DIR"
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 ==="
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 ---
#
2026-02-19 15:00:46 +00:00
# Two patches are needed, both due to hs-bindgen limitations with cbindgen's
# output for #[repr(C, u8)] tagged enums:
#
2026-02-19 15:00:46 +00:00
# 1. cbindgen emits: enum Shape_Tag { Circle, Rectangle, };
# typedef uint8_t Shape_Tag;
# hs-bindgen needs: typedef uint8_t Shape_Tag;
# #define Circle 0
# #define Rectangle 1
# See: no upstream issue yet for hs-bindgen
#
# 2. cbindgen emits: union { ... }; (anonymous)
# hs-bindgen needs: union { ... } body; (named)
# See: https://github.com/well-typed/hs-bindgen/issues/1649
echo "=== Patching header ==="
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 ---
2026-02-19 15:00:46 +00:00
#
# 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.
2026-02-19 15:00:46 +00:00
echo "=== Detecting system include paths ==="
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 ==="
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"
2026-02-19 15:00:46 +00:00
# --- Step 6: Configure Cabal ---
#
# Point Cabal at the Rust static library and C header. This writes
# cabal.project.local (gitignored) with absolute paths derived from
# the current working directory, avoiding hardcoded paths in cabal.project.
echo "=== Configuring Cabal ==="
cabal configure \
--extra-lib-dirs="$RUST_DIR/target/debug" \
--extra-lib-dirs="$RUST_DIR/target/release" \
--extra-include-dirs="$RUST_DIR"
echo "=== Done ==="
echo "Generated Haskell bindings in $HASKELL_DIR/generated/"
2026-02-19 14:15:14 +00:00
echo "Run 'cabal run' to test."