From a215b5d527a6e7485f2d34b035999201756c9cd8 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 29 Sep 2023 11:27:30 -0700 Subject: [PATCH] refactor: better organization, cleanup (#5) * cleanup: start reorganizing * ci * fix: shadowed variable name * refactor: add more binary types, move memory code to its own file * docs: show example with non-string input * cleanup: hlint * cleanup: reorganize * fix: don't free errors * cleanup: reorganize * cleanup: bring back Memory.store functions, remove unused imports * docs: add missing doc comments * docs: add link * cleanup: add explicit return code * cleanup: add tryInput * cleanup: count vowels example * fix: http example --- CHANGELOG.md | 9 +- Makefile | 3 + examples/CountVowels.hs | 19 ++- examples/HTTPGet.hs | 23 +++- examples/Hello.hs | 8 +- extism-pdk.cabal | 18 ++- src/Extism/PDK.hs | 246 +++++++++++----------------------- src/Extism/PDK/Bindings.hs | 124 +++++++++++------- src/Extism/PDK/HTTP.hs | 90 ++++++++----- src/Extism/PDK/JSON.hs | 9 +- src/Extism/PDK/Memory.hs | 261 +++++++++++++++++++++++++++++++++++++ src/Extism/PDK/MsgPack.hs | 132 +++++++++---------- src/Extism/PDK/Util.hs | 12 ++ 13 files changed, 610 insertions(+), 344 deletions(-) create mode 100644 src/Extism/PDK/Memory.hs create mode 100644 src/Extism/PDK/Util.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index bc83c9c..0ebe1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ -# Revision history for haskell-wasm +# Revision history for extism-pdk -## 0.1.0.0 -- YYYY-mm-dd +## 0.2.0.0 + +* API redesign, add automatic Haskell encoding using `ToMemory` and `FromMemory` classes + +## 0.1.0.0 -- 2023-09-28 * First version. Released on an unsuspecting world. + diff --git a/Makefile b/Makefile index df7fada..cd3df5d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ all: hello.example http_get.example count_vowels.example +update: + wasm32-wasi-cabal update + build: wasm32-wasi-cabal build diff --git a/examples/CountVowels.hs b/examples/CountVowels.hs index 3e1e5c5..89e552a 100644 --- a/examples/CountVowels.hs +++ b/examples/CountVowels.hs @@ -3,12 +3,17 @@ module CountVowels where import Extism.PDK import Extism.PDK.JSON -isVowel c = - c == 'a' || c == 'A' || - c == 'e' || c == 'E' || - c == 'i' || c == 'I' || - c == 'o' || c == 'O' || - c == 'u' || c == 'U' +isVowel c = + c == 'a' + || c == 'A' + || c == 'e' + || c == 'E' + || c == 'i' + || c == 'I' + || c == 'o' + || c == 'O' + || c == 'u' + || c == 'U' countVowels = do -- Get input string from Extism host @@ -18,4 +23,4 @@ countVowels = do -- Return a JSON object {"count": count} back to the host output $ JSONValue $ object ["count" .= count] -foreign export ccall "count_vowels" countVowels :: IO () +foreign export ccall "count_vowels" countVowels :: IO () diff --git a/examples/HTTPGet.hs b/examples/HTTPGet.hs index 1076b51..15dd439 100644 --- a/examples/HTTPGet.hs +++ b/examples/HTTPGet.hs @@ -1,16 +1,27 @@ module HTTPGet where +import Data.Int import Extism.PDK import Extism.PDK.HTTP +import Extism.PDK.Memory + +getInput = do + req <- tryInput + case req of + Right (JSONValue x) -> return x + Left e -> do + putStrLn e + url <- inputString + return $ newRequest url httpGet = do - -- Get URL from the host - url <- input - -- Create a new 'Request' - let req = newRequest url + -- Get URL or JSON encoded request from host + req <- getInput -- Send the request, get a 'Response' - res <- sendRequest req Nothing + res <- sendRequest req (Nothing :: Maybe String) -- Save response body to memory outputMemory (memory res) + -- Return code + return 0 -foreign export ccall "http_get" httpGet :: IO () +foreign export ccall "http_get" httpGet :: IO Int32 diff --git a/examples/Hello.hs b/examples/Hello.hs index 3f91d7b..3e720a5 100644 --- a/examples/Hello.hs +++ b/examples/Hello.hs @@ -1,20 +1,20 @@ module Hello where -import Extism.PDK import Data.Maybe +import Extism.PDK import Foreign.C.Types defaultGreeting = "Hello" greet g n = output $ g ++ ", " ++ n - + testing = do -- Get a name from the Extism runtime - name <- input + 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 "testing" testing :: IO () +foreign export ccall "testing" testing :: IO () diff --git a/extism-pdk.cabal b/extism-pdk.cabal index 5777a97..7915524 100644 --- a/extism-pdk.cabal +++ b/extism-pdk.cabal @@ -1,12 +1,12 @@ cabal-version: 3.0 name: extism-pdk -version: 0.1.0.0 +version: 0.2.0.0 -- A short (one-line) description of the package. synopsis: Extism Plugin Development Kit -- A longer description of the package. -description: Haskell bindings to the Extism runtime +description: Haskell bindings to the Extism runtime for use with wasm32-wasi-ghc -- A URL where users can report bugs. bug-reports: https://github.com/extism/haskell-pdk @@ -20,7 +20,14 @@ category: WASM, plugins extra-doc-files: CHANGELOG.md library - exposed-modules: Extism.PDK Extism.PDK.Bindings Extism.PDK.HTTP Extism.PDK.JSON Extism.PDK.MsgPack + exposed-modules: + Extism.PDK + Extism.PDK.Bindings + Extism.PDK.HTTP + Extism.PDK.JSON + Extism.PDK.MsgPack + Extism.PDK.Util + Extism.PDK.Memory -- Modules included in this executable, other than Main. -- other-modules: @@ -34,7 +41,8 @@ library containers >= 0.6.7 && < 0.7, extism-manifest >= 0.3.0 && <= 1.0.0, json >= 0.11 && < 0.12, - messagepack >= 0.5.5 && < 0.6 + messagepack >= 0.5.5 && < 0.6, + binary >= 0.8.9 && < 0.9.0 hs-source-dirs: src default-language: Haskell2010 @@ -45,7 +53,7 @@ executable hello build-depends: base, extism-pdk default-language: Haskell2010 ghc-options: - -optl -Wl,--export=testing + -optl -Wl,--export=testing executable http_get scope: private diff --git a/src/Extism/PDK.hs b/src/Extism/PDK.hs index c169d74..45c09e2 100644 --- a/src/Extism/PDK.hs +++ b/src/Extism/PDK.hs @@ -1,196 +1,95 @@ -{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} +{-# LANGUAGE Rank2Types #-} + +-- | +-- Extism plugin development kit, used with the [wasm32-wasi-ghc](https://gitlab.haskell.org/ghc/ghc-wasm-meta) backend to make Extism plugins +module Extism.PDK + ( module Extism.PDK, + ToBytes (..), + FromBytes (..), + JSONValue (..), + MsgPackValue (..), + ) +where -module Extism.PDK (module Extism.PDK, module Extism.Manifest) where - -import Extism.PDK.Bindings -import Extism.JSON(JSValue, JSON) -import Extism.Manifest(toString) -import Data.Word -import Data.Int import Data.ByteString as B -import Data.ByteString.Internal (c2w, w2c) -import Data.ByteString.Unsafe (unsafeUseAsCString) -import Text.JSON(JSON, decode, encode, resultToEither) -import qualified Extism.PDK.MsgPack(MsgPack, decode, encode) - -newtype JSONValue a = JSONValue a -newtype MsgPackValue a = MsgPackValue a - --- | Represents a block of memory -data Memory = Memory MemoryOffset MemoryLength - --- | 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 - -class FromBytes a where - fromBytes :: ByteString -> a - -class ToBytes a where - toBytes :: a -> ByteString - -instance FromBytes ByteString where - fromBytes bs = bs - -instance ToBytes ByteString where - toBytes bs = bs - -instance FromBytes String where - fromBytes = fromByteString - -instance ToBytes String where - toBytes = toByteString - -instance JSON a => FromBytes (JSONValue a) where - fromBytes x = - case resultToEither $ decode (fromByteString x) of - Left e -> error e - Right y -> JSONValue y - -instance JSON a => ToBytes (JSONValue a) where - toBytes (JSONValue x) = toByteString (encode x) - -instance Extism.PDK.MsgPack.MsgPack a => FromBytes (MsgPackValue a) where - fromBytes x = - case Extism.PDK.MsgPack.decode x of - Left e -> error e - Right y -> MsgPackValue y +import Extism.PDK.Bindings +import Extism.PDK.Memory +import qualified Extism.PDK.MsgPack (MsgPack, decode, encode) +import Extism.PDK.Util +import Text.JSON (JSON, decode, encode, resultToEither) -instance Extism.PDK.MsgPack.MsgPack a => ToBytes (MsgPackValue a) where - toBytes (MsgPackValue x) = Extism.PDK.MsgPack.encode x +-- | Get plugin input, returning an error message if the encoding is invalid +tryInput :: (FromBytes a) => IO (Either String a) +tryInput = fromBytes <$> inputByteString --- | Get plugin input as 'ByteString' -input :: FromBytes a => IO a +-- | Get plugin input +input :: forall a. (FromBytes a) => IO a input = do + i <- inputByteString + let x = fromBytes i + case x of + Left e -> error e + Right y -> return y + +-- | Get plugin input as a String +inputString :: IO String +inputString = do len <- extismInputLength - fromBytes <$> readInputBytes len + fromByteString <$> readInputBytes len --- | Get plugin input as 'Memory' block -inputMemory :: IO Memory -inputMemory = do +-- | Get plugin input as a ByteString +inputByteString :: IO ByteString +inputByteString = do len <- extismInputLength - offs <- extismAlloc len - Prelude.mapM_ (\x -> - extismStoreU8 (offs + x) <$> extismInputLoadU8 x) [0, 1 .. len] - return $ Memory offs len + readInputBytes len --- | Get input as 'JSON' -inputJSON :: JSON a => IO (Maybe a) +-- | Get input as 'JSON', this is similar to calling `input (JsonValue ...)` +inputJSON :: (JSON a) => IO (Either String a) inputJSON = do - s <- input :: IO String - case resultToEither $ decode s of - Left _ -> return Nothing - Right x -> return (Just x) - --- | Load data from 'Memory' block -load :: FromBytes a => Memory -> IO a -load (Memory offs len) = - fromBytes <$> readBytes offs len - --- | Store data into a 'Memory' block -store :: ToBytes a => Memory -> a -> IO () -store (Memory offs len) a = - let bs = toBytes a in - writeBytes offs len bs - --- | Set plugin output to the provided 'Memory' block -outputMemory :: Memory -> IO () -outputMemory (Memory offs len) = + s <- tryInput :: IO (Either String String) + case s of + Left e -> return (Left e) + Right x -> + case resultToEither $ decode x of + Left e -> return (Left e) + Right y -> return (Right y) + +-- | Set plugin output +output :: (ToBytes a) => a -> IO () +output x = do + Memory offs len <- alloc x extismSetOutput offs len --- | Set plugin output to the provided 'ByteString' -output :: ToBytes a => a -> IO () -output x = - let bs = toBytes x in - let len = fromIntegral $ B.length bs in - do - offs <- extismAlloc len - b <- store (Memory offs len) bs - extismSetOutput offs len - -- | Set plugin output to a JSON encoded version of the provided value -outputJSON :: JSON a => a -> IO () +outputJSON :: (JSON a) => a -> IO () outputJSON x = - output (toString x) - --- | Load string from 'Memory' block -loadString :: Memory -> IO String -loadString mem = do - bs <- load mem - return $ fromByteString bs - --- | Store string in 'Memory' block -storeString :: Memory -> String -> IO () -storeString mem s = - let bs = toByteString s in - store mem bs - --- | Allocate a new 'Memory' block -alloc :: Int -> IO Memory -alloc n = - let len = fromIntegral n in - do - offs <- extismAlloc len - return $ Memory offs len - --- | Free a 'Memory' block -free :: Memory -> IO () -free (Memory 0 _) = return () -free (Memory _ 0) = return () -free (Memory offs _) = - extismFree offs - --- | Allocate a new 'Memory' block and copy the contents of the provided 'ByteString' -allocByteString :: ByteString -> IO Memory -allocByteString bs = do - mem <- alloc (B.length bs) - store mem bs - return mem - --- | Allocate a new 'Memory' block and copy the contents of the provided 'String' -allocString :: String -> IO Memory -allocString s = - let bs = toByteString s in - allocByteString bs - --- | Get the offset of a 'Memory' block -memoryOffset (Memory offs _) = offs - --- | Get the length of a 'Memory' block -memoryLength (Memory _ len) = len - --- | Find 'Memory' block by offset -findMemory offs = do - len <- extismLength offs - return $ Memory offs len + output (encode x) -- | Get a variable from the Extism runtime -getVar :: String -> IO (Maybe ByteString) +getVar :: (FromBytes a) => String -> IO (Maybe a) getVar key = do k <- allocString key v <- extismGetVar (memoryOffset k) free k - if v == 0 then - return Nothing - else do - mem <- findMemory v - bs <- load mem - free mem - return (Just bs) + if v == 0 + then return Nothing + else do + mem <- findMemory v + bs <- load mem + free k + case bs of + Left _ -> return Nothing + Right x -> return (Just x) -- | Set a variable -setVar :: ToBytes a => String -> Maybe a -> IO () +setVar :: (ToBytes a) => String -> Maybe a -> IO () setVar key Nothing = do k <- allocString key extismSetVar (memoryOffset k) 0 free k setVar key (Just v) = do k <- allocString key - x <- allocByteString (toBytes v) + x <- alloc v extismSetVar (memoryOffset k) (memoryOffset x) free k free x @@ -201,23 +100,24 @@ getConfig key = do k <- allocString key v <- extismGetConfig (memoryOffset k) free k - if v == 0 then - return Nothing - else do - mem <- findMemory v - s <- loadString mem - free mem - return $ Just s + if v == 0 + then return Nothing + else do + mem <- findMemory v + s <- loadString mem + free mem + return $ Just s -- | Set the current error message setError :: String -> IO () setError msg = do s <- allocString msg extismSetError $ memoryOffset s - free s +-- | Log level data LogLevel = Info | Debug | Warn | Error +-- | Log to configured log file log :: LogLevel -> String -> IO () log Info msg = do s <- allocString msg diff --git a/src/Extism/PDK/Bindings.hs b/src/Extism/PDK/Bindings.hs index 6d40ece..45fd831 100644 --- a/src/Extism/PDK/Bindings.hs +++ b/src/Extism/PDK/Bindings.hs @@ -2,75 +2,109 @@ module Extism.PDK.Bindings where +import Control.Monad +import Data.ByteString as B import Data.ByteString.Internal +import Data.Int +import Data.Word +import Foreign.C.Types import Foreign.ForeignPtr import Foreign.Ptr import Foreign.Storable -import Control.Monad - import System.Exit -import Data.Word -import Data.Int -import Foreign.C.Types - -import Data.ByteString as B - +-- | Offset in Extism memory type MemoryOffset = Word64 + +-- | Offset of input from 0 to 'InputLength' type InputOffset = Word64 + +-- | Length of allocated block of memory type MemoryLength = Word64 + +-- | Total length of the input type InputLength = Word64 foreign import ccall "extism_output_set" extismSetOutput :: MemoryOffset -> MemoryLength -> IO () + foreign import ccall "extism_error_set" extismSetError :: MemoryOffset -> IO () + foreign import ccall "extism_log_info" extismLogInfo :: MemoryOffset -> IO () + foreign import ccall "extism_log_warn" extismLogWarn :: MemoryOffset -> IO () + foreign import ccall "extism_log_debug" extismLogDebug :: MemoryOffset -> IO () + foreign import ccall "extism_log_error" extismLogError :: MemoryOffset -> IO () + foreign import ccall "extism_store_u8" extismStoreU8 :: MemoryOffset -> Word8 -> IO () + foreign import ccall "extism_store_u64" extismStoreU64 :: MemoryOffset -> Word64 -> IO () + foreign import ccall "extism_load_u8" extismLoadU8 :: MemoryOffset -> IO Word8 + foreign import ccall "extism_load_u64" extismLoadU64 :: MemoryOffset -> IO Word64 + foreign import ccall "extism_alloc" extismAlloc :: MemoryLength -> IO MemoryOffset + foreign import ccall "extism_length" extismLength :: MemoryOffset -> IO MemoryLength + foreign import ccall "extism_free" extismFree :: MemoryOffset -> IO () + foreign import ccall "extism_input_length" extismInputLength :: IO InputLength + foreign import ccall "extism_input_load_u8" extismInputLoadU8 :: InputOffset -> IO Word8 + foreign import ccall "extism_input_load_u64" extismInputLoadU64 :: InputOffset -> IO Word64 + foreign import ccall "extism_config_get" extismGetConfig :: MemoryOffset -> IO MemoryOffset + foreign import ccall "extism_var_get" extismGetVar :: MemoryOffset -> IO MemoryOffset + foreign import ccall "extism_var_set" extismSetVar :: MemoryOffset -> MemoryOffset -> IO () + foreign import ccall "extism_http_request" extismHTTPRequest :: MemoryOffset -> MemoryOffset -> IO MemoryOffset + foreign import ccall "extism_http_status_code" extismHTTPStatusCode :: IO Int32 + foreign import ccall "__wasm_call_ctors" wasmConstructor :: IO () -foreign import ccall "__wasm_call_dtors" wasmDestructor :: IO () +foreign import ccall "__wasm_call_dtors" wasmDestructor :: IO () bsToWord64 :: ByteString -> IO Word64 bsToWord64 (BS fp len) = - if len /= 8 then error "invalid bytestring" - else - withForeignPtr fp (\p -> - peek $ castPtr @Word8 @Word64 p) + if len /= 8 + then error "invalid bytestring" + else + withForeignPtr + fp + ( peek . castPtr @Word8 @Word64 + ) word64ToBS :: Word64 -> ByteString -word64ToBS word = unsafeCreate 8 (\p -> - poke (castPtr @Word8 @Word64 p) word) +word64ToBS word = + unsafeCreate + 8 + ( \p -> + poke (castPtr @Word8 @Word64 p) word + ) readLoop :: (Word64 -> IO Word8) -> (Word64 -> IO Word64) -> Word64 -> Word64 -> [ByteString] -> IO ByteString readLoop f1 f8 total index acc = - if index >= total then - return $ B.concat . Prelude.reverse $ acc - else - let diff = total - index in - do - (n, x) <- if diff >= 8 then do - u <- f8 index - return (8, word64ToBS u) - else do - b <- f1 index - return (1, B.singleton b) - readLoop f1 f8 total (index + n) (x : acc) + if index >= total + then return $ B.concat . Prelude.reverse $ acc + else + let diff = total - index + in do + (n, x) <- + if diff >= 8 + then do + u <- f8 index + return (8, word64ToBS u) + else do + b <- f1 index + return (1, B.singleton b) + readLoop f1 f8 total (index + n) (x : acc) readInputBytes :: InputLength -> IO ByteString readInputBytes len = @@ -82,22 +116,24 @@ readBytes offs len = writeBytesLoop :: MemoryOffset -> MemoryOffset -> ByteString -> IO () writeBytesLoop index total src = - if index >= total then - return () - else - let diff = total - index in - do - (n, sub) <- if diff >= 8 then do - let (curr, next) = B.splitAt 8 src - u <- bsToWord64 curr - extismStoreU64 index u - return (8, next) - else do - let u = B.head src - extismStoreU8 index u - return (1, B.tail src) - writeBytesLoop (index + n) total sub + if index >= total + then return () + else + let diff = total - index + in do + (n, sub) <- + if diff >= 8 + then do + let (curr, next) = B.splitAt 8 src + u <- bsToWord64 curr + extismStoreU64 index u + return (8, next) + else do + let u = B.head src + extismStoreU8 index u + return (1, B.tail src) + writeBytesLoop (index + n) total sub writeBytes :: MemoryOffset -> MemoryLength -> ByteString -> IO () -writeBytes offs len src = - writeBytesLoop offs (offs + len) src +writeBytes offs len = + writeBytesLoop offs (offs + len) diff --git a/src/Extism/PDK/HTTP.hs b/src/Extism/PDK/HTTP.hs index 5468f91..d88f031 100644 --- a/src/Extism/PDK/HTTP.hs +++ b/src/Extism/PDK/HTTP.hs @@ -1,40 +1,42 @@ +-- | +-- Contains bindings to the Extism PDK HTTP interface module Extism.PDK.HTTP where -import Extism.Manifest(toString, HTTPRequest(..), method, headers, url) -import Extism.JSON(Nullable(..), decode, JSON, Result(..)) -import Extism.PDK.Bindings -import Extism.PDK -import Data.Word import Data.ByteString as B +import Data.Word +import Extism.JSON (JSON, Nullable (..), Result (..), decode) +import Extism.Manifest (HTTPRequest (..), headers, method, toString, url) +import Extism.PDK +import Extism.PDK.Bindings +import Extism.PDK.Memory -- | HTTP Request type Request = HTTPRequest -- | HTTP Response data Response = Response - { - statusCode :: Int - , memory :: Memory + { statusCode :: Int, + memory :: Memory } -- | Creates a new 'Request' newRequest :: String -> Request newRequest url = - HTTPRequest { - url = url - , headers = Null - , method = Null - } + HTTPRequest + { url = url, + headers = Null, + method = Null + } -- | Update a 'Request' with the provided HTTP request method (GET, POST, PUT, DELETE, ...) withMethod :: String -> Request -> Request withMethod meth req = - req { method = NotNull meth } + req {method = NotNull meth} -- | Update a 'Request' with the provided HTTP request headers withHeaders :: [(String, String)] -> Request -> Request withHeaders h req = - req { headers = NotNull h } + req {headers = NotNull h} -- | Access the Memory block associated with a 'Response' responseMemory :: Response -> Memory @@ -42,38 +44,60 @@ responseMemory (Response _ mem) = mem -- | Get the 'Response' body as a 'ByteString' responseByteString :: Response -> IO ByteString -responseByteString (Response _ mem) = load mem +responseByteString (Response _ mem) = do + a <- load mem + case a of + Left e -> error e + Right x -> return x -- | Get the 'Response' body as a 'String' responseString :: Response -> IO String responseString (Response _ mem) = loadString mem -- | Get the 'Response' body as JSON -responseJSON :: JSON a => Response -> IO (Either String a) +responseJSON :: (JSON a) => Response -> IO (Either String a) responseJSON (Response _ mem) = do json <- decode <$> loadString mem case json of Ok json -> return $ Right json Extism.JSON.Error msg -> return (Left msg) +-- | Get the 'Response' body and decode it +response :: (FromBytes a) => Response -> IO (Either String a) +response (Response _ mem) = load mem -- | Send HTTP request with an optional request body -sendRequest :: Request -> Maybe ByteString -> IO Response -sendRequest req b = - let json = Extism.Manifest.toString req in - let bodyMem = case b of - Nothing -> return $ Memory 0 0 - Just b -> allocByteString b - in - do - body <- bodyMem - j <- allocString json - res <- extismHTTPRequest (memoryOffset j) (memoryOffset body) - free j - free body - code <- extismHTTPStatusCode - if res == 0 then - return (Response (fromIntegral code) (Memory 0 0)) +sendRequestWithBody :: (ToBytes a) => Request -> a -> IO Response +sendRequestWithBody req b = do + body <- alloc b + let json = Extism.Manifest.toString req + j <- allocString json + res <- extismHTTPRequest (memoryOffset j) (memoryOffset body) + free j + free body + code <- extismHTTPStatusCode + if res == 0 + then return (Response (fromIntegral code) (Memory 0 0)) else do mem <- findMemory res return (Response (fromIntegral code) mem) + +-- | Send HTTP request with an optional request body +sendRequest :: (ToBytes a) => Request -> Maybe a -> IO Response +sendRequest req b = + let json = Extism.Manifest.toString req + in let bodyMem = case b of + Nothing -> return $ Memory 0 0 + Just b -> alloc b + in do + body <- bodyMem + j <- allocString json + res <- extismHTTPRequest (memoryOffset j) (memoryOffset body) + free j + free body + code <- extismHTTPStatusCode + if res == 0 + then return (Response (fromIntegral code) (Memory 0 0)) + else do + mem <- findMemory res + return (Response (fromIntegral code) mem) diff --git a/src/Extism/PDK/JSON.hs b/src/Extism/PDK/JSON.hs index fe85133..e6fcdb3 100644 --- a/src/Extism/PDK/JSON.hs +++ b/src/Extism/PDK/JSON.hs @@ -1,6 +1,7 @@ -module Extism.PDK.JSON ( - module Extism.PDK.JSON, - module Extism.JSON -) where +module Extism.PDK.JSON + ( module Extism.PDK.JSON, + module Extism.JSON, + ) +where import Extism.JSON diff --git a/src/Extism/PDK/Memory.hs b/src/Extism/PDK/Memory.hs new file mode 100644 index 0000000..104684e --- /dev/null +++ b/src/Extism/PDK/Memory.hs @@ -0,0 +1,261 @@ +{-# LANGUAGE FlexibleInstances #-} + +-- | +-- Extism.PDK.Memory implements a low-level interface for interacting with Extism memory +module Extism.PDK.Memory + ( Memory (..), + MemoryOffset, + MemoryLength, + FromBytes (..), + ToBytes (..), + JSONValue (..), + MsgPackValue (..), + load, + loadString, + loadByteString, + outputMemory, + memAlloc, + free, + alloc, + allocString, + allocByteString, + memoryOffset, + memoryLength, + findMemory, + ) +where + +import Data.Binary.Get +import Data.Binary.Put +import qualified Data.ByteString as B +import Data.ByteString.Internal (c2w, w2c) +import Data.Int +import Data.Word +import Extism.PDK.Bindings +import qualified Extism.PDK.MsgPack (MsgPack, decode, encode) +import Extism.PDK.Util +import Text.JSON (JSON, decode, encode, resultToEither) + +-- | Represents a block of memory by offset and length +data Memory = Memory MemoryOffset MemoryLength + +-- | Load data from 'Memory' block +load :: (FromBytes a) => Memory -> IO (Either String a) +load (Memory offs len) = do + x <- readBytes offs len + return $ fromBytes x + +-- | Store data into a 'Memory' block +store :: (ToBytes a) => Memory -> a -> IO () +store (Memory offs len) a = + let bs = toBytes a + in writeBytes offs len bs + +-- | Set plugin output to the provided 'Memory' block +outputMemory :: Memory -> IO () +outputMemory (Memory offs len) = + extismSetOutput offs len + +-- | Load ByteString from 'Memory' block +loadByteString :: Memory -> IO B.ByteString +loadByteString (Memory offs len) = do + readBytes offs len + +-- | Load string from 'Memory' block +loadString :: Memory -> IO String +loadString (Memory offs len) = + fromByteString <$> readBytes offs len + +-- | Store string in 'Memory' block +storeString :: Memory -> String -> IO () +storeString mem s = + let bs = toByteString s + in storeByteString mem bs + +-- | Store byte string in 'Memory' block +storeByteString :: Memory -> B.ByteString -> IO () +storeByteString (Memory offs len) = + writeBytes offs len + +-- | Encode a value and copy it into Extism memory, returning the Memory block +alloc :: (ToBytes a) => a -> IO Memory +alloc x = + let bs = toBytes x + in do + Memory offs len <- memAlloc (B.length bs) + writeBytes offs len bs + return $ Memory offs len + +-- | Allocate a new 'Memory' block +memAlloc :: Int -> IO Memory +memAlloc n = + let len = fromIntegral n + in do + offs <- extismAlloc len + return $ Memory offs len + +-- | Free a 'Memory' block +free :: Memory -> IO () +free (Memory 0 _) = return () +free (Memory _ 0) = return () +free (Memory offs _) = + extismFree offs + +-- | Allocate a new 'Memory' block and copy the encoded value +allocByteString :: B.ByteString -> IO Memory +allocByteString bs = do + Memory offs len <- memAlloc (B.length bs) + writeBytes offs len bs + return (Memory offs len) + +-- | Allocate a new 'Memory' block and copy the contents of the provided 'String' +allocString :: String -> IO Memory +allocString = allocByteString . toByteString + +-- | Get the offset of a 'Memory' block +memoryOffset :: Memory -> MemoryOffset +memoryOffset (Memory offs _) = offs + +-- | Get the length of a 'Memory' block +memoryLength :: Memory -> MemoryLength +memoryLength (Memory _ len) = len + +-- | Find 'Memory' block by offset +findMemory :: MemoryOffset -> IO Memory +findMemory offs = do + len <- extismLength offs + return $ Memory offs len + +-- | A class used to convert values from bytes read from linear memory +class FromBytes a where + fromBytes :: B.ByteString -> Either String a + +-- | A class used to convert values to bytes to be written into linear memory +class ToBytes a where + toBytes :: a -> B.ByteString + +-- | A wrapper type for JSON encoded values +newtype JSONValue a = JSONValue a + +-- | A wrapper type for MsgPack encoded values +newtype MsgPackValue a = MsgPackValue a + +instance FromBytes B.ByteString where + fromBytes = Right + +instance ToBytes B.ByteString where + toBytes = id + +instance FromBytes String where + fromBytes mem = + let s = fromBytes mem + in case s of + Left e -> Left e + Right x -> Right $ fromByteString x + +instance ToBytes String where + toBytes = toByteString + +instance (JSON a) => FromBytes (JSONValue a) where + fromBytes mem = + let a = fromBytes mem + in case a of + Left e -> Left e + Right x -> + case resultToEither $ decode x of + Left e -> Left e + Right y -> Right (JSONValue y) + +instance (JSON a) => ToBytes (JSONValue a) where + toBytes (JSONValue x) = toBytes (encode x) + +instance (Extism.PDK.MsgPack.MsgPack a) => FromBytes (MsgPackValue a) where + fromBytes mem = + let a = fromBytes mem + in case a of + Left e -> Left e + Right x -> + case Extism.PDK.MsgPack.decode x of + Left e -> Left e + Right y -> Right (MsgPackValue y) + +instance (Extism.PDK.MsgPack.MsgPack a) => ToBytes (MsgPackValue a) where + toBytes (MsgPackValue x) = toBytes $ Extism.PDK.MsgPack.encode x + +instance ToBytes Int32 where + toBytes i = toBytes $ B.toStrict (runPut (putInt32le i)) + +instance FromBytes Int32 where + fromBytes mem = + let bs = fromBytes mem + in case bs of + Left e -> Left e + Right x -> + case runGetOrFail getInt32le (B.fromStrict x) of + Left (_, _, e) -> Left e + Right (_, _, x) -> Right x + +instance ToBytes Int64 where + toBytes i = toBytes $ B.toStrict (runPut (putInt64le i)) + +instance FromBytes Int64 where + fromBytes mem = + let bs = fromBytes mem + in case bs of + Left e -> Left e + Right x -> + case runGetOrFail getInt64le (B.fromStrict x) of + Left (_, _, e) -> Left e + Right (_, _, x) -> Right x + +instance ToBytes Word32 where + toBytes i = toBytes $ B.toStrict (runPut (putWord32le i)) + +instance FromBytes Word32 where + fromBytes mem = + let bs = fromBytes mem + in case bs of + Left e -> Left e + Right x -> + case runGetOrFail getWord32le (B.fromStrict x) of + Left (_, _, e) -> Left e + Right (_, _, x) -> Right x + +instance ToBytes Word64 where + toBytes i = toBytes $ B.toStrict (runPut (putWord64le i)) + +instance FromBytes Word64 where + fromBytes mem = + let bs = fromBytes mem + in case bs of + Left e -> Left e + Right x -> + case runGetOrFail getWord64le (B.fromStrict x) of + Left (_, _, e) -> Left e + Right (_, _, x) -> Right x + +instance ToBytes Float where + toBytes i = toBytes $ B.toStrict (runPut (putFloatle i)) + +instance FromBytes Float where + fromBytes mem = + let bs = fromBytes mem + in case bs of + Left e -> Left e + Right x -> + case runGetOrFail getFloatle (B.fromStrict x) of + Left (_, _, e) -> Left e + Right (_, _, x) -> Right x + +instance ToBytes Double where + toBytes i = toBytes $ B.toStrict (runPut (putDoublele i)) + +instance FromBytes Double where + fromBytes mem = + let bs = fromBytes mem + in case bs of + Left e -> Left e + Right x -> + case runGetOrFail getDoublele (B.fromStrict x) of + Left (_, _, e) -> Left e + Right (_, _, x) -> Right x diff --git a/src/Extism/PDK/MsgPack.hs b/src/Extism/PDK/MsgPack.hs index 62176a3..5cc3376 100644 --- a/src/Extism/PDK/MsgPack.hs +++ b/src/Extism/PDK/MsgPack.hs @@ -1,93 +1,97 @@ -{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, TypeOperators #-} - -module Extism.PDK.MsgPack ( - module Extism.PDK.MsgPack, - module Data.MessagePack, - module Map, -) where - -import GHC.Generics -import Data.MessagePack -import Data.Int -import Data.Word -import qualified Data.Map.Strict as Map - +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE TypeOperators #-} + +-- | +-- Provides the ability to use MessagePack for plugin input/output +module Extism.PDK.MsgPack + ( module Extism.PDK.MsgPack, + module Data.MessagePack, + module Map, + ) +where + +import Data.Bifunctor (bimap) import qualified Data.ByteString as B import Data.ByteString.Internal (c2w, w2c) +import Data.Int +import qualified Data.Map.Strict as Map +import Data.MessagePack import qualified Data.Serialize as S +import Data.Word +import GHC.Generics class MsgPack a where toMsgPack :: a -> Object fromMsgPack :: Object -> Maybe a -class GMsgPack f where +class GMsgPack f where toGMsgPack :: f a -> Object fromGMsgPack :: Object -> Maybe (f a) fromGMsgPack _ = Nothing - + instance GMsgPack U1 where toGMsgPack U1 = ObjectNil fromGMsgPack ObjectNil = Just U1 - -instance (GMsgPack a, GMsgPack b) => GMsgPack ( a :*: b) where + +instance (GMsgPack a, GMsgPack b) => GMsgPack (a :*: b) where toGMsgPack (x :*: y) = array [toGMsgPack x, toGMsgPack y] - -- fromGMsgPack (ObjectArray [a, b]) = Just (a :*: b) - -instance (GMsgPack a, GMsgPack b) => GMsgPack ( a :+: b) where + +-- fromGMsgPack (ObjectArray [a, b]) = Just (a :*: b) + +instance (GMsgPack a, GMsgPack b) => GMsgPack (a :+: b) where toGMsgPack (L1 x) = toGMsgPack x toGMsgPack (R1 x) = toGMsgPack x - -instance GMsgPack a => GMsgPack (M1 i c a) where + +instance (GMsgPack a) => GMsgPack (M1 i c a) where toGMsgPack (M1 x) = toGMsgPack x - + instance (MsgPack a) => GMsgPack (K1 i a) where toGMsgPack (K1 x) = toMsgPack x toByteString x = B.pack (Prelude.map c2w x) + fromByteString bs = Prelude.map w2c $ B.unpack bs instance MsgPack Bool where - toMsgPack b = ObjectBool b + toMsgPack = ObjectBool fromMsgPack (ObjectBool b) = Just b fromMsgPack _ = Nothing - instance MsgPack String where toMsgPack s = ObjectString (toByteString s) fromMsgPack (ObjectString s) = Just (fromByteString s) fromMsgPack _ = Nothing - - + instance MsgPack B.ByteString where - toMsgPack s = ObjectBinary s + toMsgPack = ObjectBinary fromMsgPack (ObjectString s) = Just s fromMsgPack (ObjectBinary s) = Just s fromMsgPack _ = Nothing - + instance MsgPack Int where toMsgPack i = ObjectInt (fromIntegral i) fromMsgPack (ObjectInt i) = Just (fromIntegral i) fromMsgPack _ = Nothing - + instance MsgPack Int64 where - toMsgPack i = ObjectInt i + toMsgPack = ObjectInt fromMsgPack (ObjectInt i) = Just i fromMsgPack _ = Nothing - + instance MsgPack Word where toMsgPack w = ObjectUInt (fromIntegral w) fromMsgPack (ObjectUInt x) = Just (fromIntegral x) fromMsgPack _ = Nothing - + instance MsgPack Word64 where - toMsgPack w = ObjectUInt w + toMsgPack = ObjectUInt fromMsgPack (ObjectUInt x) = Just x fromMsgPack _ = Nothing - -instance MsgPack a => MsgPack (Maybe a) where + +instance (MsgPack a) => MsgPack (Maybe a) where toMsgPack Nothing = ObjectNil toMsgPack (Just a) = toMsgPack a - fromMsgPack bs = fromMsgPack bs + fromMsgPack = fromMsgPack instance MsgPack () where toMsgPack () = ObjectNil @@ -95,53 +99,49 @@ instance MsgPack () where fromMsgPack _ = Nothing instance MsgPack Float where - toMsgPack f = ObjectFloat f + toMsgPack = ObjectFloat fromMsgPack (ObjectFloat f) = Just f fromMsgPack _ = Nothing - + instance MsgPack Double where - toMsgPack d = ObjectDouble d + toMsgPack = ObjectDouble fromMsgPack (ObjectDouble d) = Just d fromMsgPack _ = Nothing - + instance MsgPack Object where toMsgPack x = x - fromMsgPack x = Just x - - -( .= ) :: MsgPack a => MsgPack b => a -> b -> (Object, Object) -( .= ) k v = (toMsgPack k, toMsgPack v) - -lookup :: MsgPack a => MsgPack b => a -> Object -> Maybe b -lookup k (ObjectMap map) = - let x = Map.lookup (toMsgPack k) map in - case x of - Nothing -> Nothing - Just x -> fromMsgPack x + fromMsgPack = Just + +(.=) :: (MsgPack a) => (MsgPack b) => a -> b -> (Object, Object) +(.=) k v = (toMsgPack k, toMsgPack v) + +lookup :: (MsgPack a) => (MsgPack b) => a -> Object -> Maybe b +lookup k (ObjectMap map) = + let x = Map.lookup (toMsgPack k) map + in fromMsgPack =<< x lookup _ _ = Nothing set k v (ObjectMap map) = ObjectMap $ Map.insert (toMsgPack k) (toMsgPack v) map -( .? ) :: MsgPack a => MsgPack b => Object -> a -> Maybe b -( .? ) a b = Extism.PDK.MsgPack.lookup b a +(.?) :: (MsgPack a) => (MsgPack b) => Object -> a -> Maybe b +(.?) a b = Extism.PDK.MsgPack.lookup b a -object :: MsgPack a => MsgPack b => [(a, b)] -> Object -object l = ObjectMap (Map.fromList $ map (\(k, v) -> (toMsgPack k, toMsgPack v)) l) +object :: (MsgPack a) => (MsgPack b) => [(a, b)] -> Object +object l = ObjectMap (Map.fromList $ map (bimap toMsgPack toMsgPack) l) -array :: MsgPack a => [a] -> Object +array :: (MsgPack a) => [a] -> Object array l = ObjectArray (map toMsgPack l) -encode :: MsgPack a => a -> B.ByteString +encode :: (MsgPack a) => a -> B.ByteString encode x = - let y = toMsgPack x in - S.encode y + let y = toMsgPack x + in S.encode y -decode :: MsgPack a => B.ByteString -> Either String a +decode :: (MsgPack a) => B.ByteString -> Either String a decode bs = case S.decode bs of Right a -> case fromMsgPack a of - Nothing -> Left "Invalid type conversion" - Just x -> Right x + Nothing -> Left "Invalid type conversion" + Just x -> Right x Left s -> Left s - diff --git a/src/Extism/PDK/Util.hs b/src/Extism/PDK/Util.hs new file mode 100644 index 0000000..3a016c9 --- /dev/null +++ b/src/Extism/PDK/Util.hs @@ -0,0 +1,12 @@ +module Extism.PDK.Util where + +import qualified Data.ByteString as B +import Data.ByteString.Internal (c2w, w2c) + +-- | Helper function to convert a string to a bytestring +toByteString :: String -> B.ByteString +toByteString x = B.pack (Prelude.map c2w x) + +-- | Helper function to convert a bytestring to a string +fromByteString :: B.ByteString -> String +fromByteString bs = Prelude.map w2c $ B.unpack bs