[MLton] Mzton an MzScheme<->MLton FFI

Jens Axel Søgaard jensaxel@soegaard.net
Tue, 20 Sep 2005 00:46:11 +0200


This is a multi-part message in MIME format.
--------------050001040308020607080609
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 8bit

Hi all,

It has been a while since I reported on my progress on the
MzScheme/MLton FFI. The main reason, besides a general lack
of time, for the long delay was that I at some point lost
my sources (I am using Linux inside a VMware - and forgot
to make backups of the source *outside* the virtual
machine - sigh).

The work I have done has been on the Scheme side, so I have
no changes to Fluet's patch from

   <http://mlton.org/pipermail/mlton/2005-July/027603.html>

which has worked fine for me - I hope it will be included
in svn version?

Today I got callbacks working, which were the last major
piece missing. To give an impression of what the FFI can,
I have attached an example from the documentation.

Feedback is welcome.

-- 
Jens Axel Søgaard

--------------050001040308020607080609
Content-Type: text/plain;
 name="doc.txt"
Content-Transfer-Encoding: 8bit
Content-Disposition: inline;
 filename="doc.txt"


Mzton - An MzScheme to MLton FFI  --  Jens Axel Søgaard
-------------------------------------------------------

OVERVIEW
--------

The Mzton FFI provides the Scheme programmer a chance to use SML in
stead of C to implement foreign libraries. The FFI is designed in the
same philosophy "stay in the fun world" as the PLT C FFI by Eli
Barzilay. Thus as many low-level technicalities as possible is
concealed from the user of the FFI.

Mzton allows Scheme code to call functions written in ML. Conversions
between Scheme and ML values are transparent. ML arrays, vectors and
references returned from ML to Scheme are automatically garbage
collected (on both sides). It is possible to pass a callback (a Scheme
function) to an ML function.

EXAMPLE
-------

To give a first impression of the usage of this FFI, let's for a
moment consider the implementation of a small library for calculations
with n-dimensional vectors. Since we want to allow mutation, we will
represent an n-dimensional vector as an array of reals in ML. 

The library consists of the following three functions:

  dotVec       : (real array) * (real array) -> real
  makeUnitVec  : int * int -> real array
  modifyVec    : (real array) * (real -> real) -> real array

The dot product is calculated by the following ML function:

    fun dotVec (A,B) = Array.foldli (fn (i, a, x) => (Array.sub(B,i)*a+x)) 0.0 A;

The type of dotVec is  (real array) * (real array) -> real  so, since
the return value has type real, which is a base type, one exports it as:

   val _ = _export "dotVec" : ( (real array)*(real array) -> real ) -> unit; dotVec;

In the Scheme program it is imported by get-ffi-obj.

    (define dot-vec 
      (get-ffi-obj "dotVec" lib (_fun $RealArray $RealArray -> $real) signal-error))

Here "dotVec" is the name that occurs in the above _export. The
variable lib holds information about the shared library in question. 


If an ML function allocates an array, vector or reference which is to
be returned to the Scheme side, it is important to register it with
the MLton garbage collector. The FFI provides return-functions for
that purpose. 

The function makeUnitVec returns a newly allocated  real array
representing the i'th basis vector of R^n.

    fun makeUnitVec (n,i) = Array.tabulate (n, fn j => if i=j then 1.0 else 0.0);  

The type of this function is  int*int -> real array  so to export it,
one writes

    val _ = _export "makeUnitVec" : ( int*int -> real array ) -> unit; (RealArrayRoot.return o makeUnitVec);

The important thing to note here is that we are not exporting
makeUnitVec, but (RealArrayRoot.return o makeUnitVec). The function
RealArrayRoot.return is nothing but the identity function, but it has
the side effect of registering the returned array with the MLton
garbage collector. When the Scheme object representing the returned ML
array is garbage collected, the Scheme garbage collector will
automatically tell the MLton garbage collector that the array has
become garbage.

To import the function in the Scheme, one writes:

    (define make-unit-vec 
      (get-ffi-obj "makeUnitVec" lib (_fun $int $int -> $RealArray) signal-error))

Given these functions we can now experiment in the REPL.

  > (make-unit-vec 2 1)
  #4(4 #<primitive:ffi:getRealArray> #<primitive:ffi:unregRealArray> #<ctype>)
  > (define v (make-unit-vec 2 1))
  > (define w (make-unit-vec 2 2))
  > (dot-vec v v)
  1.0
  > (dot-vec v w)
  0.0
  > (dot-vec w w)
  1.0

The Scheme side of Mzton provides primitives to work with ML
arrays. We can reference entries in an ML array: 

  > (ml-array-ref v 0)
  1.0
  > (ml-array-ref v 1)
  0.0

We can convert an ML array to a Scheme vector:

  > (ml-array->vector v)
  #2(1.0 0.0)

We can alter entries in an ML array:

  > (ml-array-set! v 0 3.0)
  > (dot-vec v v)
  9.0

This is even safe, since Scheme checks the types!

  > (ml-array-set! v 0 "foo")
  Scheme->C: expects argument of type <double>; given "foo"


The third function allows to demonstrate how to pass a Scheme function
to the ML side. The function modifyVec (A,f) will replace each entry x
in A with f(x) and return nothing.

    fun modifyVec (A,f) = Array.modify f A;

One would expect that this could be exported as 

    val _ = _export "modifyVec" : ( (real array)*(real -> real) -> unit ) -> unit; makeUnitVec;

but, alas, the current MLton C FFI does not allow real->real as the
type of an argument. However MLton can make indirect function calls. 

    fun exportedModifyVec (A,pointerToF)
      = let val f = (_import * : MLton.Pointer.t -> real -> real;) pointerToF
        in 
          modifyVec (A,f) 
        end;

    val _ = _export "modifyVec" : ( (real array)*MLton.Pointer.t ) -> unit; exportedModiVec;

The import in Scheme uses the type  (real array)*(real -> real) -> unit 

  (define modify-vec!   
    (get-ffi-obj "modifyVec" lib (_fun $RealArray (_fun $real -> $real) -> $unit) signal-error))

Let us try it in the REPL:


  > (define v (make-unit-vec 2 1))
  > (ml-array->vector v)
  #2(1.0 0.0)
  > (modify-vec! v (lambda (x) (+ (* 2.0 x) 3.0)))
  > (ml-array->vector v)
  #2(5.0 3.0)





--------------050001040308020607080609--