This library can be used to write Extism Plug-ins in Haskell.
Docs are available on Hackage: https://hackage.haskell.org/package/extism-pdk
Make sure you have wasm32-wasi-ghc installed, then generate an Executable
project with cabal:
cabal init
Add the library from Hackage to your cabal file:
build-depends: extism-pdk
We will also need to add some additional ghc options to expose the correct functions:
ghc-options:
-optl -Wl,--export=greet -optl -Wl,--export=hs_init -optl -Wl,--allow-undefined -no-hs-main -optl-mexec-model=reactor
The goal of writing an Extism plug-in is to compile your Haskell code to a Wasm module with exported functions that the host application can invoke. The first thing you should understand is creating an export. Let's write a simple program that exports a greet
function which will take a name as a string and return a greeting string.
{-# LANGUAGE DeriveDataTypeable #-}
module Hello where
import Data.Maybe
import Extism.PDK
import Extism.PDK.JSON
defaultGreeting = "Hello"
greet g n =
output $ g ++ ", " ++ n
testing = do
-- Get a name from the Extism runtime
name <- inputString
-- Get configured greeting
greeting <- getConfig "greeting"
-- Greet the user, if no greeting is configured then "Hello" is used
greet (fromMaybe defaultGreeting greeting) name
foreign export ccall "greet" testing :: IO ()
This example also shows how to use the getConfig
function to load runtime configuration values set by the host.
Despite not needing any system access for this plugin, we will still compile it for wasm32-wasi
, since there is no Haskell compiler targeting wasm32-unknown-unknown
:
wasm32-wasi-cabal build
This will put your compiled wasm somewhere in the dist-newstyle
directory:
cp `find dist-newstyle -name example.wasm` .
We can now test it using the Extism CLI's run
command:
extism call ./example.wasm greet --input "Benjamin"
# => Hello, Benjamin!
Configure a new greeting we can update the greeting
config key using the Extism CLI's --config
option that lets you pass in key=value
pairs:
extism call ./example.wasm greet --input "Benjamin" --config greeting="Hi there"
# => Hi there, Benjamin!
Note: We also have a web-based, plug-in tester called the Extism Playground
For a function to be available from your Wasm plug-in, you will need to add a foreign export
:
foreign export ccall "greet" greet:: IO Int32
And there are some flags to make the function public on the linker side:
ghc-options:
-optl -Wl,--export=greet -optl -Wl,--export=hs_init -optl -Wl,--allow-undefined -no-hs-main -optl-mexec-model=reactor
This will export the greet
function, the hs_init
function and compile a reactor module instead of a command-style module.
A common thing you may want to do is pass some primitive Haskell data back and forth.
-- Float
addPi = do
-- Get float value
value <- (input :: IO Float)
output $ value + 3.14
return 0
-- Integers
sum42 = do
value <- (input :: IO Int)
output $ value + 42
return 0
-- ByteString
processBytes = do
bytes <- inputByteString
-- process bytes here
output bytes
return 0
-- String
processString = do
s <- inputString
output s
return 0
We provide a JSON class that allows you to pass JSON encoded values into and out of plug-in functions:
{-# LANGUAGE DeriveDataTypeable #-}
module Add where
import Extism.PDK
import Extism.PDK.JSON
data Add = Add
{ a :: Int,
b :: Int
} deriving (Data)
data Sum = Sum { sum :: Int } deriving (Data)
add = do
value <- input
output $ JSON $ Sum (a value + b value)
return 0
foreign export ccall "add" add :: IO Int32
Variables are another key-value mechanism but it's a mutable data store that will persist across function calls. These variables will persist as long as the host has loaded and not freed the plug-in. You can use getVar and setVar to manipulate them.
count = do
c <- fromMaybe 0 <$> getVar "count"
setVar "count" (c + 1)
output c
return 0
Because Wasm modules by default do not have access to the system, printing to stdout won't work (unless you use WASI). Extism provides some simple logging macros that allow you to use the host application to log without having to give the plug-in permission to make syscalls:
module Log where
import Extism.PDK
logStuff = do
logInfo "Some info!"
logWarn "A warning!"
logError "An error!"
return 0
foreign export ccall "logStuff" logStuff:: IO Int32
From Extism CLI:
extism call my_plugin.wasm logStuff --log-level=info
2023/09/30 11:52:17 Some info!
2023/09/30 11:52:17 A warning!
2023/09/30 11:52:17 An error!
Note: From the CLI you need to pass a level with
--log-level
. If you are running the plug-in in your own host using one of our SDKs, you need to make sure that you callset_log_file
to"stdout"
or some file location.
Sometimes it is useful to let a plug-in make HTTP calls.
Note: See Request docs for more info on the request and response types:
module HTTPGet where
import Data.Int
import Extism.PDK
import Extism.PDK.HTTP
import Extism.PDK.Memory
httpGet = do
-- Get JSON encoded request from host
JSON req <- input
-- Send the request, get a 'Response'
res <- sendRequest req (Nothing :: Maybe String)
-- Save response body to memory
outputMemory (memory res)
-- Return code
return 0
foreign export ccall "httpGet" httpGet :: IO Int32
Like any other code module, Wasm not only let's you export functions to the outside world, you can import them too. Host Functions allow a plug-in to import functions defined in the host. For example, if you host application is written in Python, it can pass a Python function down to your Haskell plug-in where you can invoke it.
This topic can get fairly complicated and we have not yet fully abstracted the Wasm knowledge you need to do this correctly. So we recommend reading out concept doc on Host Functions before you get started.
Host functions in the Haskell PDK require C stubs to import a function from a particular namespace:
#define IMPORT(a, b) __attribute__((import_module(a), import_name(b)))
IMPORT("extism:host/user", "a_python_func")
uint64_t a_python_func_impl(uint64_t input);
uint64_t a_python_func(uint64_t input) {
return a_python_func_impl(input);
}
This C file should be added to the extra-source-files
and c-sources
fields in your cabal file.
From there we can use foreign import ccall
to call our stub:
import Extism.PDK.Memory
import Extism.PDK
foreign import ccall "a_python_func" aPythonFunc :: Word64 -> IO Word64
helloFromPython :: String -> IO String
helloFromPython = do
s' <- allocString "Hello!"
res <- aPythonFunc (memoryOffset s')
logInfo <$> loadString res
return 0
foreign export ccall "helloFromPython" helloFromPython :: IO Int32
To call this function, we write our input string into memory using allocString
and call the function with the returned memory handle. We then have
to load the result string from memory to access it from our Haskell program.
We can't really test this from the Extism CLI as something must provide the implementation. So let's write out the Python side here. Check out the docs for Host SDKs to implement a host function in a language of your choice.
from extism import host_fn, Plugin
@host_fn()
def a_python_func(input: str) -> str:
# just printing this out to prove we're in Python land
print("Hello from Python!")
# let's just add "!" to the input string
# but you could imagine here we could add some
# applicaiton code like query or manipulate the database
# or our application APIs
return input + "!"
Now when we load the plug-in we pass the host function:
manifest = {"wasm": [{"path": "/path/to/plugin.wasm"}]}
plugin = Plugin(manifest, functions=[a_python_func], wasi=True)
result = plugin.call('helloFromPython', b'').decode('utf-8')
print(result)
python3 app.py
# => Hello from Python!
# => An argument to send to Python!
Have a question or just want to drop in and say hi? Hop on the Discord!