Search code examples
haskelliodefinition

Haskell primPutChar definition


I'm trying to figure out how the basic IO Haskell functions are defined, so I used this reference and I got to the putChar function definition:

putChar    :: Char -> IO ()
putChar    =  primPutChar

Now, however, I cannot find more information about this primPutChar function anywhere. Maybe it might refer to a pre-compiled function, available as binary from a shared object? If that's the case, is it possible to see its source code?


Solution

  • What prim* means

    Since you're asking this question in terms of the report, let's also answer this question in terms of the report:

    Primitives that are not definable in Haskell , indicated by names starting with "prim", are defined in a system dependent manner in module PreludeBuiltin and are not shown here

    This is still the same in Haskell2010 by the way.

    How it's implemented in GHC

    However, you can have a look at base's source to see how it's implemented in GHC:

    putChar         :: Char -> IO ()
    putChar c       =  hPutChar stdout c
    

    From there you're going deep into the rabbit hole. How does hPutChar know how to print stuff? Well, it doesn't. It only "buffers" and checks that you can write:

    hPutChar :: Handle -> Char -> IO ()
    hPutChar handle c = do
        c `seq` return ()
        wantWritableHandle "hPutChar" handle $ \ handle_  -> do
         hPutcBuffered handle_ c
    

    The writing is done in writeCharBuffer which fills an internal buffer until it's full (or a line has been reached—it actually depends on the buffer mode):

    writeCharBuffer h_@Handle__{..} !cbuf = do
      -- much code omitted, like buffering
          bbuf'' <- Buffered.flushWriteBuffer haDevice bbuf'
      -- more code omitted, like buffering
    

    So where is flushWriteBuffer defined? It's actually part of stdout:

    stdout :: Handle
    stdout = unsafePerformIO $ do
       setBinaryMode FD.stdout
       enc <- getLocaleEncoding
       mkHandle FD.stdout "<stdout>" WriteHandle True (Just enc)
                    nativeNewlineMode{-translate newlines-}
                    (Just stdHandleFinalizer) Nothing
    
    stdout :: FD
    stdout = stdFD 1
    

    And a file descriptor (FD) is an instance of BufferedIO:

    instance BufferedIO FD where
      -- some code omitted
      flushWriteBuffer  fd buf = writeBuf' fd buf
    

    and writeBuf uses instance GHC.IO.Device.RawIO FD's write, and that ultimately leads to:

    writeRawBufferPtr loc !fd buf off len
      | isNonBlocking fd = unsafe_write -- unsafe is ok, it can't block
      | otherwise   = do r <- unsafe_fdReady (fdFD fd) 1 0 0
                         if r /= 0
                            then write
                            else do threadWaitWrite (fromIntegral (fdFD fd)); write
      where
        do_write call = fromIntegral `fmap`
                          throwErrnoIfMinus1RetryMayBlock loc call
                            (threadWaitWrite (fromIntegral (fdFD fd)))
        write         = if threaded then safe_write else unsafe_write
        unsafe_write  = do_write (c_write (fdFD fd) (buf `plusPtr` off) len)
        safe_write    = do_write (c_safe_write (fdFD fd) (buf `plusPtr` off) len)
    

    where we can see c_safe_write and c_write, which are usually bindings to C library functions:

    foreign import capi unsafe "HsBase.h write"
       c_write :: CInt -> Ptr Word8 -> CSize -> IO CSsize
    

    So, putChar uses write. At least in GHC's implementation. The report however doesn't require that implementation, so another compiler/runtime is allowed to use other functions.

    TL;DR

    GHC's implementation uses write with internal buffers to write things, including single characters.