
7.2 Overview of the FI
Interface structure.
Note: For ease of presentation, we assume that the Interface structure and its substructures have all been opened with open. However, we do not advise the use of open in programs because it makes them difficult to debug.
The ML side of the interface now goes as follows; note that system responses will be prefixed by the greater-than (>) sign. All of the ML code here is available in foreign/samples/hello.sml.
The first step is to the foreign code itself. This action creates an ML object called a c_structure. For example:
val hello_struct =
Interface.C.Structure.loadObjectFile("hello.so", Interface.C.Structure.IMMEDIATE_LOAD);
> val hello_struct : c_structure = _
Once this action is complete, raw foreign code will have been loaded into MLWorks. We now want to feed data as arguments to functions in the foreign code -- in this case, the hello function in the C program -- and then to accept the results of the foreign computations as they are returned to ML. However, there is no means of accessing the foreign code from ML yet. To access the foreign code we must build ML entities which can access and manipulate foreign data. The FI provides a range of features to help us do so.
The FI requires that foreign data be associated with an ML type. Moreover, there must also be some way of describing how foreign data is to be interpreted and, as it were, "understood". From an operational point of view, this understanding amounts to a qualification of what operations the foreign data may participate in, and hence what form that participation could take.
So, in the FI, each piece of foreign data comes equipped with a certificate which describes what the interpretation of the data is at any given time. Since these certificates bear information, they must themselves be represented in terms of ML values.
However, the FI further separates the data storage of data values from their representation, by mapping the actual values into "workspace" objects called stores. The interpretation of these data values is then contained in another kind of ML entity called a c_object, which is rather like a disembodied "container". The idea is that objects are generally associated with a place within some store containing the object's data value. This indirection between object value and the interpretation of that value provides considerable flexibility, even within a strongly typed framework such as ML. Such flexibility is necessary for mimicing enough of a foreign data-typing scheme.
So the next step is to build a store object which will contain the data values, such as the arguments to, and the results from, foreign calls. For example:
val hello_store = Interface.Store.store{alloc = Interface.Store.ALIGNED_4, overflow = Interface.Store.BREAK, size = 60, status = Interface.Store.RDWR_STATUS };
> val hello_store : store = _
We now have a workspace for storing data values relating to the foreign code. The parameters in the call above say that:
val void_object = Interface.C.Value.object { ctype = Interface.C.Type.VOID_TYPE, store = hello_store };
> val void_object : c_type object = _
val str_object = Interface.C.Value.object { ctype = Interface.C.Type.STRING_TYPE{ length = 30 }, store = hello_store };
> val str_object : c_type object = _
val int_object1 = Interface.C.Value.object { ctype = Interface.C.Type.INT_TYPE, store = hello_store };
> val int_object1 : c_type object = _
val int_object2 = Interface.C.Value.object { ctype = Interface.C.Type.INT_TYPE, store = hello_store };
> val int_object2 : c_type object = _
val ptr_object = Interface.C.Value.object { ctype = Interface.C.Type.ptrType(Interface.C.Type.VOID_TYPE), store = hello_store };
> val ptr_object : c_type object = _
These objects are associated with particular places in the hello_store. However, because no values have yet been bound to these objects, read operations upon them are assumed to be invalid. Once they have been written to, they can then be read safely. The following code initializes some of these objects:
Interface.C.Value.setString(str_object, "What is 65 - 42? ---- Ans is "); Interface.C.Value.setInt(int_object1, 23);The following code extracts their values:
Interface.C.Value.getString(str_object); > val it : string = "What is 65 - 42? ---- Ans is "Having set this data up, we need to use it in conjunction with the foreign code we have already loaded. To do this, we need some signature information concerning the foreign functions we want to use. This requires an emptyInterface.C.Value.getInt(int_object1); > val it : int = 23
c_signature object:
val hello_sig = Interface.C.Signature.newSignature(); > val hello_sig : c_signature = _Next add the following entry to the signature. The entry corresponds to the foreign function we wish to use:
Interface.C.Signature.defEntry(hello_sig, Interface.C.Signature.FUN_DECL { name = "hello",
source = [Interface.C.Type.ptrType(Interface.C.Type.CHAR_TYPE),
Interface.C.Type.INT_TYPE] : Interface.C.Type.c_type list,
target = Interface.C.Type.INT_TYPE }
);
Note how the form of the signature information follows the structure of the ANSI C prototype for the function.
We can now use the c_signature and c_structure information we have obtained to extract callable entries for the foreign functions they provide.
val def_hello = Interface.C.Function.defineForeignFun(hello_struct, hello_sig); > val def_hello : filename -> c_function = fnUsing this, we can obtain a
c_function object that can then be called directly:
val hello = def_hello "hello"; > val hello : c_function = _The above allows foreign functions to be extracted as ML values and bound to ML identifiers in the usual way.
We have almost reached the point at which we can call our foreign function. Before we do, we need to set up the first argument as a character pointer to some string data:
Interface.C.Value.setPtrType { ptr = ptr_object, data = str_object };
Interface.C.Value.setPtrAddrOf { ptr = ptr_object, data = str_object };
Interface.C.Value.castPtrType { ptr = ptr_object, ctype = Interface.C.Type.CHAR_TYPE };
First, the pointer was set to the appropriate type, str_object. Then it was set to point at the str_object data. Finally, the pointer was cast to the required argument type. In fact, because strings are such a frequent case, the FI can accept STRING_TYPE values directly and convert them into an appropriate CHAR_TYPE pointer, for both argument and result from a foreign function.
Finally, we can call our foreign function hello() using all we have put together:
Interface.C.Function.call hello ([ptr_object,int_object1], int_object2); > val it : unit = ()The above call required two objects to give the argument values and an object to accept the result. The string
> What is 65 - 42? ---- Ans is 23is printed to the standard output.
After the call the result value is deposited in int_object2. We can extract this value with the following:
getInt(int_object2); > val it : int = 65

Generated with Harlequin WebMaker