Basic Functionality
diff --git a/src/Makefile.am b/src/Makefile.am
index a94061d..d7872a6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -4,3 +4,19 @@
 libgpioplus_la_SOURCES =
 libgpioplus_la_LIBADD = $(COMMON_LIBS)
 
+nobase_include_HEADERS += gpioplus/chip.hpp
+libgpioplus_la_SOURCES += gpioplus/chip.cpp
+
+nobase_include_HEADERS += gpioplus/event.hpp
+libgpioplus_la_SOURCES += gpioplus/event.cpp
+
+nobase_include_HEADERS += gpioplus/handle.hpp
+libgpioplus_la_SOURCES += gpioplus/handle.cpp
+
+nobase_include_HEADERS += gpioplus/internal/fd.hpp
+libgpioplus_la_SOURCES += gpioplus/internal/fd.cpp
+
+nobase_include_HEADERS += gpioplus/internal/sys.hpp
+libgpioplus_la_SOURCES += gpioplus/internal/sys.cpp
+
+nobase_include_HEADERS += gpioplus/test/sys.hpp
diff --git a/src/gpioplus/chip.cpp b/src/gpioplus/chip.cpp
new file mode 100644
index 0000000..051ef57
--- /dev/null
+++ b/src/gpioplus/chip.cpp
@@ -0,0 +1,61 @@
+#include <cstring>
+#include <fcntl.h>
+#include <gpioplus/chip.hpp>
+#include <linux/gpio.h>
+#include <string>
+#include <system_error>
+
+namespace gpioplus
+{
+
+LineFlags::LineFlags(uint32_t flags) :
+    kernel(flags & GPIOLINE_FLAG_KERNEL), output(flags & GPIOLINE_FLAG_IS_OUT),
+    active_low(flags & GPIOLINE_FLAG_ACTIVE_LOW),
+    open_drain(flags & GPIOLINE_FLAG_OPEN_DRAIN),
+    open_source(flags & GPIOLINE_FLAG_OPEN_SOURCE)
+{
+}
+
+Chip::Chip(unsigned id, const internal::Sys* sys) :
+    fd(std::string{"/dev/gpiochip"}.append(std::to_string(id)).c_str(),
+       O_RDONLY | O_CLOEXEC, sys)
+{
+}
+
+ChipInfo Chip::getChipInfo() const
+{
+    struct gpiochip_info info;
+    memset(&info, 0, sizeof(info));
+
+    int r = fd.getSys()->gpio_get_chipinfo(*fd, &info);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpio_get_chipinfo");
+    }
+
+    return ChipInfo{info.name, info.label, info.lines};
+}
+
+LineInfo Chip::getLineInfo(uint32_t offset) const
+{
+    struct gpioline_info info;
+    memset(&info, 0, sizeof(info));
+    info.line_offset = offset;
+
+    int r = fd.getSys()->gpio_get_lineinfo(*fd, &info);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpio_get_lineinfo");
+    }
+
+    return LineInfo{info.flags, info.name, info.consumer};
+}
+
+const internal::Fd& Chip::getFd() const
+{
+    return fd;
+}
+
+} // namespace gpioplus
diff --git a/src/gpioplus/chip.hpp b/src/gpioplus/chip.hpp
new file mode 100644
index 0000000..7c913a0
--- /dev/null
+++ b/src/gpioplus/chip.hpp
@@ -0,0 +1,49 @@
+#pragma once
+#include <cstdint>
+#include <gpioplus/internal/fd.hpp>
+#include <gpioplus/internal/sys.hpp>
+#include <string>
+
+namespace gpioplus
+{
+
+struct ChipInfo
+{
+    std::string name;
+    std::string label;
+    uint32_t lines;
+};
+
+struct LineFlags
+{
+    bool kernel;
+    bool output;
+    bool active_low;
+    bool open_drain;
+    bool open_source;
+
+    LineFlags(uint32_t flags);
+};
+
+struct LineInfo
+{
+    LineFlags flags;
+    std::string name;
+    std::string consumer;
+};
+
+class Chip
+{
+  public:
+    Chip(unsigned id, const internal::Sys* sys = &internal::sys_impl);
+
+    ChipInfo getChipInfo() const;
+    LineInfo getLineInfo(uint32_t offset) const;
+
+    const internal::Fd& getFd() const;
+
+  private:
+    internal::Fd fd;
+};
+
+} // namespace gpioplus
diff --git a/src/gpioplus/event.cpp b/src/gpioplus/event.cpp
new file mode 100644
index 0000000..3fc4062
--- /dev/null
+++ b/src/gpioplus/event.cpp
@@ -0,0 +1,93 @@
+#include <cstring>
+#include <gpioplus/event.hpp>
+#include <linux/gpio.h>
+#include <optional>
+#include <stdexcept>
+#include <system_error>
+
+namespace gpioplus
+{
+
+uint32_t EventFlags::toInt() const
+{
+    uint32_t ret = 0;
+    if (rising_edge)
+    {
+        ret |= GPIOEVENT_REQUEST_RISING_EDGE;
+    }
+    if (falling_edge)
+    {
+        ret |= GPIOEVENT_REQUEST_FALLING_EDGE;
+    }
+    return ret;
+}
+
+static int build(const Chip& chip, uint32_t line_offset,
+                 HandleFlags handle_flags, EventFlags event_flags,
+                 const char* consumer_label)
+{
+    struct gpioevent_request req;
+    memset(&req, 0, sizeof(req));
+    req.lineoffset = line_offset;
+    req.handleflags = handle_flags.toInt();
+    req.eventflags = event_flags.toInt();
+    strncpy(req.consumer_label, consumer_label, sizeof(req.consumer_label) - 1);
+
+    int r = chip.getFd().getSys()->gpio_get_lineevent(*chip.getFd(), &req);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpio_get_lineevent");
+    }
+
+    return req.fd;
+}
+
+Event::Event(const Chip& chip, uint32_t line_offset, HandleFlags handle_flags,
+             EventFlags event_flags, const char* consumer_label) :
+    fd(build(chip, line_offset, handle_flags, event_flags, consumer_label),
+       chip.getFd().getSys())
+{
+}
+
+const internal::Fd& Event::getFd() const
+{
+    return fd;
+}
+
+std::optional<Event::Data> Event::read() const
+{
+    struct gpioevent_data data;
+    ssize_t read = fd.getSys()->read(*fd, &data, sizeof(data));
+    if (read == -1)
+    {
+        if (errno == EAGAIN)
+        {
+            return std::nullopt;
+        }
+        throw std::system_error(errno, std::generic_category(),
+                                "gpioevent read");
+    }
+    if (read != sizeof(data))
+    {
+        throw std::runtime_error("Event read didn't get enough data");
+    }
+
+    return Data{data.timestamp, data.id};
+}
+
+uint8_t Event::getValue() const
+{
+    struct gpiohandle_data data;
+    memset(&data, 0, sizeof(data));
+    int r = fd.getSys()->gpiohandle_get_line_values(*fd, &data);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpiohandle_get_line_values");
+    }
+
+    return data.values[0];
+}
+
+} // namespace gpioplus
diff --git a/src/gpioplus/event.hpp b/src/gpioplus/event.hpp
new file mode 100644
index 0000000..06d0a3a
--- /dev/null
+++ b/src/gpioplus/event.hpp
@@ -0,0 +1,40 @@
+#pragma once
+#include <cstdint>
+#include <gpioplus/chip.hpp>
+#include <gpioplus/handle.hpp>
+#include <gpioplus/internal/fd.hpp>
+#include <optional>
+
+namespace gpioplus
+{
+
+struct EventFlags
+{
+    bool rising_edge;
+    bool falling_edge;
+
+    uint32_t toInt() const;
+};
+
+class Event
+{
+  public:
+    Event(const Chip& chip, uint32_t line_offset, HandleFlags handle_flags,
+          EventFlags event_flags, const char* consumer_label);
+
+    const internal::Fd& getFd() const;
+
+    struct Data
+    {
+        uint64_t timestamp;
+        uint32_t id;
+    };
+    std::optional<Data> read() const;
+
+    uint8_t getValue() const;
+
+  private:
+    internal::Fd fd;
+};
+
+} // namespace gpioplus
diff --git a/src/gpioplus/handle.cpp b/src/gpioplus/handle.cpp
new file mode 100644
index 0000000..30dc12f
--- /dev/null
+++ b/src/gpioplus/handle.cpp
@@ -0,0 +1,133 @@
+#include <cstring>
+#include <gpioplus/handle.hpp>
+#include <linux/gpio.h>
+#include <stdexcept>
+#include <system_error>
+
+namespace gpioplus
+{
+
+HandleFlags::HandleFlags()
+{
+}
+
+HandleFlags::HandleFlags(LineFlags line_flags) :
+    output(line_flags.output), active_low(line_flags.active_low),
+    open_drain(line_flags.open_drain), open_source(line_flags.open_source)
+{
+}
+
+uint32_t HandleFlags::toInt() const
+{
+    uint32_t ret = 0;
+    if (output)
+    {
+        ret |= GPIOHANDLE_REQUEST_OUTPUT;
+    }
+    else
+    {
+        ret |= GPIOHANDLE_REQUEST_INPUT;
+    }
+    if (active_low)
+    {
+        ret |= GPIOHANDLE_REQUEST_ACTIVE_LOW;
+    }
+    if (open_drain)
+    {
+        ret |= GPIOHANDLE_REQUEST_OPEN_DRAIN;
+    }
+    if (open_source)
+    {
+        ret |= GPIOHANDLE_REQUEST_OPEN_SOURCE;
+    }
+    return ret;
+}
+
+static int build(const Chip& chip, const std::vector<Handle::Line>& lines,
+                 HandleFlags flags, const char* consumer_label)
+{
+    if (lines.size() > GPIOHANDLES_MAX)
+    {
+        throw std::runtime_error("Too many requested gpio handles");
+    }
+
+    struct gpiohandle_request req;
+    memset(&req, 0, sizeof(req));
+    for (size_t i = 0; i < lines.size(); ++i)
+    {
+        req.lineoffsets[i] = lines[i].offset;
+        req.default_values[i] = lines[i].default_value;
+    }
+    req.flags = flags.toInt();
+    strncpy(req.consumer_label, consumer_label, sizeof(req.consumer_label) - 1);
+    req.lines = lines.size();
+
+    int r = chip.getFd().getSys()->gpio_get_linehandle(*chip.getFd(), &req);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpio_get_linehandle");
+    }
+
+    return req.fd;
+}
+
+Handle::Handle(const Chip& chip, const std::vector<Line>& lines,
+               HandleFlags flags, const char* consumer_label) :
+    fd(build(chip, lines, flags, consumer_label), chip.getFd().getSys()),
+    nlines(lines.size())
+{
+}
+
+const internal::Fd& Handle::getFd() const
+{
+    return fd;
+}
+
+std::vector<uint8_t> Handle::getValues() const
+{
+    std::vector<uint8_t> values(nlines);
+    getValues(values);
+    return values;
+}
+
+void Handle::getValues(std::vector<uint8_t>& values) const
+{
+    struct gpiohandle_data data;
+    memset(&data, 0, sizeof(data));
+    int r = fd.getSys()->gpiohandle_get_line_values(*fd, &data);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpiohandle_get_line_values");
+    }
+
+    values.resize(nlines);
+    for (size_t i = 0; i < nlines; ++i)
+    {
+        values[i] = data.values[i];
+    }
+}
+
+void Handle::setValues(const std::vector<uint8_t>& values) const
+{
+    if (values.size() != nlines)
+    {
+        throw std::runtime_error("Handle.setValues: Invalid input size");
+    }
+
+    struct gpiohandle_data data;
+    memset(&data, 0, sizeof(data));
+    for (size_t i = 0; i < nlines; ++i)
+    {
+        data.values[i] = values[i];
+    }
+    int r = fd.getSys()->gpiohandle_set_line_values(*fd, &data);
+    if (r < 0)
+    {
+        throw std::system_error(-r, std::generic_category(),
+                                "gpiohandle_get_line_values");
+    }
+}
+
+} // namespace gpioplus
diff --git a/src/gpioplus/handle.hpp b/src/gpioplus/handle.hpp
new file mode 100644
index 0000000..7f22a64
--- /dev/null
+++ b/src/gpioplus/handle.hpp
@@ -0,0 +1,44 @@
+#pragma once
+#include <cstdint>
+#include <gpioplus/chip.hpp>
+#include <gpioplus/internal/fd.hpp>
+#include <vector>
+
+namespace gpioplus
+{
+
+struct HandleFlags
+{
+    bool output;
+    bool active_low;
+    bool open_drain;
+    bool open_source;
+
+    HandleFlags();
+    explicit HandleFlags(LineFlags line_flags);
+    uint32_t toInt() const;
+};
+
+class Handle
+{
+  public:
+    struct Line
+    {
+        uint32_t offset;
+        uint8_t default_value;
+    };
+    Handle(const Chip& chip, const std::vector<Line>& lines, HandleFlags flags,
+           const char* consumer_label);
+
+    const internal::Fd& getFd() const;
+
+    std::vector<uint8_t> getValues() const;
+    void getValues(std::vector<uint8_t>& values) const;
+    void setValues(const std::vector<uint8_t>& values) const;
+
+  private:
+    internal::Fd fd;
+    uint32_t nlines;
+};
+
+} // namespace gpioplus
diff --git a/src/gpioplus/internal/fd.cpp b/src/gpioplus/internal/fd.cpp
new file mode 100644
index 0000000..5646bd9
--- /dev/null
+++ b/src/gpioplus/internal/fd.cpp
@@ -0,0 +1,129 @@
+#include <cerrno>
+#include <fcntl.h>
+#include <gpioplus/internal/fd.hpp>
+#include <system_error>
+#include <utility>
+
+namespace gpioplus
+{
+namespace internal
+{
+
+Fd::Fd(const char* pathname, int flags, const Sys* sys) :
+    sys(sys), fd(sys->open(pathname, flags))
+{
+    if (fd < 0)
+    {
+        throw std::system_error(errno, std::generic_category(), "Opening FD");
+    }
+}
+
+Fd::Fd(int fd, const Sys* sys) : sys(sys), fd(fd)
+{
+}
+
+Fd::~Fd()
+{
+    reset();
+}
+
+static int dup(int oldfd, const Sys* sys)
+{
+    int fd = sys->dup(oldfd);
+    if (fd < 0)
+    {
+        throw std::system_error(errno, std::generic_category(), "Duping FD");
+    }
+    return fd;
+}
+
+Fd::Fd(const Fd& other) : sys(other.sys), fd(dup(other.fd, sys))
+{
+}
+
+Fd& Fd::operator=(const Fd& other)
+{
+    if (this != &other)
+    {
+        reset();
+        sys = other.sys;
+        fd = dup(other.fd, sys);
+    }
+    return *this;
+}
+
+Fd::Fd(Fd&& other) : sys(other.sys), fd(std::move(other.fd))
+{
+    other.fd = -1;
+}
+
+Fd& Fd::operator=(Fd&& other)
+{
+    if (this != &other)
+    {
+        reset();
+        sys = other.sys;
+        fd = std::move(other.fd);
+        other.fd = -1;
+    }
+    return *this;
+}
+
+int Fd::operator*() const
+{
+    return fd;
+}
+
+const Sys* Fd::getSys() const
+{
+    return sys;
+}
+
+void Fd::setBlocking(bool enabled) const
+{
+    if (enabled)
+    {
+        setFlags(getFlags() & ~O_NONBLOCK);
+    }
+    else
+    {
+        setFlags(getFlags() | O_NONBLOCK);
+    }
+}
+
+void Fd::setFlags(int flags) const
+{
+    int r = sys->fcntl_setfl(fd, flags);
+    if (r == -1)
+    {
+        throw std::system_error(errno, std::generic_category(), "fcntl_setfl");
+    }
+}
+
+int Fd::getFlags() const
+{
+    int flags = sys->fcntl_getfl(fd);
+    if (flags == -1)
+    {
+        throw std::system_error(errno, std::generic_category(), "fcntl_getfl");
+    }
+    return flags;
+}
+
+void Fd::reset()
+{
+    if (fd < 0)
+    {
+        return;
+    }
+
+    int ret = sys->close(fd);
+    fd = -1;
+    if (ret != 0)
+    {
+        throw std::system_error(errno, std::generic_category(), "Closing FD");
+    }
+}
+
+} // namespace internal
+} // namespace gpioplus
diff --git a/src/gpioplus/internal/fd.hpp b/src/gpioplus/internal/fd.hpp
new file mode 100644
index 0000000..7f54b81
--- /dev/null
+++ b/src/gpioplus/internal/fd.hpp
@@ -0,0 +1,37 @@
+#pragma once
+#include <gpioplus/internal/sys.hpp>
+
+namespace gpioplus
+{
+namespace internal
+{
+
+class Fd
+{
+  public:
+    Fd(const char* pathname, int flags, const Sys* sys);
+    Fd(int fd, const Sys* sys);
+    ~Fd();
+
+    Fd(const Fd& other);
+    Fd& operator=(const Fd& other);
+    Fd(Fd&& other);
+    Fd& operator=(Fd&& other);
+
+    int operator*() const;
+    const Sys* getSys() const;
+
+    void setBlocking(bool enabled) const;
+
+  private:
+    const Sys* sys;
+    int fd;
+
+    void setFlags(int flags) const;
+    int getFlags() const;
+
+    void reset();
+};
+
+} // namespace internal
+} // namespace gpioplus
diff --git a/src/gpioplus/internal/sys.cpp b/src/gpioplus/internal/sys.cpp
new file mode 100644
index 0000000..1035072
--- /dev/null
+++ b/src/gpioplus/internal/sys.cpp
@@ -0,0 +1,78 @@
+#include <fcntl.h>
+#include <gpioplus/internal/sys.hpp>
+#include <linux/gpio.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+namespace gpioplus
+{
+namespace internal
+{
+
+int SysImpl::open(const char* pathname, int flags) const
+{
+    return ::open(pathname, flags);
+}
+
+int SysImpl::dup(int oldfd) const
+{
+    return ::dup(oldfd);
+}
+
+int SysImpl::close(int fd) const
+{
+    return ::close(fd);
+}
+
+int SysImpl::read(int fd, void* buf, size_t count) const
+{
+    return ::read(fd, buf, count);
+}
+
+int SysImpl::fcntl_setfl(int fd, int flags) const
+{
+    return ::fcntl(fd, F_SETFL, flags);
+}
+
+int SysImpl::fcntl_getfl(int fd) const
+{
+    return ::fcntl(fd, F_GETFL);
+}
+
+int SysImpl::gpiohandle_get_line_values(int fd,
+                                        struct gpiohandle_data* data) const
+{
+    return ::ioctl(fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, data);
+}
+
+int SysImpl::gpiohandle_set_line_values(int fd,
+                                        struct gpiohandle_data* data) const
+{
+    return ::ioctl(fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, data);
+}
+
+int SysImpl::gpio_get_chipinfo(int fd, struct gpiochip_info* info) const
+{
+    return ::ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, info);
+}
+
+int SysImpl::gpio_get_lineinfo(int fd, struct gpioline_info* info) const
+{
+    return ioctl(fd, GPIO_GET_LINEINFO_IOCTL, info);
+}
+
+int SysImpl::gpio_get_linehandle(int fd,
+                                 struct gpiohandle_request* request) const
+{
+    return ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, request);
+}
+
+int SysImpl::gpio_get_lineevent(int fd, struct gpioevent_request* request) const
+{
+    return ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, request);
+}
+
+SysImpl sys_impl;
+
+} // namespace internal
+} // namespace gpioplus
diff --git a/src/gpioplus/internal/sys.hpp b/src/gpioplus/internal/sys.hpp
new file mode 100644
index 0000000..37733b6
--- /dev/null
+++ b/src/gpioplus/internal/sys.hpp
@@ -0,0 +1,62 @@
+#pragma once
+#include <cstddef>
+#include <linux/gpio.h>
+
+namespace gpioplus
+{
+namespace internal
+{
+
+class Sys
+{
+  public:
+    virtual ~Sys() = default;
+
+    virtual int open(const char* pathname, int flags) const = 0;
+    virtual int dup(int oldfd) const = 0;
+    virtual int close(int fd) const = 0;
+    virtual int read(int fd, void* buf, size_t count) const = 0;
+    virtual int fcntl_setfl(int fd, int flags) const = 0;
+    virtual int fcntl_getfl(int fd) const = 0;
+
+    virtual int
+        gpiohandle_get_line_values(int fd,
+                                   struct gpiohandle_data* data) const = 0;
+    virtual int
+        gpiohandle_set_line_values(int fd,
+                                   struct gpiohandle_data* data) const = 0;
+    virtual int gpio_get_chipinfo(int fd, struct gpiochip_info* info) const = 0;
+    virtual int gpio_get_lineinfo(int fd, struct gpioline_info* info) const = 0;
+    virtual int
+        gpio_get_linehandle(int fd,
+                            struct gpiohandle_request* request) const = 0;
+    virtual int gpio_get_lineevent(int fd,
+                                   struct gpioevent_request* request) const = 0;
+};
+
+class SysImpl : public Sys
+{
+  public:
+    int open(const char* pathname, int flags) const override;
+    int dup(int oldfd) const override;
+    int close(int fd) const override;
+    int read(int fd, void* buf, size_t count) const override;
+    int fcntl_setfl(int fd, int flags) const override;
+    int fcntl_getfl(int fd) const override;
+
+    int gpiohandle_get_line_values(int fd,
+                                   struct gpiohandle_data* data) const override;
+    int gpiohandle_set_line_values(int fd,
+                                   struct gpiohandle_data* data) const override;
+    int gpio_get_chipinfo(int fd, struct gpiochip_info* info) const override;
+    int gpio_get_lineinfo(int fd, struct gpioline_info* info) const override;
+    int gpio_get_linehandle(int fd,
+                            struct gpiohandle_request* request) const override;
+    int gpio_get_lineevent(int fd,
+                           struct gpioevent_request* request) const override;
+};
+
+extern SysImpl sys_impl;
+
+} // namespace internal
+} // namespace gpioplus