diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+
+# Changelog
+
+All notable changes to this project will be documented in this file.
+The CHANGELOG is available on [Github](https://github.com/luc-tielen/souffle-haskell.git/CHANGELOG.md).
+
+
+## [0.0.1] - 2019-10-23
+### Added
+
+- Initial version of the library
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2019 Luc Tielen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,182 @@
+
+# Souffle-haskell
+
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/luc-tielen/souffle-haskell/blob/master/LICENSE)
+
+This repo provides Haskell bindings for performing analyses with the
+[Souffle Datalog language](https://github.com/souffle-lang/souffle).
+It does this by binding directly to an "embedded" Souffle program
+(previously generated with `souffle -g`).
+
+Fun fact: this library combines both functional programming (Haskell),
+logic programming (Datalog / Souffle) and imperative / OO programming (C / C++).
+
+
+## Motivating example
+
+Let's first write a datalog program that can check if 1 point
+is reachable from another:
+
+```prolog
+// We define 2 data types:
+.decl edge(n: symbol, m: symbol)
+.decl reachable(n: symbol, m: symbol)
+
+// We indicate we are interested in "reachable" facts.
+// NOTE: If you forget to add outputs, the souffle compiler will
+//       try to be smart and remove most generated code!
+.output reachable
+
+// We write down some pre-defined facts on the datalog side.
+edge("a", "b").
+edge("b", "c").
+edge("c", "e").
+edge("e", "f").
+edge("c", "d").
+
+// And we tell datalog how to check if 1 point is reachable from another.
+reachable(x, y) :- edge(x, y).                  // base rule
+reachable(x, z) :- edge(x, y), reachable(y, z). // inductive rule
+```
+
+Now that we have the datalog code, we can generate a `path.cpp` from it
+using `souffle -g path.cpp path.dl`. `souffle-haskell` can bind to this program
+in the following way:
+
+```haskell
+-- Enable some necessary extensions:
+{-# LANGUAGE TemplateHaskell, ScopedTypeVariables, DataKinds, TypeFamilies, DeriveGeneric #-}
+
+module Main ( main ) where
+
+import Data.Foldable ( traverse_ )
+import Control.Monad.IO.Class
+import GHC.Generics
+import qualified Language.Souffle.TH as Souffle
+import qualified Language.Souffle as Souffle
+
+-- We only use template haskell for directly embedding the .cpp file into this file.
+-- If we do not do this, it will link incorrectly due to the way the
+-- C++ code is generated.
+Souffle.embedProgram "/path/to/path.cpp"
+
+
+-- We define a data type representing our datalog program.
+data Path = Path
+
+-- Facts are represent in Haskell as simple product types,
+-- Numbers map to Int32, Strings to symbols.
+
+data Edge = Edge String String
+  deriving (Eq, Show, Generic)
+
+data Reachable = Reachable String String
+  deriving (Eq, Show, Generic)
+
+-- By making Path an instance of Program, we provide Haskell with information
+-- about the datalog program. It uses this to perform compile-time checks to
+-- limit the amount of possible programmer errors to a minimum.
+instance Souffle.Program Path where
+  type ProgramFacts Path = [Edge, Reachable]
+  programName = const "path"
+
+-- By making a data type an instance of Edge, we give Haskell the
+-- necessary information to bind to the datalog fact.
+instance Souffle.Fact Edge where
+  factName = const "edge"
+
+instance Souffle.Fact Reachable where
+  factName = const "reachable"
+
+-- For simple product types, we can automatically generate the
+-- marshalling/unmarshalling code of data between Haskell and datalog.
+instance Souffle.Marshal Edge
+instance Souffle.Marshal Reachable
+
+
+main :: IO ()
+main = Souffle.runSouffle $ do
+  maybeProgram <- Souffle.init Path  -- Initializes the Souffle program.
+  case maybeProgram of
+    Nothing -> liftIO $ putStrLn "Failed to load program."
+    Just prog -> do
+      Souffle.addFact prog $ Edge "d" "i"   -- Adding a single fact from Haskell side
+      Souffle.addFacts prog [ Edge "e" "f"  -- Adding multiple facts
+                            , Edge "f" "g"
+                            , Edge "f" "g"
+                            , Edge "f" "h"
+                            , Edge "g" "i"
+                            ]
+
+      Souffle.run prog  -- Run the Souffle program
+
+      -- NOTE: You can change type param to fetch different relations
+      --       Here it requires an annotation since we directly print it
+      --       to stdout, but if passed to another function, it can infer
+      --       the correct type automatically.
+      results :: [Reachable] <- Souffle.getFacts prog
+      liftIO $ traverse_ print results
+
+      -- We can also look for a specific fact:
+      maybeFact <- Souffle.findFact prog $ Reachable "a" "c"
+      liftIO $ print $ maybeFact
+```
+
+For more examples of how to use the top level API, you can also take a look at
+the tests.
+
+
+## Getting started
+
+This library assumes that the Souffle include paths are properly set.
+This is needed in order for the C++ code to be compiled correctly.
+The easiest way to do this (that I know of) is via [Nix](https://nixos.org/nix/).
+Add `souffle` to the build inputs of your derivation and everything will
+be set correctly.
+Without Nix, you will have to follow the manual install instructions
+on the [Souffle website](https://souffle-lang.github.io/install).
+
+In your package.yaml / *.cabal file, make sure to add the following options
+(assuming package.yaml here):
+
+```yaml
+# ...
+cpp-options:
+  - -D__EMBEDDED_SOUFFLE__
+
+# ...
+```
+
+This will instruct the Souffle compiler to compile the C++ in such a way that
+it can be linked with other languages (including Haskell!).
+
+
+## Contributing
+
+TLDR: Nix-based project; the Makefile contains the most commonly used commands.
+
+
+Long version:
+
+The project makes use of [Nix](https://nixos.org/nix/download.html) to setup the development environment.
+Setup your environment by entering the following command:
+
+```bash
+$ nix-shell
+```
+
+After this command, you can build the project:
+
+```bash
+$ make configure  # configures the project
+$ make build      # builds the haskell code
+$ make lint       # runs the linter
+$ make hoogle     # starts a local hoogle webserver
+```
+
+
+## Issues
+
+Found an issue or missing a piece of functionality?
+Please open an issue with a description of the problem.
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,3 @@
+import Distribution.Simple
+main = defaultMain
+
diff --git a/cbits/souffle.cpp b/cbits/souffle.cpp
new file mode 100644
--- /dev/null
+++ b/cbits/souffle.cpp
@@ -0,0 +1,147 @@
+#include "souffle/SouffleInterface.h"
+#include "souffle.h"
+
+
+extern "C" {
+    souffle_t* souffle_init(const char* progName) {
+        auto prog = souffle::ProgramFactory::newInstance(progName);
+        return reinterpret_cast<souffle_t*>(prog);
+    }
+
+    void souffle_free(souffle_t* program) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        delete prog;
+    }
+
+    void souffle_set_num_threads(souffle_t* program, size_t num_cores) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        prog->setNumThreads(num_cores);
+    }
+
+    size_t souffle_get_num_threads(souffle_t* program) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        return prog->getNumThreads();
+    }
+
+    void souffle_run(souffle_t* program) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        prog->run();
+    }
+
+    void souffle_load_all(souffle_t* program, const char* directory) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        assert(directory);
+        prog->loadAll(directory);
+    }
+
+    void souffle_print_all(souffle_t* program) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        prog->printAll();
+    }
+
+    relation_t* souffle_relation(souffle_t* program, const char* relation_name) {
+        auto prog = reinterpret_cast<souffle::SouffleProgram*>(program);
+        assert(prog);
+        assert(relation_name);
+        auto relation = prog->getRelation(relation_name);
+        assert(relation);
+        return reinterpret_cast<relation_t*>(relation);
+    }
+
+    struct relation_iterator {
+        using iterator_t = souffle::Relation::iterator;
+        iterator_t iterator;
+        iterator_t end;
+
+        relation_iterator(const iterator_t& begin_, const iterator_t& end_)
+            : iterator(begin_)
+            , end(end_) {}
+    };
+
+    relation_iterator_t* souffle_relation_iterator(relation_t* relation) {
+        auto rel = reinterpret_cast<souffle::Relation*>(relation);
+        assert(rel);
+        relation_iterator_t* it = new relation_iterator_t(rel->begin(), rel->end());
+        return it;
+    }
+
+    void souffle_relation_iterator_free(relation_iterator_t* iterator) {
+        assert(iterator);
+        delete iterator;
+    }
+
+    bool souffle_relation_iterator_has_next(const relation_iterator_t* iterator) {
+        assert(iterator);
+        return iterator->iterator != iterator->end;
+    }
+
+    tuple_t* souffle_relation_iterator_next(relation_iterator_t* iterator) {
+        assert(iterator);
+        auto tuple = reinterpret_cast<tuple_t*>(&*iterator->iterator);
+        ++iterator->iterator;
+        return tuple;
+    }
+
+    bool souffle_contains_tuple(relation_t* relation, tuple_t* tuple) {
+        auto rel = reinterpret_cast<souffle::Relation*>(relation);
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(rel);
+        assert(t);
+        return rel->contains(*t);
+    }
+
+    tuple_t *souffle_tuple_alloc(relation_t* relation) {
+        auto rel = reinterpret_cast<souffle::Relation*>(relation);
+        assert(rel);
+        auto tuple = new souffle::tuple(rel);
+        return reinterpret_cast<tuple_t*>(tuple);
+    }
+
+    void souffle_tuple_free(tuple_t* tuple) {
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(t);
+        delete t;
+    }
+
+    void souffle_tuple_push_int(tuple_t* tuple, int32_t value) {
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(t);
+        *t << value;
+    }
+
+    void souffle_tuple_push_string(tuple_t* tuple, const char* value) {
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(t);
+        *t << value;
+    }
+
+    void souffle_tuple_add(relation_t* relation, tuple_t* tuple) {
+        auto rel = reinterpret_cast<souffle::Relation*>(relation);
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(rel);
+        assert(t);
+        rel->insert(*t);
+    }
+
+    void souffle_tuple_pop_int(tuple_t* tuple, int32_t* result) {
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(t);
+        assert(result);
+        *t >> *result;
+    }
+
+    void souffle_tuple_pop_string(tuple_t* tuple, char** result) {
+        auto t = reinterpret_cast<souffle::tuple*>(tuple);
+        assert(t);
+        assert(result);
+        std::string value;
+        *t >> value;
+        *result = strdup(value.c_str());
+    }
+}
diff --git a/cbits/souffle.h b/cbits/souffle.h
new file mode 100644
--- /dev/null
+++ b/cbits/souffle.h
@@ -0,0 +1,201 @@
+#ifndef SOUFFLE_H
+#define SOUFFLE_H
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+    // Opaque struct representing a Souffle program
+    typedef struct souffle_interface souffle_t;
+    // Opaque struct representing a Souffle relation
+    typedef struct relation relation_t;
+    // Opaque struct representing an iterator to a Souffle relation
+    typedef struct relation_iterator relation_iterator_t;
+    // Opaque struct representing a Souffle tuple (fact).
+    typedef struct tuple tuple_t;
+
+    /*
+     * Initializes a Souffle program. The name of the program should be the
+     * same as the filename (minus the .dl extension).
+     *
+     * The pointer that is returned can be NULL in case something went wrong.
+     * If a valid pointer is returned, it needs to be freed by "souffle_free"
+     * after it is no longer needed.
+     */
+    souffle_t* souffle_init(const char* progName);
+
+    /*
+     * Frees the memory in use by "program".
+     * You need to check if the pointer is non-NULL before passing it to this
+     * function. Not doing so results in undefined behavior.
+     */
+    void souffle_free(souffle_t* program);
+
+    /*
+     * Sets the number of cores this program should use.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     */
+    void souffle_set_num_threads(souffle_t* program, size_t num_cores);
+
+    /*
+     * Gets the number of cores this program should use.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     * Returns the number of cores the program will use.
+     */
+    size_t souffle_get_num_threads(souffle_t* program);
+
+    /*
+     * Runs the Souffle program.
+     * You need to check if the pointer is non-NULL before passing it to this
+     * function. Not doing so results in undefined behavior.
+     */
+    void souffle_run(souffle_t* program);
+
+    /*
+     * Load all facts from files in a certain directory.
+     * You need to check if both pointers are non-NULL before passing it to this
+     * function. Not doing so results in undefined behavior.
+     */
+    void souffle_load_all(souffle_t* program, const char* directory);
+
+    /*
+     * Write out all facts of the program to CSV files
+     * (as defined in the Souffle program).
+     *
+     * You need to check if the pointer is non-NULL before passing it to this
+     * function. Not doing so results in undefined behavior.
+     */
+    void souffle_print_all(souffle_t* program);
+
+    /*
+     * Lookup a relation in the Souffle program.
+     * You need to check if both passed pointers are non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * The returned pointer can be NULL if the relation is not found.
+     * The pointer does not need to be freed, it is managed by the Souffle program.
+     */
+    relation_t* souffle_relation(souffle_t* program, const char* relation_name);
+
+    /*
+     * Create an iterator for iterating over the facts of a relation.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * The returned pointer needs to be freed up with
+     * "souffle_relation_iterator_free" after it is no longer needed.
+     */
+    relation_iterator_t* souffle_relation_iterator(relation_t* relation);
+
+    /*
+     * Frees a relation_iterator pointer.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     */
+    void souffle_relation_iterator_free(relation_iterator_t* iterator);
+
+    /*
+     * Checks if the relation iterator contains more results.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Returns true if iterator contains more results; otherwise false.
+     */
+    bool souffle_relation_iterator_has_next(const relation_iterator_t* iterator);
+
+    /*
+     * Advances the relation iterator by 1 position.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     * Always check if there is a next record with "souffle_relation_iterator_has_next"
+     * before using this function to prevent crashes.
+     *
+     * Returns a pointer to the next record. This pointer is not allowed to be freed.
+     */
+    tuple_t* souffle_relation_iterator_next(relation_iterator_t* iterator);
+
+    /*
+     * Checks if a relation contains a certain tuple.
+     * You need to check if the passed pointers are non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Returns true if the tuple was found in the relation; otherwise false.
+     */
+    bool souffle_contains_tuple(relation_t* relation, tuple_t* tuple);
+
+    /*
+     * Allocates memory for a tuple to be added to a relation.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Returns a pointer to a new tuple. Use "souffle_tuple_free" when tuple
+     * is no longer required.
+     */
+    tuple_t* souffle_tuple_alloc(relation_t* relation);
+
+    /*
+     * Frees memory of a tuple that was previously allocated.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     */
+    void souffle_tuple_free(tuple_t* tuple);
+
+    /*
+     * Adds a tuple to a relation.
+     * You need to check if both passed pointers are non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     */
+    void souffle_tuple_add(relation_t* relation, tuple_t* tuple);
+
+    /*
+     * Pushes an integer value into a tuple.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Pushing an integer value onto a tuple that expects another type results
+     * in a crash.
+     */
+    void souffle_tuple_push_int(tuple_t* tuple, int32_t value);
+
+    /*
+     * Pushes a string value into a tuple.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Pushing a string value onto a tuple that expects another type results
+     * in a crash.
+     */
+    void souffle_tuple_push_string(tuple_t* tuple, const char* value);
+
+    /*
+     * Extracts an integer value from a tuple.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Extracting an integer value from a tuple that expects another type results
+     * in a crash.
+     * The popped integer will be stored in the result pointer.
+     */
+    void souffle_tuple_pop_int(tuple_t* tuple, int32_t* result);
+
+    /*
+     * Extracts a string value from a tuple.
+     * You need to check if the passed pointer is non-NULL before passing it
+     * to this function. Not doing so results in undefined behavior.
+     *
+     * Extracting a string value from a tuple that expects another type results
+     * in a crash.
+     * The popped string will be stored in the result pointer.
+     */
+    void souffle_tuple_pop_string(tuple_t* tuple, char** result);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/lib/Language/Souffle.hs b/lib/Language/Souffle.hs
new file mode 100644
--- /dev/null
+++ b/lib/Language/Souffle.hs
@@ -0,0 +1,340 @@
+
+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
+{-# LANGUAGE RankNTypes, FlexibleInstances, FlexibleContexts, DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables, TypeFamilies, TypeOperators #-}
+{-# LANGUAGE DerivingVia, InstanceSigs, UndecidableInstances #-}
+
+-- | This module provides the top level API of this library.
+--   It makes use of Haskell's powerful typesystem to make certain invalid states
+--   impossible to represent. It does this with a small type level DSL for
+--   describing properties of the Datalog program (see the 'Program' and 'Fact'
+--   typeclasses for more information).
+--   This module also provides a MTL-style interface to Souffle related operations
+--   so it can be integrated with existing monad transformer stacks.
+module Language.Souffle
+  ( Program(..)
+  , Fact(..)
+  , Marshal.Marshal(..)
+  , Handle
+  , MonadSouffle(..)
+  , SouffleM
+  , runSouffle
+  ) where
+
+import Prelude hiding ( init )
+import Data.Foldable ( traverse_ )
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Control.Monad.State
+import Control.Monad.RWS
+import Control.Monad.Except
+import Foreign.ForeignPtr
+import Foreign.Ptr
+import Type.Errors.Pretty
+import Data.Proxy
+import Data.Kind
+import Data.Word
+import qualified Language.Souffle.Internal as Internal
+import qualified Language.Souffle.Marshal as Marshal
+
+
+-- | A datatype representing a handle to a datalog program.
+--   The type parameter is used for keeping track of which program
+--   type the handle belongs to for additional type safety.
+newtype Handle prog = Handle (ForeignPtr Internal.Souffle)
+
+-- | A typeclass for describing a datalog program.
+--
+-- Example usage (assuming the program was generated from path.dl
+-- and contains 2 facts: Edge and Reachable):
+--
+-- @
+-- data Path = Path  -- Handle for the datalog program
+--
+-- instance Program Path where
+--   type ProgramFacts Path = '[Edge, Reachable]
+--   factName = const "path"
+-- @
+class Program a where
+  -- | A type level list of facts that belong to this program.
+  --   This list is used to check that only known facts are added to a program.
+  type ProgramFacts a :: [Type]
+
+  -- | Function for obtaining the name of a Datalog program.
+  --   This has to be the same as the name of the .dl file (minus the extension).
+  --
+  -- It uses a 'Proxy' to select the correct instance.
+  programName :: Proxy a -> String
+
+-- | A typeclass for data types representing a fact in datalog.
+class Marshal.Marshal a => Fact a where
+  -- | Function for obtaining the name of a fact
+  --   (has to be the same as described in the Datalog program).
+  --
+  -- It uses a 'Proxy' to select the correct instance.
+  --
+  -- Example usage:
+  --
+  -- @
+  -- instance Fact Edge where
+  --   factName = const "edge"
+  -- @
+  factName :: Proxy a -> String
+
+type family ContainsFact prog fact :: Constraint where
+  ContainsFact prog fact =
+    CheckContains prog (ProgramFacts prog) fact
+
+type family CheckContains prog facts fact :: Constraint where
+  CheckContains prog '[] fact =
+    TypeError ("You tried to perform an action with a fact of type '" <> fact
+    <> "' for program '" <> prog <> "'."
+    % "The program contains the following facts: " <> ProgramFacts prog <> "."
+    % "It does not contain fact: " <> fact <> "."
+    % "You can fix this error by adding the type '" <> fact
+    <> "' to the ProgramFacts type in the Program instance for " <> prog <> ".")
+  CheckContains _ (a ': _) a = ()
+  CheckContains prog (_ ': as) b = CheckContains prog as b
+
+
+-- | A monad for executing Souffle-related actions in.
+newtype SouffleM a
+  = SouffleM
+  { runSouffle :: IO a  -- ^ Returns the underlying IO action.
+  } deriving ( Functor, Applicative, Monad, MonadIO ) via IO
+
+-- | A mtl-style typeclass for Souffle-related actions.
+class Monad m => MonadSouffle m where
+  {- | Initializes a Souffle program.
+
+     The action will return 'Nothing' if it failed to load the Souffle program.
+     Otherwise it will return a 'Handle' that can be used in other functions
+     in this module.
+  -}
+  init :: Program prog => prog -> m (Maybe (Handle prog))
+
+  -- | Runs the Souffle program.
+  run :: Handle prog -> m ()
+
+  -- | Sets the number of CPU cores this Souffle program should use.
+  setNumThreads :: Handle prog -> Word64 -> m ()
+
+  -- | Gets the number of CPU cores this Souffle program should use.
+  getNumThreads :: Handle prog -> m Word64
+
+  -- | Load all facts from files in a certain directory.
+  loadFiles :: Handle prog -> FilePath -> m ()
+
+  -- | Write out all facts of the program to CSV files
+  --   (as defined in the Souffle program).
+  writeFiles :: Handle prog -> m ()
+
+  -- | Returns all facts of a program. This function makes use of type inference
+  --   to select the type of fact to return.
+  getFacts :: (Fact a, ContainsFact prog a)
+           => Handle prog -> m [a]
+
+  -- | Searches for a fact in a program.
+  --   Returns 'Nothing' if no matching fact was found;
+  --   otherwise 'Just' the fact.
+  findFact :: (Fact a, ContainsFact prog a)
+           => Handle prog -> a -> m (Maybe a)
+
+  -- | Adds a fact to the program.
+  addFact :: (Fact a, ContainsFact prog a)
+          => Handle prog -> a -> m ()
+
+  -- | Adds multiple facts to the program. This function could be implemented
+  --   in terms of 'addFact', but this is done as a minor optimization.
+  addFacts :: (Foldable t, Fact a, ContainsFact prog a)
+           => Handle prog -> t a -> m ()
+
+instance MonadSouffle SouffleM where
+  init :: forall prog. Program prog
+       => prog -> SouffleM (Maybe (Handle prog))
+  init _ =
+    let progName = programName (Proxy :: Proxy prog)
+    in SouffleM $ fmap Handle <$> Internal.init progName
+  {-# INLINABLE init #-}
+
+  run (Handle prog) = SouffleM $ Internal.run prog
+  {-# INLINABLE run #-}
+
+  setNumThreads (Handle prog) numCores =
+    SouffleM $ Internal.setNumThreads prog numCores
+  {-# INLINABLE setNumThreads #-}
+
+  getNumThreads (Handle prog) =
+    SouffleM $ Internal.getNumThreads prog
+  {-# INLINABLE getNumThreads #-}
+
+  loadFiles (Handle prog) = SouffleM . Internal.loadAll prog
+  {-# INLINABLE loadFiles #-}
+
+  writeFiles (Handle prog) = SouffleM $ Internal.printAll prog
+  {-# INLINABLE writeFiles #-}
+
+  addFact :: forall a prog. (Fact a, ContainsFact prog a)
+          => Handle prog -> a -> SouffleM ()
+  addFact (Handle prog) fact = liftIO $ do
+    let relationName = factName (Proxy :: Proxy a)
+    relation <- Internal.getRelation prog relationName
+    addFact' relation fact
+  {-# INLINABLE addFact #-}
+
+  addFacts :: forall t a prog. (Foldable t, Fact a, ContainsFact prog a)
+           => Handle prog -> t a -> SouffleM ()
+  addFacts (Handle prog) facts = liftIO $ do
+    let relationName = factName (Proxy :: Proxy a)
+    relation <- Internal.getRelation prog relationName
+    traverse_ (addFact' relation) facts
+  {-# INLINABLE addFacts #-}
+
+  getFacts :: forall a prog. (Fact a, ContainsFact prog a)
+           => Handle prog -> SouffleM [a]
+  getFacts (Handle prog) = SouffleM $ do
+    let relationName = factName (Proxy :: Proxy a)
+    relation <- Internal.getRelation prog relationName
+    Internal.getRelationIterator relation >>= go []
+    where
+      go acc it = do
+        hasNext <- Internal.relationIteratorHasNext it
+        if hasNext
+          then do
+            tuple <- Internal.relationIteratorNext it
+            result <- Marshal.runMarshalT Marshal.pop tuple
+            go (result : acc) it
+          else pure acc
+  {-# INLINABLE getFacts #-}
+
+  findFact :: forall a prog. (Fact a, ContainsFact prog a)
+             => Handle prog -> a -> SouffleM (Maybe a)
+  findFact (Handle prog) a = SouffleM $ do
+    let relationName = factName (Proxy :: Proxy a)
+    relation <- Internal.getRelation prog relationName
+    tuple <- Internal.allocTuple relation
+    withForeignPtr tuple $ Marshal.runMarshalT (Marshal.push a)
+    found <- Internal.containsTuple relation tuple
+    pure $ if found
+             then Just a
+             else Nothing
+  {-# INLINABLE findFact #-}
+
+addFact' :: Fact a => Ptr Internal.Relation -> a -> IO ()
+addFact' relation fact = do
+  tuple <- Internal.allocTuple relation
+  withForeignPtr tuple $ Marshal.runMarshalT (Marshal.push fact)
+  Internal.addTuple relation tuple
+{-# INLINABLE addFact' #-}
+
+
+instance MonadSouffle m => MonadSouffle (ReaderT r m) where
+  init = lift . init
+  {-# INLINABLE init #-}
+  run = lift . run
+  {-# INLINABLE run #-}
+  setNumThreads prog = lift . setNumThreads prog
+  {-# INLINABLE setNumThreads #-}
+  getNumThreads = lift . getNumThreads
+  {-# INLINABLE getNumThreads #-}
+  loadFiles prog = lift . loadFiles prog
+  {-# INLINABLE loadFiles #-}
+  writeFiles = lift . writeFiles
+  {-# INLINABLE writeFiles #-}
+  getFacts = lift . getFacts
+  {-# INLINABLE getFacts #-}
+  findFact prog = lift . findFact prog
+  {-# INLINABLE findFact #-}
+  addFact fact = lift . addFact fact
+  {-# INLINABLE addFact #-}
+  addFacts facts = lift . addFacts facts
+  {-# INLINABLE addFacts #-}
+
+instance (Monoid w, MonadSouffle m) => MonadSouffle (WriterT w m) where
+  init = lift . init
+  {-# INLINABLE init #-}
+  run = lift . run
+  {-# INLINABLE run #-}
+  setNumThreads prog = lift . setNumThreads prog
+  {-# INLINABLE setNumThreads #-}
+  getNumThreads = lift . getNumThreads
+  {-# INLINABLE getNumThreads #-}
+  loadFiles prog = lift . loadFiles prog
+  {-# INLINABLE loadFiles #-}
+  writeFiles = lift . writeFiles
+  {-# INLINABLE writeFiles #-}
+  getFacts = lift . getFacts
+  {-# INLINABLE getFacts #-}
+  findFact prog = lift . findFact prog
+  {-# INLINABLE findFact #-}
+  addFact fact = lift . addFact fact
+  {-# INLINABLE addFact #-}
+  addFacts facts = lift . addFacts facts
+  {-# INLINABLE addFacts #-}
+
+instance MonadSouffle m => MonadSouffle (StateT s m) where
+  init = lift . init
+  {-# INLINABLE init #-}
+  run = lift . run
+  {-# INLINABLE run #-}
+  setNumThreads prog = lift . setNumThreads prog
+  {-# INLINABLE setNumThreads #-}
+  getNumThreads = lift . getNumThreads
+  {-# INLINABLE getNumThreads #-}
+  loadFiles prog = lift . loadFiles prog
+  {-# INLINABLE loadFiles #-}
+  writeFiles = lift . writeFiles
+  {-# INLINABLE writeFiles #-}
+  getFacts = lift . getFacts
+  {-# INLINABLE getFacts #-}
+  findFact prog = lift . findFact prog
+  {-# INLINABLE findFact #-}
+  addFact fact = lift . addFact fact
+  {-# INLINABLE addFact #-}
+  addFacts facts = lift . addFacts facts
+  {-# INLINABLE addFacts #-}
+
+instance (MonadSouffle m, Monoid w) => MonadSouffle (RWST r w s m) where
+  init = lift . init
+  {-# INLINABLE init #-}
+  run = lift . run
+  {-# INLINABLE run #-}
+  setNumThreads prog = lift . setNumThreads prog
+  {-# INLINABLE setNumThreads #-}
+  getNumThreads = lift . getNumThreads
+  {-# INLINABLE getNumThreads #-}
+  loadFiles prog = lift . loadFiles prog
+  {-# INLINABLE loadFiles #-}
+  writeFiles = lift . writeFiles
+  {-# INLINABLE writeFiles #-}
+  getFacts = lift . getFacts
+  {-# INLINABLE getFacts #-}
+  findFact prog = lift . findFact prog
+  {-# INLINABLE findFact #-}
+  addFact fact = lift . addFact fact
+  {-# INLINABLE addFact #-}
+  addFacts facts = lift . addFacts facts
+  {-# INLINABLE addFacts #-}
+
+instance MonadSouffle m => MonadSouffle (ExceptT s m) where
+  init = lift . init
+  {-# INLINABLE init #-}
+  run = lift . run
+  {-# INLINABLE run #-}
+  setNumThreads prog = lift . setNumThreads prog
+  {-# INLINABLE setNumThreads #-}
+  getNumThreads = lift . getNumThreads
+  {-# INLINABLE getNumThreads #-}
+  loadFiles prog = lift . loadFiles prog
+  {-# INLINABLE loadFiles #-}
+  writeFiles = lift . writeFiles
+  {-# INLINABLE writeFiles #-}
+  getFacts = lift . getFacts
+  {-# INLINABLE getFacts #-}
+  findFact prog = lift . findFact prog
+  {-# INLINABLE findFact #-}
+  addFact fact = lift . addFact fact
+  {-# INLINABLE addFact #-}
+  addFacts facts = lift . addFacts facts
+  {-# INLINABLE addFacts #-}
+
diff --git a/lib/Language/Souffle/Internal.hs b/lib/Language/Souffle/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Language/Souffle/Internal.hs
@@ -0,0 +1,184 @@
+
+{-# Language LambdaCase #-}
+
+-- | An internal module, providing a slightly higher level interface than
+--   "Language.Souffle.Internal.Bindings".
+--   It uses more commonly found data types instead of the low level C types
+--   for easier integration with other parts of a Haskell application.
+--   Also it takes care of garbage collection so other modules do not have
+--   to take this into account anymore.
+--
+--   Used only internally, so prone to changes, use at your own risk.
+module Language.Souffle.Internal
+  ( Souffle
+  , Relation
+  , RelationIterator
+  , Tuple
+  , init
+  , setNumThreads
+  , getNumThreads
+  , run
+  , loadAll
+  , printAll
+  , getRelation
+  , getRelationIterator
+  , relationIteratorHasNext
+  , relationIteratorNext
+  , allocTuple
+  , addTuple
+  , containsTuple
+  , tuplePushInt
+  , tuplePushString
+  , tuplePopInt
+  , tuplePopString
+  ) where
+
+import Prelude hiding ( init )
+import Data.Functor ( (<&>) )
+import Data.Word
+import Data.Int
+import Foreign.Marshal.Alloc
+import Foreign.Storable
+import Foreign.C.String
+import Foreign.C.Types
+import Foreign.ForeignPtr
+import Foreign.Ptr
+import qualified Language.Souffle.Internal.Bindings as Bindings
+import Language.Souffle.Internal.Bindings
+  ( Souffle, Relation, RelationIterator, Tuple )
+
+
+{- | Initializes a Souffle program.
+
+     The string argument is the name of the program and should be the same
+     as the filename (minus the .dl extension).
+
+     The action will return 'Nothing' if it failed to load the Souffle program.
+     Otherwise it will return a pointer that can be used in other functions
+     in this module.
+-}
+init :: String -> IO (Maybe (ForeignPtr Souffle))
+init prog = do
+  ptr <- withCString prog Bindings.init
+  if ptr == nullPtr
+    then pure Nothing
+    else Just <$> newForeignPtr Bindings.free ptr
+{-# INLINABLE init #-}
+
+-- | Sets the number of CPU cores this Souffle program should use.
+setNumThreads :: ForeignPtr Souffle -> Word64 -> IO ()
+setNumThreads prog numThreads = withForeignPtr prog $ \ptr ->
+    Bindings.setNumThreads ptr $ CSize numThreads
+{-# INLINABLE setNumThreads #-}
+
+-- | Gets the number of CPU cores this Souffle program should use.
+getNumThreads :: ForeignPtr Souffle -> IO Word64
+getNumThreads prog = withForeignPtr prog $ \ptr -> do
+    (CSize numThreads) <- Bindings.getNumThreads ptr
+    pure numThreads
+{-# INLINABLE getNumThreads #-}
+
+-- | Runs the Souffle program.
+run :: ForeignPtr Souffle -> IO ()
+run prog = withForeignPtr prog Bindings.run
+{-# INLINABLE run #-}
+
+-- | Load all facts from files in a certain directory.
+loadAll :: ForeignPtr Souffle -> FilePath -> IO ()
+loadAll prog str = withForeignPtr prog $ withCString str . Bindings.loadAll
+{-# INLINABLE loadAll #-}
+
+-- | Write out all facts of the program to CSV files
+--   (as defined in the Souffle program).
+printAll :: ForeignPtr Souffle -> IO ()
+printAll prog = withForeignPtr prog Bindings.printAll
+{-# INLINABLE printAll #-}
+
+{-| Lookup a relation by name in the Souffle program.
+
+    Note that the returned pointer can be 'nullPtr' if it is not defined
+    in the Souffle program.
+-}
+getRelation :: ForeignPtr Souffle -> String -> IO (Ptr Relation)
+getRelation prog relation = withForeignPtr prog $ \ptr ->
+  withCString relation $ Bindings.getRelation ptr
+{-# INLINABLE getRelation #-}
+
+-- | Create an iterator for iterating over the facts of a relation.
+getRelationIterator :: Ptr Relation -> IO (ForeignPtr RelationIterator)
+getRelationIterator relation =
+  Bindings.getRelationIterator relation >>= newForeignPtr Bindings.freeRelationIterator
+{-# INLINABLE getRelationIterator #-}
+
+{-| Checks if the relation iterator contains more results.
+
+    Returns 'True' if there are more results; otherwise 'False'.
+-}
+relationIteratorHasNext :: ForeignPtr RelationIterator -> IO Bool
+relationIteratorHasNext iter = withForeignPtr iter $ \ptr ->
+  Bindings.relationIteratorHasNext ptr <&> \case
+    CBool 0 -> False
+    CBool _ -> True
+{-# INLINABLE relationIteratorHasNext #-}
+
+{-| Advances the relation iterator by 1 position.
+
+    Make sure to use 'relationIteratorHasNext' for checking if there are more
+    results before calling this function to avoid potential crashes.
+-}
+relationIteratorNext :: ForeignPtr RelationIterator -> IO (Ptr Tuple)
+relationIteratorNext iter = withForeignPtr iter Bindings.relationIteratorNext
+{-# INLINABLE relationIteratorNext #-}
+
+-- | Allocates memory for a tuple (fact) to be added to a relation.
+allocTuple :: Ptr Relation -> IO (ForeignPtr Tuple)
+allocTuple relation =
+  Bindings.allocTuple relation >>= newForeignPtr Bindings.freeTuple
+{-# INLINABLE allocTuple #-}
+
+-- | Adds a tuple (fact) to a relation.
+addTuple :: Ptr Relation -> ForeignPtr Tuple -> IO ()
+addTuple relation tuple =
+  withForeignPtr tuple $ Bindings.addTuple relation
+{-# INLINABLE addTuple #-}
+
+{- | Checks if a relation contains a certain tuple.
+
+     Returns True if the tuple was found in the relation; otherwise False.
+-}
+containsTuple :: Ptr Relation -> ForeignPtr Tuple -> IO Bool
+containsTuple relation tuple = withForeignPtr tuple $ \ptr ->
+  Bindings.containsTuple relation ptr <&> \case
+    CBool 0 -> False
+    CBool _ -> True
+{-# INLINABLE containsTuple #-}
+
+-- | Pushes an integer value into a tuple.
+tuplePushInt :: Ptr Tuple -> Int32 -> IO ()
+tuplePushInt tuple i = Bindings.tuplePushInt tuple (CInt i)
+{-# INLINABLE tuplePushInt #-}
+
+-- | Pushes a string value into a tuple.
+tuplePushString :: Ptr Tuple -> String -> IO ()
+tuplePushString tuple str =
+  withCString str $ Bindings.tuplePushString tuple
+{-# INLINABLE tuplePushString #-}
+
+-- | Extracts an integer value from a tuple.
+tuplePopInt :: Ptr Tuple -> IO Int32
+tuplePopInt tuple = alloca $ \ptr -> do
+  Bindings.tuplePopInt tuple ptr
+  (CInt res) <- peek ptr
+  pure res
+{-# INLINABLE tuplePopInt #-}
+
+-- | Extracts a string value from a tuple.
+tuplePopString :: Ptr Tuple -> IO String
+tuplePopString tuple = alloca $ \ptr -> do
+  Bindings.tuplePopString tuple ptr
+  cstr <- peek ptr
+  str <- peekCString cstr
+  free cstr
+  pure str
+{-# INLINABLE tuplePopString #-}
+
diff --git a/lib/Language/Souffle/Internal/Bindings.hs b/lib/Language/Souffle/Internal/Bindings.hs
new file mode 100644
--- /dev/null
+++ b/lib/Language/Souffle/Internal/Bindings.hs
@@ -0,0 +1,258 @@
+
+-- | This module provides C bindings exposed by the files in the cbits directory.
+--   This is an internal module, that is prone to have frequent changes,
+--   use at your own risk.
+module Language.Souffle.Internal.Bindings
+  ( Souffle
+  , Relation
+  , RelationIterator
+  , Tuple
+  , init
+  , free
+  , setNumThreads
+  , getNumThreads
+  , run
+  , loadAll
+  , printAll
+  , getRelation
+  , getRelationIterator
+  , freeRelationIterator
+  , relationIteratorHasNext
+  , relationIteratorNext
+  , allocTuple
+  , freeTuple
+  , addTuple
+  , containsTuple
+  , tuplePushInt
+  , tuplePushString
+  , tuplePopInt
+  , tuplePopString
+  ) where
+
+import Prelude hiding ( init )
+import Foreign.C.String
+import Foreign.C.Types
+import Foreign.Ptr
+
+
+-- | A void type, used for tagging a pointer that points to an embedded
+--   Souffle program.
+data Souffle
+
+-- | A void type, used for tagging a pointer that points to a relation.
+data Relation
+
+-- | A void type, used for tagging a pointer that points to an
+--   iterator used for iterating over a relation.
+data RelationIterator
+
+-- | A void type, used for tagging a pointer that points to a tuple
+--   (term used in the Souffle compiler for a fact).
+data Tuple
+
+
+{- | Initializes a Souffle program.
+
+     The string argument is the name of the program and should be the same
+     as the filename (minus the .dl extension).
+     The pointer that is returned can be 'nullPtr' in case something went wrong.
+     If a valid pointer is returned, it needs to be freed by 'free'
+     after it is no longer needed.
+-}
+foreign import ccall unsafe "souffle_init" init
+  :: CString -> IO (Ptr Souffle)
+
+{-| Frees the memory in use by the pointer, previously allocated by 'init'.
+
+    You need to check if the pointer is not equal to 'nullPtr'
+    before passing it to this function. Not doing so results in
+    undefined behavior (in C++).
+-}
+foreign import ccall unsafe "&souffle_free" free
+  :: FunPtr (Ptr Souffle -> IO ())
+
+{-| Sets the number of CPU cores this Souffle program should use.
+
+    You need to check if the pointer is not equal to 'nullPtr'
+    before passing it to this function. Not doing so results in
+    undefined behavior (in C++).
+-}
+foreign import ccall unsafe "souffle_set_num_threads" setNumThreads
+  :: Ptr Souffle -> CSize -> IO ()
+
+{-| Gets the number of CPU cores this Souffle program should use.
+
+    You need to check if the pointer is equal to 'nullPtr' before passing
+    it to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "souffle_get_num_threads" getNumThreads
+  :: Ptr Souffle -> IO CSize
+
+{-| Runs the Souffle program.
+
+    You need to check if the pointer is equal to 'nullPtr' before passing
+    it to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "souffle_run" run
+  :: Ptr Souffle -> IO ()
+
+{-| Load all facts from files in a certain directory.
+
+    You need to check if both pointers are not equal to 'nullPtr' before passing
+    it to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "souffle_load_all" loadAll
+  :: Ptr Souffle -> CString -> IO ()
+
+{-| Write out all facts of the program to CSV files
+    (as defined in the Souffle program).
+
+    You need to check if the pointer is not equal to 'nullPtr' before passing it
+    to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "souffle_print_all" printAll
+  :: Ptr Souffle -> IO ()
+
+{-| Lookup a relation in the Souffle program.
+
+    You need to check if both passed pointers are not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    The returned pointer can be 'nullPtr' if the relation is not found.
+    The pointer does not need to be freed, it is managed by the Souffle program.
+-}
+foreign import ccall unsafe "souffle_relation" getRelation
+  :: Ptr Souffle -> CString -> IO (Ptr Relation)
+
+{-| Create an iterator for iterating over the facts of a relation.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    The returned pointer needs to be freed with 'freeRelationIterator'
+    after it is no longer needed.
+-}
+foreign import ccall unsafe "souffle_relation_iterator" getRelationIterator
+  :: Ptr Relation -> IO (Ptr RelationIterator)
+
+{-| Frees a pointer previously allocated with 'getRelationIterator'.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "&souffle_relation_iterator_free" freeRelationIterator
+  :: FunPtr (Ptr RelationIterator -> IO ())
+
+{-| Checks if the relation iterator contains more results.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    Returns true if iterator contains more results; otherwise false.
+-}
+foreign import ccall unsafe "souffle_relation_iterator_has_next" relationIteratorHasNext
+  :: Ptr RelationIterator -> IO CBool
+
+{-| Advances the relation iterator by 1 position.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    Always check if there is a next record with 'relationIteratorHasNext'
+    before using this function to prevent crashes.
+
+    Returns a pointer to the next tuple. This pointer is not allowed to be freed
+    as it is managed by the Souffle program already.
+-}
+foreign import ccall unsafe "souffle_relation_iterator_next" relationIteratorNext
+  :: Ptr RelationIterator -> IO (Ptr Tuple)
+
+{-| Allocates memory for a tuple (fact) to be added to a relation.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    Returns a pointer to a new tuple. Use 'freeTuple' when the tuple
+    is no longer required.
+-}
+foreign import ccall unsafe "souffle_tuple_alloc" allocTuple
+  :: Ptr Relation -> IO (Ptr Tuple)
+
+{-| Frees memory of a tuple that was previously allocated (in Haskell).
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "&souffle_tuple_free" freeTuple
+  :: FunPtr (Ptr Tuple -> IO ())
+
+{-| Adds a tuple to a relation.
+
+    You need to check if both passed pointers are not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+-}
+foreign import ccall unsafe "souffle_tuple_add" addTuple
+  :: Ptr Relation -> Ptr Tuple -> IO ()
+
+{- | Checks if a relation contains a certain tuple.
+
+     You need to check if the passed pointers are non-NULL before passing it
+     to this function. Not doing so results in undefined behavior.
+
+     Returns True if the tuple was found in the relation; otherwise False.
+-}
+foreign import ccall unsafe "souffle_contains_tuple" containsTuple
+  :: Ptr Relation -> Ptr Tuple -> IO CBool
+
+{-| Pushes an integer value into a tuple.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    Pushing an integer value onto a tuple that expects another type results
+    in a crash. Pushing a value into a tuple when it already is "full"
+    also results in a crash.
+-}
+foreign import ccall unsafe "souffle_tuple_push_int" tuplePushInt
+  :: Ptr Tuple -> CInt -> IO ()
+
+{-| Pushes a string value into a tuple.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before
+    passing it to this function. Not doing so results in undefined behavior (in C++).
+
+    Pushing a string value onto a tuple that expects another type results
+    in a crash. Pushing a value into a tuple when it already is "full"
+    also results in a crash.
+-}
+foreign import ccall unsafe "souffle_tuple_push_string" tuplePushString
+  :: Ptr Tuple -> CString -> IO ()
+
+{-| Extracts an integer value from a tuple.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before passing it
+    to this function. Not doing so results in undefined behavior.
+
+    Extracting an integer value from a tuple that expects another type results
+    in a crash. Extracting a value from a tuple when it is already "empty"
+    also results in a crash.
+
+    The popped integer will be stored in the pointer that is passed in.
+-}
+foreign import ccall unsafe "souffle_tuple_pop_int" tuplePopInt
+  :: Ptr Tuple -> Ptr CInt -> IO ()
+
+{-| Extracts a string value from a tuple.
+
+    You need to check if the passed pointer is not equal to 'nullPtr' before passing it
+    to this function. Not doing so results in undefined behavior.
+
+    Extracting a string value from a tuple that expects another type results
+    in a crash. Extracting a value from a tuple when it is already "empty"
+    also results in a crash.
+
+    The popped string will be stored in the result pointer.
+-}
+foreign import ccall unsafe "souffle_tuple_pop_string" tuplePopString
+  :: Ptr Tuple -> Ptr CString -> IO ()
+
diff --git a/lib/Language/Souffle/Internal/Constraints.hs b/lib/Language/Souffle/Internal/Constraints.hs
new file mode 100644
--- /dev/null
+++ b/lib/Language/Souffle/Internal/Constraints.hs
@@ -0,0 +1,63 @@
+
+{-# LANGUAGE TypeFamilies, DataKinds, TypeOperators, UndecidableInstances #-}
+
+-- | A helper module for generating more user friendly type errors in the form
+--   of custom constraints.
+--   This is an internal module, not meant to be used directly.
+module Language.Souffle.Internal.Constraints
+  ( SimpleProduct
+  ) where
+
+import Type.Errors.Pretty
+import GHC.Generics
+import Data.Kind
+import Data.Int
+
+
+-- | A helper type family used for generating a more user-friendly type error
+--   for incompatible types when generically deriving marshalling code for
+--   the 'Language.Souffle.Marshal.Marshal' typeclass.
+--
+--   The __a__ type parameter is the original type, used when displaying the type error.
+--   The __f__ type parameter should be equal to 'Rep a', used for analyzing the
+--   structure of the data type.
+--
+--   A type error is returned if the passed in type is not a simple product type
+--   consisting of only simple types like Int32 and String.
+type family SimpleProduct (a :: Type) (f :: Type -> Type) :: Constraint where
+  SimpleProduct a f = (ProductLike a f, OnlySimpleFields a f)
+
+type family ProductLike (t :: Type) (f :: Type -> Type) :: Constraint where
+  ProductLike t (_ :*: b) = ProductLike t b
+  ProductLike t (M1 _ _ a) = ProductLike t a
+  ProductLike _ (K1 _ _) = ()
+  ProductLike t (_ :+: _) =
+    TypeError ( "Error while deriving marshalling code for type " <> t <> ":"
+              % "Cannot derive sum type, only product types are supported.")
+  ProductLike t U1 =
+    TypeError ( "Error while deriving marshalling code for type " <> t <> ":"
+              % "Cannot automatically derive code for 0 argument constructor.")
+  ProductLike t V1 =
+    TypeError ( "Error while deriving marshalling code for type " <> t <> ":"
+              % "Cannot derive void type.")
+
+type family OnlySimpleFields (t :: Type) (f :: Type -> Type) :: Constraint where
+  OnlySimpleFields t (a :*: b) = (OnlySimpleField t a, OnlySimpleFields t b)
+  OnlySimpleFields t (a :+: b) = (OnlySimpleFields t a, OnlySimpleFields t b)
+  OnlySimpleFields t (M1 _ _ a) = OnlySimpleFields t a
+  OnlySimpleFields _ U1 = ()
+  OnlySimpleFields _ V1 = ()
+  OnlySimpleFields t k = OnlySimpleField t k
+
+type family OnlySimpleField (a :: Type) (f :: Type -> Type) :: Constraint where
+  OnlySimpleField t (M1 _ _ a) = OnlySimpleField t a
+  OnlySimpleField t (K1 _ a) = DirectlyMarshallable t a
+
+type family DirectlyMarshallable (a :: Type) (b :: Type) :: Constraint where
+  DirectlyMarshallable _ Int32 = ()
+  DirectlyMarshallable _ String = ()
+  DirectlyMarshallable t a =
+    TypeError ( "Error while generating marshalling code for " <> t <> ":"
+              % "Can only marshal values of Int32 and String directly"
+             <> ", but found " <> a <> " type instead.")
+
diff --git a/lib/Language/Souffle/Marshal.hs b/lib/Language/Souffle/Marshal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Language/Souffle/Marshal.hs
@@ -0,0 +1,127 @@
+
+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
+{-# LANGUAGE DerivingVia, TypeFamilies #-}
+{-# LANGUAGE FlexibleInstances, FlexibleContexts, DataKinds #-}
+{-# LANGUAGE UndecidableInstances, DefaultSignatures #-}
+{-# LANGUAGE ScopedTypeVariables, TypeOperators #-}
+
+-- | This module exposes a uniform interface to marshal values
+--   to and from Souffle Datalog. This is done via the 'Marshal' typeclass
+--   and 'MarshalT' monad transformer.
+--   Also, a mechanism is exposed for generically deriving marshalling
+--   and unmarshalling code for simple product types.
+module Language.Souffle.Marshal
+  ( MarshalT
+  , runMarshalT
+  , Marshal(..)
+  ) where
+
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Control.Monad.State
+import Control.Monad.Except
+import Control.Monad.RWS
+import GHC.Generics
+import Foreign.Ptr
+import Data.Int
+import qualified Language.Souffle.Internal as Internal
+import qualified Language.Souffle.Internal.Constraints as C
+
+
+type Tuple = Ptr Internal.Tuple
+
+-- | A monad transformer, used solely for marshalling and unmarshalling
+--   between Haskell and Souffle Datalog.
+newtype MarshalT m a = MarshalT (ReaderT Tuple m a)
+  deriving ( Functor, Applicative, Monad
+           , MonadIO, MonadReader Tuple, MonadWriter w
+           , MonadState s, MonadRWS Tuple w s, MonadError e )
+  via ( ReaderT Tuple m )
+  deriving MonadTrans via (ReaderT Tuple)
+
+-- | Execute the monad transformer and return the result.
+--   The tuple that is passed in will be used to marshal the data back and forth.
+runMarshalT :: MarshalT m a -> Tuple -> m a
+runMarshalT (MarshalT m) = runReaderT m
+{-# INLINABLE runMarshalT #-}
+
+
+{- | A typeclass for providing a uniform API to marshal/unmarshal values
+     between Haskell and Souffle datalog.
+
+The marshalling is done via a stack-based approach, where elements are
+pushed/popped one by one. The programmer needs to make sure that the
+marshalling values happens in the correct order or unexpected things
+might happen (including crashes). Pushing and popping of fields should
+happen in the same order (from left to right, as defined in Datalog).
+
+Generic implementations for 'push' and 'pop' that perform the previously
+described behavior are available. This makes it possible to
+write very succinct code:
+
+@
+data Edge = Edge String String deriving Generic
+
+instance Marshal Edge
+@
+-}
+class Marshal a where
+  -- | Marshals a value to the datalog side.
+  push :: MonadIO m => a -> MarshalT m ()
+  -- | Unmarshals a value from the datalog side.
+  pop :: MonadIO m => MarshalT m a
+
+  default push :: (Generic a, C.SimpleProduct a (Rep a), GMarshal (Rep a), MonadIO m)
+               => a -> MarshalT m ()
+  default pop :: (Generic a, C.SimpleProduct a (Rep a), GMarshal (Rep a), MonadIO m)
+              => MarshalT m a
+  push a = gpush (from a)
+  {-# INLINABLE push #-}
+  pop = to <$> gpop
+  {-# INLINABLE pop #-}
+
+instance Marshal Int32 where
+  push int = do
+    tuple <- ask
+    liftIO $ Internal.tuplePushInt tuple int
+  {-# INLINABLE push #-}
+  pop = do
+    tuple <- ask
+    liftIO $ Internal.tuplePopInt tuple
+  {-# INLINABLE pop #-}
+
+instance Marshal String where
+  push str = do
+    tuple <- ask
+    liftIO $ Internal.tuplePushString tuple str
+  {-# INLINABLE push #-}
+  pop = do
+    tuple <- ask
+    liftIO $ Internal.tuplePopString tuple
+  {-# INLINABLE pop #-}
+
+
+class GMarshal f where
+  gpush :: MonadIO m => f a -> MarshalT m ()
+  gpop :: MonadIO m => MarshalT m (f a)
+
+instance Marshal a => GMarshal (K1 i a) where
+  gpush (K1 x) = push x
+  {-# INLINABLE gpush #-}
+  gpop = K1 <$> pop
+  {-# INLINABLE gpop #-}
+
+instance (GMarshal f, GMarshal g) => GMarshal (f :*: g) where
+  gpush (a :*: b) = do
+    gpush a
+    gpush b
+  {-# INLINABLE gpush #-}
+  gpop = (:*:) <$> gpop <*> gpop
+  {-# INLINABLE gpop #-}
+
+instance GMarshal a => GMarshal (M1 i c a) where
+  gpush (M1 x) = gpush x
+  {-# INLINABLE gpush #-}
+  gpop = M1 <$> gpop
+  {-# INLINABLE gpop #-}
+
diff --git a/lib/Language/Souffle/TH.hs b/lib/Language/Souffle/TH.hs
new file mode 100644
--- /dev/null
+++ b/lib/Language/Souffle/TH.hs
@@ -0,0 +1,25 @@
+
+-- | Helper module for easier integration of generated embedded souffle programs
+--   with Haskell code. Without these helper functions, it becomes much harder
+--   to link the C++ code directly to Haskell due to the way how Souffle
+--   generates the code.
+module Language.Souffle.TH ( embedProgram ) where
+
+import Language.Haskell.TH.Syntax
+
+
+-- | Helper function for embedding a Souffle program in Haskell.
+--   Requires the use of the TemplateHaskell language extension.
+--
+--   The passed in String should be a path relative from the root of the
+--   project where the .cpp file is located.
+--
+--   Example usage:
+--
+-- @
+-- module Main where
+-- import Language.Haskell.TH.Syntax as Souffle
+-- Souffle.embedProgram "path/to/file.cpp"  -- NOTE: call directly on top level!
+-- @
+embedProgram :: String -> Q [Dec]
+embedProgram path = [] <$ qAddForeignFilePath LangCxx path
diff --git a/souffle-haskell.cabal b/souffle-haskell.cabal
new file mode 100644
--- /dev/null
+++ b/souffle-haskell.cabal
@@ -0,0 +1,79 @@
+cabal-version: 2.2
+
+-- This file has been generated from package.yaml by hpack version 0.31.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: a74bc91fd3ca58647ff0b218a4aec49ebe837c8af230504def1bf57726751265
+
+name:           souffle-haskell
+version:        0.0.1
+synopsis:       Souffle Datalog bindings for Haskell
+description:    Souffle Datalog bindings for Haskell.
+category:       Logic Programming, Foreign Binding, Bindings
+homepage:       https://github.com/luc-tielen/souffle-haskell#README.md
+bug-reports:    https://github.com/luc-tielen/souffle-haskell/issues
+author:         Luc Tielen
+maintainer:     luc.tielen@gmail.com
+copyright:      2019 Luc Tielen
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+    LICENSE
+    cbits/souffle.h
+
+source-repository head
+  type: git
+  location: https://github.com/luc-tielen/souffle-haskell
+
+library
+  exposed-modules:
+      Language.Souffle
+      Language.Souffle.Internal
+      Language.Souffle.Internal.Bindings
+      Language.Souffle.Internal.Constraints
+      Language.Souffle.Marshal
+      Language.Souffle.TH
+  other-modules:
+      Paths_souffle_haskell
+  autogen-modules:
+      Paths_souffle_haskell
+  hs-source-dirs:
+      lib
+  default-extensions: OverloadedStrings
+  ghc-options: -Wall -Weverything -Wno-safe -Wno-unsafe -Wno-implicit-prelude -Wno-missed-specializations -Wno-all-missed-specializations -Wno-missing-import-lists -Wno-type-defaults -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-deriving-strategies -optP-Wno-nonportable-include-path -fhide-source-paths -fno-show-valid-hole-fits -fno-sort-valid-hole-fits
+  cpp-options: -std=c++17
+  cxx-options: -Wall
+  cxx-sources:
+      cbits/souffle.cpp
+  build-depends:
+      base >=4.12 && <5
+    , mtl >=2.0 && <3
+    , template-haskell >=2 && <3
+    , type-errors-pretty >=0.0.1.0 && <1
+  default-language: Haskell2010
+
+test-suite souffle-haskell-test
+  type: exitcode-stdio-1.0
+  main-is: test.hs
+  other-modules:
+      Test.TestSuiteSpec
+      Paths_souffle_haskell
+  hs-source-dirs:
+      tests
+  default-extensions: OverloadedStrings
+  ghc-options: -Wall -Weverything -Wno-safe -Wno-unsafe -Wno-implicit-prelude -Wno-missed-specializations -Wno-all-missed-specializations -Wno-missing-import-lists -Wno-type-defaults -Wno-missing-local-signatures -Wno-monomorphism-restriction -Wno-missing-deriving-strategies -optP-Wno-nonportable-include-path -fhide-source-paths -fno-show-valid-hole-fits -fno-sort-valid-hole-fits
+  cpp-options: -std=c++17 -D__EMBEDDED_SOUFFLE__
+  extra-libraries:
+      c++
+  build-depends:
+      base >=4.12 && <5
+    , hspec >=2.6.1 && <3.0.0
+    , mtl >=2.0 && <3
+    , souffle-haskell
+    , template-haskell >=2 && <3
+    , type-errors-pretty >=0.0.1.0 && <1
+  default-language: Haskell2010
diff --git a/tests/Test/TestSuiteSpec.hs b/tests/Test/TestSuiteSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test/TestSuiteSpec.hs
@@ -0,0 +1,184 @@
+
+{-# LANGUAGE TemplateHaskell, ScopedTypeVariables, DataKinds #-}
+{-# LANGUAGE TypeFamilies, DeriveGeneric #-}
+
+module Test.TestSuiteSpec ( module Test.TestSuiteSpec ) where
+
+import Test.Hspec
+import GHC.Generics
+import Data.Maybe
+import qualified Language.Souffle.TH as Souffle
+import qualified Language.Souffle as Souffle
+
+Souffle.embedProgram "tests/fixtures/path.cpp"
+
+data Path = Path
+
+data Edge = Edge String String
+  deriving (Eq, Show, Generic)
+
+data Reachable = Reachable String String
+  deriving (Eq, Show, Generic)
+
+instance Souffle.Program Path where
+  type ProgramFacts Path = [Edge, Reachable]
+  programName = const "path"
+
+instance Souffle.Fact Edge where
+  factName = const "edge"
+
+instance Souffle.Fact Reachable where
+  factName = const "reachable"
+
+instance Souffle.Marshal Edge
+instance Souffle.Marshal Reachable
+
+
+data BadPath = BadPath
+
+instance Souffle.Program BadPath where
+  type ProgramFacts BadPath = [Edge, Reachable]
+  programName = const "bad_path"
+
+{-
+-
+-import Data.Foldable ( traverse_ )
+-import Control.Monad.IO.Class
+-import GHC.Generics
+-
+-main :: IO ()
+-main = Souffle.runSouffle $ do
+-  maybeProgram <- Souffle.init Path
+-  case maybeProgram of
+-    Nothing -> liftIO $ putStrLn "Failed to load program."
+-    Just prog -> do
+-      Souffle.addFact prog $ Edge "d" "i"
+-      Souffle.addFacts prog [ Edge "e" "f"
+-                    , Edge "f" "g"
+-                    , Edge "f" "g"
+-                    , Edge "f" "h"
+-                    , Edge "g" "i"
+-                    ]
+-      Souffle.run prog
+  -}
+
+
+spec :: Spec
+spec = describe "Souffle API" $ parallel $ do
+  describe "init" $ parallel $ do
+    it "returns nothing if it cannot load a souffle program" $ do
+      prog <- Souffle.runSouffle (Souffle.init BadPath)
+      isJust prog `shouldBe` False
+
+    it "returns just the program if it can load a souffle program" $ do
+      prog <- Souffle.runSouffle (Souffle.init Path)
+      isJust prog `shouldBe` True
+
+  describe "getFacts" $ parallel $ do
+    it "can retrieve facts" $ do
+      (edges, reachables) <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        es <- Souffle.getFacts prog
+        rs <- Souffle.getFacts prog
+        pure (es , rs)
+      edges `shouldBe` [Edge "b" "c", Edge "a" "b"]
+      reachables `shouldBe` [Reachable "b" "c", Reachable "a" "c", Reachable "a" "b"]
+
+    it "returns empty list if program hasn't run yet" $ do
+      edges <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.getFacts prog
+      edges `shouldBe` ([] :: [Edge])
+
+  describe "addFact" $ parallel $ do
+    it "adds a fact" $ do
+      (edgesBefore, edgesAfter) <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        es1 <- Souffle.getFacts prog
+        Souffle.addFact prog $ Edge "e" "f"
+        Souffle.run prog
+        es2 <- Souffle.getFacts prog
+        pure (es1, es2)
+      edgesBefore `shouldBe` [Edge "b" "c", Edge "a" "b"]
+      edgesAfter `shouldBe` [Edge "e" "f", Edge "b" "c", Edge "a" "b"]
+
+    it "can add a fact even if it is marked as output" $ do
+      reachables <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.addFact prog $ Reachable "e" "f"
+        Souffle.run prog
+        Souffle.getFacts prog
+      reachables `shouldBe` [ Reachable "e" "f", Reachable "b" "c"
+                            , Reachable "a" "c", Reachable "a" "b" ]
+
+  describe "addFacts" $ parallel $
+    it "can add multiple facts at once" $ do
+      (edgesBefore, edgesAfter) <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        es1 <- Souffle.getFacts prog
+        Souffle.addFacts prog [Edge "e" "f", Edge "f" "g"]
+        Souffle.run prog
+        es2 <- Souffle.getFacts prog
+        pure (es1, es2)
+      edgesBefore `shouldBe` [Edge "b" "c", Edge "a" "b"]
+      edgesAfter `shouldBe` [Edge "f" "g", Edge "e" "f", Edge "b" "c", Edge "a" "b"]
+
+  describe "run" $ parallel $ do
+    it "is OK to run a program multiple times" $ do
+      edges <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        Souffle.run prog
+        Souffle.getFacts prog
+      edges `shouldBe` [Edge "b" "c", Edge "a" "b"]
+
+    it "discovers new facts after running with new facts" $ do
+      (reachablesBefore, reachablesAfter) <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        rs1 <- Souffle.getFacts prog
+        Souffle.addFacts prog [Edge "e" "f", Edge "f" "g"]
+        Souffle.run prog
+        rs2 <- Souffle.getFacts prog
+        pure (rs1, rs2)
+      reachablesBefore `shouldBe` [Reachable "b" "c", Reachable "a" "c", Reachable "a" "b"]
+      reachablesAfter `shouldBe` [ Reachable "f" "g", Reachable "e" "g", Reachable "e" "f"
+                                 , Reachable "b" "c",Reachable "a" "c", Reachable "a" "b" ]
+
+  describe "configuring number of cores" $ parallel $
+    it "is possible to configure number of cores" $ do
+      results <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        numCpus1 <- Souffle.getNumThreads prog
+        Souffle.setNumThreads prog 4
+        numCpus2 <- Souffle.getNumThreads prog
+        Souffle.setNumThreads prog 2
+        numCpus3 <- Souffle.getNumThreads prog
+        pure (numCpus1, numCpus2, numCpus3)
+      results `shouldBe` (1, 4, 2)
+
+  describe "findFact" $ parallel $ do
+    it "returns Nothing if no matching fact was found" $ do
+      (edge, reachable) <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        e <- Souffle.findFact prog $ Edge "c" "d"
+        r <- Souffle.findFact prog $ Reachable "d" "e"
+        pure (e, r)
+      edge `shouldBe` Nothing
+      reachable `shouldBe` Nothing
+
+    it "returns Just the fact if matching fact was found" $ do
+      (edge, reachable) <- Souffle.runSouffle $ do
+        prog <- fromJust <$> Souffle.init Path
+        Souffle.run prog
+        e <- Souffle.findFact prog $ Edge "a" "b"
+        r <- Souffle.findFact prog $ Reachable "a" "c"
+        pure (e, r)
+      edge `shouldBe` Just (Edge "a" "b")
+      reachable `shouldBe` Just (Reachable "a" "c")
+
+  -- TODO writeFiles / loadFiles
diff --git a/tests/test.hs b/tests/test.hs
new file mode 100644
--- /dev/null
+++ b/tests/test.hs
@@ -0,0 +1,2 @@
+{-# OPTIONS_GHC -Wno-missing-export-lists #-}
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
