Calling Rust from Haskell, and vice versa.

Integrating different programming languages can be a complex but incredibly rewarding endeavor. For me, it’s been a fascinating journey into the world of Foreign Function Interface (FFI), specifically exploring how to make Haskell and Rust talk to each other. It’s been a mix of head-scratching moments and amazing breakthroughs, truly a lot of fun.

My first steps: basic arithmetic with Rust

My adventure began with a simple goal: could I get Haskell to call basic arithmetic functions written in Rust? I started by creating a small Rust library. I defined add and mul functions, making sure they were #[no_mangle] and extern "C" so Haskell could find them easily.

#[unsafe(no_mangle)]
pub extern "C" fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[unsafe(no_mangle)]
pub extern "C" fn mul(left: u64, right: u64) -> u64 {
    left * right
}

On the Haskell side, I used the CApiFFI extension and Foreign.C.Types to declare these functions. It’s just as if I was writing a contract between the two languages, which for some reason had to be in C.

{-# LANGUAGE CApiFFI #-}
import Foreign.C.Types

foreign import ccall "add" add :: CInt -> CInt -> IO CInt
foreign import ccall "mul" mul :: CInt -> CInt -> IO CInt

main :: IO ()
main = do
  print 33

Loading this into GHCi, Haskell’s interactive environment, with the Rust library (ghci -L./rusty/lib/ -lrusty ./Main.hs), was my first big win. Seeing mul 55 55 return 3025 right there in the prompt felt like magic! This initial success confirmed my basic FFI setup was working.

ghci -L./rusty/lib/ -lrusty ./Main.hs
Ok, one module loaded.
ghci> mul 55 55
3025

Passing complex data structures and callbacks

My next challenge was more intricate: could I create a Rust object from Haskell, manipulate it, and then “finish” its configuration? This meant dealing with more complex data structures and managing memory across the language boundary.

Rust side: opaque pointers to the rescue

To pass complex Rust types to Haskell without revealing their internal structure, I learned about opaque pointers. I defined a WebsiteConfigOpaque struct and wrote functions (new_website and finish) to allocate and deallocate the Rust WebsiteConfiguration on the heap, returning a raw pointer to Haskell. This felt a bit like passing a sealed box across the border. Haskell knew it was a box, but not what was inside.

use hauchiwa::WebsiteConfiguration;

#[repr(C)]
pub struct WebsiteConfigOpaque {
  _private: [u8; 0],
}

type WebsiteConfigHandle = *mut WebsiteConfigOpaque;

#[unsafe(no_mangle)]
extern "C" fn new_website() -> WebsiteConfigHandle {
  let config: WebsiteConfiguration<()> = hauchiwa::Website::configure();
  let config = Box::new(config);
  Box::into_raw(config) as WebsiteConfigHandle
}

#[unsafe(no_mangle)]
pub extern "C" fn finish(ptr: WebsiteConfigHandle) -> i32 {
  if ptr.is_null() {
      return -1; // Null pointer error
  }
  let config: Box<hauchiwa::WebsiteConfiguration<()>> =
      unsafe { Box::from_raw(ptr as *mut hauchiwa::WebsiteConfiguration<()>) };
  let config = *config;
  let mut website = hauchiwa::WebsiteConfiguration::finish(config);
  match website.build(()) {
      Ok(_) => 1,
      Err(_) => 0,
  }
}

Haskell side: interacting with opaque pointers

On the Haskell side, I mirrored the opaque type and defined foreign imports for new_website and finish. The IO monad was my trusty companion here, handling the side effects of memory operations.

module Main where
import Foreign (Int32, Ptr)
import Foreign.C.Types

data WebsiteConfigOpaque
type WebsiteConfigHandle = Ptr WebsiteConfigOpaque

foreign import ccall "new_website" newWebsite :: IO WebsiteConfigHandle
foreign import ccall "finish" finish :: WebsiteConfigHandle -> IO Int32

main :: IO ()
main = do
website <- newWebsite
res <- finish website
print res

Running this in GHCi worked perfectly, returning 0, which meant the website configuration process completed without a hitch. Another small victory!

ghci -L./lib -lrusty ./Main.hs
Ok, one module loaded.
ghci> main
Loaded git repository data (+2ms)
Running Hauchiwa in build mode.
Cleaned the dist directory (+0ms)
0

The result of 0 indicates no errors during the website configuration process.

The final frontier: Haskell callbacks in Rust

This was the part that truly pushed my understanding - passing a Haskell function to Rust, allowing Rust to call back into Haskell. This felt like the ultimate challenge in FFI.

The GHCi wrapper wall

I hit a wall when I tried to use the wrapper keyword in GHCi. When I added the foreign import ccall "wrapper" declaration to my Haskell file and tried to load it, GHCi threw an error:

GHC.Linker.Loader.dynLoadObjs: Loading temp shared object failed
During interactive linking, GHCi couldn't find the following symbol:
  librusty.so: cannot open shared object file: No such file or directory

It was frustrating because I knew the library was there, and compiling to a standalone binary (ghc -O2 Main.hs -L./lib -lmylib -o out; LD_LIBRARY_PATH=./lib ./out) worked perfectly. I was so close, but GHCi was being stubborn. I wondered if it was a limitation of GHCi itself or if I was missing something fundamental.

Rust side: expecting a callback function pointer

Meanwhile, on the Rust side, I set up my do_callback function to accept a HaskellCallback type, which is essentially a C-compatible function pointer.

pub type HaskellCallback = extern "C" fn(i32) -> i32;

#[unsafe(no_mangle)]
pub extern "C" fn do_callback(f: HaskellCallback) -> i32 {
    println!("I run inside Rust");
    f(33)
}

Haskell side: creating and passing a function pointer

In Haskell, I used the wrapper keyword to create a C-callable function pointer from my haskellCallback function. This FunPtr would then be passed to the Rust do_callback.

module Main where
import Foreign (Int32, Ptr)
import Foreign.C.Types
import Foreign.Ptr (FunPtr)

-- The Haskell function to be passed as a callback
haskellCallback :: CInt -> IO CInt
haskellCallback x = do
  putStrLn $ "Haskell received: " ++ show x
  return (x + 99)

-- Turn Haskell function into a C-callable function pointer
foreign import ccall "wrapper"
  mkCallback :: (CInt -> IO CInt) -> IO (FunPtr (CInt -> IO CInt))

-- Import the Rust function that accepts a callback
foreign import ccall "do_callback"
  doCallback :: FunPtr (CInt -> IO CInt) -> IO CInt

main :: IO ()
main = do
  cb <- mkCallback haskellCallback
  res2 <- doCallback cb
  print res2

I was stuck on the GHCi issue for a bit. I even asked around, and someone mentioned that what I was doing was “pretty weird” and suggested using Cabal. While I knew Cabal was the standard, I was determined to see what I could accomplish with plain GHC and FFI. I suspected it was a linker issue, not a fundamental limitation.

Then, after some more digging (and a helpful Stack Overflow link), I had a breakthrough! I realized that in addition to the -L and -l flags for GHCi, I also needed to set the LD_LIBRARY_PATH environment variable before launching GHCi.

LD_LIBRARY_PATH=./lib ghci -L./lib -lrusty ./Main.hs
GHCi, version 9.6.7: https://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Main             ( Main.hs, interpreted )
Ok, one module loaded.
ghci> main
I run inside Rust
Haskell received: 33
132
ghci> :reload
[1 of 2] Compiling Main             ( Main.hs, interpreted ) [Source file changed]
Ok, one module loaded.
ghci> main
I run inside Rust
Haskell received: 33
9955

It worked! Seeing “I run inside Rust” followed by “Haskell received: 33” and then the final 9955 (because I changed the return value of haskellCallback during a reload) was incredibly satisfying. I could :reload my Haskell code in GHCi, and the FFI connection to the Rust library remained intact. It turns out, FFI in Haskell is quite simple for the user, it’s just that some edge cases aren’t well-documented, making it easy to hit a wall.

Conclusion

This entire exploration has been a blast. It showed me the immense power of Foreign Function Interface, enabling Haskell and Rust to communicate seamlessly, share data, and even pass functions back and forth. While there were moments of frustration, especially with GHCi’s specific linking requirements, overcoming those hurdles was incredibly rewarding. It’s amazing how much you can achieve with FFI, even without using any more complex build systems like Cabal for these specific interactions. This just goes to show how much effort went into engineering GHC and GHCi, chapeau bas.

Bibliography

Bibtex
  1. “Implementation for the "wrapper" wrapper in Haskell FFI.” [Online]. Available: https://stackoverflow.com/questions/57140931/implementation-for-the-wrapper-wrapper-in-haskell-ffi