Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tutorial: Rust FFI in Haskell #24

Merged
merged 33 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
08f3c26
introduction looks promising
shivaraj-bh Feb 17, 2024
b33b9d7
Merge branch 'master' into haskell-rust-ffi
shivaraj-bh Mar 27, 2024
008237a
haskell-rust-ffi is no longer a blog but a tutorial
shivaraj-bh Mar 27, 2024
b7c8231
moving main.rs to lib.rs is not the convention
shivaraj-bh Mar 27, 2024
e136dba
what are the dynamic library files for?
shivaraj-bh Mar 27, 2024
1f95b33
add heading anchors
shivaraj-bh Mar 27, 2024
f016a99
better intro
shivaraj-bh Mar 28, 2024
972301f
minify "create rust library" section
shivaraj-bh Mar 28, 2024
a8fbd3d
bold rust FFI doc
shivaraj-bh Mar 28, 2024
9613140
init haskell project
shivaraj-bh Mar 28, 2024
66fc6e5
nixify haskell project
shivaraj-bh Mar 28, 2024
2b21260
merge devshells
shivaraj-bh Mar 28, 2024
6cb26b6
add rust library as dep to haskell pkg
shivaraj-bh Mar 28, 2024
2d3baac
call rust code from haskell
shivaraj-bh Mar 28, 2024
50b1fcf
problems with cabal repl
shivaraj-bh Mar 28, 2024
5274f85
add template
shivaraj-bh Mar 28, 2024
3b71f94
include the global files
shivaraj-bh Mar 28, 2024
01342af
add remaining heading anchors
shivaraj-bh Mar 28, 2024
04a406a
`custom` settings problem with new pkgs is now fixed in haskell-flake
shivaraj-bh Mar 29, 2024
00a2bf2
hyphens disallowed in library names
shivaraj-bh Apr 1, 2024
d5b1410
simpler haskell pkg init
shivaraj-bh Apr 1, 2024
57ca2b0
flakes is a pre-requisite
shivaraj-bh Apr 1, 2024
0e654f1
grammar
shivaraj-bh Apr 1, 2024
53182ba
more grammar
shivaraj-bh Apr 1, 2024
7112a0a
we, not I
shivaraj-bh Apr 2, 2024
e59bdf3
cabal is not supported by highlightjs
shivaraj-bh Apr 2, 2024
c4816d0
Rust with a capital R
shivaraj-bh Apr 2, 2024
345302c
link to haskell-flake dependency documentation
shivaraj-bh Apr 2, 2024
ae455c0
wiki-link to macos
shivaraj-bh Apr 2, 2024
7b19da4
macOS
shivaraj-bh Apr 2, 2024
b28fabe
init Haskell project with `nixpkgs` from flake registry
shivaraj-bh Apr 2, 2024
b9662d0
Merge branch 'haskell-rust-ffi' of github.com:nixos-asia/website into…
shivaraj-bh Apr 2, 2024
3f40f65
misc. fixes
shivaraj-bh Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
shivaraj-bh marked this conversation as resolved.
Show resolved Hide resolved
```

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()
}
Loading