如何使用GHC API动态编译并导入一个模块 – LTS Haskell 20.20(ghc-9.2.7)?

huangapple go评论84阅读模式
英文:

How to dynamically compile and import a module using GHC API - LTS Haskell 20.20 (ghc-9.2.7)?

问题

背景

我想在Haskell中实现一个程序,该程序可以动态生成Haskell模块(hs-文件),然后编译并将其导入到生成代码的同一应用程序中。

因此,我尝试理解Stack Overflow和文档中的一些示例代码片段。但它既不能工作也不能编译。

在对给定示例的某些修改之后,我创建了以下源代码Main.hs:

import qualified GhcApiWrap as Ghw

main :: IO ()
main = Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'

GhcApiWrap.hs:

module GhcApiWrap
    (
        msLoadModuleAndExecute
    ) where

import GHC

msLoadModuleAndExecute :: String -> String -> String -> Char -> IO ()
msLoadModuleAndExecute _ _ _ _ = do
    value' <- runGhc (Just "./src/") $ do
            dflags <- getSessionDynFlags
            setSessionDynFlags $ dflags { 
                ghcLink   = LinkInMemory, 
                ghcMode = CompManager,
                objectDir   = Just "../dyn/",
                hiDir   = Just "../dyn/"
                }
            target <- guessTarget "DynExample.hs" Nothing
            setTargets [target]
            ret <- load LoadAllTargets
            case ret of
                Succeeded -> do
                    importDecl_RdrName <- parseImportDecl "import DynExample"
                    setContext [IIDecl importDecl_RdrName]

                    value <- dynCompileExpr "DynExample.nFromChar"

                    return value
                _       -> 
                    return undefined
    print $ value'

DynExample.hs:

module DynExample
    (
        nFromChar
    ) where

nFromChar :: Char -> Int
nFromChar _ = 33

... 这个程序能够编译但无法工作。

它的输出是:

ExprmntGhcApi-exe: Missing file: src/settings

如您所见,我注释掉了 import GHC.Paths (libdir),因为它不再存在。因此,我谨慎地猜测,我使用了 (Just "./src/") 代替了 (Just libdir)

我尝试了几个版本,但没有一个能与GHC 9.2.7一起工作。

问题

是否可以使用最新的Haskell Stack环境以及以下函数类型,以动态方式使用 DynExample.nFromChar 这种方式?

msLoadModuleAndExecute :: String -> String -> String -> Char -> ... Int

环境

目前,我正在使用带有以下配置的Stack:

resolver:
  url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml

... 这对应于使用GHC 9.2.7。

英文:

Background

I would like to implement a program in Haskell that can generate Haskell module (hs-File) dynamically and then compile, and import it into the same application that generated the code.

Therefore, I tried to understand some example code snippets in SO and documentation. But it doesn't work nor compiles.

After some modifications of one of the given examples, I created the following souce code, Main.hs:

import qualified GhcApiWrap as Ghw

main :: IO ()
main = Ghw.msLoadModuleAndExecute &quot;../dyn/&quot; &quot;DynExample.hs&quot; &quot;nFromChar&quot; &#39;A&#39;

GhcApiWrap.hs:

module GhcApiWrap
    (
        msLoadModuleAndExecute
    ) where

import GHC
--import GHC.Paths (libdir)  

msLoadModuleAndExecute :: String -&gt; String -&gt; String -&gt; Char -&gt; IO ()
msLoadModuleAndExecute _ _ _ _ = do
    value&#39; &lt;- runGhc (Just &quot;./src/&quot;) $ do
            dflags &lt;- getSessionDynFlags
            setSessionDynFlags $ dflags { 
                ghcLink   = LinkInMemory, 
                ghcMode = CompManager,
                objectDir   = Just &quot;../dyn/&quot;,
                hiDir   = Just &quot;../dyn/&quot;
                }
            target &lt;- guessTarget (&quot;DynExample.hs&quot;) Nothing
            setTargets [target]
            ret &lt;- load LoadAllTargets
            case ret of
                Succeeded -&gt; do
                    importDecl_RdrName &lt;- parseImportDecl $ &quot;import DynExample&quot;
                    setContext [IIDecl importDecl_RdrName]

                    value &lt;- dynCompileExpr (&quot;DynExample.nFromChar&quot;)

                    return value
                _       -&gt; 
                    return undefined
    print $ value&#39;

