Dune project with a C/C++ ctypes foreign defined function

Hi all, after a rather long hiatus from Ocaml I’m looking at trying to make use of the language once again for fun little projects in my spare time. The other day I was thinking about trying to wrap some C++ code (but I’m using C for this test) in an Ocaml library, and I ran into a something that confuses me a bit.

In a nutshell, I can create a ctypes foreign function in the bin executable code, but creating a ctypes foreign function in the lib directory and naively adding the library to the bin executable fails.

Steps I take (which may miss something very essential…)

  1. dune init project cpptest
  2. Create lib/fib.ml, lib/fib.c, and lib/fib.h

lib/fib.ml content:

open Ctypes
open Foreign
let fib = foreign "fib" (int @-> returning int)

lib/fib.c content:

#include "fib.h"

int fib(int n)
{
    if (n < 2) return 1;
    else return fib(n - 1) + fib(n - 2);
}

lib/fib.h content:

#ifndef CPPTEST_FIB_H
#define CPPTEST_FIB_H

#ifdef __cplusplus
extern "C" {
#endif

int fib(int n);

#ifdef __cplusplus
}
#endif

#endif /* CPPTEST_FIB_H */
  1. Add libraries and foreign stubs to lib/dune

lib/dune

(library
 (name cpptest)
 (foreign_stubs (language c) (names fib))
 (libraries ctypes ctypes-foreign))
  1. Update bin/main.ml and bin/dune

bin/main.ml

open Printf
open Cpptest

let () = printf "%d\n" (Fib.fib 20)

bin/dune

(executable
 (public_name cpptest)
 (name main)
 (libraries cpptest))
  1. dune build is successful
  2. dune exec cpptest results in in a symbol lookup error
Fatal error: exception Dl.DL_error("_build/install/default/bin/cpptest: undefined symbol: fib")
  1. On the other hand, putting fib.c and fib.h in bin/, and add the foreign declaration of fib in main.ml works.

bin/main.ml

open Printf
open Ctypes
open Foreign
let fib = foreign "fib" (int @-> returning int)
let () = printf "%d\n" (fib 20)

bin/dune

(executable
 (public_name cpptest)
 (name main)
 (foreign_stubs (language c) (names fib))
 (libraries ctypes ctypes-foreign))
  1. dune build is successful (just like when trying the library version)
  2. dune exec cpptest calls the C fib() function successfully (in contrast to the library version)
10946

Any insights into what I am overlooking would be very welcome!

2 Likes

ctypes-foreign looks up the symbol dynamically, and by default it looks in the current executable.

You can make it search in another location instead by passing the optional from argument to foreign, e.g.:

let lib = Dl.dlopen ~filename:"_build/default/lib/dllcpptest_stubs.so"
            ~flags:Dl.[RTLD_NOW; RTLD_GLOBAL]

let fib = foreign "fib" (int @-> returning int)
            ~from:lib

In the long term it might be easier to switch to the stub-generation interface so that symbols are resolved statically.

Thank you for the reply!

I’m not sure if I can resolve symbols statically, if I’m creating a library for use by some arbitrary other program, unless this stub-generation you speak of allows for it for the library (are you talking about Dune’s ctypes support which mention stub generation, or are you referring to something else)?

This exercise is for me to test the waters with respect to wrapping some C++ code in a library form. Because it is C++, I need to create a suitable set of C functions as a translation layer, and the C files in question seemingly always end up as a shared object for my library (and would have to be, I think, in order to support Ocaml bytecode programs if nothing else, although this is not a set-in-stone constraint for me).

As a side-note, if I set LD_LIBRARY_PATH to a directory containing dllcpptest_stubs.so (like the build lib directory), it is possible to have the wrapper ml file refer to the shared object by filename only, and not a complete path, but it feels like maybe I’m sweeping a problem under the rug if I do.