Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Out of the box Haskell plugin system

Tags:

haskell

I've read about plugins in Haskell but I can't get a satisfactory way to my purposes (ideally to use in a production environment).

My plugin system goals are:

  1. the production environment must to be out of the box (all precompiled).
  2. to load plugins is enabled reset app/service but ideally it would load and update plugins on the fly.

One minimal example could be:

The app/service ~ plugins interface

module SharedTypes (PluginInterface (..)) where

data PluginInterface =
     PluginInterface { pluginName :: String
                     , runPlugin  :: Int -> Int }

Some plugin list

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

import SharedTypes

getPlugin :: PluginInterface
getPlugin = PluginInterface "{Nth}th plugin" $ \x -> {Nth} * x

App/service

...
loadPlugins :: FilePath -> IO [PluginInterface]
loadPlugins = undefined
...

I think using dynamic compilation link library (compiling each Plugin{Nth} as shared library) could works (as FFI) but

  1. How enumerate and load each shared library at runtime? (get every getPlugin function point)
  2. Exists some better way? (Eg. some "magic" process before run application/service)

Thank you!

UPDATE

Full running example

Following the great @xnyhps answer, a full running example using ghc 7.10

SharedTypes.hs

module SharedTypes (
  PluginInterface (..)
) where

data PluginInterface =
     PluginInterface { pluginName :: String
                     , runPlugin  :: Int -> Int
                     }

Plugin1.hs

module Plugin1 (
  getPlugin
) where

import SharedTypes

getPlugin :: PluginInterface
getPlugin = PluginInterface "Plugin1" $ \x -> 1 * x

Plugin2.hs

module Plugin2 (
  getPlugin
) where

import SharedTypes

getPlugin :: PluginInterface
getPlugin = PluginInterface "Plugin2" $ \x -> 2 * x

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)

Compilation and execution

[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
like image 462
josejuan Avatar asked Jun 06 '15 10:06

josejuan


1 Answers

There is the dynamic-loader package, which allows you to load extra object files or shared libraries into your process. (The version on Hackage doesn't work with 7.10, but the current version on GitHub does.)

With this, you could do:

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

However, you have to keep in mind that the entire process is very unsafe: if you change your PluginInterface data type and try to load a plugin compiled with the old version, your application will crash. You have to hope that the getPlugin function has type PluginInterface, there's no check for that. Lastly, if the plugin comes from an untrusted source, it could execute anything, even though the function you try to call should be pure in Haskel.

like image 159
xnyhps Avatar answered Oct 14 '22 02:10

xnyhps