Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: improve readme #11

Merged
merged 4 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Example.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ module Main where

import Extism
import Extism.HostFunction
import Extism.Manifest(manifest, wasmFile)
import Extism.Manifest (manifest, wasmFile)

hello currPlugin msg = do
putStrLn <$> unwrap <$> input currPlugin 0
putStrLn . unwrap <$> input currPlugin 0
putStrLn "Hello from Haskell!"
putStrLn msg
output currPlugin 0 "{\"count\": 999}"
Expand Down
130 changes: 105 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,125 @@
# Extism
# Extism Haskell Host SDK

Haskell Host SDK for Extism
This repo contains the Haskell package for integrating with the [Extism](https://extism.org/) runtime.

> **Note**: If you're unsure what Extism is or what an SDK is see our homepage: [https://extism.org](https://extism.org).

## Documentation

Documentation is available on [Hackage](https://hackage.haskell.org/package/extism)
Documentation is available at [https://hackage.haskell.org/package/extism](https://hackage.haskell.org/package/extism)

## Installation

### Install the Extism Runtime Dependency

For this library, you first need to install the Extism Runtime. You can [download the shared object directly from a release](https://github.com/extism/extism/releases) or use the [Extism CLI](https://github.com/extism/cli) to install it.

> **Note**: This library has breaking changes and targets 1.0 of the runtime. For the time being, install the runtime from our nightly development builds on git: `sudo PATH="$PATH" env extism lib install --version git`.

### Add the library to dune

Then add `extism` to your [cabal](https://www.haskell.org/cabal/) file:

```
library
build-depends: extism
```

## Getting Started

This guide should walk you through some of the concepts in Extism and the Haskell bindings.

### Creating A Plug-in

## Example
The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). You can think of a plug-in as a code module stored in a `.wasm` file.

Since you may not have an Extism plug-in on hand to test, let's load a demo plug-in from the web:

```haskell
module Main where

import Extism
import Extism.HostFunction
import Extism.Manifest(manifest, wasmFile)

-- Host function, prints a greeting then modifies the vowel count
hello currPlugin msg = do
putStrLn "Hello from Haskell!"
main = do
let wasm = wasmURL "GET" "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"
plugin <- unwrap <$> newPlugin (manifest [wasm]) [] True
res <- unwrap <$> call plugin "count_vowels" "Hello, world!"
putStrLn res
-- Prints: {"count":3,"total":3,"vowels":"aeiouAEIOU"}"
```

-- Print userdata
putStrLn msg
> **Note**: See [the Manifest docs](https://hackage.haskell.org/package/extism-manifest) as it has a rich schema and a lot of options.

-- Return a string
result currPlugin 0 "{\"count\": 999}"
This plug-in was written in Rust and it does one thing, it counts vowels in a string. As such, it exposes one "export" function: `count_vowels`. We can call exports using [Extism.call](https://hackage.haskell.org/package/extism/docs/Extism.html#v:call):

main = do
setLogFile "stdout" LogError
All exports have a simple interface of bytes-in and bytes-out. This plug-in happens to take a string and return a JSON encoded string with a report of results.

-- Create a manifest with the WebAssembly file
let m = manifest [wasmFile "wasm/code-functions.wasm"]
This library also allowes for conversion of input/outputs types using [FromBytes](https://hackage.haskell.org/package/extism/docs/Extism.html#t:FromBytes) and [ToBytes](https://hackage.haskell.org/package/extism/docs/Extism.html#t:ToBytes)

-- Create a host function named "hello_world"
f <- hostFunction "hello_world" [I64] [I64] hello "Hello, again"
### Plug-in State

-- Load the plugin
plugin <- unwrap <$> pluginFromManifest m [f] True
Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export:

-- Call the "count_vowels" function
res <- unwrap <$> call plugin "count_vowels" "this is a test"
```haskell
ghci> unwrap <$> call plugin "count_vowels" "Hello, world!"
{"count":3,"total":9,"vowels":"aeiouAEIOU"}
ghci> unwrap <$> call plugin "count_vowels" "Hello, world!"
{"count":3,"total":12,"vowels":"aeiouAEIOU"}
```

-- Print the results
These variables will persist until this plug-in is freed or you initialize a new one.

### Configuration

Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example:

```haskell
ghci> let manifest = manifest [wasm]
ghci> plugin <- unwrap <$> newPlugin manifest [] True
ghci> res <- (unwrap <$> call plugin "count_vowels" "Yellow, world!" :: String)
ghci> res
{"count":3,"total":3,"vowels":"aeiouAEIOU"}

ghci> plugin <- withConfig (manifest [wasm]) [("vowels","aeiouyAEIOUY")] ;;
ghci> res <- (unwrap <$> call plugin "count_vowels" "Yellow, world!" :: String)
ghci> res
{"count":4,"total":4,"vowels":"aeiouAEIOUY"}
```

### Host Functions

Let's extend our count-vowels example a little bit: we can intercept the results and adjust them before returning from the plugin using a `hello_world` [host function](https://extism.org/docs/concepts/host-functions)
with `wasm/code-functions.wasm`

[Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. They are simply some OCaml functions you write which can be passed down and invoked from any language inside the plug-in.

Using [Extism.HostFunction.hostFunction](https://hackage.haskell.org/package/extism/docs/Extism-HostFunction.html#v:hostFunction) we can define a host function that can be called from the guest plug-in.

In this example, we want to expose a single function to our plugin (in Haskell types): `hello_world :: String -> String` which will intercept the original result and replace it with a new one.

Let's load the manifest like usual but load up `wasm/code-functions.wasm` plug-in:

```haskell
module Main where

import Extism
import Extism.HostFunction
import Extism.Manifest (manifest, wasmFile)

hello currPlugin () = do
putStrLn . unwrap <$> input currPlugin 0
putStrLn "Hello from Haskell!"
output currPlugin 0 "{\"count\": 999}"

main = do
setLogFile "stdout" LogError -- Enable logging to stdout
let m = manifest [wasmFile "wasm/code-functions.wasm"]
f <- hostFunction "hello_world" [I64] [I64] hello () -- Create host function, the final argument
-- is a userData argument that can store any
-- Haskell value
plugin <- unwrap <$> newPlugin m [f] True
res <- unwrap <$> call plugin "count_vowels" "this is a test"
putStrLn res
-- Prints: {"count": 999}
```

> *Note*: In order to write host functions you should get familiar with the methods on the [Extism.HostFunction](https://hackage.haskell.org/package/extism/docs/Extism-HostFunction.html) module.
42 changes: 27 additions & 15 deletions manifest/Extism/JSON.hs
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
module Extism.JSON (
module Extism.JSON,
module Text.JSON
) where
module Extism.JSON
( module Extism.JSON,
module Text.JSON,
)
where

import Text.JSON
import qualified Data.ByteString as B
import Data.ByteString.Internal (c2w, w2c)
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS (unpack)
import Data.ByteString.Internal (c2w, w2c)
import Text.JSON

data Nullable a = Null | NotNull a deriving Eq
data Nullable a = Null | NotNull a deriving (Eq, Show)

makeArray x = JSArray [showJSON a | a <- x]

isNull JSNull = True
isNull _ = False

filterNulls obj = [(a, b) | (a, b) <- obj, not (isNull b)]

object x = makeObj $ filterNulls x

objectWithNulls = makeObj

nonNull = NotNull

null' = Null

(.=) a b = (a, showJSON b)

toNullable (Just x) = NotNull x
toNullable Nothing = Null

fromNullable (NotNull x) = Just x
fromNullable Null = Nothing

fromNotNull (NotNull x) = x
fromNotNull Null = error "Value is Null"

mapNullable f Null = Null
mapNullable f (NotNull x) = NotNull (f x)

Expand All @@ -34,27 +46,27 @@ mapNullable f (NotNull x) = NotNull (f x)
Ok x -> NotNull x
Error _ -> Null
(.?) _ _ = Null

(.??) a k = toNullable $ lookup k a

find :: JSON a => String -> JSValue -> Nullable a
find :: (JSON a) => String -> JSValue -> Nullable a
find k obj = obj .? k

update :: JSON a => String -> a -> JSValue -> JSValue
update :: (JSON a) => String -> a -> JSValue -> JSValue
update k v (JSObject obj) = object $ fromJSObject obj ++ [k .= v]

instance JSON a => JSON (Nullable a) where
instance (JSON a) => JSON (Nullable a) where
showJSON (NotNull x) = showJSON x
showJSON Null = JSNull
readJSON JSNull = Ok Null
readJSON x = readJSON x


newtype Base64 = Base64 B.ByteString deriving (Eq, Show)

instance JSON Base64 where
showJSON (Base64 bs) = showJSON (BS.unpack $ B64.encode bs)
readJSON (JSString s) =
let toByteString x = B.pack (Prelude.map c2w x) in
case B64.decode (toByteString (fromJSString s)) of
Left msg -> Error msg
Right d -> Ok (Base64 d)
let toByteString x = B.pack (Prelude.map c2w x)
in case B64.decode (toByteString (fromJSString s)) of
Left msg -> Error msg
Right d -> Ok (Base64 d)
Loading
Loading