How to call a Nim function from Rust through C-FFI?
Asked Answered
S

2

6

Nim backend integration guide describes how to call a Nim function from C.

Example function:

proc fib(a: cint): cint {.exportc.} =
  if a <= 2:
    result = 1
  else:
    result = fib(a - 1) + fib(a - 2)

The procedure requires that the Nim compiler is instructed not to create a main function, avoid linking and creating a header file to FFI from:

$ nim c --noMain --noLinking --header:fib.h fib.nim

To be able to use that function, the C main has to invoke a function called NimMain() as below:

#include "fib.h"
#include <stdio.h>

int main(void)
{
  NimMain();
  for (int f = 0; f < 10; f++)
    printf("Fib of %d is %d\n", f, fib(f));
  return 0;
}

The previously mentioned generated header file is placed in the nimcache directory. The C compiler has to be instructed to compile all the files under the generated nimcache sub-directory, nimbase.h and main.c:

$ gcc -o m -I$HOME/.cache/nim/fib_d -Ipath/to/nim/lib $HOME/.cache/nim/fib_d/*.c maths.c

How can I instruct the rust compiler to look for those translation units under nimcache?

Statistical answered 23/1, 2020 at 13:26 Comment(0)
G
9

In a Rust project, one can have build scripts to compile and link third-party non-Rust code. Combined with cc crate to make invoke C/C++ compiler easier, this is rather fun.

The project layout:

├── build.rs
├── Cargo.toml
└── src
    ├── fib.nim
    └── main.rs

The build.rs itself:

use std::io::{self, Write};
use std::process::Command;

fn main() {
    let output = Command::new("nim")
        .arg("c")
        .arg("--noMain")
        .arg("--noLinking")
        .arg("--nimcache:nimcache")
        .arg("src/fib.nim")
        .output()
        .expect("Failed to invoke nim compiler");
    if !output.status.success() {
        let msg = String::from_utf8_lossy(output.stderr.as_slice());
        let _ = writeln!(io::stderr(), "\nerror occurred: {}\n", msg);
        std::process::exit(1);
    }

    cc::Build::new()
        .include("/usr/lib/nim")
        .warnings(false)
        .file("nimcache/fib.nim.c")
        .file("nimcache/stdlib_system.nim.c")
        .compile("fib_nim");
}

Notice here there are several platform-dependent bits, mainly the Nim headers location. And the Nim compiler is also told to put intermediate files into a directory called nimcache inside the project root, instead of the default one under user's home directory.

The Cargo.toml file:

[package]
name = "nim-ffi"
version = "0.1.0"
authors = ["rustacean"]
edition = "2018"

[dependencies]
libc = "0.2"

[build-dependencies]
cc = "1.0"

And finally the main Rust source file:

use libc::c_int;

extern "C" {
    fn NimMain();
    fn fib(_: c_int) -> c_int;
}

fn main() {
    // initialize nim gc memory, types and stack
    unsafe {
        NimMain();
    }

    let res = unsafe { fib(20) };
    println!("Nim fib(20) is: {}", res);
}

It builds and runs successfully:

$ cargo run
Nim fib(20) is: 6765
Gridley answered 23/1, 2020 at 16:55 Comment(1)
nim-lang.github.io/Nim/nimc.html#cross-compilation-for-android stresses the importance of calling NimMain() function at the top of the main to load its garbage collection. It'd be nice if your example included that call too.Statistical
B
0

It looks like this compiles nim to C then compiles the C program including the compiled-nim stuff.

For FFI from an other language, you need to compiled to a library (dynamic or shared) then link and access that over FFI.

I don't know if it still works but https://github.com/oderwat/nim-haskell-ffi provides an example of creating a static library then linking and FFI-calling it from Haskell.

Becker answered 23/1, 2020 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.