Monday, August 18, 2014

Bluetooth on Haskell

I'm presenting a very early draft of my bluetooth library. As its name suggests, bluetooth is a Haskell frontend to low-level Bluetooth APIs, making it similar in spirit to Python's PyBluez and Java's BlueCove.

What it can do

Currently, bluetooth only supports Linux. It has the capability of running an RFCOMM server and client. Theoretically, it should also support L2CAP servers and clients, although this has not been tested yet.

What it will eventually do

I plan to have bluetooth support each of the GHC Tier 1 platforms—that is, Windows, OS X, Linux, and FreeBSD. I want to have the capability to run the full gamut of L2CAP and RFCOMM-related functions on each OS, as well as any additional OS-specific functionality.

Motivation

Bluetooth programming on Haskell is currently in a very primitive state. As of now, there are only two packages on Hackage that make any mention of Bluetooth (as far as I can tell):

  1. network hints in its Network.Socket module that there is an AF_BLUETOOTH socket address family. However, trying to use it with network will fail, since there is no corresponding Bluetooth SockAddr.
  2. simple-bluetooth by Stephen Blackheath offers some of what my own bluetooth package offers (namely, RFCOMM client capability on Windows and Linux).
However, there is currently no comprehensive, cross-platform Haskell Bluetooth library à la PyBluez or BlueCove. I want bluetooth to fill that niche.

How bluetooth works

bluetooth can be thought of as a wrapper around network. After all, Bluetooth programming is socket-based, so Network.Socket already provides most of what one needs to implement a Bluetooth server or client. There are several gotchas with Bluetooth programming, however:
  • Every major OS has a completely different Bluetooth software stack. For example, Linux uses BlueZ, and Windows has several different stacks, including Winsock and Widcomm. Therefore, bluetooth is not likely to work identically on every OS.
  • Windows in particular is challenging to support since several Winsock functions do not work correctly on the version of MinGW-w64 that is currently shipped with GHC for Windows (only the 64-bit version, no less). For this reason, I probably won't develop a Windows version of bluetooth until this issue is resolved.
It is recommended that you have a basic understanding of Bluetooth programming before attempting to use bluetooth. I recommend this introduction by Albert Huang.

Examples

The following are abridged examples of the RFCOMM client and server examples from the bluetooth repo.

RFCOMM server

module Main where
import Data.Set

import Network.Bluetooth
import Network.Socket

main :: IO ()
main = withSocketsDo $ do
    let uuid     = serviceClassToUUID SerialPort
        proto    = RFCOMM
        settings = defaultSDPInfo {
            sdpServiceName = Just "Roto-Rooter Data Router"
          , sdpProviderName = Just "Roto-Rooter"
          , sdpDescription = Just "An experimental plumbing router"
          , sdpServiceClasses = singleton SerialPort
          , sdpProfiles = singleton SerialPort
        }

    handshakeSock <- bluetoothSocket proto
    btPort <- bluetoothBindAnyPort handshakeSock anyAddr
    bluetoothListen handshakeSock 1
    service <- registerSDPService uuid settings proto btPort
    (connSock, connAddr) <- bluetoothAccept handshakeSock
    putStrLn $ "Established connection with address " ++ show connAddr

    message <- recv connSock 4096
    putStrLn $ "Received message! [" ++ message ++ "]"
    let response = reverse message
    respBytes <- send connSock response
    putStrLn $ "Sent response! " ++ show respBytes ++ " bytes."

    close connSock
    close handshakeSock
    closeSDPService service

RFCOMM client

module Main where

import Network.Bluetooth
import Network.Socket

import System.IO

main :: IO ()
main = withSocketsDo $ do
    let addr = read "12:34:56:78:90:00"
        port = 1
    
    sock <- bluetoothSocket RFCOMM
    bluetoothConnect sock addr port
    putStrLn "Established a connection!"
    
    putStr "Please enter a message to send: "
    hFlush stdout
    message <- getLine
    
    messBytes <- send sock message
    response <- recv sock 4096
    putStrLn $ "Received reponse! [" ++ reponse ++ "]"

    close sock

Sunday, June 1, 2014

Haskell, DLLs, and You

Haskell libraries are suitable for the large majority of a functional programmer's use cases. Sometimes, however, there are needs that Haskell alone cannot fulfill. One common example is interfacing with low-level OS utilities. Since many of these tools are written in C, Haskell provides a useful -XForeignFunctionInterface (FFI) extension to use C functions and data directly. Below is an example file (Main1.hsc) that utilizes the FFI to call srand() and rand() on Linux:
{-# LANGUAGE ForeignFunctionInterface #-}
module Main where

import Foreign.C.Types

#include <stdlib.h>

foreign import ccall "srand"
    c_srand :: CUInt -> IO ()

foreign import ccall "rand"
    c_rand :: IO CInt

main :: IO ()
main = readLn >>= c_srand >> c_rand >>= print
Notice that the file extension is .hsc, not .hs. This is because it uses a special #include construct that must be processed by the hsc2hs program (which comes with the Haskell Platform). If you use cabal, this is done automatically. Otherwise, hsc2hs can be invoked like so:
$ hsc2hs Main1.hsc
$ runhaskell Main1.hs
42
71876166
$ runhaskell Main1.hs
27
1416980517
Generally, this process is pretty straightforward for Linux header files. What about other operating systems? One OS in particular, Windows, also provides C headers for its API, but they are much more difficult for hsc2hs to use successfully. Here is a simple example (Main2.hsc) that uses the Windows API function timeGetTime(), which retrieves the current system time in milliseconds:
{-# LANGUAGE ForeignFunctionInterface #-}
module Main where

#include <timeapi.h>

import System.Win32.Types

foreign import ccall "timeGetTime"
    c_timeGetTime :: IO DWORD

main :: IO ()
main = putStrLn . show =<< c_timeGetTime
If you attempt to simply run hsc2hs Main2.hsc, you will get an error resembling Main2.hsc:4:21: fatal error: timeapi.h: No such file or directory. That's not surprising, since we're referencing a Windows API header that isn't shipped with the version of MinGW that the Haskell Platform for Windows uses. To get around this, we'll try including the necessary header location manually (using Cygwin):
$ hsc2hs Main2.hsc -I"C:\Program Files (x86)\Windows Kits\8.1\Include\um"
In file included from Main2.hsc:4:0:
C:\Program Files (x86)\Windows Kits\8.1\Include\um/timeapi.h:17:20: fatal error: apiset.h: No such file or directory
compilation terminated.
Oh no! timeapi.h depends on a header file located in a different directory. That means we should only need to link that additional directory to resolve the issue, right?
$ hsc2hs Main2.hsc -I"C:\Program Files (x86)\Windows Kits\8.1\Include\um" -I"C:\Program Files (x86)\Windows Kits\8.1\Include\shared"
In file included from C:\Program Files (x86)\Windows Kits\8.1\Include\um/timeapi.h:21:0,
                 from Main2.hsc:4:
C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:94:21: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'MMVERSION'
C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:98:32: error: expected declaration specifiers or '...' before 'return'
C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:98:9: error: function definition declared 'typedef'
C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h: In function '_Return_type_success_':
C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:98:45: error: expected declaration specifiers before 'UINT'
C:\Program Files (x86)\Windows Kits\8.1\Include\um/mmsyscom.h:102:14: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'FAR'
...
Ack! As far as I can tell, Windows-style headers are simply not compatible with MinGW. It is clear that using Windows-style header files directly in hsc2hs is a fool's errand.

What can be done about this? One approach is to compile the code into a dynamically linked library (DLL) and pass it to the Haskell compiler's linker. This has the advantage of not needing the #include construct. As an example, we can create a simple DLL project in Microsoft Visual Studio (I used Visual Studio 2013 to perform the following steps):
  1. Create a new Win32 Console Application project (I will give it the name dll_example). Make sure its application type is "DLL" in the correspding wizard menu. For good measure, check "Empty project" and uncheck "Security Development Lifecycle (SDL) checks".
  2. In the Solution Explorer, right-click on "Header Files" and click Add > New Item. Select "Header file (.h)" and name it dll_example.h.
  3. Use the following code for dll_example.h:
    #ifndef DLL_EXAMPLE_H
    #define DLL_EXAMPLE_H
    
    #include <windows.h>
    
    #ifdef DLL_EXAMPLE_DLL_EXPORTS
    #define DLL_EXAMPLE_DLL_API __declspec(dllexport)
    #else
    #define DLL_EXAMPLE_DLL_API __declspec(dllimport)
    #endif
    
    DLL_EXAMPLE_DLL_API DWORD time_get_time();
    
    #endif
    
    Note that we use windows.h since it automatically brings all of the definitions of timeapi.h into scope, as well as other needed definitions (such as DWORD).
  4. In the Solution Explorer, right-click on "Source Files" and click Add > New Item. Select "C++ File (.cpp)" and name it dll_example.cpp.
  5. Use the following code for dll_example.cpp:
    #include "dll_example.h"
    #define DLL_EXAMPLE_API
    #pragma comment(lib, "Winmm.lib")
    
    DWORD time_get_time() {
     return timeGetTime();
    }
    
  6. In the Solution Explorer, right-click on "Resource Files" and click Add > New Item. In the left sidebar, click Visual C++ > Code, then click "Module-Definition File (.def)" and name it dll_example.def.
  7. Give dll_example.def the following definition:
    LIBRARY dll_example
    EXPORTS
     time_get_time
    
  8. Click Build > Build Solution. When it is finished, copy the newly created dll_example.dll (it should be in a directory similar to <project directory>/Debug) to the same directory where Main2.hsc is located.
Now we can dynamically link the DLL file by removing the #include <timeapi.h> line from Main2.hsc entirely, changing "timeGetTime" to "time_get_time", renaming it to Main2.hs, and compiling it like so:
$ ghc --make Main2.hs -L. -lWinmm -ldll_example
[1 of 1] Compiling Main             ( Main2.hs, Main2.o )
Linking Main2.exe ...
$ ./Main2.exe
95495217
$ ./Main2.exe
95496824
Great! We seemed to have successfully used the DLL. But what happens when we attempt to execute Main2.exe independently of dll_example.dll?
$ cp Main2.exe ../Main2.exe
$ cd ..
$ ./Main2.exe
.../Main2.exe:  error while loading shared libraries: ?: cannot open shared object file: No such file or directory
Urgh. As it turns out, Windows needs to look up the dynamically linked libraries every time the executable is run. Notable locations that Windows searches include the executable's directory and the directories defined in the PATH environment variable.

This is rather inconvenient for a Haskell programmer, as cabal installs all of its compiled Haskell executables in %APPDATA%/cabal/bin, far away from the custom DLL files it needs. What is the best solution to this problem? This GHC wiki page suggest several approaches, but since I am a fan of straightforward fixes, I prefer to simply copy the needed DLL files directly to %APPDATA%/cabal/bin. Since that's tedious to do manually, we can configure cabal to automate this process.

During my attempts to get cabal to use DLLs during compilation, I discovered that cabal's extra-lib-dirs field only accepts absolute paths. This is a problem for us, since we need to use a custom DLL file whose location is relative to the package's root directory. (There are claims that you can use ${pkgroot} to retrieve this location, but I was not able to get it to work). This solution should resolve both of the aforementioned cabal issues:
  1. Create a new cabal project (i.e., cabal init). Put all of the Main2.hs in the project, and put dll_example.dll in <package root>/lib.
  2. In Main2.cabal, make sure that extra-source-files includes lib/dll_example.dll, and that extra-libraries includes Winmm and dll_example.
  3. Adapt Setup.hs to use this code:
    import Control.Monad
    
    import Debug.Trace
    
    import Distribution.PackageDescription
    import Distribution.Simple
    import Distribution.Simple.LocalBuildInfo
    import Distribution.Simple.Setup
    
    import System.Directory
    import System.FilePath
    
    dllFileName :: FilePath
    dllFileName = "dll_example" <.> "dll"
    
    dllSourceDir :: IO FilePath
    dllSourceDir = do
        curDir <- getCurrentDirectory
        return $ curDir </> "lib"
    
    dllSourcePath :: IO FilePath
    dllSourcePath = do
        sourceDir <- dllSourceDir
        return $ sourceDir </> dllFileName
    
    copyDll :: String -> FilePath -> FilePath -> IO ()
    copyDll message sourcePath destPath = do
        putStrLn message
        putStr "Copying... "
        copyFile sourcePath destPath
        putStrLn "Done."
    
    patchDesc :: FilePath -> PackageDescription -> PackageDescription
    patchDesc sourceDir desc = let Just lib = library desc
                                   lbi = libBuildInfo lib
                                   newlbi = lbi { extraLibDirs = sourceDir : extraLibDirs lbi }
                               in desc { library = Just $ lib { libBuildInfo = newlbi } }
        
    customBuild :: FilePath -> PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
    customBuild sourceDir desc linfo hooks flags = do
        let installDir = bindir $ absoluteInstallDirs desc linfo NoCopyDest
            destPath = installDir </> dllFileName
        sourcePath <- dllSourcePath
        dllExists <- doesFileExist destPath
        
        when (not dllExists) $ copyDll (dllFileName ++ " is not in application data.") sourcePath destPath
        
        destTime <- getModificationTime destPath
        sourceTime <- getModificationTime sourcePath
        
        when (destTime < sourceTime) $ copyDll (dllFileName ++ " is out-of-date.") sourcePath destPath
        
        buildHook simpleUserHooks (patchDesc sourceDir desc) linfo hooks flags
    
    customInstall :: FilePath -> PackageDescription -> LocalBuildInfo -> UserHooks -> InstallFlags -> IO ()
    customInstall sourceDir desc = instHook simpleUserHooks $ patchDesc sourceDir desc
    
    customPostConf :: FilePath -> Args -> ConfigFlags -> PackageDescription -> LocalBuildInfo -> IO ()
    customPostConf sourceDir args conf desc linfo = postConf simpleUserHooks args conf (patchDesc sourceDir desc) linfo
    
    main :: IO ()
    main = do
        sourceDir <- dllSourceDir
        defaultMainWithHooks $ simpleUserHooks
            { buildHook = customBuild sourceDir
            , instHook = customInstall sourceDir
            , postConf = customPostConf sourceDir
            }
    
That code should be sufficient to tell cabal where dll_example.dll is during compilation, where dll_example.dll should be copied to, and when it needs to be copied (i.e., if it doesn't exist or is out-of-date). You should now be able to compile the project simply with cabal install without worrying about ugly GCC flags.

The downside to this approach is now there are extra files to manage if the user ever wants to uninstall Main2. One way to resolve this is to provide users with a makefile containing an uninstall command that automatically removes Main2.exe and dll_example.dll from %APPDATA%/cabal/bin. If you want to see an example of this, check out the hermit-bluetooth repo, the project in which I encountered all of these problems (and motivated me to make this blog post so that maybe I can save other people some time).

Working with DLLs in Haskell tends to be quite gruesome, and I'd recommend avoiding it whenever possible. In same cases, though, dynamic linking is the only feasible solution to one's problems (especially on Windows), so it's nice to know that the infrastructure for interfacing with DLLs exists (even if actually using that interface is a tad unpleasant).