[MLton] Callback to function pointer?

Wesley W. Terpstra wesley@terpstra.ca
Thu, 14 Jul 2005 16:46:48 +0200


On Thu, Jul 14, 2005 at 08:43:14AM -0400, Matthew Fluet wrote:
> > What I wonder is why doesn't _export "foo": int; return a setter for the
> > symbol "foo" with type int -> unit?

I was trying ask the abstract question, ignoring the fact that the
underlying implementation details for the two cases differ.

> Ultimately, because simple C types are fundamentally different from C 
> function types.  Also, you can get much the same effect using 
> MLton.Pointer functions:
> 
>  (let val ptr = _import # "foo": MLton.Pointer.t;
>   in MLton.Pointer.setInt32 (ptr, 0, 5)
>   end)

However, as you mentioned, that doesn't have type transparency.

> I note that this idiom strongly suggests changing _import # to _address,
> since it makes it clear that it makes available the address, suitable for
> both getting and setting.

I agree.

>   _import "foo": ffiTy;
> 
> is elaborated into:
> 
>   (let ptr = _address "foo": MLton.Pointer.t
>    in MLton.Pointer.get???(ptr, 0)
>    end)

But, that's after the type checking, right?
So, although that works inside MLton, that does not work for the user.

>   _export "foo": ffiTy;
> to elaborate in a similar fashion:
>   (let ptr = _address "foo": MLton.Pointer.t
>    in fn x => MLton.Pointer.set???(ptr, 0, x)
>    end)

I think this is the wrong name...
Perhaps _store, like you suggest later.
_export to me strongly suggests that the symbol name is created.

> > foo.c:
> > extern int myglobal;
> > int myfun() { printf("%d\n", myglobal); return 0; }
> > foo.sml:
> > fun dowork x = (_export "myglobal": int; x; _import "myfun": unit -> int; ())
> 
> I agree that would be a little bit more inline with the word 'export'.  I 
> don't see that it would be terribly difficult -- we could associate a 
> CType.t option with the FFI_Symbol prim and ensure that we create the 
> symbol with the right C-type.

Then perhaps _export could just begin accepting non-arrow types?

> An alternative response would be to note that 'import' and 'export' for 
> non-functional types are really the wrong words

At first I disagreed with you.

Then I realized that, as you said earlier, _import is different for
functions and for values. The key difference is that in C, a value is 
really a 'ref value', so that we need to fetch it repeatedly. Functions
are just 'normal' from an SML point of view (ie: unchanging) and so only
need to be fetched once.

In fact, _import * has absolutely nothing to do with importing of anything.
What it really does is create bridge code to call via a function pointer.

Nevertheless, I'm not sure the distinction merits user-visibility.

>   _fetch "foo" : int;  ==>  let val ptr = _address "foo" : MLton.Pointer.t
>                             in MLton.Pointer.getInt32(ptr, 0)
>                             end
> 
>   _store "foo" : int;  ==>  let val ptr = _address "foo" : MLton.Pointer.t
>                             in fn x => MLton.Pointer.setInt32(ptr, 0, x)
>                             end

These make sense and certainly convey no implication of creating the symbol.
However, I think it would be better if they operated on MLton.Pointer.t
instead of a given symbol. This is strictly more powerful.

> the only advantage of _fetch/_store over the desugared counter-parts is
> that one can use opaque (but FFI compatible) types with
> _fetch/_store, but not with MLton.Pointer.* functions.

... which I think is fairly important for practical FFI use.

> Note that _store, unlike _export, has no static component, so it is 
> perfectly valid to put _store in a loop. 

It's also perfectly valid to put _export in a loop, as long as you are
prepared to have it overwrite the "symbol's value" each time.

> Again, more evidence to the fact that functions are so not first class in
> C that it is questionably how much we should try to map them to first
> class ML functions.

I think this comment is in error; functions are the only SML-like thing in
C. It's C values that are all screwy, being references as they are.

_address "symbol";		==> MLton.Pointer.t

	I didn't really understand why one needs multiple types?
	Is always returning MLton.Pointer.t a bad thing?

_store *: 'a;			==> (MLton.Pointer.t * 'a) -> unit
_store "symbol": 'a;		==> 'a -> unit

	'a must be of non-arrow FFI type
	result is a normal first-class SML function

_import *: 'a;			==> MLton.Pointer.t -> 'a
_import "symbol": 'a;		==> 'a

	reads the named object 'immediately'
	if 'a is of non-arrow FFI type, grab result immediately
	if 'a if of arrow FFI type, returns SML glue code that
	calls the symbol as it points to a C function

_export "symbol": 'a;		==> 'a -> unit

	creates the named symbol

	for non-arrow types, otherwise identical to _store "symbol"
	for arrow types, a gateway function is emitted, and a SML function
	is returned which set's the SML function called by the gateway

What do you think?

An aside, I think _import * and _store * are vastly superior to the
MLton.Pointer.setWord* family of functions. In fact, MLton.Pointer.get* 
is completely superfluous even as things stand now, AFAICS.

After writing that summary, I noticed something that surprised me.
MLton can already do the "faster_sneaky_hack" in the opposite direction:

sneaky.c:
int faster_sneaky_hack;
sneaky.sml:
fun coerce fd =
  let
    val ptr = _import # "faster_sneaky_hack"; (* or _address *)
  in
    MLton.Pointer.setInt32 (ptr, 0, Int32.fromInt fd);
    _import "faster_sneaky_hack": Posix.IO.file_desc;
  end

The only reason that one can't go the other way is that MLton.Pointer.set* is
insufficiently powerful... hence _store. The other problem, needing sneaky.c
just to create the symbol is fixed by extending _export.

-- 
Wesley W. Terpstra