packages feed

extism (empty) → 0.1.0

raw patch · 6 files changed

+337/−0 lines, 6 filesdep +HUnitdep +basedep +bytestring

Dependencies added: HUnit, base, bytestring, extism, extism-manifest, json

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for extism++## 0.2.0.0 -- 2023-01-16++* First version. Released on an unsuspecting world.
+ Example.hs view
@@ -0,0 +1,16 @@+module Main where++import Extism+import Extism.Manifest(manifest, wasmFile)++unwrap (Right x) = x+unwrap (Left (ExtismError msg)) = do+  error msg++main = do+  let m = manifest [wasmFile "../wasm/code.wasm"]+  context <- Extism.newContext+  plugin <- unwrap <$> Extism.pluginFromManifest context m False+  res <- unwrap <$> Extism.call plugin "count_vowels" (Extism.toByteString "this is a test")+  putStrLn (Extism.fromByteString res)+  Extism.free plugin
+ extism.cabal view
@@ -0,0 +1,45 @@+cabal-version:      3.0+name:               extism+version:            0.1.0+license:            BSD-3-Clause+maintainer:         oss@extism.org+author:             Extism authors+bug-reports:        https://github.com/extism/extism+synopsis:           Extism bindings+description:        Bindings to Extism, the universal plugin system+category:           Plugins, WebAssembly+extra-source-files: CHANGELOG.md++library+    exposed-modules:    Extism+    reexported-modules: Extism.Manifest+    hs-source-dirs:     src+    other-modules:      Extism.Bindings+    default-language:   Haskell2010+    extra-libraries:    extism+    extra-lib-dirs:     /usr/local/lib+    build-depends:+        base              >= 4.16.1 && < 4.18.0,+        bytestring        >= 0.11.3 && < 0.12,+        json              >= 0.10 && < 0.11,+        extism-manifest   >= 0.0.0 && < 0.2.0++test-suite extism-example+    type:             exitcode-stdio-1.0+    main-is:          Example.hs+    default-language: Haskell2010+    build-depends:+        base,+        extism,+        bytestring++test-suite extism-test+    type:             exitcode-stdio-1.0+    main-is:          Test.hs+    hs-source-dirs:   test+    default-language: Haskell2010+    build-depends:+        base,+        extism,+        bytestring,+        HUnit
+ src/Extism.hs view
@@ -0,0 +1,174 @@+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++-- | 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)
+ src/Extism/Bindings.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE ForeignFunctionInterface #-}++module Extism.Bindings where++import Foreign.C.Types+import Foreign.Ptr+import Foreign.C.String+import Data.Int+import Data.Word++newtype ExtismContext = ExtismContext () deriving Show+newtype ExtismFunction = ExtismFunction () deriving Show++foreign import ccall safe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)+foreign import ccall safe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())+foreign import ccall safe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO Int32+foreign import ccall safe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO CBool+foreign import ccall safe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32+foreign import ccall safe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool+foreign import ccall safe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString+foreign import ccall safe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64+foreign import ccall safe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)+foreign import ccall safe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool+foreign import ccall safe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool+foreign import ccall safe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()+foreign import ccall safe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()+foreign import ccall safe "extism.h extism_version" extism_version :: IO CString
+ test/Test.hs view
@@ -0,0 +1,70 @@+import Test.HUnit+import Extism+import Extism.Manifest+++unwrap (Right x) = return x+unwrap (Left (ExtismError msg)) =+  assertFailure msg++defaultManifest = manifest [wasmFile "../../wasm/code.wasm"]++initPlugin :: Context -> IO Plugin+initPlugin context =+  Extism.pluginFromManifest context defaultManifest False >>= unwrap++pluginFunctionExists = do+  withContext (\ctx -> do+    p <- initPlugin ctx+    exists <- functionExists p "count_vowels"+    assertBool "function exists" exists+    exists' <- functionExists p "function_doesnt_exist"+    assertBool "function doesn't exist" (not exists'))++checkCallResult p = do+    res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap+    assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)++pluginCall = do+  withContext (\ctx -> do+    p <- initPlugin ctx+    checkCallResult p)++pluginMultiple = do+  withContext (\ctx -> do+    p <- initPlugin ctx+    checkCallResult p+    q <- initPlugin ctx+    r <- initPlugin ctx+    checkCallResult q+    checkCallResult r)++pluginUpdate = do+  withContext (\ctx -> do+    p <- initPlugin ctx+    updateManifest p defaultManifest True >>= unwrap+    checkCallResult p)++pluginConfig = do+  withContext (\ctx -> do+    p <- initPlugin ctx+    b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]+    assertBool "set config" b)++testSetLogFile = do+  b <- setLogFile "stderr" Extism.Error+  assertBool "set log file" b++t name f = TestLabel name (TestCase f)++main = do+  runTestTT (TestList+    [+      t "Plugin.FunctionExists" pluginFunctionExists+      , t "Plugin.Call" pluginCall+      , t "Plugin.Multiple" pluginMultiple+      , t "Plugin.Update" pluginUpdate+      , t "Plugin.Config" pluginConfig+      , t "SetLogFile" testSetLogFile+    ])+