Ch.9 SML# feature: direct interface to C

§ 9.3. Basic examples of importing C functions

Let us import some standard C library functions to SML#. At first, we need to look up their prototype declarations from the manual or the header file of the functions we intend to import. Here, suppose we want to import the following functions.

double pow(double, double);
void srand(unsigned);
int rand(void);

Next, we need to write the types of the above functions by using the interoperable types that corresponds to the argument and return types of those functions.

val pow = _import "pow" : (real, real) -> real
val srand = _import "srand" : word -> ()
val rand = _import "rand" : () -> int

Then these C functions are imported to SML# as SML# functions of the following types.

val pow : real * real -> real
val srand : word -> unit
val rand : unit -> int

The printf function can also be imported to SML# by using the notation of the variable length argument list. The prototype of printf is given below.

int printf(const char *, ...);

Corresponding to this prototype, we can import printf by the following _import declaration.

val printfIntReal = _import "printf" : (string, ...(int, real)) -> int

The type of printfIntReal is as follows.

val printfIntReal : string * int * real -> int

We note that when calling this printIntReal function, its first argument must be a output format string that requires just two arguments of an integer and a floating-point number in this order.

Importing a function that takes pointer arguments needs special attention. In C, we often use pointer arguments in two ways; passing a large data structure by a reference to it, or specifying a buffer to store the result of a function. An interoperable type is corresponding to one of the usage of pointer arguments. So we need to carefully choose an interoperable type representing a pointer argument by its usage according to the manual of the C function we intend to import.

Let us import some C functions that have pointer arguments. Suppose we want to import modf function. The prototype of modf is as follows.

double modf(double, double *);

According to the manual of modf, the second argument of pointer type must specify the destination buffer of the result of this function. So we specify an interoperable type of a mutable value to the type of the second argument.

val modf = _import "modf" : (real, real ref) -> real

We need to pay special attention to pointers to char. In C, there are a lot of meaning a char pointer exactly means. For example, a char pointer usually means a null-terminated string. In the other case, a char pointer is used as a pointer to most generic type of binary data buffers. Suppose we try to import sprintf function. Its prototype is given below.

int sprintf(char *, const char *, ...);

The first char pointer is a pointer to a destination buffer, and the second one represents a null-terminated string. According to this difference of usage of two pointers, we give the following type annotation to the _import declaration.

val sprintfInt = _import "sprintf" : (char array, string, ...(int)) -> int

We can import any user-defined C function to SML#, while we only use standard C library functions to describe how to import C functions so far. Figure 9.1 shows an example of importing an user-defined function to SML# and passing a structure from SML# to the C function.

sample.c file:

#include <math.h>
double f(const struct {double x; double y;} *s) {
  return sqrt(s->x * s->x + s->y * s->y);
}

sample.sml file:

val f = _import "f" : real * real -> real
val x = (1.1, 2.2);
val y = f x;
print ("result : " ^ Real.toString y ^ "\n");

Execution:

# gcc -c sample.c
# smlsharp sample.sml sample.o
# a.out
result : 2.459675
Figure 9.1. Passing a tuple to user-defined C function