hs-bindgen-attribute 0.9.2

Handy macro to generate C-FFI bindings from Rust to Haskell
Documentation
use displaydoc::Display;
use hs_bindgen_types::{ArrowIter, HsType};
use std::str::FromStr;
use thiserror::Error;

/// Produce the content of `lib/{module}.hs` given a list of Signature
pub(crate) fn template(module: &str, signatures: &[Signature]) -> String {
    let modulename = module.replace("/", ".");
    let names = signatures
        .iter()
        .map(|x| x.fn_name.clone())
        .collect::<Vec<String>>()
        .join(", ");
    let imports = signatures
        .iter()
        .map(|sig| {
            format!(
                "foreign import ccall {} \"__c_{}\" {sig}",
                if sig.fn_safe {
                    "safe"
                } else {
                    warning(sig);
                    "unsafe"
                },
                sig.fn_name
            )
        })
        .collect::<Vec<String>>()
        .join("\n");
    format!(
        "-- This file was generated by `hs-bindgen` crate and contains C FFI bindings
-- wrappers for every Rust function annotated with `#[hs_bindgen]`

{{-# LANGUAGE ForeignFunctionInterface #-}}

-- Why not rather using `{{-# LANGUAGE CApiFFI #-}}` language extension?
--
-- * Because it's GHC specific and not part of the Haskell standard:
--   https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/ffi.html ;
--
-- * Because the capabilities it gave (by rather works on top of symbols of a C
--   header file) can't work in our case. Maybe we want a future with an
--   {{-# LANGUAGE RustApiFFI #-}} language extension that would enable us to
--   work on top of a `.rs` source file (or a `.rlib`, but this is unlikely as
--   this format has purposely no public specification).

{{-# OPTIONS_GHC -Wno-unused-imports #-}}

module {modulename} ({names}) where

import Data.Int
import Data.Word
import Foreign.C.String
import Foreign.C.Types
import Foreign.Ptr

{imports}"
    )
}

/// Warn user about what Haskell `unsafe` keyword does ...
pub(crate) fn warning(_sig: &Signature) {
    #[cfg(DIAGNOSTICS)]
    proc_macro::Diagnostic::spanned(
        [proc_macro::Span::call_site()].as_ref(),
        proc_macro::Level::Warning,
        format!(
            "Using: `foreign import ccall unsafe __c_, {} {_sig}`
means that Haskell Garbage-Collector will be locked during the foreign call.
/!\\ Do not use it for long computations in a multithreaded application or
it will slow down a lot your whole program ...",
            _sig.fn_name
        ),
    )
    .emit();
}

#[derive(Display, Error, Debug)]
pub enum Error {
    /** you should provide targeted Haskell type signature as attribute:
     * `#[hs_bindgen(HS SIGNATURE)]`
     */
    MissingSig,
    /** given Haskell function definition is `{0}` but should have the form:
     * `NAME :: TYPE`
     *
     * n.b. you can prefix function name like "unsafe NAME :: TYPE" and it will
     * expand as: foreign import ccall unsafe __c_NAME NAME :: TYPE (knowing it
     * default to foreign import ccall safe   __c_NAME NAME :: TYPE ) ...
     * ... /!\ Hope you know what you're doing!
     */
    MalformedSig(String),
    /// Haskell type error: {0}
    HsType(String),
}

/// Data structure that represent an Haskell function signature:
/// {fn_name} :: {fn_type[0]} -> {fn_type[1]} -> ... -> {fn_type[n-1]}
///
/// FIXME: consider moving this struct and its traits' implementation into
/// `hs-bindgen-types`
pub(crate) struct Signature {
    pub(crate) fn_name: String,
    pub(crate) fn_safe: bool,
    pub(crate) fn_type: Vec<HsType>,
}

impl std::fmt::Display for Signature {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{} :: {}",
            self.fn_name,
            self.fn_type
                .iter()
                .map(|x| x.to_string())
                .collect::<Vec<String>>()
                .join(" -> ")
        )
    }
}

impl FromStr for Signature {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        (!s.is_empty()).then_some(()).ok_or(Error::MissingSig)?;
        let mut x = s.split("::");
        let fn_name = x.next().ok_or(Error::MissingSig)?.trim();
        let fn_safe = !fn_name.starts_with("unsafe ");
        let fn_name = if fn_safe {
            fn_name.trim_start_matches("safe ")
        } else {
            fn_name.trim_start_matches("unsafe ")
        }
        .trim_start()
        .to_string();

        let fn_type = ArrowIter::from(x.next().ok_or_else(|| Error::MalformedSig(s.to_string()))?)
            .map(|ty| {
                ty.parse::<HsType>()
                    .map_err(|ty| Error::HsType(ty.to_string()))
            })
            .collect::<Result<Vec<HsType>, Error>>()?;
        assert!(x.next().is_none(), "{}", Error::MalformedSig(s.to_string()));
        Ok(Signature {
            fn_name,
            fn_safe,
            fn_type,
        })
    }
}