Creating Guile bindings for MPFR, part 1

C, Guile, the REPL and everything in-between

The logical next step following the series on Goblins for number theory is to import software that is useful to number theory into Guile. This means creating bindings to C libraries, in particular libraries handling numbers with arbitrarily many digits. Since Guile uses GMP for handling big integers, there are already C functions transforming between both integer types. This would be enough to create the bindings needed to call my primality proving software from Guile. But I would like to start with a more clear-cut example for learning purposes. With the goal of providing an outcome of independent usefulness I have decided to start creating bindings to MPFR. This could also provide an occasion to look into the numerical tower of Guile.

Integrating C libraries into Guile

The first term that comes to mind when handling from within one language software written in another language is foreign function interface. Guile has mechanisms for opening dynamic libraries and calling the functions in it. This is all nice and svelte when these functions operate on scalar C data types; these have counterparts in the Scheme world. Additionally there is limited support for C structs, but the available functions look dangerously close to bean, erh, byte counting instead of providing a higher level interface. Apart from that, being reduced to what is called wrapped pointers, a euphemism for the infamous void * in C, the interface is not really type aware.

The (maybe more modern?) alternative to this opaque handling of wrapped pointers are foreign object types, which make it possible to define separate types for arbitrarily complex C data structures. These can even be equipped with mechanisms to make them compatible with the garbage collector of Guile. The price to pay is that we have to write a separate C library for providing the interface (much like the Java Native Interface, for instance, as used in PariDroid, or the approach taken in PariTwine to make C libraries available to the PARI/GP computer algebra system). Then the functionality from the foreign function interface comes in handy to dynamically load this intermediate library.

Defining MPFR numbers as a foreign object type

Writing the helper library is essentially a matter of following the Guile documentation for creating foreign object types and objects, and copy-pasting and adapting relevant boilerplate. So let us create a file guile_mpfr.c for the library code. We start by putting the following lines into the file:

#include <stdio.h>
#include <mpfr.h>
#include <libguile.h>

static SCM mpfr_type;

The first line will be invaluable for printf debugging; it needs to come first to enable the input and output functions of MPFR. The second one is necessary because eventually we will use bits and pieces of MPFR, the third one because we already use Guile. And the last one creates a variable that is local to the library and will hold our new type mpfr_type as a C variable of type SCM. As a caveat, notice that the word type is used with two different meanings here. On one hand, in C everything Guile is handled by the unique type SCM (for a reader coming from number theory, this will be reminiscent of the GEN generic C type in the PARI library); this is essentially an array of bytes or words. On the other hand, Guile (and PARI) have their own internal type systems; these are handled through the contents of the SCM (or GEN) values (by reserving initial bytes to hold the type information).

Let us start by doing things backwards and write the functions that transform between Scheme values of the still to be defined new type mpfr_type and MPFR values; the latter are given by C pointers of type mpfr_ptr. With this knowledge the transformation functions are essentially boilerplate:

SCM scm_from_mpfrptr (mpfr_ptr x)
{
   return scm_make_foreign_object_1 (mpfr_type, x);
}

mpfr_ptr scm_to_mpfrptr (SCM val)
{
   scm_assert_foreign_object_type (mpfr_type, val);
   return (mpfr_ptr) scm_foreign_object_ref (val, 0);
}

The call to scm_make_foreign_object_1 takes as input our MPFR variable and wraps and tags it as an mpfr_type. Conversely scm_foreign_object_ref unwraps the MPFR variable as a void *, which we convert to the correct type before returning it. The call to scm_assert_foreign_object_type is a safety measure to throw an error whenever we call the function with an SCM value that is not of the (internal) type mpfr_type.

Next let us write the code that creates and destroys MPFR variables as foreign objects. For good C hygiene, we make sure to write the two functions involving malloc and free in lockstep (although, strictly speaking, we currently only need the code for freeing variables to pass it to the Guile garbage collector).

