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 +5/−0
- Example.hs +16/−0
- extism.cabal +45/−0
- src/Extism.hs +174/−0
- src/Extism/Bindings.hs +27/−0
- test/Test.hs +70/−0
+ 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+ ])+