module Extism (module Extism, module Extism.Manifest) where import Data.Int import Data.Word import Control.Monad (void) import Foreign.ForeignPtr import Foreign.C.String import Foreign.Ptr import Data.ByteString as B import Data.ByteString.Internal (c2w, w2c) import Data.ByteString.Unsafe (unsafeUseAsCString) import Data.Bifunctor (second) import Text.JSON (encode, toJSObject, showJSON) import Extism.Manifest (Manifest, toString) import Extism.Bindings -- | Context for managing plugins newtype Context = Context (ForeignPtr ExtismContext) -- | Plugins can be used to call WASM function data Plugin = Plugin Context Int32 data CancelHandle = CancelHandle (Ptr ExtismCancelHandle) -- | Log level data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show) -- | Extism error newtype Error = ExtismError String deriving Show -- | Result type type Result a = Either Error a -- | Helper function to convert a 'String' to a 'ByteString' toByteString :: String -> ByteString toByteString x = B.pack (Prelude.map c2w x) -- | Helper function to convert a 'ByteString' to a 'String' fromByteString :: ByteString -> String fromByteString bs = Prelude.map w2c $ B.unpack bs -- | Get the Extism version string extismVersion :: () -> IO String extismVersion () = do v <- extism_version peekCString v -- | Remove all registered plugins in a 'Context' reset :: Context -> IO () reset (Context ctx) = withForeignPtr ctx extism_context_reset -- | Create a new 'Context' newContext :: IO Context newContext = do ptr <- extism_context_new fptr <- newForeignPtr extism_context_free ptr return (Context fptr) -- | Execute a function with a new 'Context' that is destroyed when it returns withContext :: (Context -> IO a) -> IO a withContext f = do ctx <- newContext f ctx -- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should -- | be linked plugin :: Context -> B.ByteString -> Bool -> IO (Result Plugin) plugin c wasm useWasi = let length = fromIntegral (B.length wasm) in let wasi = fromInteger (if useWasi then 1 else 0) in let Context ctx = c in do withForeignPtr ctx (\ctx -> do p <- unsafeUseAsCString wasm (\s -> extism_plugin_new ctx (castPtr s) length nullPtr 0 wasi ) if p < 0 then do err <- extism_error ctx (-1) e <- peekCString err return $ Left (ExtismError e) else return $ Right (Plugin c p)) -- | Create a 'Plugin' from a 'Manifest' pluginFromManifest :: Context -> Manifest -> Bool -> IO (Result Plugin) pluginFromManifest ctx manifest useWasi = let wasm = toByteString $ toString manifest in plugin ctx wasm useWasi -- | Update a 'Plugin' with a new WASM module update :: Plugin -> B.ByteString -> Bool -> IO (Result ()) update (Plugin (Context ctx) id) wasm useWasi = let length = fromIntegral (B.length wasm) in let wasi = fromInteger (if useWasi then 1 else 0) in do withForeignPtr ctx (\ctx -> do b <- unsafeUseAsCString wasm (\s -> extism_plugin_update ctx id (castPtr s) length nullPtr 0 wasi) if b <= 0 then do err <- extism_error ctx (-1) e <- peekCString err return $ Left (ExtismError e) else return (Right ())) -- | Update a 'Plugin' with a new 'Manifest' updateManifest :: Plugin -> Manifest -> Bool -> IO (Result ()) updateManifest plugin manifest useWasi = let wasm = toByteString $ toString manifest in update plugin wasm useWasi -- | Check if a 'Plugin' is valid isValid :: Plugin -> Bool isValid (Plugin _ p) = p >= 0 -- | Set configuration values for a plugin setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool setConfig (Plugin (Context ctx) plugin) x = if plugin < 0 then return False else let obj = toJSObject [(k, showJSON v) | (k, v) <- x] in let bs = toByteString (encode obj) in let length = fromIntegral (B.length bs) in unsafeUseAsCString bs (\s -> do withForeignPtr ctx (\ctx -> do b <- extism_plugin_config ctx plugin (castPtr s) length return $ b /= 0)) levelStr Error = "error" levelStr Debug = "debug" levelStr Warn = "warn" levelStr Trace = "trace" levelStr Info = "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 (Context ctx) plugin) name = do withForeignPtr ctx (\ctx -> do b <- withCString name (extism_plugin_function_exists ctx plugin) if b == 1 then return True else return False) --- | Call a function provided by the given plugin call :: Plugin -> String -> B.ByteString -> IO (Result B.ByteString) call (Plugin (Context ctx) plugin) name input = let length = fromIntegral (B.length input) in do withForeignPtr ctx (\ctx -> do rc <- withCString name (\name -> unsafeUseAsCString input (\input -> extism_plugin_call ctx plugin name (castPtr input) length)) err <- extism_error ctx plugin if err /= nullPtr then do e <- peekCString err return $ Left (ExtismError e) else if rc == 0 then do length <- extism_plugin_output_length ctx plugin ptr <- extism_plugin_output_data ctx plugin buf <- packCStringLen (castPtr ptr, fromIntegral length) return $ Right buf else return $ Left (ExtismError "Call failed")) -- | Free a 'Plugin', this will automatically be called for every plugin -- | associated with a 'Context' when that 'Context' is freed free :: Plugin -> IO () free (Plugin (Context ctx) plugin) = withForeignPtr ctx (`extism_plugin_free` plugin) cancelHandle :: Plugin -> IO CancelHandle cancelHandle (Plugin (Context ctx) plugin) = do handle <- withForeignPtr ctx (\ctx -> extism_plugin_cancel_handle ctx plugin) return (CancelHandle handle) cancel :: CancelHandle -> IO Bool cancel (CancelHandle handle) = extism_plugin_cancel handle