Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C types with void explicitly in the parameter list (e.g. void (*)(void)) are assigned the wrong Haskell type #155

Open
lehmacdj opened this issue Feb 5, 2025 · 7 comments · May be fixed by #156

Comments

@lehmacdj
Copy link

lehmacdj commented Feb 5, 2025

When attempting to invoke a function with a void argument like in this snippet:

-- | - (void)prepareForReadingItemsAtURLs:(NSArray<NSURL *> *)readingURLs
--        options:(NSFileCoordinatorReadingOptions)readingOptions
--        writingItemsAtURLs:(NSArray<NSURL *> *)writingURLs
--        options:(NSFileCoordinatorWritingOptions)writingOptions
--        error:(NSError **)outError
--        byAccessor:(void (^)(void (^completionHandler)(void)))batchAccessor;
m_NSFileCoordinator_prepareForReadingAndWritingItems ::
  Ptr NSFileCoordinator ->
  Ptr NSArray ->  -- readingURLs
  NSFileCoordinatorReadingOptions ->
  Ptr NSArray ->  -- writingURLs
  NSFileCoordinatorWritingOptions ->
  Ptr (Ptr NSError) ->
  IO () ->
  IO ()
m_NSFileCoordinator_prepareForReadingAndWritingItems fileCoordinator readingURLs readingOptions writingURLs writingOptions errorPtr accessor = do
  -- let accessor' :: FunPtr (CInt -> IO ()) -> IO () = \completionHandler -> accessor ($(C.peekFunPtr [t| CInt -> IO () |]) completionHandler 0)
  [C.block| void {
    [$(NSFileCoordinator *fileCoordinator)
      prepareForReadingItemsAtURLs: $(NSArray *readingURLs)
      options: $(NSFileCoordinatorReadingOptions readingOptions)
      writingItemsAtURLs: $(NSArray *writingURLs)
      options: $(NSFileCoordinatorWritingOptions writingOptions)
      error: $(NSError **errorPtr)
      byAccessor: ^(void (^completionHandler)(void)) {
        $fun:(void (*accessor)(void))();
        completionHandler()
      }
    ];
  }
  |]

I get an error like so:

• Unacceptable argument type in foreign declaration:
    ‘()’ cannot be marshalled in a foreign call
• When checking declaration:
    foreign import ccall safe "wrapper" inline_c_ffi_6989586621681841573
      :: (() -> IO ()) -> IO (FunPtr (() -> IO ()))

Inspecting the FunPtr docs, I see that this is not the expected type for a function pointer of type void (*)(void) instead it should have type IO () or (). The bug is in buildArr in the antiquoter for $fun:. I'm working on a fix and will make a PR shortly.

@lehmacdj lehmacdj changed the title Handle void arguments correctly C types with void explicitly in the parameter list (e.g. void (*)(void)) are assigned the wrong Haskell type Feb 5, 2025
@lehmacdj lehmacdj linked a pull request Feb 5, 2025 that will close this issue
@lehmacdj
Copy link
Author

lehmacdj commented Feb 5, 2025

As a workaround you can declare the type without a parameter list, e.g. void (*)() for a void (*)(void) function. This is the correct C++ syntax but technically incorrect for C.

@junjihashimoto
Copy link
Collaborator

junjihashimoto commented Feb 8, 2025

Hi,

The current version works as follows.
I don't think it's wrong.
If necessary, you can just cast it in the program.

Hspec.it "converts function pointer without a parameter" $ do
    shouldBeType
    (cty "void (*f)()")
    [t| FunPtr (IO ()) |]
Hspec.it "converts void parameter function pointer" $ do
    shouldBeType
    (cty "void (*f)(void)")
    [t| FunPtr (() -> IO ()) |]

@junjihashimoto
Copy link
Collaborator

In the above example, there is no need to cast, so what if you changed $fun:(void (*accessor)(void))(); to $fun:(void (*accessor)())(); instead?

@lehmacdj
Copy link
Author

Yes that works as a workaround I agree.

However, I think that FunPtr (() -> IO ()) is never a valid FunPtr type. Quoting from Foreign.Ptr’s documentation:

A value of type FunPtr a is a pointer to a function callable from foreign code. The type a will normally be a foreign type, a function type with zero or more arguments where

Notice how () is only included as a special case for return types. This is also the source of the error I got with my original code snippet. With the current implementation it is most likely impossible to use void (*)(void) in any context where it is translated into a Haskell type because in every case the resulting FunPtr type will be rejected by GHC’s FFI.

Further more in C, not specifying void in the parameter list technically defines a prototype without information about function parameters. Supporting this in inline-c seems completely unnecessary (and incompatible with C++), however it seems harmless to support the void in the parameter list when most likely any existing use would fail to compile, due to FunPtr (() -> IO ()) being rejected by the FFI.

@lehmacdj
Copy link
Author

lehmacdj commented Feb 10, 2025

As a further more minimal example this program fails to compile with any combination of the 3 lines in main uncommented:

#!/usr/bin/env stack
{- stack script
  --resolver lts-21.24
  --package inline-c
-}

{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import Foreign
import qualified Language.C.Inline as C

C.context (C.baseCtx <> C.funCtx)

dummy :: () -> IO ()
dummy () = pure ()

main :: IO ()
main = do
  -- wrapped :: FunPtr (() -> IO ()) <- $(C.mkFunPtr [t|() -> IO ()|]) dummy
  let unwrapped = $(C.peekFunPtr [t|() -> IO ()|]) (undefined :: FunPtr (() -> IO ()))
  -- [C.exp| void { $fun:(void (*dummy)(void))() } |]
  pure ()

With this configuration the error is:

/Users/devin/src/haskell/experiments/ffi/inline-c-void-params.hs:24:20: error:
    • Unacceptable argument type in foreign declaration:
        ‘()’ cannot be marshalled in a foreign call
    • When checking declaration:
        foreign import ccall safe "dynamic" inline_c_ffi_6989586621679020388
          :: FunPtr (() -> IO ()) -> () -> IO ()
   |
24 |   let unwrapped = $(C.peekFunPtr [t|() -> IO ()|]) (undefined :: FunPtr (() -> IO ()))
   |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

@junjihashimoto
Copy link
Collaborator

At first glance it seems harmless.
But, do we need to support these as well?
I don't think these will actually be used, so I think it's fine to have an error.

()-> () -> IO ()
()-> Int -> IO ()
Int -> () -> IO ()

@lehmacdj
Copy link
Author

Going by the documentation on FunPtr I think all of these are also illegal. Certainly the equivalent C types aren't valid, void (*)(int, void) is nonsense. Might be worth checking the spec for the FFI aka the Haskell Report, but we can probably trust base's documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants