Search code examples
c++haskellghcghcihugs

Writing Haskell interpreter in C++ (using ghc or hugs as library)


I'm writing a C++ application that needs to interpret and evaluate haskell code. This code isn't known at compile time but given by the user. Is there a way to use a haskell compiler/interpreter (like GHCi or hugs) as a library?

  • I found FFI but this seems only to work for haskell code that is known at compile time.
  • I found the GHC API and hint, but they seem only to work when I want to interpret haskell code from out of haskell.

Solution

  • Instead of using the GHC api I would suggest binding to Hint for this particular approach, which is just a simplified wrapper around the GHC api. The reason I would recommend this is because the GHC api has a bit of a steep learning curve.

    But anyway, Like I said In my comment, depending on how deep you want this to go it would require surprisingly few FFI calls. Below I give an example on how to run expressions from a loaded file and return the results (only if there's a show instance). This is just the basics, returning the results as a structure should be possible too.

    module FFIInterpreter where
    
    import Language.Haskell.Interpreter
    
    import Data.IORef
    import Foreign.StablePtr
    
    type Session = Interpreter ()
    type Context = StablePtr (IORef Session)
    
    -- @@ Export
    -- | Create a new empty Context to be used when calling any functions inside
    --   this class.
    --   .
    --   String: The path to the module to load or the module name
    createContext :: ModuleName -> IO Context
    createContext name 
      = do let session = newModule name 
           _ <- runInterpreter session
           liftIO $ newStablePtr =<< newIORef session
    
    newModule :: ModuleName -> Session
    newModule name = loadModules [name] >> setTopLevelModules [name]
    
    -- @@ Export
    -- | free a context up
    freeContext :: Context -> IO ()
    freeContext = freeStablePtr
    
    -- @@ Export = evalExpression
    runExpr :: Context -> String -> IO String
    runExpr env input
      = do env_value <- deRefStablePtr env
           tcs_value <- readIORef env_value
           result    <- runInterpreter (tcs_value >> eval input) 
           return $ either show id result
    

    Since we have to exit haskell land we have to have some way to refer to the Context, We can do this with a StablePtr and I just wrap it in an IORef to make it mutable in case you want to change things in the future. Note that the GHC API does not support type checking an in-memory buffer, so you have to save the code you want to interpret to a temporary file before loading it.

    The -- @@ Annotations are for my tool Hs2lib, don't mind them if you don't use it.

    My test file is

    module Test where
    
    import Control.Monad
    import Control.Monad.Instances
    
    -- | This function calculates the value \x->x*x
    bar :: Int -> Int
    bar = join (*)
    

    and we can test this using a simple test

    *FFIInterpreter> session <- createContext "Test"
    *FFIInterpreter> runExpr session "bar 5"
    "25"
    

    So yeah, it works in Haskell, now to make it work outside of haskell.

    Just add to the top of the file a few instructions for Hs2lib on how to marshal ModuleName because that type is defined in a file which it doesn't have the source to.

    {- @@ INSTANCE ModuleName 0                 @@ -}
    {- @@ HS2HS ModuleName CWString             @@ -}
    {- @@ IMPORT "Data.IORef"                   @@ -}
    {- @@ IMPORT "Language.Haskell.Interpreter" @@ -}
    {- @@ HS2C  ModuleName "wchar_t*@4"         @@ -}
    

    or

    {- @@ HS2C  ModuleName "wchar_t*@8"         @@ -}
    

    if on a 64bit architecture,

    and Just invoke Hs2lib

    PS Haskell\FFIInterpreter> hs2lib .\FFIInterpreter.hs -n "HsInterpreter"
    Linking main.exe ...
    Done.
    

    And you'll end up with among others, an Include file with

    #ifdef __cplusplus
    extern "C" {
    #endif
    // Runtime control methods
    // HsStart :: IO ()
    extern CALLTYPE(void) HsStart ( void );
    
    // HsEnd :: IO ()
    extern CALLTYPE(void) HsEnd ( void );
    
    // createContext :: ModuleName -> IO (StablePtr (IORef (Interpreter ())))
    //
    // Create a new empty Context to be used when calling any functionsinside this class.
    // String: The path to the module to load or themodule name
    //
    extern CALLTYPE(void*) createContext (wchar_t* arg1);
    
    // freeContext :: StablePtr (IORef (Interpreter ())) -> IO ()
    //
    // free a context up
    //
    extern CALLTYPE(void) freeContext (void* arg1);
    
    // evalExpression :: StablePtr (IORef (Interpreter ())) -> String -> IO String
    extern CALLTYPE(wchar_t*) evalExpression (void* arg1, wchar_t* arg2);
    
    #ifdef __cplusplus
    }
    #endif
    

    I haven't tested the C++ side, but there's no reason it shouldn't work. This is a very barebones example, if you compile it to a dynamic lib you probably want to redirect stdout, stderr and stdin.