Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.
like image 200
Heinzi Avatar asked Dec 27 '11 15:12

Heinzi


1 Answers

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.

like image 122
Phyx Avatar answered Oct 24 '22 10:10

Phyx