Opened 10 years ago

Closed 8 years ago

Last modified 7 years ago

#736 closed feature request (fixed)

Allowing any newtype of the IO monad to be used in FFI and extra optional entry point

Reported by: brianh@… Owned by: simonpj
Priority: high Milestone: 6.8.1
Component: Compiler Version: 6.4.1
Keywords: FFI foreign monad entry point Cc: Bulat.Ziganshin@… ekmett@…
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: Test Case: ffi-deriv1
Blocked By: Blocking:
Related Tickets: Differential Rev(s):
Wiki Page:


Hi - When designing an API it is desirable to be able to encode the correct usage patterns for functions in the API in the type of the functions themselves, rather than relying on the user understanding the documentation and having to use runtime checks to ensure correct usage. Consider the following callback which uses a typical C API (DirectX) to draw something to the screen:

     void Render(int width, int height){

In Haskell with the FFI at present, we can define an equivalent API and use it as follows:

     type RenderCallback = Int -> Int -> IO ()
     clear :: IO ()
     scene :: IO () -> IO ()
     drawSquare :: IO ()

     onRender :: RenderCallback -> IO ()
     runGraphicsWindow :: IO () -> IO ()

     render :: RenderCallback
     render w h = do
                    scene $ do

     main = runGraphicsWindow $ do
                                  onRender render

This is all very well, but just like the C equivalent, it doesn't encode the fact that drawSquare can only be called between BeginScene and EndScene. For example the following render callback would result in a runtime error or at least an unexpected result for the user:

     badRender w h = drawSquare

To allow the type checker to enforce correct usage, we can use different monads which just wrap the IO monad as follows:

     newtype RenderM a = RenderM (IO a) deriving (Functor, Monad, MonadIO)
     newtype DrawM a = DrawM (IO a) deriving (Functor, Monad, MonadIO)

     type RenderCallback = Int -> Int -> RenderM ()

     clear :: RenderM ()
     scene :: DrawM () -> RenderM ()
     drawSquare :: DrawM ()

Now the good render function is well typed and the badRender function is ill typed.

With the current GHC implementation, it is possible to provide the interface above by using some fiddly wrapper functions to remove the wrapper monads and replace them with the IO monad, for example:

    type RenderCallbackIO = Int -> Int -> IO ()

    foreign import ccall "wrapper" mkRenderCallbackIO ::
         RenderCallbackIO -> IO (FunPtr RenderCallbackIO)

    dropRenderM :: RenderCallback -> RenderCallbackIO
    dropRenderM f x y = let RenderM io = f x y in io

    foreign import ccall api_onRender :: FunPtr RenderCallbackIO -> IO ()

    onRender :: RenderCallback -> IO ()
    onRender f = mkRenderCallbackIO (dropRenderM f) >>= api_onRender

    foreign import ccall api_clear :: IO ()

    clear :: RenderM ()
    clear = liftIO $ api_clear

As far as I can tell, GHC currently optimizes out all the overhead involved in converting between RenderM and IO. However the extra marshalling functions are fiddly to write, in particular since different versions of dropRenderM would be needed for different numbers of arguments in whatever function returns something in RenderM, and all these extra functions also obscure the simplicity of the original design.

Therefore I propose that for any monad M defined by:

     newtype M a = M (IO a) deriving (Functor, Monad, MonadIO)

M a should be able to appear in place of IO a anywhere in a foreign function definition since all 'M' does is to enforce typing on the Haskell side and has no relevance to the foreign language API, just as IO has no relevance to the foreign language either. This would mean we'd no longer have to write extra wrapper functions and rely on the compiler optimizing them out.

A related point is that the "API-safety == type correctness" gained by using different monads can at the moment be subverted because the entry point into a Haskell program is the main function which returns a value of type IO (). This means that initialization code for any API must be able to run in the IO monad. However every monad discussed above allows you to lift IO operations into it, so there is nothing to stop someone trying to make a nested re-initialization of the API in the middle of a callback...

It is necessary to allow IO actions to be lifted into the callback monads so the callbacks can make use of IORefs etc. However it is undesirable to allow the API to be re-initialized (in such a nested way).

Therefore I propose (perhaps this should have been a separate ticket but I don't know how to link two tickets together so I've bundled both issues in this ticket) that there should be an alternative entry point into a Haskell program with the following type:

     newtype MainM a = MainM (IO a) deriving (Functor, Monad, MonadIO)

     _main :: MainM ()

with this default implementation:

     _main = liftIO $ main

so that all existing programs will still work. If _main is explicitly defined by the user, the user's definition should be used instead, and any definition of "main" will have no special significance. This would allow the API's initialization function to be safely typed as:

     runGraphicsWindow :: IO () -> MainM ()

     _main = runGraphicsWindow $ do
                                   onRender render

Thus the user would be prevented from making nested calls to the initialization function.

Change History (9)

comment:1 Changed 9 years ago by igloo

  • Milestone set to 6.8

This is something we might want to consider for haskell' too.

comment:2 Changed 8 years ago by guest

  • Cc Bulat.Ziganshin@… added

comment:3 Changed 8 years ago by simonmar

  • Test Case set to ffi-deriv1

This is allegedly done:

Tue Apr 11 13:04:41 BST 2006  [email protected]
  * Allow IO to be wrapped in a newtype in foreign import/export

and we have a test:

Wed Apr 26 19:36:36 BST 2006  [email protected]
  * Add test for newtypes in FFI

But the test is currently failing, alas.

    Unacceptable result type in foreign declaration: RenderM ()
    When checking declaration:
	foreign import ccall safe "static  &duma_onRender" duma_onRender :: FunPtr RenderCallback
									    -> RenderM ()

    Unacceptable result type in foreign declaration: RenderM ()
    When checking declaration:
	foreign import ccall safe "wrapper" mkRenderCallback :: RenderCallback
								-> RenderM (FunPtr RenderCallback)

    Unacceptable result type in foreign declaration:
	RenderM (FunPtr RenderCallback)
    When checking declaration:
	foreign import ccall safe "wrapper" mkRenderCallback :: RenderCallback
								-> RenderM (FunPtr RenderCallback)

comment:4 Changed 8 years ago by simonmar

  • Owner set to simonpj
  • Priority changed from normal to high

On further inspection, ffi-deriv is failing because this case in TyCon.lhs doesn't match:

coreExpandTyCon_maybe (AlgTyCon {algTcRec = NonRecursive,	-- Not recursive
         algTcRhs = NewTyCon { nt_etad_rhs = etad_rhs, nt_co = Nothing }}) tys
   = case etad_rhs of	-- Don't do this in the pattern match, lest we accidentally
			-- match the etad_rhs of a *recursive* newtype
	(tvs,rhs) -> expand tvs rhs tys

for the newtype in question, the nt_co field is not Nothing. I have no clue where to go from here: clearly the Nothing is there for a good reason, because removing it causes a core lint failure later.

comment:5 Changed 8 years ago by guest

  • Cc Bulat.Ziganshin@… added; Bulat.Ziganshin@… removed

comment:6 Changed 8 years ago by simonpj

  • Resolution set to fixed
  • Status changed from new to closed

Now fixed.


comment:7 Changed 8 years ago by igloo

  • Milestone changed from 6.8 branch to 6.8.1

comment:8 Changed 7 years ago by simonmar

  • Architecture changed from Multiple to Unknown/Multiple

comment:9 Changed 7 years ago by simonmar

  • Operating System changed from Multiple to Unknown/Multiple
Note: See TracTickets for help on using tickets.