Skip to content

Commit

Permalink
Tutorial: Rust FFI in Haskell (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
shivaraj-bh authored Apr 2, 2024
1 parent 36eacdb commit 80562b1
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 0 deletions.
230 changes: 230 additions & 0 deletions en/haskell-rust-ffi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Rust FFI in Haskell

This #[[tutorial|tutorial]] will guide you through using [[nix]] to simplify the workflow of incorporating [[rust]] library as a dependency in your [[haskell]] project via [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface). If you are new to [[nix]] and [[flakes]], we recommend starting with the [[nix-tutorial]].

> [!info] Foreign Function Interface (FFI)
> This isn't solely restricted to Haskell and Rust, it can be used between any two languages that can establish a common ground to communicate, such as C.
The objective of this tutorial is to demonstrate calling a Rust function that returns `Hello, from rust!` from within a Haskell package. Let's begin by setting up the Rust library.

{#init-rust}
## Initialize Rust Project

Start by initializing a new Rust project using [rust-nix-template](https://github.com/srid/rust-nix-template):

```sh
git clone https://github.com/srid/rust-nix-template.git
cd rust-nix-template
```

Now, let's run the project:

```sh
nix develop
just run
```

{#rust-lib}
## Create a Rust Library

The template we've initialized is a binary project, but we need a library project. The library should export a function callable from Haskell. For simplicity, let's export a function named `hello` that returns a `C-style string`. To do so, create a new file named `src/lib.rs` with the following contents and `git add src/lib.rs`:

[[haskell-rust-ffi/lib.rs]]
![[haskell-rust-ffi/lib.rs]]

> [!info] Calling Rust code from C
> You can learn more about it [here](https://doc.rust-lang.org/nomicon/ffi.html#calling-rust-code-from-c).
Now, the library builds, but we need the dynamic library files required for FFI. To achieve this, let's add a `crate-type` to the `Cargo.toml`:

```toml
[lib]
crate-type = ["cdylib"]
```

After running `cargo build`, you should find a `librust_nix_template.dylib`[^hyphens-disallowed] (if you are on macOS) or `librust_nix_template.so` (if you are on Linux) in the `target/debug` directory.

[^hyphens-disallowed]: Note that the hyphens are disallowed in the library name; hence it's named `librust_nix_template.dylib`. Explicitly setting the name of the library with hyphens will fail while parsing the manifest with: `library target names cannot contain hyphens: rust-nix-template`

{#init-haskell}
## Initialize Haskell Project

Fetch `cabal-install` and `ghc` from the `nixpkgs` in [flake registry](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-registry.html) and initialize a new Haskell project:

```sh
nix shell nixpkgs#ghc nixpkgs#cabal-install -c cabal -- init -n --exe -m --simple hello-haskell -d base --overwrite
```

{#nixify-haskell}
## Nixify Haskell Project

We will utilize [haskell-flake](https://community.flake.parts/haskell-flake) to nixify the Haskell project. Add the following to `./hello-haskell/default.nix`:

[[haskell-rust-ffi/hs/default.nix]]
![[haskell-rust-ffi/hs/default.nix]]

Additionally, add the following to `flake.nix`:

```nix
{
inputs.haskell-flake.url = "github:srid/haskell-flake";
outputs = inputs:
# Inside `mkFlake`
{
imports = [
inputs.haskell-flake.flakeModule
./hello-haskell
];
};
}
```

Stage the changes:

```sh
git add hello-haskell
```

Now, you can run `nix run .#hello-haskell` to build and execute the Haskell project.

{#merge-devshell}
## Merge Rust and Haskell Development Environments

In the previous section, we created `devShells.haskell`. Let's merge it with the Rust development environment in `flake.nix`:

```nix
{
# Inside devShells.default
inputsFrom = [
# ...
self'.devShells.haskell
];
}
```

Now, re-enter the shell, and you'll have both Rust and Haskell development environments:

```sh
exit
nix develop
cd hello-haskell && cabal build
cd .. && cargo build
```

{#add-rust-lib}
## Add Rust Library as a Dependency

Just like any other dependency, you'll first add it to your `hello-haskell/hello-haskell.cabal` file:

```text
executable hello-haskell
-- ...
extra-libraries: rust_nix_template
```

Try building it:

```sh
cd hello-haskell && cabal build
```

You'll likely encounter an error like this:

```sh
...
* Missing (or bad) C library: rust_nix_template
...
```

The easiest solution might seem to be `export LIBRARY_PATH=../target/debug`. However, this is not reproducible and would mean running an additional command to setup the prerequisite to build the Haskell package. Even worse if the Rust project is in a different repository.

Often, the easiest solution isn't the simplest. Let's use Nix to simplify this process.

When you use Nix, you set up all the prerequisites beforehand, which is why you'll encounter an error when trying to re-enter the devShell without explicitly specifying where the Rust project is:

```sh
...
error: function 'anonymous lambda' called without required argument 'rust_nix_template'
...
```
To specify the Rust project as a dependency, we [setup haskell-flake dependency overrides](https://community.flake.parts/haskell-flake/dependency) by editing `hello-haskell/default.nix` to:
```nix
{
# Inside haskellProjects.default
settings = {
rust_nix_template.custom = _: self'.packages.default;
};
}
```

This process eliminates the need for manual Rust project building as it's wired as a prerequisite to the Haskell package.

{#call-rust}
## Call Rust function from Haskell

Replace the contents of `hello-haskell/app/Main.hs` with:

[[haskell-rust-ffi/hs/Main.hs]]
![[haskell-rust-ffi/hs/Main.hs]]

The implementation above is based on the [Haskell FFI documentation](https://wiki.haskell.org/Foreign_Function_Interface). Now, run the Haskell project:

```sh
nix run .#hello-haskell
```

You should see the output `Hello, from rust!`.

> [!note] macOS caveat
> If you are on [[macos]], the Haskell package will not run because `dlopen` will be looking for the `.dylib` file in the temporary build directory (`/private/tmp/nix-build-rust-nix...`). To fix this, you need to include [fixDarwinDylibNames](https://github.com/NixOS/nixpkgs/blob/af8fd52e05c81eafcfd4fb9fe7d3553b61472712/pkgs/build-support/setup-hooks/fix-darwin-dylib-names.sh) in `flake.nix`:
>
>```nix
>{
> # Inside `perSystem.packages.default`
> # ...
> buildInputs = if pkgs.stdenv.isDarwin then [ pkgs.fixDarwinDylibNames ] else [ ];
> postInstall = ''
> ${if pkgs.stdenv.isDarwin then "fixDarwinDylibNames" else ""}
> '';
>}
>```
{#cabal-repl}
## Problems with `cabal repl`

`cabal repl` doesn't look for `NIX_LDFLAGS` to find the dynamic library, see why [here](https://discourse.nixos.org/t/shared-libraries-error-with-cabal-repl-in-nix-shell/8921/10). This can be worked around in `hello-haskell/default.nix` using:

```nix
{
# Inside `devShells.haskell`
shellHook = ''
export LIBRARY_PATH=${config.haskellProjects.default.outputs.finalPackages.rust_nix_template}/lib
'';
}
```

Re-enter the shell, and you're set:

```sh
cd hello-haskell && cabal repl
Build profile: -w ghc-9.4.8 -O1
In order, the following will be built (use -v for more details):
- hello-haskell-0.1.0.0 (exe:hello-haskell) (ephemeral targets)
Preprocessing executable 'hello-haskell' for hello-haskell-0.1.0.0..
GHCi, version 9.4.8: https://www.haskell.org/ghc/ :? for help
[1 of 2] Compiling Main ( app/Main.hs, interpreted )
Ok, one module loaded.
ghci> main
Hello, from rust!
```

> [!note] What about `ghci`?
> If you use `ghci` you will need to link the library manually: `ghci -lrust_nix_template`. See the [documentation](https://downloads.haskell.org/ghc/latest/docs/users_guide/ghci.html#extra-libraries).
{#tpl}
## Template

You can find the template at <https://github.com/shivaraj-bh/haskell-rust-ffi-template>. This template also includes formatting setup with [[treefmt|treefmt-nix]] and VSCode integration.
1 change: 1 addition & 0 deletions en/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ order: -100
- [[hm-tutorial]]#
- [[dev]] tutorial series
- [[nixify-haskell]]
- [[haskell-rust-ffi]]
- [ ] CI/CD tutorial series


Expand Down
17 changes: 17 additions & 0 deletions global/haskell-rust-ffi/hs/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}

{-# HLINT ignore "Use camelCase" #-}

module Main where

import Foreign.C.String (CString, peekCString)

-- | The `hello` function exported by the `rust_nix_template` library.
foreign import ccall "hello" hello_rust :: IO CString

-- | Call `hello_rust` and convert the result to a Haskell `String`.
hello_haskell :: IO String
hello_haskell = hello_rust >>= peekCString

main :: IO ()
main = hello_haskell >>= putStrLn
15 changes: 15 additions & 0 deletions global/haskell-rust-ffi/hs/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
perSystem = { config, pkgs, self', ... }: {
haskellProjects.default = {
projectRoot = ./.;
autoWire = [ "packages" "checks" "apps" ];
};

devShells.haskell = pkgs.mkShell {
name = "hello-haskell";
inputsFrom = [
config.haskellProjects.default.outputs.devShell
];
};
};
}
9 changes: 9 additions & 0 deletions global/haskell-rust-ffi/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use std::ffi::CString;
use std::os::raw::c_char;

/// A function that returns "Hello, from rust!" as a C style string.
#[no_mangle]
pub extern "C" fn hello() -> *mut c_char {
let s = CString::new("Hello, from rust!").unwrap();
s.into_raw()
}

0 comments on commit 80562b1

Please sign in to comment.