开箱即用的Haskell插件系统

jos*_*uan 8 haskell

我已经阅读过Haskell中的插件,但是我无法满足自己的目的(最好是在生产环境中使用).

我的插件系统目标是:

  1. 生产环境必须开箱即用(全部预编译).
  2. 加载插件已启用重置应用程序/服务,但理想情况下它会动态加载和更新插件.

一个最小的例子可能是:

app/service~plugins界面

module SharedTypes (PluginInterface (..)) where

data PluginInterface =
     PluginInterface { pluginName :: String
                     , runPlugin  :: Int -> Int }
Run Code Online (Sandbox Code Playgroud)

一些插件列表

-- ~/plugins/plugin{Nth}.??   (with N=1..)
module Plugin{Nth}( getPlugin ) where

import SharedTypes

getPlugin :: PluginInterface
getPlugin = PluginInterface "{Nth}th plugin" $ \x -> {Nth} * x
Run Code Online (Sandbox Code Playgroud)

应用/服务

...
loadPlugins :: FilePath -> IO [PluginInterface]
loadPlugins = undefined
...
Run Code Online (Sandbox Code Playgroud)

我认为使用动态编译链接库(将每个编译Plugin{Nth}为共享库)可以工作(作为FFI)但是

  1. 如何在运行时枚举和加载每个共享库?(获得每个getPlugin功能点)
  2. 存在一些更好的方法?(例如,在运行应用程序/服务之前的一些"神奇"过程)

谢谢!

UPDATE

完整运行的例子

在伟大的@xnyhps回答之后,一个完整的运行示例使用 ghc 7.10

SharedTypes.hs

module SharedTypes (
  PluginInterface (..)
) where

data PluginInterface =
     PluginInterface { pluginName :: String
                     , runPlugin  :: Int -> Int
                     }
Run Code Online (Sandbox Code Playgroud)

Plugin1.hs

module Plugin1 (
  getPlugin
) where

import SharedTypes

getPlugin :: PluginInterface
getPlugin = PluginInterface "Plugin1" $ \x -> 1 * x
Run Code Online (Sandbox Code Playgroud)

Plugin2.hs

module Plugin2 (
  getPlugin
) where

import SharedTypes

getPlugin :: PluginInterface
getPlugin = PluginInterface "Plugin2" $ \x -> 2 * x
Run Code Online (Sandbox Code Playgroud)

app.hs

import SharedTypes
import System.Plugins.DynamicLoader
import System.Directory
import Data.Maybe
import Control.Applicative
import Data.List
import System.FilePath
import Control.Monad

loadPlugins :: FilePath -> IO [PluginInterface]
loadPlugins path = getDirectoryContents path >>= mapM loadPlugin . filter (".plugin" `isSuffixOf`)
  where loadPlugin file = do
          m <- loadModuleFromPath (combine path file)  -- absolute path
                                  (Just path)          -- base of qualified name (or you'll get not found)
          resolveFunctions
          getPlugin <- loadFunction m "getPlugin"
          return getPlugin

main = do

  -- and others used by plugins
  addDLL "/usr/lib/ghc-7.10.1/base_I5BErHzyOm07EBNpKBEeUv/libHSbase-4.8.0.0-I5BErHzyOm07EBNpKBEeUv-ghc7.10.1.so"
  loadModuleFromPath "/srv/despierto/home/josejuan/Projects/Solveet/PluginSystem/SharedTypes.o" Nothing

  plugins <- loadPlugins "/srv/despierto/home/josejuan/Projects/Solveet/PluginSystem/plugins"

  forM_ plugins $ \plugin -> do
    putStrLn $ "Plugin name: " ++ pluginName plugin
    putStrLn $ "     Run := " ++ show (runPlugin plugin 34)
Run Code Online (Sandbox Code Playgroud)

编译和执行

[josejuan@centella PluginSystem]$ ghc --make -dynamic -fPIC -O3 Plugin1.hs
[1 of 2] Compiling SharedTypes      ( SharedTypes.hs, SharedTypes.o )
[2 of 2] Compiling Plugin1          ( Plugin1.hs, Plugin1.o )
[josejuan@centella PluginSystem]$ ghc --make -dynamic -fPIC -O3 Plugin2.hs
[2 of 2] Compiling Plugin2          ( Plugin2.hs, Plugin2.o )
[josejuan@centella PluginSystem]$ mv Plugin1.o plugins/Plugin1.plugin
[josejuan@centella PluginSystem]$ mv Plugin2.o plugins/Plugin2.plugin
[josejuan@centella PluginSystem]$ ghc --make -dynamic -fPIC -O3 app.hs
[2 of 2] Compiling Main             ( app.hs, app.o )
Linking app ...
[josejuan@centella PluginSystem]$ ./app
Plugin name: Plugin1
     Run := 34
Plugin name: Plugin2
     Run := 68
Run Code Online (Sandbox Code Playgroud)

xny*_*hps 6

一个动态加载程序包,允许您将额外的目标文件或共享库加载到您的进程中.(Hackage上的版本不适用于7.10,但GitHub上的当前版本确实如此.)

有了这个,你可以这样做:

import System.Plugins.DynamicLoader
import System.Directory

loadPlugins :: FilePath -> IO [PluginInterface]
loadPlugins path = do
    files <- getDirectoryContents path
    mapM (\plugin_path -> do
        m <- loadModuleFromPath (path ++ "/" ++ plugin_path) (Just path)
        resolveFunctions
        plugin <- loadFunction m "getPlugin"
        return plugin) files
Run Code Online (Sandbox Code Playgroud)

但是,您必须记住整个过程非常不安全:如果您更改PluginInterface数据类型并尝试加载使用旧版本编译的插件,您的应用程序将崩溃.你必须希望getPlugin函数有类型PluginInterface,没有检查.最后,如果插件来自不受信任的源,它可以执行任何操作,即使您尝试调用的函数在Haskel中应该是纯粹的.