我正在编写一个需要解释和评估 haskell 代码的 C++ 应用程序。此代码在编译时未知,但由用户提供。有没有办法使用 haskell 编译器/解释器(如 GHCi 或 hugs)作为库?
- 我找到了 FFI,但这似乎只适用于编译时已知的 haskell 代码。
- 我找到了 GHC API 和提示,但它们似乎只在我想从 haskell 中解释 haskell 代码时才起作用。
我建议不要使用 GHC api,而是为这种特定方法绑定到Hint,这只是 GHC api 的简化包装。我推荐这个的原因是因为 GHC api 的学习曲线有点陡峭。
但无论如何,就像我在评论中所说的那样,取决于你希望它走多远,它需要的 FFI 调用很少。下面我将举例说明如何从加载的文件中运行表达式并返回结果(仅当存在 show 实例时)。这只是基础,也应该可以将结果作为结构返回。
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
由于我们必须退出 haskell 领域,我们必须有一些方法来引用上下文,我们可以使用 a 来做到这一点,StablePtr
我只是将它包装在 anIORef
中以使其可变,以防您将来想要更改内容。请注意,GHC API 不支持对内存缓冲区进行类型检查,因此您必须在加载之前将要解释的代码保存到临时文件中。
注释适用于我的-- @@
工具 Hs2lib,如果您不使用它,请不要介意它们。
我的测试文件是
module Test where
import Control.Monad
import Control.Monad.Instances
-- | This function calculates the value \x->x*x
bar :: Int -> Int
bar = join (*)
我们可以用一个简单的测试来测试它
*FFIInterpreter> session <- createContext "Test"
*FFIInterpreter> runExpr session "bar 5"
"25"
所以是的,它在 Haskell 中工作,现在让它在 Haskell 之外工作。
只需在文件顶部添加一些关于如何编组的 Hs2lib 说明,ModuleName
因为该类型是在它没有源的文件中定义的。
{- @@ INSTANCE ModuleName 0 @@ -}
{- @@ HS2HS ModuleName CWString @@ -}
{- @@ IMPORT "Data.IORef" @@ -}
{- @@ IMPORT "Language.Haskell.Interpreter" @@ -}
{- @@ HS2C ModuleName "wchar_t*@4" @@ -}
或者
{- @@ HS2C ModuleName "wchar_t*@8" @@ -}
如果在 64 位架构上,
并调用Hs2lib
PS Haskell\FFIInterpreter> hs2lib .\FFIInterpreter.hs -n "HsInterpreter"
Linking main.exe ...
Done.
你最终会得到一个包含文件
#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
我还没有测试过 C++ 方面,但没有理由它不应该工作。这是一个非常简单的示例,如果您将其编译为动态库,您可能希望重定向 stdout、stderr 和 stdin。
由于 GHC 是用 Haskell 编写的,因此它的 API 只能从 Haskell 获得。正如 Daniel Wagner 所建议的那样,在 Haskell 中编写所需的接口并使用 FFI 将它们绑定到 C 将是最简单的路线。这可能比直接将 GHC API 绑定到 C 更容易;你可以利用 Haskell 的优势来构建你需要的接口,并且只在顶层用 C++ 与它们交互。
注意 Haskell 的 FFI 只会导出到 C;如果你想要一个 C++-ish 包装器,你必须把它写成另一个层。
(顺便说一句,拥抱是古老且无人维护的。)