extism-1.0.0.0: src/Extism.hs
-- |
-- A Haskell Extism host
--
-- Requires a libextism installation, see [https://extism.org/docs/install](https://extism.org/docs/install)
module Extism
( module Extism.Manifest,
Function (..),
Plugin (..),
CancelHandle (..),
LogLevel (..),
Error (..),
Result (..),
extismVersion,
newPlugin,
isValid,
setConfig,
setLogFile,
functionExists,
call,
cancelHandle,
cancel,
pluginID,
unwrap,
ToBytes (..),
Encoding,
FromBytes (..),
JSON (..),
)
where
import qualified Data.ByteString as B
import Data.ByteString.Internal (c2w, unsafePackLenAddress, w2c)
import qualified Data.ByteString.Lazy as BL
import Data.ByteString.Unsafe (unsafeUseAsCString)
import Data.Int
import qualified Data.UUID (UUID, fromByteString, toString)
import Data.Word
import Extism.Bindings
import Extism.Encoding
import Extism.Manifest (Manifest)
import Foreign.C.String
import Foreign.Concurrent
import Foreign.ForeignPtr
import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import Foreign.Ptr
import Foreign.StablePtr
import Foreign.Storable
import GHC.Ptr
import qualified Text.JSON (Result (..), decode, encode, showJSON, toJSObject)
-- | Host function, see 'Extism.HostFunction.hostFunction'
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ()) deriving (Eq)
-- | Plugins can be used to call WASM function
newtype Plugin = Plugin (ForeignPtr ExtismPlugin) deriving (Eq, Show)
-- | Cancellation handle for Plugins
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle) deriving (Eq, Show)
-- | Log level
data LogLevel = LogError | LogWarn | LogInfo | LogDebug | LogTrace deriving (Show, Eq)
-- | Get the Extism version string
extismVersion :: () -> IO String
extismVersion () = do
v <- extism_version
peekCString v
-- | Defines types that can be used to pass Wasm data into a plugin
class PluginInput a where
pluginInput :: a -> B.ByteString
instance PluginInput B.ByteString where
pluginInput = id
instance PluginInput Manifest where
pluginInput m = toByteString $ Text.JSON.encode m
-- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should
-- | be linked
newPlugin :: (PluginInput a) => a -> [Function] -> Bool -> IO (Result Plugin)
newPlugin input functions useWasi =
let wasm = pluginInput input
in let nfunctions = fromIntegral (length functions)
in let length' = fromIntegral (B.length wasm)
in let wasi = fromInteger (if useWasi then 1 else 0)
in do
funcs <- mapM (\(Function ptr _) -> withForeignPtr ptr return) functions
alloca
( \e -> do
let errmsg = (e :: Ptr CString)
p <-
unsafeUseAsCString
wasm
( \s ->
withArray
funcs
( \funcs ->
extism_plugin_new (castPtr s) length' funcs nfunctions wasi errmsg
)
)
if p == nullPtr
then do
err <- peek errmsg
e <- peekCString err
extism_plugin_new_error_free err
return $ Left (ExtismError e)
else do
ptr <- Foreign.Concurrent.newForeignPtr p (extism_plugin_free p)
return $ Right (Plugin ptr)
)
-- | Check if a 'Plugin' is valid
isValid :: Plugin -> IO Bool
isValid (Plugin p) = withForeignPtr p (\x -> return (x /= nullPtr))
-- | Set configuration values for a plugin
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
setConfig (Plugin plugin) x =
let obj = Text.JSON.toJSObject [(k, Text.JSON.showJSON v) | (k, v) <- x]
in let bs = toByteString (Text.JSON.encode obj)
in let length' = fromIntegral (B.length bs)
in unsafeUseAsCString
bs
( \s ->
withForeignPtr
plugin
( \plugin' -> do
b <- extism_plugin_config plugin' (castPtr s) length'
return $ b /= 0
)
)
levelStr LogError = "error"
levelStr LogDebug = "debug"
levelStr LogWarn = "warn"
levelStr LogTrace = "trace"
levelStr LogInfo = "info"
-- | Set the log file and level, this is a global configuration
setLogFile :: String -> LogLevel -> IO Bool
setLogFile filename level =
let s = levelStr level
in withCString
filename
( \f ->
withCString
s
( \l -> do
b <- extism_log_file f l
return $ b /= 0
)
)
-- | Check if a function exists in the given plugin
functionExists :: Plugin -> String -> IO Bool
functionExists (Plugin plugin) name =
withForeignPtr
plugin
( \plugin' -> do
b <- withCString name (extism_plugin_function_exists plugin')
if b == 1 then return True else return False
)
--- | Call a function provided by the given plugin
call :: (ToBytes a, FromBytes b) => Plugin -> String -> a -> IO (Result b)
call (Plugin plugin) name inp =
let input = toBytes inp
in let length' = fromIntegral (B.length input)
in withForeignPtr
plugin
( \plugin' -> do
rc <-
withCString
name
( \name' ->
unsafeUseAsCString
input
( \input' ->
extism_plugin_call plugin' name' (castPtr input') length'
)
)
err <- extism_error plugin'
if err /= nullPtr
then do
e <- peekCString err
return $ Left (ExtismError e)
else
if rc == 0
then do
len <- extism_plugin_output_length plugin'
Ptr ptr <- extism_plugin_output_data plugin'
x <- unsafePackLenAddress (fromIntegral len) ptr
return $ fromBytes x
else return $ Left (ExtismError "Call failed")
)
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
-- | from another thread.
cancelHandle :: Plugin -> IO CancelHandle
cancelHandle (Plugin plugin) = do
handle <- withForeignPtr plugin extism_plugin_cancel_handle
return (CancelHandle handle)
-- | Cancel a running plugin using a 'CancelHandle'
cancel :: CancelHandle -> IO Bool
cancel (CancelHandle handle) =
extism_plugin_cancel handle
pluginID :: Plugin -> IO Data.UUID.UUID
pluginID (Plugin plugin) =
withForeignPtr
plugin
( \plugin' -> do
ptr <- extism_plugin_id plugin'
buf <- B.packCStringLen (castPtr ptr, 16)
case Data.UUID.fromByteString (BL.fromStrict buf) of
Nothing -> error "Invalid Plugin ID"
Just x -> return x
)
unwrap (Right x) = x
unwrap (Left (ExtismError msg)) =
error msg