DynExample.hs:

module DynExample
    (
        nFromChar
    ) where

nFromChar :: Char -&gt; Int
nFromChar _ = 33

...which compiles but doesn't work.

Its ouput is:

ExprmntGhcApi-exe: Missing file: src/settings

As you can see, I commented out import GHC.Paths (libdir) because it doesn't exist anymore. Therefore, as a humble guess I used (Just &quot;./src/&quot;) instead of (Just libdir).

I tried several versions that are "flying around", but none of them work with GHC 9.2.7.

Question

Is it, and how is it possible to use DynExample.nFromChar dynamically this way using an up-to-date Haskell Stack environment, and having the following type of the function?

msLoadModuleAndExecute :: String -&gt; String -&gt; String -&gt; Char -&gt; ... Int

Environment

At the moment, I am using Stack with:

resolver:
  url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml

...which translates to usage of GHC 9.2.7.

答案1

得分: 2

所以,通过添加对ghc-paths包的依赖,解决了import GHC.Paths (libdir)的问题。

接下来的问题是正确地为guessTarget提供参数。从文档中可以看到:

尝试猜测字符串引用的目标是什么。这个函数实现了--make/GHCi命令行语法用于文件名。

因此,需要提供模块的路径而不仅仅是名称。

最后的问题是,dynCompileExpr返回一个Dynamic对象,应该将其转换为您的函数类型Char -> Int,然后进行计算。

将所有这些组合起来,我们可以得到:

module GhcApiWrap
    (
        msLoadModuleAndExecute
    ) where

import Control.Exception.Safe
import Data.Dynamic
import GHC
import GHC.Paths (libdir)
import System.FilePath

msLoadModuleAndExecute :: FilePath -> FilePath -> String -> Char -> IO Int
msLoadModuleAndExecute buildDir modulePath funcName arg = do
    dynFunc <- runGhc (Just libdir) $ do
        dflags <- getSessionDynFlags
        setSessionDynFlags $ dflags { ghcLink   = LinkInMemory
                                    , ghcMode   = CompManager
                                    , objectDir = Just buildDir
                                    , hiDir     = Just buildDir
                                    }
        target <- guessTarget modulePath Nothing
        setTargets [target]
        loadStatus <- load LoadAllTargets
        case loadStatus of
            Succeeded -> do
                let moduleName = dropExtension $ takeFileName modulePath
                importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
                setContext [IIDecl importDecl_RdrName]
                dynCompileExpr $ moduleName ++ "." ++ funcName
            Failed    -> throwString $ "could not load the module: " ++ modulePath
    case fromDynamic dynFunc of
        Just func -> pure $ func arg
        Nothing   -> throwString $ funcName ++ " has type '" ++ show dynFunc ++ "' but expected type `Char -> Int'"

可以通过以下方式运行它:

import qualified GhcApiWrap as Ghw

main :: IO ()
main = print =<< Ghw.msLoadModuleAndExecute "../dyn/" "DynExample.hs" "nFromChar" 'A'

但我认为更好的做法是简化这个函数,只保留compileAndLoad,使其更通用。