static SCM mpfr_malloc ()
{
   mpfr_ptr x;

   x = (mpfr_ptr) malloc (sizeof (__mpfr_struct));
   mpfr_init (x);

   return scm_from_mpfrptr (x);
}

void mpfr_free (SCM val)
{
   mpfr_ptr x;

   x = scm_to_mpfrptr (val);
   mpfr_clear (x);
   free (x);
}

The mpfr_ptr type is in fact a pointer to an internal struct __mpfr_struct. In the mpfr_malloc function, we malloc such a data structure, then initialise the actual MPFR variable by a call to mpfr_init (which executes an additional call to malloc for an internal field of the struct), then wrap the resulting MPFR variable into a Scheme value. The mpfr_free function exactly reverses these operations.

Finally we are ready to define the MPFR object type and to assign it to the mpfr_type variable.

static void init_mpfr_type (void)
{
   SCM name, slots;
   scm_t_struct_finalize finaliser;

   name = scm_from_utf8_symbol ("mpfr");
   slots = scm_list_1 (scm_from_utf8_symbol ("data"));
   finaliser = (scm_t_struct_finalize) mpfr_free;
   mpfr_type = scm_make_foreign_object_type (name, slots, finaliser);

   mpfr_set_default_prec (113);
}

The heart of the function is the call to scm_make_foreign_object_type, which takes three arguments. The name is an arbitrary symbol (here, the string "mpfr" is transformed into the symbol 'mpfr; if these internals are confusing, you may think of it as an identifyer given by a string). The slots can handle an arbitrary number (as a list) of symbols that determine data slots; here we need only one slot to hold an MPFR value. We attach the symbol 'data to it, which is arbitrary since we actually handle this unique slot by its number in scm_from_mpfrptr and scm_to_mpfrptr. And finaliser is a C function that is called when the Guile garbage collector tries to delete a foreign object; this function needs to free all memory that has been allocated outside the control of Guile when creating the object (if there is no such memory, one may use the NULL pointer instead of a finaliser function).

Remember that MPFR handles real floating point numbers of arbitrary precision. Each variable can hold its own precision, but there is also a library-wide default value. We take the opportunity in init_mpfr_type to set this value to 113 bits, that is, quadruple precision. So the call to mpfr_init in mpfr_malloc will use this default precision (a more functional, stateless approach would be to pass the desired precision at each variable initialisation as a call mpfr_init2 (x, prec), see the second instalment of this miniseries).

Now the helper library containing all the above code in this order can be compiled via a Makefile containing the lines

guile_mpfr.so : guile_mpfr.c
        gcc -shared -o guile_mpfr.so -fPIC `pkg-config --cflags guile-3.0` `pkg-config --libs guile-3.0` -lmpfr -lgmp guile_mpfr.c

(be careful to use the tabulator key and not spaces at the beginning of the second line). Then running make creates the dynamic library file guile_mpfr.so, using flags directly extracted from the pkg-config file of Guile (in particular on Guix, this adds -I and -L command line arguments pointing directly to the correct subdirectory in /gnu/store/).

Conversion functions

Before being able to compute in Guile with MPFR variables, we need to connect the newly created MPFR island to the existing Guile continent by defining bridges, that is, functions that transform values between them. So we add the following to guile_mpfr.c:

SCM integer_to_mpfr (SCM val)
{
   SCM res;
   mpfr_ptr x;
   mpz_t z;

   res = mpfr_malloc ();
   x = scm_to_mpfrptr (res);
   mpz_init (z);
   scm_to_mpz (val, z);
   mpfr_set_z (x, z, MPFR_RNDN);
   mpz_clear (z);

   return res;
}

For this to work, we need Guile to be compiled with GMP support. The default guile package in Guix is currently compiled with mini-gmp, an embedded version of GMP, so that the function scm_to_mpz does not get defined. For the full experience, one needs to install the guile-with-gmp package.

The structure of the function is straightforward. We allocate a new Scheme variable res of type mpfr_type by a call to mpfr_malloc, which will hold the result to be returned by the function. To be able to use MPFR functionality, we define the pointer x as a handle for the wrapped MPFR variable. Then we call the Guile function scm_to_mpz that transforms a Guile integer into a GMP integer (abstracting away from the question whether the integer is an immediate, that is, immediately stored in a machine word, or a pointer to an array of words; and hopefully causing an error when called with inappropriate arguments). The MPFR function mpfr_set_z then assigns this GMP integer to x. Every MPFR function takes an additional argument, the rounding mode; here and in the following, we use MPFR_RNDN, which means that all values that cannot be represented exactly at our target precision of 113 bits are rounded to the nearest representable value. With an integer this can happen if it is particularly large in absolute value.

In the converse direction, let us for the moment content ourselves with transforming an MPFR value to a Scheme value containing a double.

SCM mpfr_to_double (SCM val)
{
   double d;

   d = mpfr_get_d (scm_to_mpfrptr (val), MPFR_RNDN);

   return scm_from_double (d);
}

The code should be self-explaining; again we (usually) need to round when reducing from the 113 bit MPFR precision to the 53 bit double precision.

For good measure, we add one more function that prints an MPFR value. It uses the MPFR function mpfr_out_str to directly print to stdout, using the decimal system (10) and as many digits as necessary to uniquely specify the floating point number at 113 bits of precision (indicated by the argument 0). This will need refinement to handle Guile output ports, but for the time being it comes handy for debugging purposes, and to see if and what our upcoming functions compute with MPFR values.

void scm_mpfr_print (SCM x)
{
   mpfr_out_str (stdout, 10, 0, scm_to_mpfrptr (x), MPFR_RNDN);
   printf ("\n");
}

Arithmetic functions

Having conversion functions is enough to freely mix C code that works with Scheme objects and MPFR functions. However, it is our goal to eventually make the MPFR functions accessible from the Guile REPL. To this purpose, we need to write for every MPFR function an equivalent C function operating on foreign objects of type mpfr_type, which is essentially a mechanical exercise: Create a foreign object for the result, access the mpfr_ptr pointers behind each foreign object and call the MPFR function on the pointers. The code is very similar to what we have seen above for conversions. For example, let us add multiplication and division:

SCM scm_mpfr_mul (SCM val1, SCM val2)
{
   SCM res;
   mpfr_ptr x1, x2, x;
   res = mpfr_malloc ();
   x = scm_to_mpfrptr (res);
   x1 = scm_to_mpfrptr (val1);
   x2 = scm_to_mpfrptr (val2);
   mpfr_mul (x, x1, x2, MPFR_RNDN);
   return res;
}

SCM scm_mpfr_div (SCM val1, SCM val2)
{
   SCM res;
   mpfr_ptr x1, x2, x;
   res = mpfr_malloc ();
   x = scm_to_mpfrptr (res);
   x1 = scm_to_mpfrptr (val1);
   x2 = scm_to_mpfrptr (val2);
   mpfr_div (x, x1, x2, MPFR_RNDN);
   return res;
}

Automation

The excitement of writing wrapper functions by copy-pasting wears off very fast. Luckily we can use GCC macros to write the main content of the functions once and for all for a given prototype. For instance, the function scm_mpfr_div can be obtained from scm_mpfr_mul by replacing the two occurrences of mul by div. Since this happens in the middle of identifiers (scm_mpfr_mul itself and the function name mpfr_mul), we need a special construct, token concatenation through the ## preprocessing operator. The resulting code then becomes self-explaining; to keep the visual formatting, one just needs to also concatenate the different lines by ending them with a \.

#define GUILE_MPFR_F_FF(name) \
SCM scm_mpfr_ ## name (SCM val1, SCM val2) \
{ \
   SCM res; \
   mpfr_ptr x1, x2, x; \
   res = mpfr_malloc (); \
   x = scm_to_mpfrptr (res); \
   x1 = scm_to_mpfrptr (val1); \
   x2 = scm_to_mpfrptr (val2); \
   mpfr_ ## name (x, x1, x2, MPFR_RNDN); \
   return res; \
}

GUILE_MPFR_F_FF(mul)
GUILE_MPFR_F_FF(div)

The idea behind the macro name GUILE_MPFR_F_FF is to encode the prototype of the MPFR function: It has one output and two inputs of MPFR type. Using analogous macros we complete the example by adding the sine function and the constant π as follows:

#define GUILE_MPFR_F_F(name) \
SCM scm_mpfr_ ## name (SCM val1) \
{ \
   SCM res; \
   mpfr_ptr x1, x; \
   res = mpfr_malloc (); \
   x = scm_to_mpfrptr (res); \
   x1 = scm_to_mpfrptr (val1); \
   mpfr_ ## name (x, x1, MPFR_RNDN); \
   return res; \
}

#define GUILE_MPFR_F(name) \
SCM scm_mpfr_ ## name (void) \
{ \
   SCM res; \
   mpfr_ptr x; \
   res = mpfr_malloc (); \
   x = scm_to_mpfrptr (res); \
   mpfr_const_ ## name (x, MPFR_RNDN); \
   return res; \
}

GUILE_MPFR_F_F(sin)
GUILE_MPFR_F(pi)

Registering C functions

Despite all the talk about bridging between C and Scheme and the use of the keyword SCM hinting at Scheme, so far we have remained firmly with C. It is time to register the C functions as Scheme variables. This is done by the scm_c_define_gsubr C function. For instance,

scm_c_define_gsubr ("mpfr-mul", 2, 0, 0, scm_mpfr_mul);

takes the scm_mpfr_mul C function and imports it into the Scheme REPL under the name mpfr-mul; it also specifies that the function takes two mandatory arguments (and no optional and remaining arguments).

We tie all the initialisation code (creation of the type and exporting of the C functions to Scheme) together into one function in guile-mpfr.c, which will serve as the entry point of the library:

void init_guile_mpfr (void)
{
   init_mpfr_type ();
   scm_c_define_gsubr ("integer->mpfr", 1, 0, 0, integer_to_mpfr);
   scm_c_define_gsubr ("mpfr->double", 1, 0, 0, mpfr_to_double);
   scm_c_define_gsubr ("mpfr-sin", 1, 0, 0, scm_mpfr_sin);
   scm_c_define_gsubr ("mpfr-mul", 2, 0, 0, scm_mpfr_mul);
   scm_c_define_gsubr ("mpfr-div", 2, 0, 0, scm_mpfr_div);
   scm_c_define_gsubr ("mpfr-pi", 0, 0, 0, scm_mpfr_pi);
   scm_c_define_gsubr ("mpfr-print", 1, 0, 0, scm_mpfr_print);
}

Using the REPL

Running make we now obtain the compiled dynamic library guile-mpfr.so. It can be loaded using the foreign function interface as a foreign extension into a running Guile REPL by the Guile procedure load-extension. We go one step further and create a Guile module. For this, it is enough to place the following Guile code into the file mpfr.scm in the current directory:

(define-module (mpfr)
  #:export (integer->mpfr
            mpfr->double
            mpfr-print
            mpfr-sin
            mpfr-mul
            mpfr-div
            mpfr-pi))

