diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for extism
+
+## 0.2.0.0 -- 2023-01-16
+
+* First version. Released on an unsuspecting world.
diff --git a/Example.hs b/Example.hs
new file mode 100644
--- /dev/null
+++ b/Example.hs
@@ -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
diff --git a/extism.cabal b/extism.cabal
new file mode 100644
--- /dev/null
+++ b/extism.cabal
@@ -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
diff --git a/src/Extism.hs b/src/Extism.hs
new file mode 100644
--- /dev/null
+++ b/src/Extism.hs
@@ -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)
diff --git a/src/Extism/Bindings.hs b/src/Extism/Bindings.hs
new file mode 100644
--- /dev/null
+++ b/src/Extism/Bindings.hs
@@ -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
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -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
+    ])
+
