This is an introductory example.
First we need an interface between the application and the plugin. This module needs to be visible to both the app and the plugin, in the interest of clear and well-defined interfaces:
module StringProcAPI (Interface(..), plugin) where data Interface = Interface { stringProcessor :: String -> String } plugin :: Interface plugin = Interface { stringProcessor = id }
Here we define Interface as the inteface signature for the object passed between plugin and application. We'll use the record syntax as it looks intuitive in the plugin. We provide a default instance, the plugin value, that can be overwritten in the actual plugin, ensuring sensible behaviour in the absence of any plugins. The API should theoretically be compiled with -Onot to avoid interface details leaking out into the .hi file.
This is our plugin. Note that the plugin will be compiled entirely seperately from the application. It must only rely on the API, and nothing in the application source.
module StringProcPlugin (resource) where import StringProcAPI (plugin) resource = plugin { stringProcessor = reverse }
Using the record syntax we overwrite the function field with our own value, reverse. The value resource is the magic symbol that must be defined, and which the application will use to find the data the plugin exports.
Now, we can make this even easier on the plugin writer by the use of a ``stub'' file. makeWith lets you merge a plugin source with another Haskell file, and compiles the result into the actual plugin object. So the application can provide a stub file containing module declarations and imports, and a default plugin value. Here is an application-provided stub, factoring out compulsory syntax and type declarations from the plugin:
module StringProcPlugin ( resource ) where import StringProcAPI resource :: Interface resource = plugin
By factoring out compulsory syntax, the plugin author only has to provide an overriding instance of the resource field. So all the plugin actually consists of, is:
resource = plugin { stringProcessor = reverse }
That is all the code we need! This file may be called anything at all.
More complex APIs may have more fields, of course. The nice thing about this arrangement is that the user will write some simple syntax, which will nonetheless by typechecked safely against the API. Errors are also reported using line numbers from the source file, not the stub, which makes things less confusing.
Now we need to write an application that can use values of the kind defined in the API, and which can compile and load plugins. The basic mechanism to compile and load a plugin is as follows:
This code calls make to compile the plugin source, yielding wrapper around a handle to an object file. The object can then be loaded using load, and the code associated with the symbol resource is retrieved.do status <- make "StringProcPlugin.hs" [] obj <- case status of MakeSuccess _ o -> return o MakeFailure e -> mapM_ putStrLn e >> error "failed" m_v <- load obj ["."] [] "resource" val <- case m_v of LoadSuccess _ v -> return v _ -> error "load failed"
We embed this code in a simple shell-like loop, applying the function exported by the plugin:
import System.Plugins import StringProcessorAPI import System.Console.Readline import System.Exit source = "Plugin.hs" stub = "Plugin.stub" symbol = "resource" main = do s <- makeWith source stub [] o <- case s of MakeSuccess _ obj -> do ls <- load obj ["."] [] symbol case ls of LoadSuccess m v -> return (m,v) LoadFailure err -> error "load failed" MakeFailure e -> mapM_ putStrLn e >> error "compile failed" shell o shell o@(m,plugin) = do s <- readline "> " cmd <- case s of Nothing -> exitWith ExitSuccess Just (':':'q':_) -> exitWith ExitSuccess Just s -> addHistory s >> return s s <- makeWith source stub [] -- maybe recompile the source o' <- case s of MakeSuccess ReComp o -> do ls <- reload m symbol case ls of LoadSuccess m' v' -> return (m',v') LoadFailure err -> error "reload failed" MakeSuccess NotReq _ -> return o MakeFailure e -> mapM_ putStrLn e >> shell o eval cmd o' shell o' eval ":?" _ = putStrLn ":?\n:q\n<string>" eval s (_,plugin) = let fn = (stringProcessor plugin) in putStrLn (fn s)
We have to import the hs-plugins library, and the API. The main loop proceeds by compiling and loading the plugin for the first time, and then calls shell, the interpeter loop. This loop lets us apply the function in the plugin to strings we supply. We have to pass around the (Module, a) pair we get back from reload, so that we can pass it to eval to do the real work. The first eval case is where we use the record syntax to select the function field out of v, the plugin interface object, and we apply it to s. Try it out:
paprika$ ./a.out Loading package base ... linking ... done Loading objects API Plugin ... done > :? ":?" ":q" "<string>" > abcdefg gfedcba
Now, if we edit the plugin while the shell is running, the next time we type something at the prompt the plugin will be unloaded, recompiled and reloaded. Because the plugin is really an EDSL, we can use any Haskell we want, so we'll change the plugin to:
import Data.Char resource = plugin { stringProcessor = my_fn } my_fn s = map toUpper (reverse s)
Back to the shell:
> abcdefg Compiling plugin ... done Reloading Plugin ... done GFEDCBA
And that's it: dynamically recompiled and reload Haskell code!
It is quite easy to load multiple plugins, that all implement the common plugin API, and that all export the same value (though implemented differently). This make hs-plugins suitable for applications that wish to allow an arbitrary number of plugins. The main problem with multiple plugins is that they may share dependencies, and if load naïvely loaded all dependencies found in the set of .hi files associated with all the plugins, the GHC rts would crash. To solve this the hs-plugins dynamic loader maintains state storing a list of what modules and packages have been loaded already. If load is called on a module that is already loaded, or dependencies are attempted to load, that have already been loaded, the dynamic loader ignores these extra dependencies. This makes it quite easy to write an application that will allows an arbitrary number of plugins to be loaded. An example follows.
First we need to define the API that a plugin must type check against, in order to be valid.
module API where data Interface = Interface { valueOf :: String -> String } plugin :: Interface plugin = Interface { valueOf = id }
We can then implement a number of plugins that provide values of type "Interface". We show three plugins that export string manipulation functions:
module Plugin1 where import API import Data.Char resource = plugin { valueOf = \s -> map toUpper s }
module Plugin2 where import API import Data.Char resource = plugin { valueOf = \s -> map toLower s }
module Plugin3 where import API resource = plugin { valueOf = reverse }
And finally we need to write an application that would use these plugins. Remember that the application is written without knowledge of the plugins, and the plugins are written without knowledge of the application. They are each implemented only in terms of the API, a shared module and .hi file. An application needs to make the API interface available to plugin authors, by distributing the API object file and .hi file with the application.
import System.Plugins import API main = do let plist = ["Plugin1.o", "Plugin2.o", "Plugin3.o"] plugins <- mapM (\p -> load p ["."] [] "resource") plist let functions = map (valueOf . fromLoadSuc) plugins mapM_ (\f -> putStrLn $ f "haskell is for hackers") functions fromLoadSuc (LoadFailure _) = error "load failed" fromLoadSuc (LoadSuccess _ v) = v
This application simply loads all the plugins and retrieves the functions they export. It then applies each of these functions to a string, printing the result. We assume for this example that the plugins are compiled once only, and are not compiled dynamically via make. This implies that you have to use GHC to generate the .hi file for each plugin. A sample Makefile to compile the plugins, and the api:
all: ghc -Onot -c API.hs ghc -O -c Plugin1.hs ghc -O -c Plugin2.hs ghc -O -c Plugin3.hs
Ghc creates .hi files for each plugin, which can be inspected using the Plugins.BinIface.readBinIface function. It parses the .hi file, generating, roughly, the following:
interface "Main" Main module dependencies: A, B package dependencies: base, haskell98, lang, unix
which says that the plugin depends upon a variety of system packages, and the modules A and B. All these dependencies must be loaded before the plugin itself.
You then need to compile the application against the API, and against the hs-plugins library:
ghc -O --make -package plugins Main.hs
Running the application produces the following result. Note that the verbose output can be switched off by compiling hs-plugins without the -DDEBUG flag. If you look at the .hi file, using ghc -show-iface, you'll see that they all depend on the base package, and on the API, but the state stored in the dynamic loader ensures that these shared modules are only loaded once:
Loading package base ... linking ... done Loading object API Plugin1 ... done Loading object Plugin2 ... done Loading object Plugin3 ... done HASKELL IS FOR HACKERS haskell is for hackers srekcah rof si lleksah
Archives of plugins can be loaded in one go if they have been linked into a .o GHCi package, see loadPackage.