# `cargo-cabal` A tool that helps you to turn in one command a Rust crate into a Haskell Cabal library! To generate bindings, you need to annotate the Rust function you want to expose with [`hs-bindgen`](https://github.com/yvan-sraka/hs-bindgen) macro. ## Getting started Here a little screencast demonstrating how it works (commands walkthrough are just pasted below): ![asciinema](extra/cargo-cabal-opt.gif) > **N.B.** You need in your `$PATH` a working Rust and Haskell environment, > if you use [Nix](https://nixos.org) you can just enter: > `nix-shell -p cabal-install ghc cargo rustc` --- Welcome in this little `cargo-cabal` / `hs-bindgen` demo 🙂 Let's start by creating a dumb Rust library! ```text $ cargo new --lib greetings Created library `greetings` package $ tree greetings greetings ├── Cargo.toml └── src └── lib.rs 1 directory, 2 files $ cd greetings ``` Add `hs-bindgen` to the dependencies list: ```text $ cargo add hs-bindgen --features full Updating crates.io index Adding hs-bindgen v0.8.0 to dependencies. Features: + antlion + full + std ``` And use it to decorate the function we want to expose: * `src/lib.rs`: ```rust use hs_bindgen::*; #[hs_bindgen] fn hello(name: &str) { println!("Hello, {name}!"); } ``` ```text $ cargo build Compiling proc-macro2 v1.0.47 Compiling quote v1.0.21 Compiling unicode-ident v1.0.5 Compiling syn v1.0.105 Compiling serde_derive v1.0.149 Compiling semver-parser v0.7.0 Compiling serde v1.0.149 Compiling thiserror v1.0.37 Compiling antlion v0.3.1 Compiling semver v0.9.0 Compiling semver v1.0.14 Compiling lazy_static v1.4.0 Compiling hs-bindgen-traits v0.8.0 Compiling rustc_version v0.2.3 Compiling hs-bindgen-attribute v0.7.2 Compiling thiserror-impl v1.0.37 Compiling displaydoc v0.2.3 Compiling hs-bindgen-types v0.8.0 Compiling toml v0.5.9 Compiling hs-bindgen v0.8.0 Compiling greetings v0.1.0 (/Users/yvan/demo/greetings) error: custom attribute panicked --> src/lib.rs:3:1 | 3 | #[hs_bindgen] | ^^^^^^^^^^^^^ | = help: message: fail to read content of `hsbindgen.toml` configuration file n.b. you have to run the command `cargo-cabal` to generate it: Os { code: 2, kind: NotFound, message: "No such file or directory" } error: could not compile `greetings` due to previous error ``` So, we will use `cargo-cabal` to check our setup and generate Cabal files: ```text $ cargo install cargo-cabal Updating crates.io index Ignored package `cargo-cabal v0.7.0` is already installed, use --force to override $ cargo cabal init Error: Your `Cargo.toml` file should contain a [lib] section with a `crate-type` field that contains either `staticlib` or `cdylib` value, e.g.: [lib] crate-type = ["staticlib"] ``` > **N.B.** if you're a Nix user, rather than rely on impure `cargo install`, > feel free to just `nix run github:yvan-sraka/cargo-cabal -- cabal init` Right, we edit the `Cargo.toml` accordingly: * `Cargo.toml`: ```toml [package] name = "greetings" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] hs-bindgen = { version = "0.8.0", features = ["full"] } [lib] crate-type = ["staticlib"] ``` ```text $ cargo cabal init Cabal files generated! ********************** You should now be able to compile your library with `cabal build` and should add `hs-bindgen` to your crate dependencies list and decorate the Rust function you want to expose with `#[hs_bindgen]` attribute macro. $ ls Cargo.lock Cargo.toml Setup.lhs greetings.cabal src target ``` ```text $ cargo build Compiling greetings v0.1.0 (/Users/yvan/demo/greetings) Finished dev [unoptimized + debuginfo] target(s) in 1.06s $ cabal build Build profile: -w ghc-9.0.2 -O1 In order, the following will be built (use -v for more details): - greetings-0.1.0 (lib:greetings) (first run) [1 of 1] Compiling Main ( omitted ... ) Linking /Users/yvan/demo/dist-newstyle/build/aarch64-osx/ghc-9.0.2/greetings-0.1.0/setup/setup ... Configuring greetings-0.1.0... Preprocessing library for greetings-0.1.0.. Building library for greetings-0.1.0.. [1 of 1] Compiling Greetings ( src/Greetings.hs, omitted ... ) ``` It works! And so `cargo build` too if you just want to use the library in a Rust project! --- Now let's try to use our freshly generated library in an Haskell app 😉 ```text $ cd .. $ cabal init --non-interactive test [Log] Guessing dependencies... [Log] Using cabal specification: 3.8 [Warning] unknown license type, you must put a copy in LICENSE yourself. [Log] Creating fresh file CHANGELOG.md... [Log] Creating fresh directory ./app... [Log] Creating fresh file app/Main.hs... [Log] Creating fresh file test.cabal... [Warning] No synopsis given. You should edit the .cabal file and add one. [Info] You may want to edit the .cabal file and add a Description field. $ tree test test ├── app │   └── Main.hs ├── CHANGELOG.md └── test.cabal 1 directory, 3 files ``` We create a `cabal.project` (equivalent to cargo workspace) to perform a local test without having to upload `greetings` on hackage: * `cabal.project`: ```cabal packages: ./greetings ./test ``` We edit `test.cabal` to make it depends on `greetings` library: * `test/test.cabal` (content partially omitted): ```cabal executable test -- Other library packages from which modules are imported. build-depends: base, greetings ``` We write a minimalist `main` function that will make call `hello` from `Greetings` module * `test/app/Main.hs`: ```haskell module Main where import Foreign.C.String import Greetings main :: IO () main = withCString "Rust 🦀" hello ``` Let's check if everything works as expected: ```text $ cabal run test Build profile: -w ghc-9.0.2 -O1 In order, the following will be built (use -v for more details): - test-0.1.0.0 (exe:test) (first run) Configuring executable 'test' for test-0.1.0.0.. Preprocessing executable 'test' for test-0.1.0.0.. Building executable 'test' for test-0.1.0.0.. [1 of 1] Compiling Main ( app/Main.hs, omitted ... ) Linking /Users/yvan/demo/dist-newstyle/build/aarch64-osx/ghc-9.0.2/test-0.1.0.0/x/test/build/test/test ... Hello, Rust 🦀! ``` That's all folks! Happy hacking 🙂 ## Nix support The `--enable-nix` CLI arg makes `cargo-cabal` generate a [haskell.nix](https://github.com/input-output-hk/haskell.nix) / [naersk](https://github.com/nix-community/naersk) based `flake.nix` rather than the `Setup.lhs`. > **N.B.** when first working with `hs-bindgen` and Nix flakes, checking if > `Cargo.lock` isn't in `.gitignore` and running `cargo build` and > `git add --all` before `nix build`, will save you a lot of pain 😉 ## Acknowledgments ⚠️ This is still a working experiment, not yet production ready. `cargo-cabal` was heavily inspired by other interoperability initiatives, as [`wasm-pack`](https://github.com/rustwasm/wasm-pack) and [`Maturin`](https://github.com/PyO3/maturin). This project was part of a work assignment as an [IOG](https://github.com/input-output-hk) contractor. ## License Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or [MIT license](LICENSE-MIT) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.