(load-extension "guile_mpfr" "init_guile_mpfr")

The load-extension procedure opens the dynamic library and initialises it by calling the init_guile_mpfr function. As seen above, this function defines a number of Scheme procedures which, so far, in C parlance, are static to the module; but they are in fact #:exported through the module definition. To use the module, we run

export GUILE_EXTENSIONS_PATH=.:$GUILE_EXTENSIONS_PATH
make
guile -L .

in the terminal. The first line tells Guile to look for extension libraries in the current directory; the second line is there because it is better to call make too often than not often enough; the last line starts a Guile REPL and tells it to look for modules in the current directory.

In the REPL, we can now run the following code:

(use-modules (mpfr))
(mpfr-print (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))
(mpfr->double (mpfr-mul (integer->mpfr 2) (mpfr-sin (mpfr-div (mpfr-pi) (integer->mpfr 3)))))

which should print the square root of 3 twice, once in quadruple precision, once (doubly) rounded to double precision.

Time to breathe before continuing with the next topic! The current state of the files is recorded as commit 486c622 of the guile-mpfr project in the multiprecision group on Codeberg.

Excursion: GOOPS!

Now that we have quadruple precision numbers, it is tempting to somehow insert them into the Guile numerical tower. Numbers are a special class of Guile objects in the GOOPS, the Guile Object Oriented Programming System, which works through polymorphic functions. So in addition to mpfr_type (which, somehow confusingly, is actually a C variable), we define an object type holding MPFR values in the file mpfr-goops.scm.

