用 C++ 编写 Haskell 解释器(使用 ghc 或 Hugs 作为库)

发布于 2024-12-23 04:20:45 字数 227 浏览 3 评论 0原文

我正在编写一个需要解释和评估 haskell 代码的 C++ 应用程序。该代码在编译时未知,但由用户给出。 有没有办法使用 haskell 编译器/解释器(如 GHCi 或 Hugs)作为库?

  • 我找到了 FFI,但这似乎只适用于编译时已知的 haskell 代码。
  • 我找到了 GHC API 和提示,但它们似乎只在我想从 haskell 之外解释 haskell 代码时才起作用。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

陌生 2024-12-30 04:20:45

我建议不要使用 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 Land,所以我们必须有某种方法来引用上下文,我们可以使用 StablePtr 来做到这一点,我只需将其包装在 IORef 中即可如果你想在未来改变事情,它是可变的。请注意,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。

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.

两仪 2024-12-30 04:20:45

由于 GHC 是用 Haskell 编写的,因此它的 API 只能从 Haskell 获得。正如 Daniel Wagner 所建议的,在 Haskell 中编写所需的接口并使用 FFI 将它们绑定到 C 将是最简单的途径。这可能比使用 GHC API 直接绑定到 C 更容易;你可以利用 Haskell 的优势来构建你需要的接口,并且只在顶层用 C++ 与它们交互。

注意Haskell的FFI只会导出到C;如果您想要一个类似 C++ 的包装器,则必须将其编写为另一层。

(顺便说一句,拥抱是古老的并且没有维护。)

Since GHC is written in Haskell, its API is exclusively available from Haskell. Writing the interfaces you need in Haskell and binding them to C with the FFI, as Daniel Wagner suggested, is going to be the simplest route. This is probably easier than using a direct binding of the GHC API to C would be; you get to use Haskell's strengths to build the interfaces you need, and only interface with them in C++ at the top layer.

Note that Haskell's FFI will only export to C; if you want a C++-ish wrapper around it, you'll have to write it as another layer.

(BTW, Hugs is ancient and unmaintained.)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文