marshalling parameters (poor/lazy man's IDL)
Ross Paterson
ross at soi.city.ac.uk
Thu May 13 05:45:01 EDT 2004
Suppose you want to call a C function with signature
int f(long, double *, float *, short *);
where the first parameter is passed by value, the second by reference,
the third is an output and the the last is updated. In IDL terms:
int f([in]long, [in,ref]double *, [out]float *, [in,out]short *);
Now you could just use HDirect, which will write everything for you,
but let's suppose you choose not to for some reason. Then you declare
the foreign function
foreign import ccall "f"
c_f :: CLong -> Ptr CDouble -> Ptr CFloat -> Ptr CShort -> IO CInt
and you write a Haskell wrapper that does the parameter marshalling by hand:
f :: CLong -> CDouble -> CShort -> IO (CFloat, CShort, CInt)
f a b d =
with b $ \ pb ->
alloca $ \ pc ->
with d $ \ pd -> do
r <- c_f a pb pc pd
c <- peek pc
d' <- peek pd
return (c, d', r)
The last bit could be any computation using c, d' and r, but the rest
is boilerplate. The plan is to rewrite that as
f :: CLong -> CDouble -> CShort -> IO (CFloat, CShort, CInt)
f a b d = call c_f (val a . ref b . out . inout d) $ \ c d' r ->
return (c, d', r)
for suitable combinators call, val (because "in" is a reserved word),
ref, out and inout. (You may be reminded of Danvy's printf.)
The call combinator takes 3 arguments:
- a worker function, usually a foreign function
- a specification, consisting of a composition of parameter descriptions
in the same order as the function signature, and
- a continuation that takes the values of the outputs (again in the same
order as they occur in the signature) and the result.
The val, ref and inout combinators take the values to be passed;
the out and inout combinators give rise to parameters of the final
continuation.
When value parameters occur first (as they often do), we can move them
into the worker function:
f :: CLong -> CDouble -> CShort -> IO (CFloat, CShort, CInt)
f a b d = call (c_f a) (ref b . out . inout d) $ \ c d' r ->
return (c, d', r)
There's also a variant call_ that ignores the result:
f_ :: CLong -> CDouble -> CShort -> IO (CFloat, CShort)
f_ a b d = call_ c_f (val a . ref b . out . inout d) $ \ c d' ->
return (c, d')
It remains to define the combinators. The following is Haskell 98.
> module Param where
> import Foreign
> import Foreign.C
The parameter specifiers build a function that combines a worker function
and a continuation operating on the results of the worker, to produce an
IO action. Typically each specifier adds a parameter to the worker, and
may add one to the continuation. So in general their types look like
(rf -> rk -> IO b) -> (ExtraParam -> rf) -> (ExtraResult -> rk) -> IO b
Value and reference parameters also take the input value. There is no
extra result, and the combinators are fairly simple:
> val :: a -> (rf -> rk -> IO b) -> (a -> rf) -> rk -> IO b
> val a spec f k = spec (f a) k
> ref :: Storable a => a -> (rf -> rk -> IO b) -> (Ptr a -> rf) -> rk -> IO b
> ref a spec f k = with a $ \ p -> spec (f p) k
In the case of output parameters, the worker takes an extra pointer,
and we need to peek at that pointer to get the extra output, while
preserving all the arguments that follow it. For that, we need a class
that provides a generalized =<<:
> class LiftIO r where
> liftIO :: (a -> r) -> IO a -> r
> instance LiftIO (IO c) where
> liftIO = (=<<)
> instance LiftIO r => LiftIO (b -> r) where
> liftIO f v b = liftIO (flip f b) v
and we use this to peek at the pointer:
> out :: (Storable a, LiftIO rk) =>
> (rf -> rk -> IO b) -> (Ptr a -> rf) -> (a -> rk) -> IO b
> out spec f k = alloca $ \ p -> spec (f p) (liftIO k (peek p))
Inout parameters combine reference and output parameters:
> inout :: (Storable a, LiftIO rk) =>
> a -> (rf -> rk -> IO b) -> (Ptr a -> rf) -> (a -> rk) -> IO b
> inout a spec f k = with a $ \ p -> spec (f p) (liftIO k (peek p))
Finally, 'call' applies this composition of parameter specifications to
the base case, which is simply monadic binding of the result continuation
to a worker action:
> call :: rf -> ((IO a -> (a -> IO b) -> IO b) -> rf -> rk -> IO b) -> rk -> IO b
> call f spec_fn k = spec_fn (>>=) f k
Here's the variant that ignores the result:
> call_ :: rf -> ((IO a -> IO b -> IO b) -> rf -> rk -> IO b) -> rk -> IO b
> call_ f spec_fn k = spec_fn (>>) f k
There are also other parameter marshalling combinators not used in the
above example.
A 'Maybe' value passed as either a null pointer or a reference:
> unique :: Storable a =>
> Maybe a -> (rf -> rk -> IO b) -> (Ptr a -> rf) -> rk -> IO b
> unique mb_a spec f k = withMaybe mb_a $ \ p -> spec (f p) k
> where
> withMaybe :: Storable a => Maybe a -> (Ptr a -> IO b) -> IO b
> withMaybe Nothing k = k nullPtr
> withMaybe (Just a) k = with a k
Null-terminated strings:
> string :: String -> (rf -> rk -> IO b) -> (CString -> rf) -> rk -> IO b
> string s spec f k = withCString s $ \ p -> spec (f p) k
Strings followed by their length:
> stringLen :: Integral n =>
> String -> (rf -> rk -> IO b) -> (Ptr CChar -> n -> rf) -> rk -> IO b
> stringLen s spec f k =
> withCStringLen s $ \ (p, n) -> spec (f p (fromIntegral n)) k
Arrays followed by their length:
> arrayLen :: (Storable a, Integral n) =>
> [a] -> (rf -> rk -> IO b) -> (Ptr a -> n -> rf) -> rk -> IO b
> arrayLen arr spec f k =
> withArrayLen arr $ \ n p -> spec (f p (fromIntegral n)) k
More information about the FFI
mailing list