Opened 7 years ago

Last modified 3 years ago

#7204 new feature request

Use a class to control FFI marshalling

Reported by: simonpj Owned by:
Priority: normal Milestone:
Component: Compiler Version: 7.4.2
Keywords: Cc:
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: Differential Rev(s):
Wiki Page:


There has been a string of tickets concerning argument/result types for foreign declarations: #3008, #5529, #5610, #5664. This ticket suggest a new idea that Simon and I came up with this morning.

The current story is that a newtype can only be used in an FFI decl if the newtype's data constructor is in scope. This is enshrined in the FFI spec, but it's inconvenient and somewhat controversial. But suppose instead the ability to be passed to a foreign call was controlled by a class? Thus

class Marshal a where
  type RepType a
  marshal :: a -> RepType a
  unMarshal :: RepType a -> a

instance Marshal Int where
  type RepType Int = Int
  marshal = id
  unMarshal = id

newtype Age a = MkAge a
instance Marshal a => Marshal (Age a) where
  type RepType (Age a) = RepType a
  marshal (Age x) = marshal x
  unMarshal x = Age x

An author can control whether a newtype is marshalable by making it an instance of Marshal (or not). Moreover newtype deriving will work just fine on class Marshal so you can write

newtype Age a = MkAge a deriving( Marshal )

The FFI stub generation machinery would do the following. Given a declaration

foreign import foo :: T -> IO S

it will generate a Haskell foo thus:

foo :: T -> S
foo t = case (marshal t) of
          I# x# -> case "ccall foo x#" of
                      r# -> unMarshal (F# r#)

(I'm being a bit sloppy about the IO part, becuase it's not part of the main point here.)

In this example I've assumed that (after some type-level reductions)

  RepType T = Int
  RepType S = Float

but it should be OK provided the RepType T reduces to one of a fixed set of primitive types that GHC knows how to marshal.

So the rules become that an argument type T must satisfy two conditions:

  • T must be an instance of Marshal
  • (RepType T) must reduce to one of a fixed family of primitive types, Int, Float and so on.

Change History (9)

comment:1 Changed 7 years ago by igloo

Two things occur to me:

Currently, if a library defines a type T, uses it with the FFI, but exports T abstractly, then users of the library can't use T with the FFI. With this proposal, it would not be possible to not export the instance. I'm not sure if that's really a problem, and there's always the workaround of manually marshaling for the library-internal FFI calls.

The other issue is that a lot of hackage would suddenly become dependent on ATs. Actually, provided library writers use "deriving Marshal" wherever possible, correct code will work if the Haskell implementation just ignores the deriving clause and removes newtype wrappers at FFI calls, but ideally we wouldn't rely on such a workaround.

So I wonder what the state of ATs is? Is there a simple and correct spec for the extension that it would be reasonable to require Haskell implementations to implement?

comment:2 Changed 7 years ago by illissius

Newtype deriving doesn't currently work on classes with ATs:

    Can't make a derived instance of `Marshal (Age a)'
      (even with cunning newtype deriving):
      the class has associated types
    In the newtype declaration for `Age'

comment:3 Changed 7 years ago by simonpj

You're right. And arguably newtype deriving should work with at suitably-simple AT. Given

  class C a where
    type F a :: <kind>

  newtype T a b c = MkT <reptype> deriving( C )

we should generate an instance looking like

  instance C <reptype> => C (T a b c) where
    F (T a b c) = F <reptype>

That's another new extension to the (already complex) newtype deriving, but people will surely be asking for it before long!

comment:4 Changed 7 years ago by igloo

Milestone: 7.8.1

comment:5 Changed 6 years ago by thoughtpolice


Moving to 7.10.1

comment:6 Changed 5 years ago by thoughtpolice


Moving to 7.12.1 milestone; if you feel this is an error and should be addressed sooner, please move it back to the 7.10.1 milestone.

comment:7 Changed 4 years ago by thoughtpolice


Milestone renamed

comment:8 Changed 4 years ago by thomie

Milestone: 8.0.1

comment:9 Changed 3 years ago by RyanGlScott

Now that #2721/#8165 have been fixed, the technical obstacle of not being able to use GeneralizedNewtypeDeriving on classes with associated types is gone. The only question remaining is if we want to pursue this design.

Note: See TracTickets for help on using tickets.