(define-module (mpfr-goops)
  #:use-module (mpfr)
  #:use-module (oop goops)
  #:export (<mpfr>
            mpfr-ref
            mul))

(define-class <mpfr> (<number>)
  (val #:init-value (integer->mpfr 0) #:init-keyword #:val #:accessor mpfr-ref))

Here we define the <mpfr> class as a subclass of the more general <number> class, itself a direct descendent of the most abstract class <top>. We provide it with one slot, val (again the name does not matter since we will access its content differently), which is supposed to hold one element of type mpfr_type. If an <mpfr> object is created without any further parameter, then our choice of #:init-value sets it to 0. But #:init-keyword adds the named parameter #:val to the creation procedure to choose a different initial value. Finally #:accessor adds a procedure with the name mpfr-ref to get and set the value. The following code snippet from the Guile REPL illustrates how to create, modify and print such objects.

(use-modules (mpfr) (oop goops) (mpfr-goops))
(define x (make <mpfr>))
(mpfr-print (mpfr-ref x))
(set! (mpfr-ref x) (integer->mpfr 1))
(mpfr-print (mpfr-ref x))
(define y (make <mpfr> #:val (integer->mpfr 2)))
(mpfr-print (mpfr-ref y))

First we create a variable x containing the common initial value 0, then replace this value by 1. The variable y is directly created holding the integral value 2.

We can now add a polymorphic multiplication function into the mpfr-goops module, which follows precisely the new-+ example from the Guile documentation:

(define-method (mul)
  (make <mpfr> #:val (integer->mpfr 1)))

(define-method (mul (x <mpfr>))
  (make <mpfr> #:val (mpfr-ref x)))

(define-method (mul (x <integer>))
  (make <mpfr> #:val (integer->mpfr x)))

(define-method (mul (x <mpfr>) . args)
  (make <mpfr>
    #:val (mpfr-mul (mpfr-ref x) (mpfr-ref (apply mul args)))))

(define-method (mul (x <integer>) . args)
  (mul (mul x) (apply mul args)))

It recursively defines a procedure mul that computes the product of its arguments, an arbitrary number of integers and <mpfr> objects in any order, by folding over the arguments. First, the product of no arguments is set to the neutral element 1. Then the product of one argument is set to its <mpfr> value; if it is an integer, one needs to add an explicit type cast. Finally the product of two or more arguments is obtained by multiplying the first argument with the product of all other ones.

We can now print the factorial of 4 by running

(mpfr-print (mpfr-ref (mul 3 y 4)))

in the above Guile REPL session.

We are not going to pursue this approach further; given that there are <integer>, <real> and <complex> numbers (and even though the last one is of no interest for real arithmetic, but would be relevant to Guile bindings for MPC), there is a combinatorial explosion for the different argument types. As is common when programming in C with the MPFR library, one can always add an explicit type conversion before calling a function, and then in fact the GOOPS-free approach of the previous section is sufficient. Nevertheless, the mpfr-goops module is available for illustrative purposes in commit 8121125 of the guile-mpfr project.

This post has covered the general steps of creating bindings from Guile to a C library, first by creating wrappers at the layer of C programming, which is also the language in which the foundations of Guile are written, and then on the level of calling the wrapped functions from a Guile REPL. The next post will cover facets that are particular to the MPFR library, namely the handling of floating point precisions and rounding modes. Nevertheless, it will also be an occasion to learn more Scheme in the process.