Haskell,GHC 8:动态加载/导入模块

12

我需要类似以下内容的东西:

-- Main.hs
module Main where

main :: IO ()
main = do
  <import Plugin>
  print Plugin.computation

有了像插件一样的东西

-- Plugin.hs
module Plugin where

computation :: Int
computation = 4

然而,我需要插件与主应用程序一起编译。它们需要一起部署。模块的导入(而不是编译)应该动态发生。
我在途中找到了Dynamically loading compiled Haskell module - GHC 7.6,它可以在GHC 8.0.2上正常工作,除了一个问题:执行应用程序时需要将插件的源文件放在当前工作目录中。

编辑(07.12.2017)

是否可能使用GHC API从字符串中加载模块而不是从文件中加载?http://hackage.haskell.org/package/ghc-8.2.1/docs/GHC.html#t:Target表明这是可能的,但是文档有很多漏洞,我找不到实现这一点的方法。如果可以实现这一点,我可以使用file-embed将插件源文件包含到编译后的二进制文件中。 例如:
module Main where

-- Dynamic loading of modules
import GHC
import GHC.Paths ( libdir )
import DynFlags
import Unsafe.Coerce

import Data.Time.Clock (getCurrentTime)
import StringBuffer

pluginModuleNameStr :: String
pluginModuleNameStr = "MyPlugin"

pluginSourceStr :: String
pluginSourceStr = unlines
  [ "module MyPlugin where"
  , "computation :: Int"
  , "computation = 4"
  ]

pluginModuleName :: ModuleName
pluginModuleName = mkModuleName pluginModuleNameStr

pluginSource :: StringBuffer
pluginSource = stringToStringBuffer pluginSourceStr

main :: IO ()
main = do
    currentTime <- getCurrentTime
    defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
      result <- runGhc (Just libdir) $ do
        dflags <- getSessionDynFlags
        setSessionDynFlags dflags
        let target = Target { targetId = TargetModule $ pluginModuleName
                            , targetAllowObjCode = True
                            , targetContents = Just ( pluginSource
                                                    , currentTime
                                                    )
                            }
        setTargets [target]
        r <- load LoadAllTargets
        case r of
          Failed    -> error "Compilation failed"
          Succeeded -> do
            setContext [IIDecl $ simpleImportDecl pluginModuleName]
            result <- compileExpr ("MyPlugin.computation")
            let result' = unsafeCoerce result :: Int
            return result'
      print result

然而,这会导致

<command-line>: panic! (the 'impossible' happened)
  (GHC version 8.0.2 for x86_64-apple-darwin):
    module ‘MyPlugin’ is a package module

编辑(2017年8月12日)

我可以通过将源代码写入临时文件并像链接的帖子中那样加载它来直接将“插件”编译到最终二进制文件中(动态加载已编译的Haskell模块-GHC 7.6)。然而,如果插件从Hackage导入包,则无法很好地运行:

module Main where

import Control.Monad.IO.Class (liftIO)
import DynFlags
import GHC
import GHC.Paths (libdir)
import System.Directory (getTemporaryDirectory, removePathForcibly)
import Unsafe.Coerce (unsafeCoerce)

pluginModuleNameStr :: String
pluginModuleNameStr = "MyPlugin"

pluginSourceStr :: String
pluginSourceStr = unlines
  [ "module MyPlugin where"
  , "import Data.Aeson"
  , "computation :: Int"
  , "computation = 4"
  ]

writeTempFile :: IO FilePath
writeTempFile = do
  dir <- getTemporaryDirectory
  let file = dir ++ "/" ++ pluginModuleNameStr ++ ".hs"
  writeFile file pluginSourceStr
  return file

main :: IO ()
main = do
  moduleFile <- writeTempFile
  defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
    result <- runGhc (Just libdir) $ do
      dflags <- getSessionDynFlags
      setSessionDynFlags dflags
      target <- guessTarget moduleFile Nothing
      setTargets [target]
      r <- load LoadAllTargets
      liftIO $ removePathForcibly moduleFile
      case r of
        Failed -> error "Compilation failed"
        Succeeded -> do
          setContext [IIDecl $ simpleImportDecl $ mkModuleName pluginModuleNameStr]
          result <- compileExpr "MyPlugin.computation"
          let result' = unsafeCoerce result :: Int
          return result'
    print result

有没有办法在插件中包含语句 import Data.Aeson 的情况下加载包?如果我将它添加到插件字符串中,它会失败。

/var/folders/t2/hp9y8x6s6rs7zg21hdzvhbf40000gn/T/MyPlugin.hs:2:1: error:
    Failed to load interface for ‘Data.Aeson’
    Perhaps you meant Data.Version (from base-4.9.1.0)
    Use -v to see a list of the files searched for.
haskell-loader-exe: panic! (the 'impossible' happened)
  (GHC version 8.0.2 for x86_64-apple-darwin):
  Compilation failed
CallStack (from HasCallStack):
  error, called at app/Main.hs:40:19 in main:Main

我请求的原因是数据库支持:我们使用Persistent访问数据库,需要动态导入来支持多个数据库(MySQL、PostgreSQL和SQLite),同时仍然允许最终用户只安装其中一个数据库服务器(换句话说:如果他们只使用PostgreSQL,则不要求用户安装所有这三个数据库)。当用户实际配置主应用程序使用该模块时,才应加载特定于数据库的模块。

如果我不import Database.Persist.MySQL,那么应用程序不需要安装MySQL。否则,应用程序将失败,例如:

dyld: Library not loaded: 
/usr/local/opt/mysql/lib/libmysqlclient.20.dylib

在 macOS 上。

我有一种感觉,这可能是 ghc backpack 的一个使用案例,但我不能确定,因为我自己还没有使用过它。此外,它仅在 ghc-8.2 上可用。 - lehins
谢谢,但我不确定我们是否可以在这个时候将项目升级到 GHC 8.2。此外,我们使用的是 Stack,目前与 backpack 不兼容(尽管他们正在努力解决)。 - eugenk
我编辑了帖子以展示我的当前方法。 - eugenk
如果我确保存在一个 .hs 文件,那么错误消息就会不同,因此 GHC 的构建系统中的某些内容正在检查文件的存在与文档注释相符,这是 IDE 的一个特性,其中文件可以存在,但应使用一些正在进行的内容。 - codeshot
我的情况下出现的下一个错误是:“缓冲区需要预处理;交互式检查已禁用”。 - codeshot
这是由于使用了Cpp选项,如果不使用该选项,则内容可以正常加载并使用动态字符串。 - codeshot
1个回答

1
看起来必须存在一个与模块名匹配的文件——似乎文件内容并不重要。 在Linux上,我甚至可以将它设置为指向/dev/null的符号链接,但是指向自身的符号链接就不行了。

如果有人知道如何强制解释一个模块,那么可能就不需要磁盘上的文件了。如果是这样,请分享你的知识! - codeshot
即使强制一个模块被解释执行,仍需要磁盘上的文件。我寻找了一些钩取 GHC API 访问文件系统的方法,但看起来 GHC 使用 IO monad 进行访问,而且没有任何 ModuleStore 类型类可供实现。 - codeshot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接