{-# LANGUAGE ScopedTypeVariables #-}

module GhcApiWrap
    (
        msCompileAndLoad
    ) where

import Control.Exception.Safe
import Data.Dynamic
import Data.Proxy
import Data.Typeable
import GHC
import GHC.Paths (libdir)
import System.FilePath

msCompileAndLoad :: forall a. Typeable a => FilePath -> FilePath -> String -> IO a
msCompileAndLoad buildDir modulePath symbolName = do
    dynSymbol <- runGhc (Just libdir) $ do
        dflags <- getSessionDynFlags
        setSessionDynFlags $ dflags { ghcLink   = LinkInMemory
                                    , ghcMode   = CompManager
                                    , objectDir = Just buildDir
                                    , hiDir     = Just buildDir
                                    }
        target <- guessTarget modulePath Nothing
        setTargets [target]
        loadStatus <- load LoadAllTargets
        case loadStatus of
            Succeeded -> do
                let moduleName = dropExtension $ takeFileName modulePath
                importDecl_RdrName <- parseImportDecl $ "import " ++ moduleName
                setContext [IIDecl importDecl_RdrName]
                dynCompileExpr $ moduleName ++ "." ++ symbolName
            Failed    -> throwString $ "could not load the module: " ++ modulePath
    case fromDynamic dynSymbol of
        Just x  -> pure x
        Nothing -> throwString $ symbolName ++ " has type '" ++ show dynSymbol
                 ++ "' but expected type '" ++ show symbolType ++ "'"
  where
    symbolType = typeRep (Proxy :: Proxy a)

然后我们可以使用它:

import qualified GhcApiWrap as Ghw

main :: IO ()
main = do
    nFromChar <- Ghw.msCompileAndLoad "../dyn/" "DynExample.hs" "nFromChar"
    print $ (nFromChar :: Char -> Int) 'A'

希望这对你有帮助。

英文:

So, issue with import GHC.Paths (libdir) solved by adding a dependence to the ghc-paths package.

Next issue was with provide correct argument to the guessTarget. From the doc:

> Attempts to guess what Target a string refers to. This function implements the --make/GHCi command-line syntax for filenames.

So, need provide path to the module not just the name.

And last problem is that dynCompileExpr returns Dynamic object which should be casted to type of your function Char -&gt; Int and then calculate.

Combine all together we can get:

module GhcApiWrap
    (
        msLoadModuleAndExecute
    ) where

import Control.Exception.Safe
import Data.Dynamic
import GHC
import GHC.Paths (libdir)
import System.FilePath

msLoadModuleAndExecute :: FilePath -&gt; FilePath -&gt; String -&gt; Char -&gt; IO Int
msLoadModuleAndExecute buildDir modulePath funcName arg = do
    dynFunc &lt;- runGhc (Just libdir) $ do
        dflags &lt;- getSessionDynFlags
        setSessionDynFlags $ dflags { ghcLink   = LinkInMemory
                                    , ghcMode   = CompManager
                                    , objectDir = Just buildDir
                                    , hiDir     = Just buildDir
                                    }
        target &lt;- guessTarget modulePath Nothing
        setTargets [target]
        loadStatus &lt;- load LoadAllTargets
        case loadStatus of
            Succeeded -&gt; do
                let moduleName = dropExtension $ takeFileName modulePath
                importDecl_RdrName &lt;- parseImportDecl $ &quot;import &quot; ++ moduleName
                setContext [IIDecl importDecl_RdrName]
                dynCompileExpr $ moduleName ++ &quot;.&quot; ++ funcName
            Failed    -&gt; throwString $ &quot;could not load the module: &quot; ++ modulePath
    case fromDynamic dynFunc of
        Just func -&gt; pure $ func arg
        Nothing   -&gt; throwString $ funcName ++ &quot; has type &#39;&quot; ++ show dynFunc ++ &quot; but expected type `Char -&gt; Int&#39;&quot;

which can be run with:

import qualified GhcApiWrap as Ghw

main :: IO ()
main = print =&lt;&lt; Ghw.msLoadModuleAndExecute &quot;../dyn/&quot; &quot;DynExample.hs&quot; &quot;nFromChar&quot; &#39;A&#39;

But I think will be better simplify this function on just compileAndLoad that make it more generic.

{-# LANGUAGE ScopedTypeVariables #-}

module GhcApiWrap
    (
        msCompileAndLoad
    ) where

import Control.Exception.Safe
import Data.Dynamic
import Data.Proxy
import Data.Typeable
import GHC
import GHC.Paths (libdir)
import System.FilePath

msCompileAndLoad :: forall a. Typeable a =&gt; FilePath -&gt; FilePath -&gt; String -&gt; IO a
msCompileAndLoad buildDir modulePath symbolName = do
    dynSymbol &lt;- runGhc (Just libdir) $ do
        dflags &lt;- getSessionDynFlags
        setSessionDynFlags $ dflags { ghcLink   = LinkInMemory
                                    , ghcMode   = CompManager
                                    , objectDir = Just buildDir
                                    , hiDir     = Just buildDir
                                    }
        target &lt;- guessTarget modulePath Nothing
        setTargets [target]
        loadStatus &lt;- load LoadAllTargets
        case loadStatus of
            Succeeded -&gt; do
                let moduleName = dropExtension $ takeFileName modulePath
                importDecl_RdrName &lt;- parseImportDecl $ &quot;import &quot; ++ moduleName
                setContext [IIDecl importDecl_RdrName]
                dynCompileExpr $ moduleName ++ &quot;.&quot; ++ symbolName
            Failed    -&gt; throwString $ &quot;could not load the module: &quot; ++ modulePath
    case fromDynamic dynSymbol of
        Just x  -&gt; pure x
        Nothing -&gt; throwString $ symbolName ++ &quot; has type &#39;&quot; ++ show dynSymbol
                 ++ &quot;&#39; but expected type &#39;&quot; ++ show symbolType ++ &quot;&#39;&quot;
  where
    symbolType = typeRep (Proxy :: Proxy a)

Then we can use it:

import qualified GhcApiWrap as Ghw

main :: IO ()
main = do
    nFromChar &lt;- Ghw.msCompileAndLoad &quot;../dyn/&quot; &quot;DynExample.hs&quot; &quot;nFromChar&quot;
    print $ (nFromChar :: Char -&gt; Int) &#39;A&#39;

答案2

得分: 0

以下是您提供的内容的中文翻译:

"freestyle" 和 "K. A. Buhr" 的答案有效。

但是,我创建了一个备用版本,应该具有以下特点:

  • 不抛出异常,而是通过 Either 转发错误消息
  • 捕获所有异常以通过 Either 转发其错误消息
  • 仅在需要时修改 dflags
  • 避免使用 do 表示法
  • 避免使用 case 构造
  • 在合理情况下使用限定的导入
  • 描述所有必需的内容,包括堆栈设置细节
  • 使用模块名称来暗示文件的扩展名为“hs”
  • 无 hlint 提示

我对代码进行了测试,包括以下方面:

  • 函数
  • 模块文件不可用
    (例如,“错误:异常:找不到文件:./dyn/DynExample.hs”)
  • 编译器错误与模块文件相关
    (例如,“错误:无法加载模块“DynExample”!”)
  • 与导入符号相关的类型检查错误
    (例如,“错误:符号 nFromChar 的类型为“<<Char -> Int>>”,但期望类型为“Char -> Integer””)

GhcApiWrap

{-# LANGUAGE ScopedTypeVariables #-}

module GhcApiWrap
    (
        compileAndLoad
    ) where

import qualified Control.Exception.Safe as Exc
import qualified Data.Dynamic as Dyn
import qualified Data.Proxy as Px
import qualified Data.Typeable as T
import qualified GHC as Ghc
import qualified GHC.Paths as Pth
import Data.Functor ((<&>))

compileAndLoad :: forall a. T.Typeable a => String -> String -> String -> IO (Either String a)
compileAndLoad dir mdl sym = runGhcCatched >>= ethSym
  where
    runGhcCatched = Ghc.runGhc (Just Pth.libdir) (loadModule dir mdl sym)
                    `Exc.catchAny`
                    (\ex -> return (Left ("Exception: " ++ show ex)))
    ethSym (Right dynSym) = return (ethSym' (Dyn.fromDynamic dynSym) (sErrFromDynSym dynSym))
    ethSym (Left sErr) = return (Left sErr)
    sErrFromDynSym dynSym = "Symbol " ++ sym ++ " has type \"" ++ show dynSym ++ 
        "\" but expected type \"" ++ show symbolType ++ "\""
    ethSym' (Just dynSym) _ = Right dynSym
    ethSym' Nothing sErr = Left sErr
    symbolType = T.typeRep (Px.Proxy :: Px.Proxy a)

loadModule :: Ghc.GhcMonad m => String -> String -> String -> m (Either String Dyn.Dynamic)
loadModule dir mdl sym = 
        Ghc.getSessionDynFlags >>=
        Ghc.setSessionDynFlags >> 
        Ghc.guessTarget (dir ++ mdl ++ ".hs") Nothing >>= 
        (\target -> Ghc.setTargets [target]) >> 
        Ghc.load Ghc.LoadAllTargets >>=
        returnFromLoadStatus
    where
        returnFromLoadStatus Ghc.Succeeded = dynSym <&> Right
        returnFromLoadStatus Ghc.Failed = return (Left ("Could not load the module \"" ++ mdl ++ "\"!"))
        dynSym = Ghc.parseImportDecl ("import " ++ mdl) >>= 
                 (\importDecl -> Ghc.setContext [Ghc.IIDecl importDecl]) >> 
                 Ghc.dynCompileExpr (mdl ++ "." ++ sym)

Usage

{-# LANGUAGE LambdaCase #-}

import qualified GhcApiWrap as Ghw

main :: IO ()
main =  Ghw.compileAndLoad "./dyn/" "DynExample" "nFromChar" >>=
        (\case 
            (Right nFromChar) -> putStrLn ("nFromChar 'A' := " ++ show ((nFromChar :: Char -> Integer) 'A'))
            (Left sErrorMessage) -> putStrLn ("Error: " ++ sErrorMessage))

动态文件以供阅读(“DynExample.hs”)

注意:文件必须位于与目录“src”并排的目录“dyn”中,以与上面的代码(“Usage”)兼容。

module DynExample
    (
        nFromChar
    ) where

import qualified Data.Char as Chr

nFromChar :: Char -> Int
nFromChar = Chr.ord

输出

nFromChar 'A' := 65

package.yaml

...
description:...

dependencies:
- base >= 4.7 && < 5
- ghc
- ghc-paths
- safe-exceptions

ghc-options:
...

stack.yaml

...
resolver:
  url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
...

希望这些翻译对您有所帮助。

英文:

The answer from freestyle and K. A. Buhr works.

However, I created an alternative version, which should have the following characteristics:

  • not throwing exceptions but forwarding error messages via Either
  • catching all exceptions to forward its error messages via Either
  • not modifying the dflags if not needed
  • avoiding the do notation
  • avoiding case constructions
  • using qualified imports, where reasonably possible
  • describing all necessities including stack setup details
  • usage of the module name implying extension “hs” for the file
  • hlint free

I tested the code regarding

  • function
  • unavailability of the module file
    (e.g. "Error: Exception: can't find file: ./dyn/DynExample.hs")
  • compiler error regarding the module file
    (e.g. "Error: Could not load the module "DynExample"!")
  • error regarding type check vs. imported symbol
    (e.g. "Error: Symbol nFromChar has type "<<Char -> Int>>" but expected type "Char -> Integer"")

GhcApiWrap

{-# LANGUAGE ScopedTypeVariables #-}
module GhcApiWrap
(
compileAndLoad
) where
import qualified Control.Exception.Safe as Exc
import qualified Data.Dynamic as Dyn
import qualified Data.Proxy as Px
import qualified Data.Typeable as T
import qualified GHC as Ghc
import qualified GHC.Paths as Pth
import Data.Functor ((&lt;&amp;&gt;))
compileAndLoad :: forall a. T.Typeable a =&gt; String -&gt; String -&gt; String -&gt; IO (Either String a)
compileAndLoad dir mdl sym = runGhcCatched &gt;&gt;= ethSym
where
runGhcCatched = Ghc.runGhc (Just Pth.libdir) (loadModule dir mdl sym)
`Exc.catchAny`
(\ex -&gt; return (Left (&quot;Exception: &quot; ++ show ex)))
ethSym (Right dynSym) = return (ethSym&#39; (Dyn.fromDynamic dynSym) (sErrFromDynSym dynSym))
ethSym (Left sErr) = return (Left sErr)
sErrFromDynSym dynSym = &quot;Symbol &quot; ++ sym ++ &quot; has type \&quot;&quot; ++ show dynSym ++ 
&quot;\&quot; but expected type \&quot;&quot; ++ show symbolType ++ &quot;\&quot;&quot;
ethSym&#39; (Just dynSym) _ = Right dynSym
ethSym&#39; Nothing sErr = Left sErr
symbolType = T.typeRep (Px.Proxy :: Px.Proxy a)
loadModule :: Ghc.GhcMonad m =&gt; String -&gt; String -&gt; String -&gt; m (Either String Dyn.Dynamic)
loadModule dir mdl sym = 
Ghc.getSessionDynFlags &gt;&gt;= 
Ghc.setSessionDynFlags &gt;&gt; 
Ghc.guessTarget (dir ++ mdl ++ &quot;.hs&quot;) Nothing &gt;&gt;= 
(\target -&gt; Ghc.setTargets [target]) &gt;&gt; 
Ghc.load Ghc.LoadAllTargets &gt;&gt;= 
returnFromLoadStatus
where
returnFromLoadStatus Ghc.Succeeded = dynSym &lt;&amp;&gt; Right
returnFromLoadStatus Ghc.Failed = return (Left (&quot;Could not load the module \&quot;&quot; ++ mdl ++ &quot;\&quot;!&quot;))
dynSym = Ghc.parseImportDecl (&quot;import &quot; ++ mdl) &gt;&gt;= 
(\importDecl -&gt; Ghc.setContext [Ghc.IIDecl importDecl]) &gt;&gt; 
Ghc.dynCompileExpr (mdl ++ &quot;.&quot; ++ sym)

Usage

{-# LANGUAGE LambdaCase #-}
import qualified GhcApiWrap as Ghw
main :: IO ()
main =  Ghw.compileAndLoad &quot;./dyn/&quot; &quot;DynExample&quot; &quot;nFromChar&quot; &gt;&gt;= 
(\case 
(Right nFromChar) -&gt; putStrLn (&quot;nFromChar &#39;A&#39; := &quot; ++ show ((nFromChar :: Char -&gt; Integer) &#39;A&#39;))
(Left sErrorMassage) -&gt; putStrLn (&quot;Error: &quot; ++ sErrorMassage))

Dynamic file to read ("DynExample.hs")

NOTE: The file has to be located in diretcory dyn side by side to the directory src, in order to be compatible with the code above ("Usage").

module DynExample
(
nFromChar
) where
import qualified Data.Char as Chr
nFromChar :: Char -&gt; Int
nFromChar = Chr.ord

Output

nFromChar &#39;A&#39; := 65

package.yaml

...
description:...
dependencies:
- base &gt;= 4.7 &amp;&amp; &lt; 5
- ghc
- ghc-paths
- safe-exceptions
ghc-options:
...

stack.yaml

...
resolver:
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/20.yaml
...

huangapple
  • 本文由 发表于 2023年8月4日 23:30:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76837326.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定