Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/yvan-sraka/cargo-cabal

A tool that helps you to turn in one command a Rust crate into a Haskell Cabal library!
https://github.com/yvan-sraka/cargo-cabal

haskell rust

Last synced: about 2 months ago
JSON representation

A tool that helps you to turn in one command a Rust crate into a Haskell Cabal library!

Awesome Lists containing this project

README

        

# `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 🦀!
```

Now let's see if we can use the GHCi repl to call the functions defined in Rust.

```text
λ> withCString "aaa" hello
ghc-9.4.8: ^^ Could not load '__c_hello', dependency unresolved. See top entry above.

GHC.ByteCode.Linker: can't find label
During interactive linking, GHCi couldn't find the following symbol:
__c_hello
This may be due to you not asking GHCi to load extra object files,
archives or DLLs needed by your current session. Restart GHCi, specifying
the missing library using the -L/path/to/object/dir and -lmissinglibname
flags, or simply by naming the relevant files on the GHCi command line.
Alternatively, this link failure might indicate a bug in GHCi.
If you suspect the latter, please report this as a GHC bug:
https://www.haskell.org/ghc/reportabug
```

It seems like GHCi doesn't know how to link the library containing external
functions. To fix this we first need to change the type of the Rust crate to
`cdylib` in `Cargo.toml`. The reason why we need to do this is that GHCi can
only load dynamic libraries, not static ones.

```toml
[lib]
crate-type = ["cdylib"]
```
Now we need to tell GHCi explicitly where to look for those libraries. We can
do that by specifying the path in a `.ghci` file at the root of the project.

```ghci
:set -Ltarget/debug -lgreetings
```

After rebuilding the project with the necessary changes we can try once again:

```text
$ cabal repl
Build profile: -w ghc-9.4.8 -O1
In order, the following will be built (use -v for more details):
- greetings-0.1.0 (ephemeral targets)
Preprocessing library for greetings-0.1.0..
GHCi, version 9.4.8: https://www.haskell.org/ghc/ :? for help
Loaded GHCi configuration from /path/to/project/greetings/.ghci
[1 of 1] Compiling Greetings ( src/Greetings.hs, interpreted )
Ok, one module loaded.
λ> withCString "Rust" hello
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.